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

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

package io.quarkus.oidc.runtime;

import static io.quarkus.oidc.common.runtime.OidcConstants.TOKEN_SCOPE;
import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRoutingContextAttribute;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

import javax.crypto.SecretKey;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwe.JsonWebEncryption;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.lang.JoseException;

import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.Authentication;
import io.quarkus.oidc.RefreshToken;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.providers.KnownOidcProviders;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.StringPermission;
import io.quarkus.security.credential.TokenCredential;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder;
import io.smallrye.jwt.algorithm.ContentEncryptionAlgorithm;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.impl.CookieImpl;
import io.vertx.core.http.impl.ServerCookie;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;

public final class OidcUtils {
    private static final Logger LOG = Logger.getLogger(OidcUtils.class);

    public static final String CONFIG_METADATA_ATTRIBUTE = "configuration-metadata";
    public static final String USER_INFO_ATTRIBUTE = "userinfo";
    public static final String INTROSPECTION_ATTRIBUTE = "introspection";
    public static final String TENANT_ID_ATTRIBUTE = "tenant-id";
    public static final String TENANT_ID_SET_BY_ANNOTATION = "tenant-id-set-by-annotation";
    public static final String TENANT_ID_SET_BY_SESSION_COOKIE = "tenant-id-set-by-session-cookie";
    public static final String TENANT_ID_SET_BY_STATE_COOKIE = "tenant-id-set-by-state-cookie";
    public static final String DEFAULT_TENANT_ID = "Default";
    public static final String SESSION_COOKIE_NAME = "q_session";
    public static final String SESSION_COOKIE_CHUNK_START = "chunk_";
    public static final String SESSION_COOKIE_CHUNK = "_" + SESSION_COOKIE_CHUNK_START;
    public static final String ACCESS_TOKEN_COOKIE_SUFFIX = "_at";
    public static final String REFRESH_TOKEN_COOKIE_SUFFIX = "_rt";
    public static final String SESSION_AT_COOKIE_NAME = SESSION_COOKIE_NAME + ACCESS_TOKEN_COOKIE_SUFFIX;
    public static final String SESSION_RT_COOKIE_NAME = SESSION_COOKIE_NAME + REFRESH_TOKEN_COOKIE_SUFFIX;
    public static final String STATE_COOKIE_NAME = "q_auth";

    // Browsers enforce that the total Set-Cookie expression such as
    // `q_session_tenant-a=,Path=/somepath,Expires=...` does not exceed 4096
    // Setting the max cookie value length to 4056 gives extra 40 bytes to cover for the name, path, expires attributes in most cases
    // and can be tuned further if necessary.
    public static final Integer MAX_COOKIE_VALUE_LENGTH = 4056;
    public static final String POST_LOGOUT_COOKIE_NAME = "q_post_logout";
    public static final String DEFAULT_SCOPE_SEPARATOR = " ";
    public static final String ANNOTATION_BASED_TENANT_RESOLUTION_ENABLED = "io.quarkus.oidc.runtime.select-tenants-with-annotation";
    static final String UNDERSCORE = "_";
    static final String CODE_ACCESS_TOKEN_RESULT = "code_flow_access_token_result";
    static final String CODE_ACCESS_TOKEN_FAILURE = "code_flow_access_token_failure";
    static final String COMMA = ",";
    static final Uni VOID_UNI = Uni.createFrom().voidItem();
    static final BlockingTaskRunner deleteTokensRequestContext = new BlockingTaskRunner();

    /**
     * This pattern uses a positive lookahead to split an expression around the forward slashes
     * ignoring those which are located inside a pair of the double quotes.
     */
    private static final Pattern CLAIM_PATH_PATTERN = Pattern.compile("\\/(?=(?:(?:[^\"]*\"){2})*[^\"]*$)");
    private static final String EXTRACTED_BEARER_TOKEN = "quarkus.oidc.extracted-bearer-token";
    public static final String QUARKUS_IDENTITY_EXPIRE_TIME = "quarkus.identity.expire-time";

    private OidcUtils() {

    }

    public static String getSessionCookie(RoutingContext context, OidcTenantConfig oidcTenantConfig) {
        final Map cookies = context.request().cookieMap();
        return getSessionCookie(context.data(), cookies, oidcTenantConfig);
    }

    public static String getSessionCookie(Map context, Map cookies,
            OidcTenantConfig oidcTenantConfig) {
        if (cookies.isEmpty()) {
            return null;
        }
        final String sessionCookieName = getSessionCookieName(oidcTenantConfig);

        if (cookies.containsKey(sessionCookieName)) {
            context.put(OidcUtils.SESSION_COOKIE_NAME, List.of(sessionCookieName));
            return cookies.get(sessionCookieName).getValue();
        } else {
            final String sessionChunkPrefix = sessionCookieName + OidcUtils.SESSION_COOKIE_CHUNK;

            SortedMap sessionCookies = new TreeMap<>(new Comparator() {

                @Override
                public int compare(String s1, String s2) {
                    // at this point it is guaranteed cookie names end with `chunk_`
                    int lastUnderscoreIndex1 = s1.lastIndexOf(UNDERSCORE);
                    int lastUnderscoreIndex2 = s2.lastIndexOf(UNDERSCORE);
                    Integer pos1 = Integer.valueOf(s1.substring(lastUnderscoreIndex1 + 1));
                    Integer pos2 = Integer.valueOf(s2.substring(lastUnderscoreIndex2 + 1));
                    return pos1.compareTo(pos2);
                }

            });
            for (String cookieName : cookies.keySet()) {
                if (cookieName.startsWith(sessionChunkPrefix)) {
                    sessionCookies.put(cookieName, cookies.get(cookieName).getValue());
                }
            }
            if (!sessionCookies.isEmpty()) {
                context.put(OidcUtils.SESSION_COOKIE_NAME, new ArrayList(sessionCookies.keySet()));

                StringBuilder sessionCookieValue = new StringBuilder();
                for (String value : sessionCookies.values()) {
                    sessionCookieValue.append(value);
                }
                return sessionCookieValue.toString();
            }
        }
        return null;
    }

    public static String getSessionCookieName(OidcTenantConfig oidcConfig) {
        return OidcUtils.SESSION_COOKIE_NAME + getCookieSuffix(oidcConfig);
    }

    public static String getCookieSuffix(OidcTenantConfig oidcConfig) {
        String tenantId = oidcConfig.tenantId.get();
        boolean cookieSuffixConfigured = oidcConfig.authentication.cookieSuffix.isPresent();
        String tenantIdSuffix = (cookieSuffixConfigured || !DEFAULT_TENANT_ID.equals(tenantId)) ? UNDERSCORE + tenantId : "";

        return cookieSuffixConfigured
                ? (tenantIdSuffix + UNDERSCORE + oidcConfig.authentication.cookieSuffix.get())
                : tenantIdSuffix;
    }

    public static boolean isServiceApp(OidcTenantConfig oidcConfig) {
        return ApplicationType.SERVICE.equals(oidcConfig.applicationType.orElse(ApplicationType.SERVICE));
    }

    public static boolean isWebApp(OidcTenantConfig oidcConfig) {
        return ApplicationType.WEB_APP.equals(oidcConfig.applicationType.orElse(ApplicationType.SERVICE));
    }

    public static boolean isEncryptedToken(String token) {
        return new StringTokenizer(token, ".").countTokens() == 5;
    }

    public static boolean isOpaqueToken(String token) {
        return new StringTokenizer(token, ".").countTokens() != 3;
    }

    public static JsonObject decodeJwtContent(String jwt) {
        String encodedContent = getJwtContentPart(jwt);
        if (encodedContent == null) {
            return null;
        }
        return decodeAsJsonObject(encodedContent);
    }

    public static String decodeJwtContentAsString(String jwt) {
        StringTokenizer tokens = new StringTokenizer(jwt, ".");
        // part 1: skip the token headers
        tokens.nextToken();
        if (!tokens.hasMoreTokens()) {
            return null;
        }
        // part 2: token content
        String encodedContent = tokens.nextToken();

        // let's check only 1 more signature part is available
        if (tokens.countTokens() != 1) {
            return null;
        }
        try {
            return base64UrlDecode(encodedContent);
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }

    public static String getJwtContentPart(String jwt) {
        StringTokenizer tokens = new StringTokenizer(jwt, ".");
        // part 1: skip the token headers
        tokens.nextToken();
        if (!tokens.hasMoreTokens()) {
            return null;
        }
        // part 2: token content
        String encodedContent = tokens.nextToken();

        // let's check only 1 more signature part is available
        if (tokens.countTokens() != 1) {
            return null;
        }
        return encodedContent;
    }

    private static JsonObject decodeAsJsonObject(String encodedContent) {
        try {
            return new JsonObject(base64UrlDecode(encodedContent));
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }

    private static String base64UrlDecode(String encodedContent) {
        return new String(Base64.getUrlDecoder().decode(encodedContent), StandardCharsets.UTF_8);
    }

    public static JsonObject decodeJwtHeaders(String jwt) {
        StringTokenizer tokens = new StringTokenizer(jwt, ".");
        return decodeAsJsonObject(tokens.nextToken());
    }

    public static String decodeJwtHeadersAsString(String jwt) {
        StringTokenizer tokens = new StringTokenizer(jwt, ".");
        return base64UrlDecode(tokens.nextToken());
    }

    public static List findRoles(String clientId, OidcTenantConfig.Roles rolesConfig, JsonObject json) {
        // If the user configured specific paths - check and enforce the claims at these paths exist
        if (rolesConfig.getRoleClaimPath().isPresent()) {
            List roles = new LinkedList<>();
            for (String roleClaimPath : rolesConfig.getRoleClaimPath().get()) {
                roles.addAll(findClaimWithRoles(rolesConfig, roleClaimPath.trim(), json));
            }
            return roles;
        }

        // Check 'groups' next
        List groups = findClaimWithRoles(rolesConfig, Claims.groups.name(), json);
        if (!groups.isEmpty()) {
            return groups;
        } else {
            // Finally, check if this token has been issued by Keycloak.
            // Return an empty or populated list of realm and resource access roles
            List allRoles = new LinkedList<>();
            allRoles.addAll(findClaimWithRoles(rolesConfig, "realm_access/roles", json));
            if (clientId != null) {
                allRoles.addAll(findClaimWithRoles(rolesConfig, "resource_access/" + clientId + "/roles", json));
            }

            return allRoles;
        }

    }

    private static List findClaimWithRoles(OidcTenantConfig.Roles rolesConfig, String claimPath,
            JsonObject json) {
        Object claimValue = findClaimValue(claimPath, json, splitClaimPath(claimPath), 0);

        if (claimValue instanceof JsonArray) {
            return convertJsonArrayToList((JsonArray) claimValue);
        } else if (claimValue != null) {
            String sep = rolesConfig.getRoleClaimSeparator().isPresent() ? rolesConfig.getRoleClaimSeparator().get() : " ";
            if (claimValue.toString().isBlank()) {
                return Collections.emptyList();
            }
            return Arrays.asList(claimValue.toString().split(sep));
        } else {
            return Collections.emptyList();
        }
    }

    private static String[] splitClaimPath(String claimPath) {
        return claimPath.indexOf('/') > 0 ? CLAIM_PATH_PATTERN.split(claimPath) : new String[] { claimPath };
    }

    private static Object findClaimValue(String claimPath, JsonObject json, String[] pathArray, int step) {
        Object claimValue = json.getValue(pathArray[step].replace("\"", ""));
        if (claimValue == null) {
            LOG.debugf("No claim exists at the path '%s' at the path segment '%s'", claimPath, pathArray[step]);
        } else if (step + 1 < pathArray.length) {
            if (claimValue instanceof JsonObject) {
                int nextStep = step + 1;
                return findClaimValue(claimPath, (JsonObject) claimValue, pathArray, nextStep);
            } else {
                LOG.debugf("Claim value at the path '%s' is not a json object", claimPath);
            }
        }

        return claimValue;
    }

    private static List convertJsonArrayToList(JsonArray claimValue) {
        List list = new ArrayList<>(claimValue.size());
        for (int i = 0; i < claimValue.size(); i++) {
            String claimValueStr = claimValue.getString(i);
            if (claimValueStr == null || claimValueStr.isBlank()) {
                continue;
            }
            list.add(claimValue.getString(i));
        }
        return list;
    }

    static QuarkusSecurityIdentity validateAndCreateIdentity(Map requestData, TokenCredential credential,
            TenantConfigContext resolvedContext, JsonObject tokenJson, JsonObject rolesJson, UserInfo userInfo,
            TokenIntrospection introspectionResult, TokenAuthenticationRequest request) {

        OidcTenantConfig config = resolvedContext.oidcConfig;
        QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
        builder.addCredential(credential);
        AuthorizationCodeTokens codeTokens = (AuthorizationCodeTokens) requestData.get(AuthorizationCodeTokens.class.getName());
        if (codeTokens != null) {
            RefreshToken refreshTokenCredential = new RefreshToken(codeTokens.getRefreshToken());
            builder.addCredential(refreshTokenCredential);
            builder.addCredential(new AccessTokenCredential(codeTokens.getAccessToken(), refreshTokenCredential));
        }
        JsonWebToken jwtPrincipal;
        try {
            JwtClaims jwtClaims = JwtClaims.parse(tokenJson.encode());
            jwtClaims.setClaim(Claims.raw_token.name(), credential.getToken());
            jwtPrincipal = new OidcJwtCallerPrincipal(jwtClaims, credential,
                    config.token.principalClaim.isPresent() ? config.token.principalClaim.get() : null);
        } catch (InvalidJwtException e) {
            throw new AuthenticationFailedException(e);
        }
        builder.addAttribute(QUARKUS_IDENTITY_EXPIRE_TIME, jwtPrincipal.getExpirationTime());
        builder.setPrincipal(jwtPrincipal);
        var vertxContext = getRoutingContextAttribute(request);
        setRoutingContextAttribute(builder, vertxContext);
        setSecurityIdentityRoles(builder, config, rolesJson);
        setSecurityIdentityPermissions(builder, config, rolesJson);
        setSecurityIdentityUserInfo(builder, userInfo);
        setSecurityIdentityIntrospection(builder, introspectionResult);
        setSecurityIdentityConfigMetadata(builder, resolvedContext);
        setBlockingApiAttribute(builder, vertxContext);
        setTenantIdAttribute(builder, config);
        TokenVerificationResult codeFlowAccessTokenResult = (TokenVerificationResult) requestData.get(CODE_ACCESS_TOKEN_RESULT);
        if (codeFlowAccessTokenResult != null) {
            builder.addAttribute(CODE_ACCESS_TOKEN_RESULT, codeFlowAccessTokenResult);
        }
        return builder.build();
    }

    static void setSecurityIdentityPermissions(QuarkusSecurityIdentity.Builder builder, OidcTenantConfig config,
            JsonObject permissionsJson) {
        addTokenScopesAsPermissions(builder, findClaimWithRoles(config.getRoles(), TOKEN_SCOPE, permissionsJson));
    }

    static void addTokenScopesAsPermissions(Builder builder, Collection scopes) {
        if (!scopes.isEmpty()) {
            builder.addPermissionChecker(new Function>() {

                private final Permission[] permissions = transformScopesToPermissions(scopes);

                @Override
                public Uni apply(Permission requiredPermission) {
                    for (Permission possessedPermission : permissions) {
                        if (possessedPermission.implies(requiredPermission)) {
                            // access granted
                            return Uni.createFrom().item(Boolean.TRUE);
                        }
                    }
                    // access denied
                    return Uni.createFrom().item(Boolean.FALSE);
                }
            });
        }
    }

    static Permission[] transformScopesToPermissions(Collection scopes) {
        final Permission[] permissions = new Permission[scopes.size()];
        int i = 0;
        for (String scope : scopes) {
            int semicolonIndex = scope.indexOf(':');
            if (semicolonIndex > 0 && semicolonIndex < scope.length() - 1) {
                permissions[i++] = new StringPermission(scope.substring(0, semicolonIndex),
                        scope.substring(semicolonIndex + 1));
            } else {
                permissions[i++] = new StringPermission(scope);
            }
        }
        return permissions;
    }

    public static void setSecurityIdentityRoles(QuarkusSecurityIdentity.Builder builder, OidcTenantConfig config,
            JsonObject rolesJson) {
        String clientId = config.getClientId().isPresent() ? config.getClientId().get() : null;
        for (String role : findRoles(clientId, config.getRoles(), rolesJson)) {
            builder.addRole(role);
        }
    }

    public static void setBlockingApiAttribute(QuarkusSecurityIdentity.Builder builder, RoutingContext vertxContext) {
        if (vertxContext != null) {
            builder.addAttribute(AuthenticationRequestContext.class.getName(),
                    vertxContext.get(AuthenticationRequestContext.class.getName()));
        }
    }

    public static void setTenantIdAttribute(QuarkusSecurityIdentity.Builder builder, OidcTenantConfig config) {
        builder.addAttribute(TENANT_ID_ATTRIBUTE, config.tenantId.orElse(DEFAULT_TENANT_ID));
    }

    public static void setRoutingContextAttribute(QuarkusSecurityIdentity.Builder builder, RoutingContext routingContext) {
        builder.addAttribute(RoutingContext.class.getName(), routingContext);
    }

    public static void setSecurityIdentityUserInfo(QuarkusSecurityIdentity.Builder builder, UserInfo userInfo) {
        if (userInfo != null) {
            builder.addAttribute(USER_INFO_ATTRIBUTE, userInfo);
        }
    }

    public static void setSecurityIdentityIntrospection(Builder builder, TokenIntrospection introspectionResult) {
        if (introspectionResult != null) {
            builder.addAttribute(INTROSPECTION_ATTRIBUTE, introspectionResult);
        }
    }

    public static void setSecurityIdentityConfigMetadata(QuarkusSecurityIdentity.Builder builder,
            TenantConfigContext resolvedContext) {
        if (resolvedContext.provider.client != null) {
            builder.addAttribute(CONFIG_METADATA_ATTRIBUTE, resolvedContext.provider.client.getMetadata());
        }
    }

    public static void validatePrimaryJwtTokenType(OidcTenantConfig.Token tokenConfig, JsonObject tokenJson) {
        if (tokenJson.containsKey("typ")) {
            String type = tokenJson.getString("typ");
            if (tokenConfig.getTokenType().isPresent() && !tokenConfig.getTokenType().get().equals(type)) {
                throw new OIDCException("Invalid token type");
            } else if ("Refresh".equals(type)) {
                // At least check it is not a refresh token issued by Keycloak
                throw new OIDCException("Refresh token can only be used with the refresh token grant");
            }
        }
    }

    static Uni removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig,
            TokenStateManager tokenStateManager) {
        List cookieNames = context.get(SESSION_COOKIE_NAME);
        if (cookieNames != null) {
            LOG.debugf("Remove session cookie names: %s", cookieNames);
            StringBuilder cookieValue = new StringBuilder();
            for (String cookieName : cookieNames) {
                cookieValue.append(removeCookie(context, oidcConfig, cookieName));
            }
            return tokenStateManager.deleteTokens(context, oidcConfig, cookieValue.toString(),
                    deleteTokensRequestContext);
        } else {
            return VOID_UNI;
        }
    }

    public static String removeCookie(RoutingContext context, OidcTenantConfig oidcConfig, String cookieName) {
        ServerCookie cookie = (ServerCookie) context.cookieMap().get(cookieName);
        String cookieValue = null;
        if (cookie != null) {
            cookieValue = cookie.getValue();
            removeCookie(context, cookie, oidcConfig);
        }
        return cookieValue;
    }

    static void removeCookie(RoutingContext context, ServerCookie cookie, OidcTenantConfig oidcConfig) {
        if (cookie != null) {
            cookie.setValue("");
            cookie.setMaxAge(0);
            Authentication auth = oidcConfig.getAuthentication();
            setCookiePath(context, auth, cookie);
            if (auth.cookieDomain.isPresent()) {
                cookie.setDomain(auth.cookieDomain.get());
            }
        }
    }

    static void setCookiePath(RoutingContext context, Authentication auth, ServerCookie cookie) {
        if (auth.cookiePathHeader.isPresent() && context.request().headers().contains(auth.cookiePathHeader.get())) {
            cookie.setPath(context.request().getHeader(auth.cookiePathHeader.get()));
        } else {
            cookie.setPath(auth.getCookiePath());
        }
    }

    /**
     * Merge the current tenant and well-known OpenId Connect provider configurations.
     * Initialized properties take priority over uninitialized properties.
     *
     * Initialized properties in the current tenant configuration take priority
     * over the same initialized properties in the well-known OpenId Connect provider configuration.
     *
     * Tenant id property of the current tenant must be set before the merge operation.
     *
     * @param tenant current tenant configuration
     * @param provider well-known OpenId Connect provider configuration
     * @return merged configuration
     */
    static OidcTenantConfig mergeTenantConfig(OidcTenantConfig tenant, OidcTenantConfig provider) {
        if (tenant.tenantId.isEmpty()) {
            // OidcRecorder sets it before the merge operation
            throw new IllegalStateException();
        }
        // root properties
        if (tenant.authServerUrl.isEmpty()) {
            tenant.authServerUrl = provider.authServerUrl;
        }
        if (tenant.applicationType.isEmpty()) {
            tenant.applicationType = provider.applicationType;
        }
        if (tenant.discoveryEnabled.isEmpty()) {
            tenant.discoveryEnabled = provider.discoveryEnabled;
        }
        if (tenant.authorizationPath.isEmpty()) {
            tenant.authorizationPath = provider.authorizationPath;
        }
        if (tenant.jwksPath.isEmpty()) {
            tenant.jwksPath = provider.jwksPath;
        }
        if (tenant.tokenPath.isEmpty()) {
            tenant.tokenPath = provider.tokenPath;
        }
        if (tenant.userInfoPath.isEmpty()) {
            tenant.userInfoPath = provider.userInfoPath;
        }

        // authentication
        if (tenant.authentication.idTokenRequired.isEmpty()) {
            tenant.authentication.idTokenRequired = provider.authentication.idTokenRequired;
        }
        if (tenant.authentication.userInfoRequired.isEmpty()) {
            tenant.authentication.userInfoRequired = provider.authentication.userInfoRequired;
        }
        if (tenant.authentication.pkceRequired.isEmpty()) {
            tenant.authentication.pkceRequired = provider.authentication.pkceRequired;
        }
        if (tenant.authentication.scopes.isEmpty()) {
            tenant.authentication.scopes = provider.authentication.scopes;
        }
        if (tenant.authentication.scopeSeparator.isEmpty()) {
            tenant.authentication.scopeSeparator = provider.authentication.scopeSeparator;
        }
        if (tenant.authentication.addOpenidScope.isEmpty()) {
            tenant.authentication.addOpenidScope = provider.authentication.addOpenidScope;
        }
        if (tenant.authentication.forceRedirectHttpsScheme.isEmpty()) {
            tenant.authentication.forceRedirectHttpsScheme = provider.authentication.forceRedirectHttpsScheme;
        }
        if (tenant.authentication.responseMode.isEmpty()) {
            tenant.authentication.responseMode = provider.authentication.responseMode;
        }
        if (tenant.authentication.redirectPath.isEmpty()) {
            tenant.authentication.redirectPath = provider.authentication.redirectPath;
        }

        // credentials
        if (tenant.credentials.clientSecret.method.isEmpty()) {
            tenant.credentials.clientSecret.method = provider.credentials.clientSecret.method;
        }
        if (tenant.credentials.jwt.audience.isEmpty()) {
            tenant.credentials.jwt.audience = provider.credentials.jwt.audience;
        }
        if (tenant.credentials.jwt.signatureAlgorithm.isEmpty()) {
            tenant.credentials.jwt.signatureAlgorithm = provider.credentials.jwt.signatureAlgorithm;
        }

        // token
        if (tenant.token.issuer.isEmpty()) {
            tenant.token.issuer = provider.token.issuer;
        }
        if (tenant.token.principalClaim.isEmpty()) {
            tenant.token.principalClaim = provider.token.principalClaim;
        }
        if (tenant.token.verifyAccessTokenWithUserInfo.isEmpty()) {
            tenant.token.verifyAccessTokenWithUserInfo = provider.token.verifyAccessTokenWithUserInfo;
        }

        return tenant;
    }

    static OidcTenantConfig resolveProviderConfig(OidcTenantConfig oidcTenantConfig) {
        if (oidcTenantConfig != null && oidcTenantConfig.provider.isPresent()) {
            return OidcUtils.mergeTenantConfig(oidcTenantConfig,
                    KnownOidcProviders.provider(oidcTenantConfig.provider.get()));
        } else {
            return oidcTenantConfig;
        }

    }

    public static byte[] getSha256Digest(byte[] value) throws NoSuchAlgorithmException {
        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
        sha256.update(value);
        return sha256.digest();
    }

    public static String encryptJson(JsonObject json, SecretKey key) throws Exception {
        return encryptString(json.encode(), key);
    }

    public static String encryptString(String jweString, SecretKey key) throws Exception {
        return encryptString(jweString, key, KeyEncryptionAlgorithm.A256GCMKW);
    }

    public static String encryptString(String jweString, SecretKey key, KeyEncryptionAlgorithm algorithm) throws Exception {
        JsonWebEncryption jwe = new JsonWebEncryption();
        jwe.setAlgorithmHeaderValue(algorithm.getAlgorithm());
        jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithm.A256GCM.getAlgorithm());
        jwe.setKey(key);
        jwe.setPlaintext(jweString);
        return jwe.getCompactSerialization();
    }

    public static JsonObject decryptJson(String jweString, Key key) throws Exception {
        return new JsonObject(decryptString(jweString, key));
    }

    public static String decryptString(String jweString, Key key) throws Exception {
        return decryptString(jweString, key, KeyEncryptionAlgorithm.A256GCMKW);
    }

    public static String decryptString(String jweString, Key key, KeyEncryptionAlgorithm algorithm) throws JoseException {
        JsonWebEncryption jwe = new JsonWebEncryption();
        jwe.setAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT,
                algorithm.getAlgorithm()));
        jwe.setKey(key);
        jwe.setCompactSerialization(jweString);
        return jwe.getPlaintextString();
    }

    public static boolean isFormUrlEncodedRequest(RoutingContext context) {
        String contentType = context.request().getHeader("Content-Type");
        return context.request().method() == HttpMethod.POST
                && contentType != null
                && (contentType.equals(HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString())
                        || contentType.startsWith(HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString() + ";"));
    }

    public static Uni getFormUrlEncodedData(RoutingContext context) {
        context.request().setExpectMultipart(true);
        return Uni.createFrom().emitter(new Consumer>() {
            @Override
            public void accept(UniEmitter t) {
                context.request().endHandler(new Handler() {
                    @Override
                    public void handle(Void event) {
                        t.complete(context.request().formAttributes());
                    }
                });
                context.request().resume();
            }
        });
    }

    public static String encodeScopes(OidcTenantConfig oidcConfig) {
        return OidcCommonUtils.urlEncode(String.join(oidcConfig.authentication.scopeSeparator.orElse(DEFAULT_SCOPE_SEPARATOR),
                getAllScopes(oidcConfig)));
    }

    public static List getAllScopes(OidcTenantConfig oidcConfig) {
        List oidcConfigScopes = oidcConfig.getAuthentication().scopes.isPresent()
                ? oidcConfig.getAuthentication().scopes.get()
                : Collections.emptyList();
        List scopes = new ArrayList<>(oidcConfigScopes.size() + 1);
        if (oidcConfig.getAuthentication().addOpenidScope.orElse(true)) {
            scopes.add(OidcConstants.OPENID_SCOPE);
        }
        scopes.addAll(oidcConfigScopes);
        // Extra scopes if any
        String extraScopeValue = oidcConfig.getAuthentication().getExtraParams()
                .get(OidcConstants.TOKEN_SCOPE);
        if (extraScopeValue != null) {
            String[] extraScopes = extraScopeValue.split(COMMA);
            scopes.addAll(List.of(extraScopes));
        }

        return scopes;
    }

    public static boolean isSessionCookie(String cookieName) {
        return cookieName.startsWith(SESSION_COOKIE_NAME)
                && !cookieName.regionMatches(SESSION_COOKIE_NAME.length(), ACCESS_TOKEN_COOKIE_SUFFIX, 0, 3)
                && !cookieName.regionMatches(SESSION_COOKIE_NAME.length(), REFRESH_TOKEN_COOKIE_SUFFIX, 0, 3);
    }

    static String extractBearerToken(RoutingContext context, OidcTenantConfig oidcConfig) {
        if (context.get(EXTRACTED_BEARER_TOKEN) != null) {
            return context.get(EXTRACTED_BEARER_TOKEN);
        }
        final HttpServerRequest request = context.request();
        String header = oidcConfig.token.header.isPresent() ? oidcConfig.token.header.get()
                : HttpHeaders.AUTHORIZATION.toString();
        LOG.debugf("Looking for a token in the %s header", header);
        final String headerValue = request.headers().get(header);

        if (headerValue == null) {
            return null;
        }

        int idx = headerValue.indexOf(' ');
        final String scheme = idx > 0 ? headerValue.substring(0, idx) : null;

        if (scheme != null) {
            LOG.debugf("Authorization scheme: %s", scheme);
        }

        if (scheme == null && !header.equalsIgnoreCase(HttpHeaders.AUTHORIZATION.toString())) {
            return headerValue;
        }

        if (!oidcConfig.token.authorizationScheme.equalsIgnoreCase(scheme)) {
            return null;
        }

        return headerValue.substring(idx + 1);
    }

    static void storeExtractedBearerToken(RoutingContext context, String token) {
        context.put(EXTRACTED_BEARER_TOKEN, token);
    }

    public static String getTenantIdFromCookie(String cookiePrefix, String cookieName, boolean sessionCookie) {
        // It has already been checked the cookieName starts with the cookiePrefix
        if (cookieName.length() == cookiePrefix.length()) {
            return OidcUtils.DEFAULT_TENANT_ID;
        } else {
            String suffix = cookieName.substring(cookiePrefix.length() + 1);

            if (sessionCookie && suffix.startsWith(OidcUtils.SESSION_COOKIE_CHUNK_START)) {
                return OidcUtils.DEFAULT_TENANT_ID;
            } else {
                // It can be either a tenant_id, or a tenant_id and cookie suffix property, example, q_session_github or q_session_github_test
                // or it can be a session cookie chunk like q_session_chunk_1 in which case the suffix will be chunk_1
                int index = suffix.indexOf("_", 0);
                return index == -1 ? suffix : suffix.substring(0, index);
            }
        }
    }

    public static boolean cacheUserInfoInIdToken(DefaultTenantConfigResolver resolver, OidcTenantConfig oidcConfig) {

        if (resolver.getUserInfoCache() != null && oidcConfig.allowUserInfoCache) {
            return false;
        }
        if (oidcConfig.cacheUserInfoInIdtoken.isPresent()) {
            return oidcConfig.cacheUserInfoInIdtoken.get();
        }
        return resolver.getTokenStateManager() instanceof DefaultTokenStateManager
                && oidcConfig.tokenStateManager.encryptionRequired;
    }

    public static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig,
            String name, String value, long maxAge) {
        ServerCookie cookie = new CookieImpl(name, value);
        cookie.setHttpOnly(true);
        cookie.setSecure(oidcConfig.authentication.cookieForceSecure || context.request().isSSL());
        cookie.setMaxAge(maxAge);
        LOG.debugf(name + " cookie 'max-age' parameter is set to %d", maxAge);
        Authentication auth = oidcConfig.getAuthentication();
        OidcUtils.setCookiePath(context, oidcConfig.getAuthentication(), cookie);
        if (auth.cookieDomain.isPresent()) {
            cookie.setDomain(auth.getCookieDomain().get());
        }
        context.response().addCookie(cookie);
        return cookie;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy