Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT Auth 2.0 impl #3872

Merged
merged 3 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/security/jersey/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2016, 2020 Oracle and/or its affiliates.
# Copyright (c) 2016, 2022 Oracle and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,8 @@ security:
# Will use exceptions instead of aborting the request - this should make no difference
# to user unless you use a custom error handler
use-abort-with: false
# Will fail in case of authentication failure even if the authentication is not required.
fail-on-failure-if-optional: true
config:
# Configuration of secured config (encryption of passwords in property files)
# Set to true for production - if set to true, clear text passwords will cause failure
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2021 Oracle and/or its affiliates.
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,7 +20,6 @@
import java.util.Optional;
import java.util.Set;

import io.helidon.security.SecurityException;
import io.helidon.security.jwt.Jwt;
import io.helidon.security.jwt.JwtException;
import io.helidon.security.jwt.JwtUtil;
Expand All @@ -32,7 +31,6 @@
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import org.eclipse.microprofile.jwt.ClaimValue;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

Expand Down Expand Up @@ -111,16 +109,9 @@ public <T> T getClaim(String claimName, Class<T> clazz) {
? (T) getJsonValue(claimName).orElse(null)
: (T) getClaim(claims);
} catch (IllegalArgumentException ignored) {
//If claimName is name of the custom claim
Object value = getJsonValue(claimName).orElse(null);
if ((value != null)
&& (clazz != ClaimValue.class)
&& (clazz != Optional.class)
&& !clazz.isAssignableFrom(value.getClass())) {
throw new SecurityException("Cannot set instance of " + value.getClass().getName()
+ " to the field of type " + clazz.getName());
}
return (T) value;
return (T) getJsonValue(claimName)
.map(val -> convertClass(clazz, val))
.orElse(null);
}
}

Expand Down Expand Up @@ -161,6 +152,10 @@ private Optional<JsonValue> getJsonValue(String claimName) {

private Object convert(Claims claims, JsonValue value) {
Class<?> claimClass = claims.getType();
return convertClass(claimClass, value);
}

private Object convertClass(Class<?> claimClass, JsonValue value) {
if (claimClass.equals(String.class)) {
if (value instanceof JsonString) {
return ((JsonString) value).getString();
Expand All @@ -178,8 +173,13 @@ private Object convert(Claims claims, JsonValue value) {
return value;
}

if (value instanceof JsonString) {
return ((JsonString) value).getString();
if (Boolean.TYPE.equals(claimClass) || Boolean.class.equals(claimClass)){
if (JsonValue.TRUE.equals(value)) {
return true;
}
if (JsonValue.FALSE.equals(value)) {
return false;
}
}

return value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2020 Oracle and/or its affiliates.
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,10 +22,9 @@
import java.util.Optional;
import java.util.Set;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.inject.Provider;
Expand Down Expand Up @@ -55,15 +54,16 @@ public Class<? extends Annotation> annotationType() {

private final JwtAuthCdiExtension.MpClaimQualifier qualifier;
private final Type type;
private final BeanManager bm;
private final Class<? extends Annotation> scope;

ClaimProducer(JwtAuthCdiExtension.MpClaimQualifier q, Type type, BeanManager bm) {
ClaimProducer(JwtAuthCdiExtension.MpClaimQualifier q, Type type, Class<? extends Annotation> scope) {
this.qualifier = q;
this.bm = bm;
this.scope = scope;
Type actualType = type;
if (type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) type;
if (Provider.class.equals(paramType.getRawType())) {
if (Provider.class.equals(paramType.getRawType())
|| Instance.class.equals(paramType.getRawType())) {
actualType = paramType.getActualTypeArguments()[0];
}
}
Expand Down Expand Up @@ -154,7 +154,7 @@ public Set<Annotation> getQualifiers() {

@Override
public Class<? extends Annotation> getScope() {
return Dependent.class;
return scope;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2021 Oracle and/or its affiliates.
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,21 +21,23 @@
import io.helidon.security.jwt.SignedJwt;
import io.helidon.security.providers.common.TokenCredential;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;

/**
* Producer of JsonWebTokenImpl for CDI.
*/
// must be in RequestScoped - ApplicationScoped fails some tests
@RequestScoped
@ApplicationScoped
class JsonWebTokenProducer {
@Context
@Inject
private SecurityContext securityContext;

@Produces
@RequestScoped
public JsonWebToken produceToken() {
return securityContext.user()
.map(this::toJsonWebToken)
Expand All @@ -55,6 +57,7 @@ private JsonWebTokenImpl toJsonWebToken(Subject subject) {

@Produces
@Impl
@RequestScoped
public JsonWebTokenImpl produceTokenImpl() {
return (JsonWebTokenImpl) produceToken();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2020 Oracle and/or its affiliates.
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -108,6 +108,11 @@ public AnalyzerResponse analyze(Class<?> maybeAnnotated, AnalyzerResponse previo
// resource method analysis
@Override
public AnalyzerResponse analyze(Method maybeAnnotated, AnalyzerResponse previousResponse) {
if (isRolesAllowed(maybeAnnotated)) {
return AnalyzerResponse.builder(previousResponse)
.authenticationResponse(Flag.REQUIRED)
.build();
}
return AnalyzerResponse.builder(previousResponse)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2020 Oracle and/or its affiliates.
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,7 +38,9 @@

import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
Expand Down Expand Up @@ -77,7 +79,19 @@
* JWT Authentication CDI extension class.
*/
public class JwtAuthCdiExtension implements Extension {

private static final Set<Class<?>> CUSTOM_CLAIM_ALLOWED_TYPES;

static {
CUSTOM_CLAIM_ALLOWED_TYPES = Set.of(Long.class,
long.class,
String.class,
Boolean.class,
boolean.class);
}

private final List<ClaimIP> qualifiers = new LinkedList<>();
private final List<ClaimIP> claimValueQualifiers = new LinkedList<>();
private Config config;

/**
Expand Down Expand Up @@ -123,7 +137,11 @@ void collectClaimProducer(@Observes ProcessInjectionPoint<?, ?> pip) {
pip.configureInjectionPoint()
.addQualifier(q);

qualifiers.add(new ClaimIP(q, type));
if (ft.getField0().getRawType().equals(ClaimValue.class)) {
claimValueQualifiers.add(new ClaimIP(q, type));
} else {
qualifiers.add(new ClaimIP(q, type));
}
}
}

Expand All @@ -135,7 +153,8 @@ void collectClaimProducer(@Observes ProcessInjectionPoint<?, ?> pip) {
*/
void registerClaimProducers(@Observes AfterBeanDiscovery abd, BeanManager bm) {
// each injection point will have its own bean
qualifiers.forEach(q -> abd.addBean(new ClaimProducer(q.qualifier, q.type, bm)));
qualifiers.forEach(q -> abd.addBean(new ClaimProducer(q.qualifier, q.type, Dependent.class)));
claimValueQualifiers.forEach(q -> abd.addBean(new ClaimProducer(q.qualifier, q.type, RequestScoped.class)));
}

/**
Expand Down Expand Up @@ -310,12 +329,14 @@ private void validateBaseType(ClaimLiteral claimLiteral, Class<?> clazz) {
+ " of type " + claimLiteral.fieldTypeString);

} catch (IllegalArgumentException ignored) {
//If claim requested claim is the custom claim, its unwrapped field type has to be JsonValue or its subtype
if (!JsonValue.class.isAssignableFrom(clazz)) {
throw new DeploymentException("Field type has to be JsonValue or its subtype while using custom claim name. "
+ "Field " + claimLiteral.id + " can not be type: "
+ claimLiteral.fieldTypeString);
//If claim requested claim is the custom claim, its unwrapped field type has to be Long, Boolean, String or
// JsonValue (or its subtype)
if (CUSTOM_CLAIM_ALLOWED_TYPES.contains(clazz) || JsonValue.class.isAssignableFrom(clazz)) {
return;
}
throw new DeploymentException("Field type has to be Long, Boolean, String or JsonValue (or its subtype) while using "
+ "custom claim name. Field " + claimLiteral.id + " can not be type: "
+ claimLiteral.fieldTypeString);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2021 Oracle and/or its affiliates.
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -279,7 +279,7 @@ AuthenticationResponse authenticate(ProviderRequest providerRequest, LoginConfig
}
// validate user principal is present
Jwt.addUserPrincipalValidator(validators);
validators.add(Jwt.ExpirationValidator.create(false));
validators.add(Jwt.ExpirationValidator.create(true));

Errors validate = jwt.validate(validators);

Expand All @@ -291,13 +291,7 @@ AuthenticationResponse authenticate(ProviderRequest providerRequest, LoginConfig
} else {
return AuthenticationResponse.failed(errors.toString());
}
}).orElseGet(() -> {
if (optional) {
return AuthenticationResponse.abstain();
} else {
return AuthenticationResponse.failed("Header not available or in a wrong format");
}
});
}).orElseGet(AuthenticationResponse::abstain);
}

private Optional<String> findCookie(Map<String, List<String>> headers) {
Expand Down
15 changes: 9 additions & 6 deletions microprofile/tests/tck/tck-jwt-auth/pom.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2018, 2021 Oracle and/or its affiliates.
Copyright (c) 2018, 2022 Oracle and/or its affiliates.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -26,11 +26,6 @@
<artifactId>tck-jwt-auth</artifactId>
<name>Helidon Microprofile Tests TCK JWT-Auth</name>

<properties>
<!-- 3.0.0-JAKARTA -->
<skipTests>true</skipTests>
</properties>

<dependencies>
<dependency>
<groupId>org.eclipse.microprofile.jwt</groupId>
Expand All @@ -43,6 +38,14 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.bundles</groupId>
<artifactId>helidon-microprofile-core</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.jwt</groupId>
<artifactId>helidon-microprofile-jwt-auth</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.tests</groupId>
<artifactId>helidon-arquillian</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2020 Oracle and/or its affiliates.
# Copyright (c) 2020, 2022 Oracle and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -17,4 +17,6 @@
# Configure this source as the least important one
config_ordinal: 0
mp.jwt.verify.issuer: "https://server.example.com"
helidon.mp.jwt.verify.publickey.location: "/publicKey.pem"
helidon.mp.jwt.verify.publickey.location: "/publicKey.pem"
security.jersey.fail-on-failure-if-optional: true
security.jersey.analyzers.jwt.secure-by-default: false
Loading