io.quarkus.oidc.runtime.OidcProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of quarkus-oidc Show documentation
Show all versions of quarkus-oidc Show documentation
Secure your applications with OpenID Connect Adapter and IDP such as Keycloak
package io.quarkus.oidc.runtime;
import java.io.Closeable;
import java.security.Key;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.eclipse.microprofile.jwt.Claims;
import org.jboss.logging.Logger;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.ErrorCodeValidator;
import org.jose4j.jwt.consumer.ErrorCodes;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.jwt.consumer.Validator;
import org.jose4j.jwx.HeaderParameterNames;
import org.jose4j.jwx.JsonWebStructure;
import org.jose4j.keys.resolvers.VerificationKeyResolver;
import org.jose4j.lang.UnresolvableKeyException;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.AuthenticationFailedException;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
public class OidcProvider implements Closeable {
private static final Logger LOG = Logger.getLogger(OidcProvider.class);
private static final String ANY_ISSUER = "any";
private static final String[] ASYMMETRIC_SUPPORTED_ALGORITHMS = new String[] { SignatureAlgorithm.RS256.getAlgorithm(),
SignatureAlgorithm.RS384.getAlgorithm(),
SignatureAlgorithm.RS512.getAlgorithm(),
SignatureAlgorithm.ES256.getAlgorithm(),
SignatureAlgorithm.ES384.getAlgorithm(),
SignatureAlgorithm.ES512.getAlgorithm() };
private static final AlgorithmConstraints ASYMMETRIC_ALGORITHM_CONSTRAINTS = new AlgorithmConstraints(
AlgorithmConstraints.ConstraintType.PERMIT, ASYMMETRIC_SUPPORTED_ALGORITHMS);
private static final AlgorithmConstraints SYMMETRIC_ALGORITHM_CONSTRAINTS = new AlgorithmConstraints(
AlgorithmConstraints.ConstraintType.PERMIT, SignatureAlgorithm.HS256.getAlgorithm());
final OidcProviderClient client;
final RefreshableVerificationKeyResolver asymmetricKeyResolver;
final OidcTenantConfig oidcConfig;
final String issuer;
final String[] audience;
final Map requiredClaims;
final Key tokenDecryptionKey;
public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) {
this.client = client;
this.oidcConfig = oidcConfig;
this.asymmetricKeyResolver = jwks == null ? null
: new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval);
this.issuer = checkIssuerProp();
this.audience = checkAudienceProp();
this.requiredClaims = checkRequiredClaimsProp();
this.tokenDecryptionKey = tokenDecryptionKey;
}
public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) {
this.client = null;
this.oidcConfig = oidcConfig;
this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc);
this.issuer = checkIssuerProp();
this.audience = checkAudienceProp();
this.requiredClaims = checkRequiredClaimsProp();
this.tokenDecryptionKey = tokenDecryptionKey;
}
private String checkIssuerProp() {
String issuerProp = null;
if (oidcConfig != null) {
issuerProp = oidcConfig.token.issuer.orElse(null);
if (issuerProp == null && client != null) {
issuerProp = client.getMetadata().getIssuer();
}
}
return ANY_ISSUER.equals(issuerProp) ? null : issuerProp;
}
private String[] checkAudienceProp() {
List audienceProp = oidcConfig != null ? oidcConfig.token.audience.orElse(null) : null;
return audienceProp != null ? audienceProp.toArray(new String[] {}) : null;
}
private Map checkRequiredClaimsProp() {
return oidcConfig != null ? oidcConfig.token.requiredClaims : null;
}
public TokenVerificationResult verifySelfSignedJwtToken(String token) throws InvalidJwtException {
return verifyJwtTokenInternal(token, SYMMETRIC_ALGORITHM_CONSTRAINTS, new SymmetricKeyResolver(), true);
}
public TokenVerificationResult verifyJwtToken(String token) throws InvalidJwtException {
return verifyJwtTokenInternal(token, ASYMMETRIC_ALGORITHM_CONSTRAINTS, asymmetricKeyResolver, true);
}
public TokenVerificationResult verifyLogoutJwtToken(String token) throws InvalidJwtException {
final boolean enforceExpReq = !oidcConfig.token.age.isPresent();
TokenVerificationResult result = verifyJwtTokenInternal(token, ASYMMETRIC_ALGORITHM_CONSTRAINTS, asymmetricKeyResolver,
enforceExpReq);
if (!enforceExpReq) {
// Expiry check was skipped during the initial verification but if the logout token contains the exp claim
// then it must be verified
final Long exp = result.localVerificationResult.getLong(Claims.exp.name());
if (exp != null) {
verifyExpiry(exp);
}
}
return result;
}
private TokenVerificationResult verifyJwtTokenInternal(String token, AlgorithmConstraints algConstraints,
VerificationKeyResolver verificationKeyResolver, boolean enforceExpReq) throws InvalidJwtException {
JwtConsumerBuilder builder = new JwtConsumerBuilder();
builder.setVerificationKeyResolver(verificationKeyResolver);
builder.setJwsAlgorithmConstraints(algConstraints);
if (enforceExpReq) {
builder.setRequireExpirationTime();
}
builder.setRequireIssuedAt();
if (issuer != null) {
builder.setExpectedIssuer(issuer);
}
if (audience != null) {
builder.setExpectedAudience(audience);
} else {
builder.setSkipDefaultAudienceValidation();
}
if (requiredClaims != null) {
builder.registerValidator(new CustomClaimsValidator(requiredClaims));
}
if (oidcConfig.token.lifespanGrace.isPresent()) {
final int lifespanGrace = oidcConfig.token.lifespanGrace.getAsInt();
builder.setAllowedClockSkewInSeconds(lifespanGrace);
}
builder.setRelaxVerificationKeyValidation();
try {
JwtConsumer jwtConsumer = builder.build();
jwtConsumer.processToClaims(token);
} catch (InvalidJwtException ex) {
String detail = "";
List details = ex.getErrorDetails();
if (!details.isEmpty()) {
detail = details.get(0).getErrorMessage();
}
if (oidcConfig.clientId.isPresent()) {
LOG.debugf("Verification of the token issued to client %s has failed: %s", oidcConfig.clientId.get(), detail);
} else {
LOG.debugf("Token verification has failed: %s", detail);
}
throw ex;
}
TokenVerificationResult result = new TokenVerificationResult(OidcUtils.decodeJwtContent(token), null);
if (oidcConfig.token.age.isPresent()) {
final long now = now() / 1000;
// 'iat' is guaranteed to be present if no exception has been thrown by 'verifyJwtTokenInternal'
final long iat = result.localVerificationResult.getLong(Claims.iat.name());
if (now - iat > oidcConfig.token.age.get().toSeconds() + getLifespanGrace()) {
final String errorMessage = "Logout token age exceeds the configured token age property";
LOG.debugf(errorMessage);
throw new InvalidJwtException(errorMessage,
List.of(new ErrorCodeValidator.Error(ErrorCodes.ISSUED_AT_INVALID_PAST, errorMessage)), null);
}
}
return result;
}
public Uni refreshJwksAndVerifyJwtToken(String token) {
return asymmetricKeyResolver.refresh().onItem()
.transformToUni(new Function>() {
@Override
public Uni extends TokenVerificationResult> apply(Void v) {
try {
return Uni.createFrom().item(verifyJwtToken(token));
} catch (Throwable t) {
return Uni.createFrom().failure(t);
}
}
});
}
public Uni introspectToken(String token) {
if (client.getMetadata().getIntrospectionUri() == null) {
LOG.debugf(
"Token issued to client %s can not be introspected because the introspection endpoint address is unknown - "
+ "please check if your OpenId Connect Provider supports the token introspection",
oidcConfig.clientId.get());
throw new AuthenticationFailedException();
}
return client.introspectToken(token).onItemOrFailure()
.transform(new BiFunction() {
@Override
public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwable t) {
if (t != null) {
throw new AuthenticationFailedException(t);
}
if (!Boolean.TRUE.equals(introspectionResult.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) {
LOG.debugf("Token issued to client %s is not active", oidcConfig.clientId.get());
throw new AuthenticationFailedException();
}
Long exp = introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP);
if (exp != null) {
verifyExpiry(exp);
}
return introspectionResult;
}
});
}
private void verifyExpiry(Long exp) {
if (now() / 1000 > exp + getLifespanGrace()) {
LOG.debugf("Token issued to client %s has expired", oidcConfig.clientId.get());
throw new AuthenticationFailedException();
}
}
private int getLifespanGrace() {
return client.getOidcConfig().token.lifespanGrace.isPresent()
? client.getOidcConfig().token.lifespanGrace.getAsInt()
: 0;
}
private static final long now() {
return System.currentTimeMillis();
}
public Uni getUserInfo(String accessToken) {
return client.getUserInfo(accessToken);
}
public Uni getCodeFlowTokens(String code, String redirectUri, String codeVerifier) {
return client.getAuthorizationCodeTokens(code, redirectUri, codeVerifier);
}
public Uni refreshTokens(String refreshToken) {
return client.refreshAuthorizationCodeTokens(refreshToken);
}
@Override
public void close() {
if (client != null) {
client.close();
}
}
private class JsonWebKeyResolver implements RefreshableVerificationKeyResolver {
volatile JsonWebKeySet jwks;
volatile long lastForcedRefreshTime;
volatile long forcedJwksRefreshIntervalMilliSecs;
JsonWebKeyResolver(JsonWebKeySet jwks, Duration forcedJwksRefreshInterval) {
this.jwks = jwks;
this.forcedJwksRefreshIntervalMilliSecs = forcedJwksRefreshInterval.toMillis();
}
@Override
public Key resolveKey(JsonWebSignature jws, List nestingContext)
throws UnresolvableKeyException {
Key key = null;
// Try 'kid' first
String kid = jws.getKeyIdHeaderValue();
if (kid != null) {
key = getKeyWithId(jws, kid);
if (key == null) {
// if `kid` was set then the key must exist
throw new UnresolvableKeyException(String.format("JWK with kid '%s' is not available", kid));
}
}
String thumbprint = null;
if (key == null) {
thumbprint = jws.getHeader(HeaderParameterNames.X509_CERTIFICATE_THUMBPRINT);
if (thumbprint != null) {
key = getKeyWithThumbprint(jws, thumbprint);
if (key == null) {
// if only `x5t` was set then the key must exist
throw new UnresolvableKeyException(
String.format("JWK with thumbprint '%s' is not available", thumbprint));
}
}
}
if (key == null && kid == null && thumbprint == null) {
key = jwks.getKeyWithoutKeyIdAndThumbprint();
}
if (key == null) {
throw new UnresolvableKeyException(
String.format("JWK is not available, neither 'kid' nor 'x5t' token headers are set", kid));
} else {
return key;
}
}
private Key getKeyWithId(JsonWebSignature jws, String kid) {
if (kid != null) {
return jwks.getKeyWithId(kid);
} else {
LOG.debug("Token 'kid' header is not set");
return null;
}
}
private Key getKeyWithThumbprint(JsonWebSignature jws, String thumbprint) {
if (thumbprint != null) {
return jwks.getKeyWithThumbprint(thumbprint);
} else {
LOG.debug("Token 'x5t' header is not set");
return null;
}
}
public Uni refresh() {
final long now = now();
if (now > lastForcedRefreshTime + forcedJwksRefreshIntervalMilliSecs) {
lastForcedRefreshTime = now;
return client.getJsonWebKeySet().onItem().transformToUni(new Function>() {
@Override
public Uni extends Void> apply(JsonWebKeySet t) {
jwks = t;
return Uni.createFrom().voidItem();
}
});
} else {
return Uni.createFrom().voidItem();
}
}
}
private static class LocalPublicKeyResolver implements RefreshableVerificationKeyResolver {
Key key;
LocalPublicKeyResolver(String publicKeyEnc) {
try {
key = KeyUtils.decodePublicKey(publicKeyEnc);
} catch (Exception ex) {
throw new OIDCException(ex);
}
}
@Override
public Key resolveKey(JsonWebSignature jws, List nestingContext)
throws UnresolvableKeyException {
return key;
}
}
private class SymmetricKeyResolver implements VerificationKeyResolver {
@Override
public Key resolveKey(JsonWebSignature jws, List nestingContext)
throws UnresolvableKeyException {
return KeyUtils.createSecretKeyFromSecret(oidcConfig.credentials.secret.get());
}
}
public OidcConfigurationMetadata getMetadata() {
return client.getMetadata();
}
private static interface RefreshableVerificationKeyResolver extends VerificationKeyResolver {
default Uni refresh() {
return Uni.createFrom().voidItem();
}
}
private static class CustomClaimsValidator implements Validator {
private final Map customClaims;
public CustomClaimsValidator(Map customClaims) {
this.customClaims = customClaims;
}
@Override
public String validate(JwtContext jwtContext) throws MalformedClaimException {
var claims = jwtContext.getJwtClaims();
for (var targetClaim : customClaims.entrySet()) {
var claimName = targetClaim.getKey();
if (!claims.hasClaim(claimName)) {
return "claim " + claimName + " is missing";
}
if (!claims.isClaimValueString(claimName)) {
throw new MalformedClaimException("expected claim " + claimName + " to be a string");
}
var claimValue = claims.getStringClaimValue(claimName);
var targetValue = targetClaim.getValue();
if (!claimValue.equals(targetValue)) {
return "claim " + claimName + "does not match expected value of " + targetValue;
}
}
return null;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy