All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.oidc.runtime.OidcProvider Maven / Gradle / Ivy

Go to download

Secure your applications with OpenID Connect Adapter and IDP such as Keycloak

There is a newer version: 3.17.5
Show newest version
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 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 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