Skip to content

Commit

Permalink
Support for MP JWT 2.1 (#674)
Browse files Browse the repository at this point in the history
* Support for mp.jwt.verify.token.age property

* Support for mp.jwt.decrypt.key.algorithm property

* Update MP JWT version to 2.1 in README
  • Loading branch information
sberyozkin authored Jan 31, 2023
1 parent ca5706c commit d62a598
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 31 deletions.
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ image:https://img.shields.io/maven-central/v/io.smallrye/smallrye-jwt?color=gree

= SmallRye JWT

SmallRye JWT is a library for implementing the {microprofile-jwt}[{mp-jwt-name}]. Currently it is focused on supporting the MP-JWT 1.2 spec. It deals with the decryption and/or signature verification of the JWT token and parsing it into a JsonWebToken implementation.
SmallRye JWT is a library for implementing the {microprofile-jwt}[{mp-jwt-name}]. Currently it is focused on supporting the MP-JWT 2.1 spec. It deals with the decryption and/or signature verification of the JWT token and parsing it into a JsonWebToken implementation.

== Instructions

Expand Down
4 changes: 3 additions & 1 deletion doc/modules/ROOT/pages/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ include::attributes.adoc[]
|mp.jwt.verify.publickey.location|none|Config property allows for an external or internal location of Public Key to be specified.
|mp.jwt.verify.publickey.algorithm|`RS256`|Signature algorithm. Set it to `ES256` to support the Elliptic Curve signature algorithm.
|mp.jwt.decrypt.key.location|none|Config property allows for an external or internal location of Private Decryption Key to be specified.
|mp.jwt.decrypt.key.algorithm|RSA-OAEP, RSA-OAEP-256|Decryption algorithm, both `RSA-OAEP` and `RSA-OAEP-256` will be supported by default. Set it to `RSA-OAEP-256` to support `RSA-OAEP` with `SHA-256` only.
|mp.jwt.verify.issuer|none|Expected value of the JWT `iss` (issuer) claim.
|mp.jwt.verify.audiences|`none`|Comma separated list of the audiences that a token `aud` claim may contain.
|mp.jwt.verify.token.age|`none`|Number of seconds that must not elapse since the `iat` (issued at) time.
|mp.jwt.token.header|`Authorization`|Set this property if another header such as `Cookie` is used to pass the token.
|mp.jwt.token.cookie|`Bearer`|Name of the cookie containing a token. This property will be effective only if `mp.jwt.token.header` is set to `Cookie`.
|===
Expand Down Expand Up @@ -60,7 +62,7 @@ SmallRye JWT supports many properties which can be used to customize the token p
|smallrye.jwt.required.claims|none|Comma separated list of the claims that a token must contain.
|smallrye.jwt.decrypt.key.location|none|Config property allows for an external or internal location of Private Decryption Key to be specified. This property is deprecated, use `mp.jwt.decrypt.key.location`.
|smallrye.jwt.decrypt.key|none|Decryption key supplied as a string.
|smallrye.jwt.decrypt.algorithm|`RSA_OAEP`|Decryption algorithm.
|smallrye.jwt.decrypt.algorithm|`RSA_OAEP`|Decryption algorithm. This property is deprecated, use `mp.jwt.decrypt.key.algorithm`.
|smallrye.jwt.token.decryption.kid|none|Decryption Key identifier. If it is set then the decryption JWK key as well every JWT token must have a matching `kid` header.
|smallrye.jwt.client.tls.certificate|none|TLS trusted certificate which may need to be configured if the keys have to be fetched over `HTTPS`. If this property is set then the `smallrye.jwt.client.tls.certificate.path` property will be ignored.
|smallrye.jwt.client.tls.certificate.path|none|Path to TLS trusted certificate which may need to be configured if the keys have to be fetched over `HTTPS`. This property will be ignored if the `smallrye.jwt.client.tls.certificate` property is set.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.jose4j.lang.UnresolvableKeyException;

import io.smallrye.jwt.KeyFormat;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.util.KeyUtils;

/**
Expand Down Expand Up @@ -68,8 +69,13 @@ public Key resolveKey(JsonWebEncryption jwe, List<JsonWebStructure> nestingConte
}

private Key tryAsDecryptionJwk(JsonWebEncryption jwe) throws UnresolvableKeyException {
JsonWebKey jwk = super.tryAsJwk(jwe, authContextInfo.getKeyEncryptionAlgorithm().getAlgorithm());
return fromJwkToDecryptionKey(jwk);
for (KeyEncryptionAlgorithm algo : authContextInfo.getKeyEncryptionAlgorithm()) {
JsonWebKey jwk = super.tryAsJwk(jwe, algo.getAlgorithm());
if (jwk != null) {
return fromJwkToDecryptionKey(jwk);
}
}
return null;
}

private Key fromJwkToDecryptionKey(JsonWebKey jwk) {
Expand Down Expand Up @@ -100,9 +106,13 @@ protected void initializeKeyContent() throws Exception {
return;
}
}
JsonWebKey jwk = loadFromJwk(content, authContextInfo.getTokenDecryptionKeyId(),
authContextInfo.getKeyEncryptionAlgorithm().getAlgorithm());
key = fromJwkToDecryptionKey(jwk);
for (KeyEncryptionAlgorithm keyAlgo : authContextInfo.getKeyEncryptionAlgorithm()) {
JsonWebKey jwk = loadFromJwk(content, authContextInfo.getTokenDecryptionKeyId(),
keyAlgo.getAlgorithm());
if (jwk != null) {
key = fromJwkToDecryptionKey(jwk);
}
}
}

static PrivateKey tryAsPEMPrivateKey(String content) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.Collections;
import java.util.Set;

import javax.crypto.SecretKey;

Expand Down Expand Up @@ -159,9 +161,10 @@ private void setSignatureAlgorithmIfNeeded(JWTAuthContextInfo newAuthContextInfo

private void setKeyEncryptionAlgorithmIfNeeded(JWTAuthContextInfo newAuthContextInfo, String algoStart,
KeyEncryptionAlgorithm newAlgo) {
KeyEncryptionAlgorithm algo = newAuthContextInfo.getKeyEncryptionAlgorithm();
if (algo == null || !algo.getAlgorithm().startsWith(algoStart)) {
newAuthContextInfo.setKeyEncryptionAlgorithm(newAlgo);
Set<KeyEncryptionAlgorithm> algo = newAuthContextInfo.getKeyEncryptionAlgorithm();
if (algo == null ||
!algo.stream().anyMatch(s -> s.getAlgorithm().startsWith(algoStart))) {
newAuthContextInfo.setKeyEncryptionAlgorithm(Collections.singleton(newAlgo));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static java.util.Collections.emptyList;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -35,6 +36,8 @@
import org.jose4j.lang.JoseException;
import org.jose4j.lang.UnresolvableKeyException;

import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;

/**
* Default JWT token validator
*
Expand Down Expand Up @@ -62,7 +65,7 @@ private String decryptSignedToken(String token, JWTAuthContextInfo authContextIn
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setAlgorithmConstraints(
new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT,
authContextInfo.getKeyEncryptionAlgorithm().getAlgorithm()));
encryptionAlgorithms(authContextInfo)));
if (authContextInfo.getPrivateDecryptionKey() != null) {
jwe.setKey(authContextInfo.getPrivateDecryptionKey());
} else if (authContextInfo.getSecretDecryptionKey() != null) {
Expand All @@ -85,6 +88,14 @@ private String decryptSignedToken(String token, JWTAuthContextInfo authContextIn
}
}

private String[] encryptionAlgorithms(JWTAuthContextInfo authContextInfo) {
Set<String> algorithms = new HashSet<>();
for (KeyEncryptionAlgorithm keyEncAlgo : authContextInfo.getKeyEncryptionAlgorithm()) {
algorithms.add(keyEncAlgo.getAlgorithm());
}
return algorithms.toArray(new String[] {});
}

private JwtContext parseClaims(String token, JWTAuthContextInfo authContextInfo, ProtectionLevel level)
throws ParseException {
try {
Expand Down Expand Up @@ -113,13 +124,13 @@ private JwtContext parseClaims(String token, JWTAuthContextInfo authContextInfo,
}
builder.setJweAlgorithmConstraints(
new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT,
authContextInfo.getKeyEncryptionAlgorithm().getAlgorithm()));
encryptionAlgorithms(authContextInfo)));
}

builder.setRequireExpirationTime();

final boolean issuedAtRequired = authContextInfo.getMaxTimeToLiveSecs() == null
|| authContextInfo.getMaxTimeToLiveSecs() > 0;
|| authContextInfo.getMaxTimeToLiveSecs() > 0 || authContextInfo.getTokenAge() != null;
if (issuedAtRequired) {
builder.setRequireIssuedAt();
}
Expand Down Expand Up @@ -211,8 +222,15 @@ private void verifyIatAndExpAndTimeToLive(JWTAuthContextInfo authContextInfo, Jw
if (exp.getValue() - iat.getValue() > maxTimeToLiveSecs) {
throw PrincipalMessages.msg.expExceeded(exp, maxTimeToLiveSecs, iat);
}
} else {
PrincipalLogging.log.noMaxTTLSpecified();
}

final Long tokenAge = authContextInfo.getTokenAge();

if (tokenAge != null) {
long now = System.currentTimeMillis() / 1000;
if (now - iat.getValue() > tokenAge) {
throw PrincipalMessages.msg.tokenAgeExceeded(tokenAge);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

Expand All @@ -39,6 +41,7 @@ public class JWTAuthContextInfo {
private String issuedBy;
private int expGracePeriodSecs = 60;
private Long maxTimeToLiveSecs;
private Long tokenAge;
private String publicKeyLocation;
private String publicKeyContent;
private String decryptionKeyLocation;
Expand All @@ -57,7 +60,8 @@ public class JWTAuthContextInfo {
private String defaultGroupsClaim;
private String groupsPath;
private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RS256;
private KeyEncryptionAlgorithm keyEncryptionAlgorithm = KeyEncryptionAlgorithm.RSA_OAEP;
private Set<KeyEncryptionAlgorithm> keyEncryptionAlgorithm = new HashSet<>(Arrays.asList(KeyEncryptionAlgorithm.RSA_OAEP,
KeyEncryptionAlgorithm.RSA_OAEP_256));
private KeyFormat keyFormat = KeyFormat.ANY;
private Set<String> expectedAudience;
private String groupsSeparator = " ";
Expand Down Expand Up @@ -102,6 +106,7 @@ public JWTAuthContextInfo(JWTAuthContextInfo orig) {
this.issuedBy = orig.getIssuedBy();
this.expGracePeriodSecs = orig.getExpGracePeriodSecs();
this.maxTimeToLiveSecs = orig.getMaxTimeToLiveSecs();
this.tokenAge = orig.getTokenAge();
this.publicKeyLocation = orig.getPublicKeyLocation();
this.publicKeyContent = orig.getPublicKeyContent();
this.decryptionKeyLocation = orig.getDecryptionKeyLocation();
Expand Down Expand Up @@ -217,11 +222,11 @@ public void setDecryptionKeyLocation(String keyLocation) {
this.decryptionKeyLocation = keyLocation;
}

public KeyEncryptionAlgorithm getKeyEncryptionAlgorithm() {
public Set<KeyEncryptionAlgorithm> getKeyEncryptionAlgorithm() {
return this.keyEncryptionAlgorithm;
}

public void setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm algorithm) {
public void setKeyEncryptionAlgorithm(Set<KeyEncryptionAlgorithm> algorithm) {
this.keyEncryptionAlgorithm = algorithm;
}

Expand Down Expand Up @@ -395,6 +400,7 @@ public String toString() {
", issuedBy='" + issuedBy + '\'' +
", expGracePeriodSecs=" + expGracePeriodSecs +
", maxTimeToLiveSecs=" + maxTimeToLiveSecs +
", tokenAge=" + tokenAge +
", publicKeyLocation='" + publicKeyLocation + '\'' +
", publicKeyContent='" + publicKeyContent + '\'' +
", decryptionKeyLocation='" + decryptionKeyLocation + '\'' +
Expand Down Expand Up @@ -489,4 +495,12 @@ public int getHttpProxyPort() {
public void setHttpProxyPort(int httpProxyPort) {
this.httpProxyPort = httpProxyPort;
}

public Long getTokenAge() {
return tokenAge;
}

public void setTokenAge(Long tokenAge) {
this.tokenAge = tokenAge;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ interface PrincipalLogging extends BasicLogger {
@Message(id = 8012, value = "Claim value at the path %s is not a json object")
void claimValueIsNotAJson(String claimPath);

@LogMessage(level = Logger.Level.DEBUG)
@Message(id = 8013, value = "No max TTL has been specified in configuration")
void noMaxTTLSpecified();

@LogMessage(level = Logger.Level.DEBUG)
@Message(id = 8014, value = "Required claims %s are not present in the JWT")
void missingClaims(String missingClaims);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,7 @@ interface PrincipalMessages {

@Message(id = 7017, value = "New JWTCallerPrincipalFactory instance can not be created")
ParseException newJWTCallerPrincipalFactoryFailure(@Cause Throwable throwable);
}

@Message(id = 7018, value = "The token age has exceeded %d seconds")
ParseException tokenAgeExceeded(long tokenAge);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

Expand Down Expand Up @@ -176,10 +179,13 @@ private static JWTAuthContextInfoProvider create(String publicKey,
provider.groupsPath = Optional.empty();
provider.expGracePeriodSecs = 60;
provider.maxTimeToLiveSecs = Optional.empty();
provider.mpJwtVerifyTokenAge = Optional.empty();
provider.jwksRefreshInterval = 60;
provider.forcedJwksRefreshInterval = 30;
provider.signatureAlgorithm = Optional.of(SignatureAlgorithm.RS256);
provider.keyEncryptionAlgorithm = KeyEncryptionAlgorithm.RSA_OAEP;
provider.keyEncryptionAlgorithm = Optional.empty();
provider.mpJwtDecryptKeyAlgorithm = new HashSet<>(Arrays.asList(KeyEncryptionAlgorithm.RSA_OAEP,
KeyEncryptionAlgorithm.RSA_OAEP_256));
provider.keyFormat = KeyFormat.ANY;
provider.mpJwtVerifyAudiences = Optional.empty();
provider.expectedAudience = Optional.empty();
Expand Down Expand Up @@ -232,6 +238,14 @@ private static JWTAuthContextInfoProvider create(String publicKey,
@ConfigProperty(name = "mp.jwt.decrypt.key.location", defaultValue = NONE)
private String mpJwtDecryptKeyLocation;

/**
* @since 2.1
*/
@Inject
@ConfigProperty(name = "mp.jwt.decrypt.key.algorithm", defaultValue = "RSA_OAEP,RSA_OAEP_256")
private Set<KeyEncryptionAlgorithm> mpJwtDecryptKeyAlgorithm = new HashSet<>(Arrays.asList(KeyEncryptionAlgorithm.RSA_OAEP,
KeyEncryptionAlgorithm.RSA_OAEP_256));;

@Inject
@ConfigProperty(name = "smallrye.jwt.decrypt.key")
private Optional<String> jwtDecryptKey;
Expand Down Expand Up @@ -260,8 +274,9 @@ private static JWTAuthContextInfoProvider create(String publicKey,
* Supported JSON Web Algorithm encryption algorithm.
*/
@Inject
@ConfigProperty(name = "smallrye.jwt.decrypt.algorithm", defaultValue = "RSA_OAEP")
private KeyEncryptionAlgorithm keyEncryptionAlgorithm;
@ConfigProperty(name = "smallrye.jwt.decrypt.algorithm")
@Deprecated
private Optional<KeyEncryptionAlgorithm> keyEncryptionAlgorithm;

/**
* @since 1.2
Expand All @@ -284,6 +299,13 @@ private static JWTAuthContextInfoProvider create(String publicKey,
@ConfigProperty(name = "mp.jwt.verify.audiences")
Optional<Set<String>> mpJwtVerifyAudiences;

/**
* @since 2.1
*/
@Inject
@ConfigProperty(name = "mp.jwt.verify.token.age")
Optional<Long> mpJwtVerifyTokenAge;

// SmallRye JWT specific properties
/**
* HTTP header which is expected to contain a JWT token, default value is 'Authorization'
Expand Down Expand Up @@ -689,6 +711,7 @@ Optional<JWTAuthContextInfo> getOptionalContextInfo() {
SmallryeJwtUtils.setContextGroupsPath(contextInfo, groupsPath);
contextInfo.setExpGracePeriodSecs(expGracePeriodSecs);
contextInfo.setMaxTimeToLiveSecs(maxTimeToLiveSecs.orElse(null));
contextInfo.setTokenAge(mpJwtVerifyTokenAge.orElse(null));
contextInfo.setJwksRefreshInterval(jwksRefreshInterval);
contextInfo.setForcedJwksRefreshInterval(forcedJwksRefreshInterval);
final Optional<SignatureAlgorithm> resolvedAlgorithm;
Expand All @@ -712,7 +735,14 @@ Optional<JWTAuthContextInfo> getOptionalContextInfo() {
contextInfo.setSignatureAlgorithm(SignatureAlgorithm.RS256);
}

contextInfo.setKeyEncryptionAlgorithm(keyEncryptionAlgorithm);
final Set<KeyEncryptionAlgorithm> theDecryptionKeyAlgorithm;
if (!keyEncryptionAlgorithm.isEmpty()) {
ConfigLogging.log.replacedConfig("smallrye.jwt.decrypt.algorithm", "mp.jwt.decrypt.key.algorithm");
theDecryptionKeyAlgorithm = Collections.singleton(keyEncryptionAlgorithm.get());
} else {
theDecryptionKeyAlgorithm = mpJwtDecryptKeyAlgorithm;
}
contextInfo.setKeyEncryptionAlgorithm(theDecryptionKeyAlgorithm);
contextInfo.setKeyFormat(keyFormat);
if (mpJwtVerifyAudiences.isPresent()) {
contextInfo.setExpectedAudience(mpJwtVerifyAudiences.get());
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<bouncycastle.version>1.70</bouncycastle.version>
<version.jakarta.servlet.api>5.0.0</version.jakarta.servlet.api>
<version.jakarta.security.enterprise.api>2.0.0</version.jakarta.security.enterprise.api>
<version.eclipse.microprofile.jwt>2.0</version.eclipse.microprofile.jwt>
<version.eclipse.microprofile.jwt>2.1</version.eclipse.microprofile.jwt>
<version.microprofile.config>3.0</version.microprofile.config>
<version.jose4j>0.9.2</version.jose4j>
<version.mokito>5.0.0</version.mokito>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Collections;

import javax.crypto.SecretKey;

Expand Down Expand Up @@ -202,7 +203,7 @@ public void testDecryptWithRsaPrivateKeyRsaOaep256() throws Exception {

JWTAuthContextInfo config = new JWTAuthContextInfo();
config.setDecryptionKeyLocation("/privateKey.pem");
config.setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP_256);
config.setKeyEncryptionAlgorithm(Collections.singleton(KeyEncryptionAlgorithm.RSA_OAEP_256));
JsonWebToken jwt = new DefaultJWTParser().parse(jwtString, config);

assertEquals("jdoe@example.com", jwt.getName());
Expand All @@ -217,7 +218,7 @@ public void testDecryptWithRsaPrivateKeyInnerSigned() throws Exception {
JWTAuthContextInfo config = new JWTAuthContextInfo();
config.setDecryptionKeyLocation("/privateKey.pem");
config.setPublicKeyLocation("/publicKey.pem");
config.setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP);
config.setKeyEncryptionAlgorithm(Collections.singleton(KeyEncryptionAlgorithm.RSA_OAEP));
JsonWebToken jwt = new DefaultJWTParser().parse(jwtString, config);

assertEquals("jdoe@example.com", jwt.getName());
Expand Down Expand Up @@ -281,7 +282,7 @@ public void testDecryptVerifyWithSecretKey() throws Exception {
.encrypt(secretKey);
JWTAuthContextInfo config = new JWTAuthContextInfo();
config.setSecretDecryptionKey(secretKey);
config.setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm.A256KW);
config.setKeyEncryptionAlgorithm(Collections.singleton(KeyEncryptionAlgorithm.A256KW));
config.setSecretVerificationKey(secretKey);
config.setSignatureAlgorithm(SignatureAlgorithm.HS256);
JsonWebToken jwt = new DefaultJWTParser().parse(jwtString, config);
Expand Down

0 comments on commit d62a598

Please sign in to comment.