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

io.helidon.security.providers.oidc.TenantAuthenticationHandler Maven / Gradle / Ivy

There is a newer version: 4.1.6
Show newest version
/*
 * Copyright (c) 2023, 2024 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.security.providers.oidc;

import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import io.helidon.common.Errors;
import io.helidon.common.LazyValue;
import io.helidon.common.parameters.Parameters;
import io.helidon.http.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.SetCookie;
import io.helidon.http.Status;
import io.helidon.security.AuthenticationResponse;
import io.helidon.security.EndpointConfig;
import io.helidon.security.Grant;
import io.helidon.security.Principal;
import io.helidon.security.ProviderRequest;
import io.helidon.security.Role;
import io.helidon.security.Security;
import io.helidon.security.SecurityEnvironment;
import io.helidon.security.SecurityException;
import io.helidon.security.SecurityLevel;
import io.helidon.security.SecurityResponse;
import io.helidon.security.Subject;
import io.helidon.security.abac.scope.ScopeValidator;
import io.helidon.security.jwt.Jwt;
import io.helidon.security.jwt.JwtException;
import io.helidon.security.jwt.JwtUtil;
import io.helidon.security.jwt.JwtValidator;
import io.helidon.security.jwt.SignedJwt;
import io.helidon.security.jwt.jwk.JwkKeys;
import io.helidon.security.providers.common.TokenCredential;
import io.helidon.security.providers.oidc.common.OidcConfig;
import io.helidon.security.providers.oidc.common.Tenant;
import io.helidon.security.providers.oidc.common.TenantConfig;
import io.helidon.security.util.TokenHandler;
import io.helidon.webclient.api.HttpClientRequest;
import io.helidon.webclient.api.HttpClientResponse;
import io.helidon.webclient.api.WebClient;

import jakarta.json.JsonObject;

import static io.helidon.security.providers.oidc.OidcFeature.JSON_BUILDER_FACTORY;
import static io.helidon.security.providers.oidc.OidcFeature.JSON_READER_FACTORY;
import static io.helidon.security.providers.oidc.common.spi.TenantConfigFinder.DEFAULT_TENANT_ID;

/**
 * Authentication handler.
 */
class TenantAuthenticationHandler {
    private static final System.Logger LOGGER = System.getLogger(TenantAuthenticationHandler.class.getName());
    private static final TokenHandler PARAM_HEADER_HANDLER = TokenHandler.forHeader(OidcConfig.PARAM_HEADER_NAME);
    private static final TokenHandler PARAM_ID_HEADER_HANDLER = TokenHandler.forHeader(OidcConfig.PARAM_ID_HEADER_NAME);
    private static final LazyValue RANDOM = LazyValue.create(SecureRandom::new);
    private static final JwtValidator TIME_VALIDATORS = JwtValidator.builder()
            .addDefaultTimeValidators()
            .build();

    private final boolean optional;
    private final OidcConfig oidcConfig;
    private final TenantConfig tenantConfig;
    private final Tenant tenant;
    private final boolean useJwtGroups;
    private final BiFunction jwtValidator;
    private final BiConsumer scopeAppender;
    private final Pattern attemptPattern;

    TenantAuthenticationHandler(OidcConfig oidcConfig, Tenant tenant, boolean useJwtGroups, boolean optional) {
        this.oidcConfig = oidcConfig;
        this.tenant = tenant;
        this.tenantConfig = tenant.tenantConfig();
        this.useJwtGroups = useJwtGroups;
        this.optional = optional;

        attemptPattern = Pattern.compile(".*?" + oidcConfig.redirectAttemptParam() + "=(\\d+).*");
        if (tenantConfig.validateJwtWithJwk()) {
            this.jwtValidator = (signedJwt, collector) -> {
                JwkKeys jwk = tenant.signJwk();
                Errors errors = signedJwt.verifySignature(jwk);
                errors.forEach(errorMessage -> {
                    switch (errorMessage.getSeverity()) {
                    case FATAL:
                        collector.fatal(errorMessage.getSource(), errorMessage.getMessage());
                        break;
                    case WARN:
                        collector.warn(errorMessage.getSource(), errorMessage.getMessage());
                        break;
                    case HINT:
                    default:
                        collector.hint(errorMessage.getSource(), errorMessage.getMessage());
                        break;
                    }
                });
                return collector;
            };
        } else {
            this.jwtValidator = (signedJwt, collector) -> {
                Parameters.Builder form = Parameters.builder("oidc-form-params")
                        .add("token", signedJwt.tokenContent());

                HttpClientRequest post = tenant.appWebClient()
                        .post()
                        .uri(tenant.introspectUri())
                        .header(HeaderValues.ACCEPT_JSON)
                        .headers(it -> it.add(HeaderNames.CACHE_CONTROL, "no-cache, no-store, must-revalidate"));

                OidcUtil.updateRequest(OidcConfig.RequestType.INTROSPECT_JWT, tenantConfig, form);

                try (HttpClientResponse response = post.submit(form.build())) {
                    if (response.status().family() == Status.Family.SUCCESSFUL) {
                        try {
                            JsonObject jsonObject = response.as(JsonObject.class);
                            if (!jsonObject.getBoolean("active")) {
                                collector.fatal(jsonObject, "Token is not active");
                            }
                        } catch (Exception e) {
                            collector.fatal(e, "Failed to validate token, request failed: "
                                    + "Failed to read JSON from response");
                        }
                    } else {
                        String message;
                        try {
                            message = response.as(String.class);
                            collector.fatal(response.status(),
                                            "Failed to validate token, response " + "status: " + response.status() + ", "
                                                    + "entity: " + message);
                        } catch (Exception e) {
                            collector.fatal(e, "Failed to validate token, request failed: Failed to process error entity");
                        }
                    }
                } catch (Exception e) {
                    collector.fatal(e, "Failed to validate token, request failed: Failed to invoke request");
                }
                return collector;
            };
        }
        // clean the scope audience - must end with / if exists
        String configuredScopeAudience = tenantConfig.scopeAudience();
        if (configuredScopeAudience == null || configuredScopeAudience.isEmpty()) {
            this.scopeAppender = StringBuilder::append;
        } else {
            if (configuredScopeAudience.endsWith("/")) {
                this.scopeAppender = (stringBuilder, scope) -> stringBuilder.append(configuredScopeAudience).append(scope);
            } else {
                this.scopeAppender = (stringBuilder, scope) -> stringBuilder.append(configuredScopeAudience)
                        .append("/")
                        .append(scope);
            }
        }
    }

    AuthenticationResponse authenticate(String tenantId, ProviderRequest providerRequest) {
        /*
        1. Get id token from request - if available, validate it and process access token
        2. If not - skip to access token validation directly
         */
        Optional idToken = Optional.empty();
        try {
            if (oidcConfig.useParam()) {
                idToken = idToken.or(() -> PARAM_ID_HEADER_HANDLER.extractToken(providerRequest.env().headers()));
                if (idToken.isEmpty()) {
                    idToken = idToken.or(() -> providerRequest.env()
                            .queryParams()
                            .first(oidcConfig.idTokenParamName()).asOptional());
                }
            }
            if (oidcConfig.useCookie() && idToken.isEmpty()) {
                // only do this for cookies
                Optional cookie = oidcConfig.idTokenCookieHandler()
                        .findCookie(providerRequest.env().headers());
                if (cookie.isPresent()) {
                    try {
                        String idTokenValue = cookie.get();
                        return validateIdToken(tenantId, providerRequest, idTokenValue);
                    } catch (Exception e) {
                        if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                            LOGGER.log(System.Logger.Level.DEBUG, "Invalid id token in cookie", e);
                        }
                        return errorResponse(providerRequest,
                                             Status.UNAUTHORIZED_401,
                                             null,
                                             "Invalid id token",
                                             tenantId);
                    }
                }
            }
        } catch (SecurityException e) {
            LOGGER.log(System.Logger.Level.DEBUG, "Failed to extract token from one of the configured locations", e);
            return failOrAbstain("Failed to extract one of the configured tokens" + e);
        }
        if (idToken.isPresent()) {
            return validateIdToken(tenantId, providerRequest, idToken.get());
        } else {
            return processAccessToken(tenantId, providerRequest, null);
        }

    }

    private AuthenticationResponse processAccessToken(String tenantId, ProviderRequest providerRequest, Jwt idToken) {
        /*
        Access token is mandatory!
        1. Get access token from request - if available, validate it and continue
        2. If not - Redirect to login page
         */
        List missingLocations = new LinkedList<>();

        Optional token = Optional.empty();
        try {
            if (oidcConfig.useHeader()) {
                token = token.or(() -> oidcConfig.headerHandler().extractToken(providerRequest.env().headers()));

                if (token.isEmpty()) {
                    missingLocations.add("header");
                }
            }

            if (oidcConfig.useParam()) {
                token = token.or(() -> PARAM_HEADER_HANDLER.extractToken(providerRequest.env().headers()));

                if (token.isEmpty()) {
                    token = token.or(() -> providerRequest.env().queryParams().first(oidcConfig.paramName()).asOptional());
                }

                if (token.isEmpty()) {
                    missingLocations.add("query-param");
                }
            }

            if (oidcConfig.useCookie()) {
                if (token.isEmpty()) {
                    // only do this for cookies
                    Optional cookie = oidcConfig.tokenCookieHandler()
                            .findCookie(providerRequest.env().headers());
                    if (cookie.isEmpty()) {
                        missingLocations.add("cookie");
                    } else {
                        try {
                            String tokenValue = cookie.get();
                            String decodedJson = new String(Base64.getDecoder().decode(tokenValue), StandardCharsets.UTF_8);
                            JsonObject jsonObject = JSON_READER_FACTORY.createReader(new StringReader(decodedJson)).readObject();
                            if (oidcConfig.accessTokenIpCheck()) {
                                Object userIp = providerRequest.env().abacAttribute("userIp").orElseThrow();
                                if (!jsonObject.getString("remotePeer").equals(userIp)) {
                                    if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                                        LOGGER.log(System.Logger.Level.DEBUG,
                                                   "Current peer IP does not match the one this access token was issued for");
                                    }
                                    return errorResponse(providerRequest,
                                                         Status.UNAUTHORIZED_401,
                                                         "peer_host_mismatch",
                                                         "Peer host access token mismatch",
                                                         tenantId);
                                }
                            }
                            return validateAccessToken(tenantId, providerRequest, jsonObject.getString("accessToken"), idToken);
                        } catch (Exception e) {
                            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                                LOGGER.log(System.Logger.Level.DEBUG, "Invalid access token in cookie", e);
                            }
                            return errorResponse(providerRequest,
                                                 Status.UNAUTHORIZED_401,
                                                 null,
                                                 "Invalid access token",
                                                 tenantId);
                        }
                    }
                }
            }
        } catch (SecurityException e) {
            LOGGER.log(System.Logger.Level.DEBUG, "Failed to extract access token from one of the configured locations", e);
            return failOrAbstain("Failed to extract one of the configured tokens" + e);
        }

        if (token.isPresent()) {
            return validateAccessToken(tenantId, providerRequest, token.get(), idToken);
        } else {
            LOGGER.log(System.Logger.Level.DEBUG, () -> "Missing access token, could not find in either of: " + missingLocations);
            return errorResponse(providerRequest,
                                 Status.UNAUTHORIZED_401,
                                 null,
                                 "Missing access token, could not find in either of: " + missingLocations,
                                 tenantId);
        }
    }

    private Set expectedScopes(ProviderRequest request) {

        Set result = new HashSet<>();

        for (SecurityLevel securityLevel : request.endpointConfig().securityLevels()) {
            List expectedScopes = securityLevel.combineAnnotations(ScopeValidator.Scopes.class,
                                                                                          EndpointConfig.AnnotationScope
                                                                                                  .values());
            expectedScopes.stream()
                    .map(ScopeValidator.Scopes::value)
                    .map(Arrays::asList)
                    .map(List::stream)
                    .forEach(stream -> stream.map(ScopeValidator.Scope::value)
                            .forEach(result::add));

            List expectedScopeAnnotations = securityLevel.combineAnnotations(ScopeValidator.Scope.class,
                                                                                                   EndpointConfig.AnnotationScope
                                                                                                           .values());

            expectedScopeAnnotations.stream()
                    .map(ScopeValidator.Scope::value)
                    .forEach(result::add);
        }

        return result;
    }

    private AuthenticationResponse errorResponse(ProviderRequest providerRequest,
                                                 Status status,
                                                 String code,
                                                 String description,
                                                 String tenantId) {
        if (oidcConfig.shouldRedirect()) {
            // make sure we do not exceed redirect limit
            String origUri = origUri(providerRequest);
            int redirectAttempt = redirectAttempt(origUri);
            if (redirectAttempt >= oidcConfig.maxRedirects()) {
                return errorResponseNoRedirect(code, description, status);
            }
            String state = generateRandomString();

            Set expectedScopes = expectedScopes(providerRequest);

            StringBuilder scopes = new StringBuilder(tenantConfig.baseScopes());

            for (String expectedScope : expectedScopes) {
                if (!scopes.isEmpty()) {
                    // space after base scopes
                    scopes.append(' ');
                }
                String scope = expectedScope;
                if (scope.startsWith("/")) {
                    scope = scope.substring(1);
                }
                scopeAppender.accept(scopes, scope);
            }

            String scopeString;
            scopeString = URLEncoder.encode(scopes.toString(), StandardCharsets.UTF_8);

            String authorizationEndpoint = tenant.authorizationEndpointUri();
            String nonce = UUID.randomUUID().toString();
            String redirectUri;
            if (DEFAULT_TENANT_ID.equals(tenantId)) {
                redirectUri = encode(redirectUri(providerRequest.env()));
            } else {
                redirectUri = encode(redirectUri(providerRequest.env()) + "?"
                                             + encode(oidcConfig.tenantParamName()) + "=" + encode(tenantId));
            }

            String queryString = "?" + "client_id=" + tenantConfig.clientId() + "&"
                    + "response_type=code&"
                    + "redirect_uri=" + redirectUri + "&"
                    + "scope=" + scopeString + "&"
                    + "nonce=" + nonce + "&"
                    + "state=" + state;

            JsonObject stateJson = JSON_BUILDER_FACTORY.createObjectBuilder()
                    .add("originalUri", origUri)
                    .add("state", state)
                    .add("nonce", nonce)
                    .build();

            String stateBase64 = Base64.getEncoder().encodeToString(stateJson.toString().getBytes(StandardCharsets.UTF_8));
            SetCookie cookie = oidcConfig.stateCookieHandler().createCookie(stateBase64).build();

            // must redirect
            return AuthenticationResponse
                    .builder()
                    .status(SecurityResponse.SecurityStatus.FAILURE_FINISH)
                    .statusCode(Status.TEMPORARY_REDIRECT_307.code())
                    .responseHeader(HeaderNames.SET_COOKIE.defaultCase(), cookie.toString())
                    .description("Redirecting to identity server: " + description)
                    .responseHeader("Location", authorizationEndpoint + queryString)
                    .build();
        } else {
            return errorResponseNoRedirect(code, description, status);
        }
    }

    private String redirectUri(SecurityEnvironment env) {
        for (Map.Entry> entry : env.headers().entrySet()) {
            if (entry.getKey().equalsIgnoreCase("host") && !entry.getValue().isEmpty()) {
                String firstHost = entry.getValue().getFirst();
                String schema = oidcConfig.forceHttpsRedirects() ? "https" : env.transport();
                return oidcConfig.redirectUriWithHost(schema + "://" + firstHost);
            }
        }

        return oidcConfig.redirectUriWithHost();
    }

    private AuthenticationResponse failOrAbstain(String message) {
        if (optional) {
            return AuthenticationResponse.builder()
                    .status(SecurityResponse.SecurityStatus.ABSTAIN)
                    .description(message)
                    .build();
        } else {
            return AuthenticationResponse.builder()
                    .status(AuthenticationResponse.SecurityStatus.FAILURE)
                    .description(message)
                    .build();
        }
    }

    private AuthenticationResponse errorResponseNoRedirect(String code, String description, Status status) {
        if (optional) {
            return AuthenticationResponse.builder()
                    .status(SecurityResponse.SecurityStatus.ABSTAIN)
                    .description(description)
                    .build();
        }
        if (null == code) {
            return AuthenticationResponse.builder()
                    .status(SecurityResponse.SecurityStatus.FAILURE)
                    .statusCode(Status.UNAUTHORIZED_401.code())
                    .responseHeader(HeaderNames.WWW_AUTHENTICATE.defaultCase(),
                                    "Bearer realm=\"" + tenantConfig.realm() + "\"")
                    .description(description)
                    .build();
        } else {
            return AuthenticationResponse.builder()
                    .status(SecurityResponse.SecurityStatus.FAILURE)
                    .statusCode(status.code())
                    .responseHeader(HeaderNames.WWW_AUTHENTICATE.defaultCase(), errorHeader(code, description))
                    .description(description)
                    .build();
        }
    }

    private int redirectAttempt(String state) {
        if (state.contains("?")) {
            // there are parameters
            Matcher matcher = attemptPattern.matcher(state);
            if (matcher.matches()) {
                return Integer.parseInt(matcher.group(1));
            }
        }

        return 1;
    }

    private String errorHeader(String code, String description) {
        return "Bearer realm=\"" + tenantConfig.realm() + "\", error=\"" + code + "\", error_description=\"" + description + "\"";
    }

    String origUri(ProviderRequest providerRequest) {
        List origUri = providerRequest.env().headers()
                .getOrDefault(Security.HEADER_ORIG_URI, List.of());

        if (origUri.isEmpty()) {
            URI targetUri = providerRequest.env().targetUri();
            String query = targetUri.getQuery();
            String path = targetUri.getPath();
            if (query == null || query.isEmpty()) {
                return path;
            } else {
                return path + "?" + query;
            }
        }

        return origUri.getFirst();
    }

    private String encode(String state) {
        return URLEncoder.encode(state, StandardCharsets.UTF_8);
    }

    private AuthenticationResponse validateIdToken(String tenantId, ProviderRequest providerRequest, String idToken) {
        SignedJwt signedJwt;
        try {
            signedJwt = SignedJwt.parseToken(idToken);
        } catch (Exception e) {
            //invalid token
            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                LOGGER.log(System.Logger.Level.DEBUG, "Could not parse inbound id token", e);
            }
            return AuthenticationResponse.failed("Invalid id token", e);
        }

        try {
            Errors errors;
            if (oidcConfig.idTokenSignatureValidation()) {
                errors = jwtValidator.apply(signedJwt, Errors.collector()).collect();
            } else {
                errors = Errors.collector().collect();
            }
            Jwt jwt = signedJwt.getJwt();

            JwtValidator.Builder jwtValidatorBuilder = JwtValidator.builder()
                    .addDefaultTimeValidators()
                    .addCriticalValidator()
                    .addUserPrincipalValidator()
                    .addAudienceValidator(tenantConfig.clientId());

            if (tenant.issuer() != null) {
                jwtValidatorBuilder.addIssuerValidator(tenant.issuer());
            }

            JwtValidator jwtValidation = jwtValidatorBuilder.build();
            Errors validationErrors = jwtValidation.validate(jwt);

            if (errors.isValid() && validationErrors.isValid()) {
                return processAccessToken(tenantId, providerRequest, jwt);
            } else {
                if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                    errors.log(LOGGER);
                    validationErrors.log(LOGGER);
                }
                return errorResponse(providerRequest,
                                     Status.UNAUTHORIZED_401,
                                     "invalid_id_token",
                                     "Id token not valid",
                                     tenantId);
            }
        } catch (Exception e) {
            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate request", e);
            }
            return AuthenticationResponse.failed("Failed to validate JWT", e);
        }
    }

    private AuthenticationResponse validateAccessToken(String tenantId,
                                                       ProviderRequest providerRequest,
                                                       String token,
                                                       Jwt idToken) {
        SignedJwt signedJwt;
        try {
            signedJwt = SignedJwt.parseToken(token);
        } catch (Exception e) {
            //invalid token
            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                LOGGER.log(System.Logger.Level.DEBUG, "Could not parse inbound token", e);
            }
            return AuthenticationResponse.failed("Invalid token", e);
        }

        try {
            Errors.Collector collector;
            if (oidcConfig.tokenSignatureValidation()) {
                collector = jwtValidator.apply(signedJwt, Errors.collector());
            } else {
                collector = Errors.collector();
            }
            Errors timeErrors = TIME_VALIDATORS.validate(signedJwt.getJwt());
            if (timeErrors.isValid()) {
                return processValidationResult(providerRequest, signedJwt, idToken, tenantId, collector);
            } else {
                //Access token expired, we should attempt to refresh it
                Optional refreshToken = oidcConfig.refreshTokenCookieHandler()
                        .findCookie(providerRequest.env().headers());
                //If we have no refresh token to use. Continue with evaluation and reuse failure mechanism.
                return refreshToken.map(refreshTokenValue -> refreshAccessToken(providerRequest,
                                                                                refreshTokenValue,
                                                                                idToken,
                                                                                tenantId))
                        .orElseGet(() -> processValidationResult(providerRequest, signedJwt, idToken, tenantId, collector));
            }
        } catch (Exception e) {
            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate request", e);
            }
            return AuthenticationResponse.failed("Failed to validate JWT", e);
        }
    }

    private AuthenticationResponse refreshAccessToken(ProviderRequest providerRequest,
                                                      String refreshTokenString,
                                                      Jwt idToken,
                                                      String tenantId) {
        try {
            WebClient webClient = tenant.appWebClient();
            Parameters.Builder form = Parameters.builder("oidc-form-params")
                    .add("grant_type", "refresh_token")
                    .add("refresh_token", refreshTokenString)
                    .add("client_id", tenantConfig.clientId());

            HttpClientRequest post = webClient.post()
                    .uri(tenant.tokenEndpointUri())
                    .header(HeaderValues.ACCEPT_JSON);

            try (HttpClientResponse response = post.submit(form.build())) {
                if (response.status().family() == Status.Family.SUCCESSFUL) {
                    try {
                        JsonObject jsonObject = response.as(JsonObject.class);
                        String accessToken = jsonObject.getString("access_token");
                        String refreshToken = jsonObject.getString("refresh_token", null);

                        SignedJwt signedAccessToken;
                        try {
                            signedAccessToken = SignedJwt.parseToken(accessToken);
                        } catch (Exception e) {
                            //invalid token
                            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                                LOGGER.log(System.Logger.Level.DEBUG, "Could not parse refreshed access token", e);
                            }
                            return AuthenticationResponse.failed("Invalid access token", e);
                        }
                        Errors.Collector newAccessTokenCollector = jwtValidator.apply(signedAccessToken, Errors.collector());
                        Object remotePeer = providerRequest.env().abacAttribute("userIp").orElseThrow();

                        JsonObject accessTokenCookie = JSON_BUILDER_FACTORY.createObjectBuilder()
                                .add("accessToken", signedAccessToken.tokenContent())
                                .add("remotePeer", remotePeer.toString())
                                .build();
                        String base64 = Base64.getEncoder()
                                .encodeToString(accessTokenCookie.toString().getBytes(StandardCharsets.UTF_8));

                        List setCookieParts = new ArrayList<>();
                        setCookieParts.add(oidcConfig.tokenCookieHandler()
                                                   .createCookie(base64)
                                                   .build()
                                                   .toString());
                        if (refreshToken != null) {
                            setCookieParts.add(oidcConfig.refreshTokenCookieHandler()
                                                       .createCookie(refreshToken)
                                                       .build()
                                                       .toString());
                        }
                        return processValidationResult(providerRequest,
                                                       signedAccessToken,
                                                       idToken,
                                                       tenantId,
                                                       newAccessTokenCollector,
                                                       setCookieParts);
                    } catch (Exception e) {
                        return errorResponse(providerRequest,
                                             Status.UNAUTHORIZED_401,
                                             "refresh_access_token_failure",
                                             "Failed to refresh access token",
                                             tenantId);
                    }
                } else {
                    String message;
                    try {
                        message = response.as(String.class);
                        return errorResponse(providerRequest,
                                             Status.UNAUTHORIZED_401,
                                             "access_token_refresh_failed",
                                             "Failed to refresh access token. Response status was: "
                                                     + response.status() + " "
                                                     + "with message: " + message,
                                             tenantId);
                    } catch (Exception e) {
                        return AuthenticationResponse.failed(
                                "Failed to refresh access token, request failed: Failed to process error entity",
                                e);
                    }
                }
            } catch (Exception e) {
                return AuthenticationResponse.failed(
                        "Failed to refresh access token, request failed: Failed to invoke request",
                        e);
            }
        } catch (Exception e) {
            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate refresh token", e);
            }
            return AuthenticationResponse.failed("Failed to validate refresh token", e);
        }
    }

    private AuthenticationResponse processValidationResult(ProviderRequest providerRequest,
                                                           SignedJwt signedJwt,
                                                           Jwt idToken,
                                                           String tenantId,
                                                           Errors.Collector collector) {
        return processValidationResult(providerRequest, signedJwt, idToken, tenantId, collector, List.of());
    }

    private AuthenticationResponse processValidationResult(ProviderRequest providerRequest,
                                                           SignedJwt signedJwt,
                                                           Jwt idToken,
                                                           String tenantId,
                                                           Errors.Collector collector,
                                                           List cookies) {
        Jwt jwt = signedJwt.getJwt();
        Errors errors = collector.collect();
        JwtValidator.Builder jwtValidatorBuilder = JwtValidator.builder()
                .addDefaultTimeValidators()
                .addCriticalValidator()
                .addUserPrincipalValidator();
        if (tenant.issuer() != null) {
            jwtValidatorBuilder.addIssuerValidator(tenant.issuer());
        }
        if (tenantConfig.checkAudience()) {
            jwtValidatorBuilder.addAudienceValidator(tenantConfig.audience());
        }
        JwtValidator jwtValidation = jwtValidatorBuilder.build();
        Errors validationErrors = jwtValidation.validate(jwt);

        if (errors.isValid() && validationErrors.isValid()) {

            errors.log(LOGGER);
            Subject subject = buildSubject(jwt, signedJwt, idToken);

            Set scopes = subject.grantsByType("scope")
                    .stream()
                    .map(Grant::getName)
                    .collect(Collectors.toSet());

            // make sure we have the correct scopes
            Set expectedScopes = expectedScopes(providerRequest);
            List missingScopes = new LinkedList<>();
            for (String expectedScope : expectedScopes) {
                if (!scopes.contains(expectedScope)) {
                    missingScopes.add(expectedScope);
                }
            }

            if (missingScopes.isEmpty()) {
                AuthenticationResponse.Builder response = AuthenticationResponse.builder()
                        .status(SecurityResponse.SecurityStatus.SUCCESS)
                        .user(subject);

                if (cookies.isEmpty()) {
                    return response.build();
                } else {
                    return response
                            .responseHeader(HeaderNames.SET_COOKIE.defaultCase(), cookies)
                            .build();
                }
            } else {
                return errorResponse(providerRequest,
                                     Status.FORBIDDEN_403,
                                     "insufficient_scope",
                                     "Scopes " + missingScopes + " are missing",
                                     tenantId);
            }
        } else {
            if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                // only log errors when details requested
                errors.log(LOGGER);
                validationErrors.log(LOGGER);
            }
            return errorResponse(providerRequest,
                                 Status.UNAUTHORIZED_401,
                                 "invalid_token",
                                 "Token not valid",
                                 tenantId);
        }
    }

    private Subject buildSubject(Jwt jwt, SignedJwt signedJwt, Jwt idToken) {
        Principal principal = buildPrincipal(jwt, idToken);

        TokenCredential.Builder builder = TokenCredential.builder();
        jwt.issueTime().ifPresent(builder::issueTime);
        jwt.expirationTime().ifPresent(builder::expTime);
        jwt.issuer().ifPresent(builder::issuer);
        builder.token(signedJwt.tokenContent());
        builder.addToken(Jwt.class, jwt);
        builder.addToken(SignedJwt.class, signedJwt);

        Subject.Builder subjectBuilder = Subject.builder()
                .principal(principal)
                .addPublicCredential(TokenCredential.class, builder.build());

        if (useJwtGroups) {
            Optional> userGroups = jwt.userGroups();
            userGroups.ifPresent(groups -> groups.forEach(group -> subjectBuilder.addGrant(Role.create(group))));
        }

        Optional> scopes = jwt.scopes();
        scopes.ifPresent(scopeList -> scopeList.forEach(scope -> subjectBuilder.addGrant(Grant.builder()
                                                                                                 .name(scope)
                                                                                                 .type("scope")
                                                                                                 .build())));

        return subjectBuilder.build();

    }

    private Principal buildPrincipal(Jwt accessToken, Jwt idToken) {
        Jwt tokenToUse = idToken;
        if (idToken == null) {
            tokenToUse = accessToken;
        }

        String subject = tokenToUse.subject()
                .orElseThrow(() -> new JwtException("JWT does not contain subject claim, cannot create principal."));

        String name = tokenToUse.preferredUsername()
                .orElse(subject);

        Principal.Builder builder = Principal.builder();

        builder.name(name)
                .id(subject);

        tokenToUse.payloadClaims()
                .forEach((key, jsonValue) -> builder.addAttribute(key, JwtUtil.toObject(jsonValue)));

        tokenToUse.email().ifPresent(value -> builder.addAttribute("email", value));
        tokenToUse.emailVerified().ifPresent(value -> builder.addAttribute("email_verified", value));
        tokenToUse.locale().ifPresent(value -> builder.addAttribute("locale", value));
        tokenToUse.familyName().ifPresent(value -> builder.addAttribute("family_name", value));
        tokenToUse.givenName().ifPresent(value -> builder.addAttribute("given_name", value));
        tokenToUse.fullName().ifPresent(value -> builder.addAttribute("full_name", value));

        return builder.build();
    }

    //Obtained from https://www.baeldung.com/java-random-string
    private static String generateRandomString() {
        int leftLimit = 48; // numeral '0'
        int rightLimit = 122; // letter 'z'
        int targetStringLength = 10;

        return RANDOM.get().ints(leftLimit, rightLimit + 1)
                .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
                .limit(targetStringLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy