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

Support for MP JWT 2.1 #674

Merged
merged 3 commits into from
Jan 31, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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