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 upgrade to 2.1 version #6268

Merged
merged 5 commits into from
Mar 8, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion dependencies/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
<version.lib.microprofile-fault-tolerance-api>4.0</version.lib.microprofile-fault-tolerance-api>
<version.lib.microprofile-graphql>2.0</version.lib.microprofile-graphql>
<version.lib.microprofile-health>4.0</version.lib.microprofile-health>
<version.lib.microprofile-jwt>2.0</version.lib.microprofile-jwt>
<version.lib.microprofile-jwt>2.1</version.lib.microprofile-jwt>
<version.lib.microprofile-metrics-api>4.0</version.lib.microprofile-metrics-api>
<version.lib.microprofile-openapi-api>3.0</version.lib.microprofile-openapi-api>
<version.lib.microprofile-reactive-messaging-api>3.0</version.lib.microprofile-reactive-messaging-api>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
Expand Down Expand Up @@ -100,9 +101,6 @@
* Provider that provides JWT authentication.
*/
public class JwtAuthProvider extends SynchronousProvider implements AuthenticationProvider, OutboundSecurityProvider {
private static final System.Logger LOGGER = System.getLogger(JwtAuthProvider.class.getName());

private static final JsonReaderFactory JSON = Json.createReaderFactory(Collections.emptyMap());

/**
* Configure this for outbound requests to override user to use.
Expand All @@ -116,6 +114,9 @@ public class JwtAuthProvider extends SynchronousProvider implements Authenticati
* Configuration key for expected audiences of incoming tokens. Used for validation of JWT.
*/
public static final String CONFIG_EXPECTED_AUDIENCES = "mp.jwt.verify.audiences";

private static final String CONFIG_EXPECTED_MAX_TOKEN_AGE = "mp.jwt.verify.token.age";
Verdent marked this conversation as resolved.
Show resolved Hide resolved
private static final String CONFIG_CLOCK_SKEW = "mp.jwt.verify.clock.skew";
/**
* Configuration of Cookie property name which contains JWT token.
*
Expand All @@ -128,6 +129,8 @@ public class JwtAuthProvider extends SynchronousProvider implements Authenticati
* Default value is {@link Http.Header#AUTHORIZATION}.
*/
private static final String CONFIG_JWT_HEADER = "mp.jwt.token.header";
private static final System.Logger LOGGER = System.getLogger(JwtAuthProvider.class.getName());
private static final JsonReaderFactory JSON = Json.createReaderFactory(Collections.emptyMap());

private final boolean optional;
private final boolean authenticate;
Expand All @@ -147,7 +150,10 @@ public class JwtAuthProvider extends SynchronousProvider implements Authenticati
private final Map<OutboundTarget, JwtOutboundTarget> targetToJwtConfig = new IdentityHashMap<>();
private final String expectedIssuer;
private final String cookiePrefix;
private final String decryptionKeyAlgorithm;
private final boolean useCookie;
private final Duration expectedMaxTokenAge;
private final Duration clockSkew;

private JwtAuthProvider(Builder builder) {
this.optional = builder.optional;
Expand All @@ -167,6 +173,9 @@ private JwtAuthProvider(Builder builder) {
this.useCookie = builder.useCookie;
this.decryptionKeys = builder.decryptionKeys;
this.defaultDecryptionJwk = builder.defaultDecryptionJwk;
this.decryptionKeyAlgorithm = builder.decryptionKeyAlgorithm;
this.expectedMaxTokenAge = builder.expectedMaxTokenAge;
this.clockSkew = builder.clockSkew;

if (null == atnTokenHandler) {
defaultTokenHandler = TokenHandler.builder()
Expand Down Expand Up @@ -252,7 +261,14 @@ AuthenticationResponse authenticate(ProviderRequest providerRequest, LoginConfig
throw new JwtException("Header \"cty\" (content type) must be set to \"JWT\" "
+ "for encrypted tokens");
}
signedJwt = encryptedJwt.decrypt(decryptionKeys.get(), defaultDecryptionJwk.get());
List<Validator<EncryptedJwt>> validators = new LinkedList<>();
EncryptedJwt.addKekValidator(validators, decryptionKeyAlgorithm, true);
Errors errors = encryptedJwt.validate(validators);
if (errors.isValid()) {
signedJwt = encryptedJwt.decrypt(decryptionKeys.get(), defaultDecryptionJwk.get());
} else {
return AuthenticationResponse.failed(errors.toString());
}
} else {
signedJwt = SignedJwt.parseToken(token);
}
Expand All @@ -278,7 +294,13 @@ AuthenticationResponse authenticate(ProviderRequest providerRequest, LoginConfig
}
// validate user principal is present
Jwt.addUserPrincipalValidator(validators);
validators.add(Jwt.ExpirationValidator.create(true));
validators.add(Jwt.ExpirationValidator.create(Instant.now(),
(int) clockSkew.getSeconds(),
ChronoUnit.SECONDS,
true));
if (expectedMaxTokenAge != null) {
Jwt.addMaxTokenAgeValidator(validators, expectedMaxTokenAge, clockSkew, true);
}

Errors validate = jwt.validate(validators);

Expand Down Expand Up @@ -579,6 +601,7 @@ public static class Builder implements io.helidon.common.Builder<Builder, JwtAut
private static final String CONFIG_PUBLIC_KEY = "mp.jwt.verify.publickey";
private static final String CONFIG_PUBLIC_KEY_PATH = "mp.jwt.verify.publickey.location";
private static final String CONFIG_JWT_DECRYPT_KEY_LOCATION = "mp.jwt.decrypt.key.location";
private static final String CONFIG_JWT_DECRYPT_KEY_ALGORITHM = "mp.jwt.decrypt.key.algorithm";
private static final String JSON_START_MARK = "{";
private static final Pattern PUBLIC_KEY_PATTERN = Pattern.compile(
"-+BEGIN\\s+.*PUBLIC\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" // Header
Expand Down Expand Up @@ -615,8 +638,11 @@ public static class Builder implements io.helidon.common.Builder<Builder, JwtAut
private String publicKey;
private String cookieProperty = "Bearer";
private String decryptKeyLocation;
private String decryptionKeyAlgorithm;
private boolean useCookie = false;
private boolean loadOnStartup = false;
private Duration expectedMaxTokenAge = null;
private Duration clockSkew = Duration.ofSeconds(5);

private Builder() {
}
Expand Down Expand Up @@ -1101,9 +1127,12 @@ public Builder config(Config config) {
mpConfig.getOptionalValue(CONFIG_PUBLIC_KEY_PATH, String.class).ifPresent(this::publicKeyPath);
mpConfig.getOptionalValue(CONFIG_EXPECTED_ISSUER, String.class).ifPresent(this::expectedIssuer);
mpConfig.getOptionalValue(CONFIG_EXPECTED_AUDIENCES, String[].class).map(List::of).ifPresent(this::expectedAudiences);
mpConfig.getOptionalValue(CONFIG_EXPECTED_MAX_TOKEN_AGE, int.class).ifPresent(this::expectedMaxTokenAge);
mpConfig.getOptionalValue(CONFIG_COOKIE_PROPERTY_NAME, String.class).ifPresent(this::cookieProperty);
mpConfig.getOptionalValue(CONFIG_JWT_HEADER, String.class).ifPresent(this::jwtHeader);
mpConfig.getOptionalValue(CONFIG_JWT_DECRYPT_KEY_LOCATION, String.class).ifPresent(this::decryptKeyLocation);
mpConfig.getOptionalValue(CONFIG_JWT_DECRYPT_KEY_ALGORITHM, String.class).ifPresent(this::decryptKeyAlgorithm);
mpConfig.getOptionalValue(CONFIG_CLOCK_SKEW, int.class).ifPresent(this::clockSkew);

if (null == publicKey && null == publicKeyPath) {
// this is a fix for incomplete TCK tests
Expand Down Expand Up @@ -1194,6 +1223,17 @@ public Builder expectedAudiences(Collection<String> audiences) {
return this;
}

/**
* Maximal expected token age. If this value is set, {@code iat} claim needs to be present in the JWT.
*
* @param expectedMaxTokenAge expected maximal token age in seconds
* @return updated builder instance
*/
public Builder expectedMaxTokenAge(int expectedMaxTokenAge) {
this.expectedMaxTokenAge = Duration.ofSeconds(expectedMaxTokenAge);
return this;
}

/**
* Private key to decryption of encrypted claims.
*
Expand All @@ -1205,6 +1245,17 @@ public Builder decryptKeyLocation(String decryptKeyLocation) {
return this;
}

/**
* Expected decryption key algorithm.
*
* @param decryptionKeyAlgorithm expected decryption key algorithm
* @return updated builder instance
*/
public Builder decryptKeyAlgorithm(String decryptionKeyAlgorithm) {
this.decryptionKeyAlgorithm = decryptionKeyAlgorithm;
Verdent marked this conversation as resolved.
Show resolved Hide resolved
return this;
}

/**
* Whether to load JWK verification keys on server startup
* Default value is {@code false}.
Expand All @@ -1217,6 +1268,17 @@ public Builder loadOnStartup(boolean loadOnStartup) {
return this;
}

/**
* Clock skew to be accounted for in token expiration and max age validations.
*
* @param clockSkew clock skew
* @return updated builder instance
*/
public Builder clockSkew(int clockSkew) {
this.clockSkew = Duration.ofSeconds(clockSkew);
return this;
}

private void verifyKeys(Config config) {
config.get("jwk.resource").as(Resource::create).ifPresent(this::verifyJwk);
}
Expand Down