io.quarkus.oidc.runtime.CodeAuthenticationMechanism Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of quarkus-oidc Show documentation
Show all versions of quarkus-oidc Show documentation
Secure your applications with OpenID Connect Adapter and IDP such as Keycloak
package io.quarkus.oidc.runtime;
import static io.quarkus.oidc.runtime.OidcIdentityProvider.NEW_AUTHENTICATION;
import static io.quarkus.oidc.runtime.OidcIdentityProvider.REFRESH_TOKEN_GRANT_RESPONSE;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;
import org.jose4j.jwt.consumer.ErrorCodes;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.lang.JoseException;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.logging.Log;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.IdTokenCredential;
import io.quarkus.oidc.JavaScriptRequestChecker;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.Authentication;
import io.quarkus.oidc.OidcTenantConfig.Authentication.ResponseMode;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.AuthenticationRedirectException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.build.JwtClaimsBuilder;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.core.MultiMap;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.CookieSameSite;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.impl.CookieImpl;
import io.vertx.core.http.impl.ServerCookie;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {
static final String AMP = "&";
static final String EQ = "=";
static final String COMMA = ",";
static final String UNDERSCORE = "_";
static final String COOKIE_DELIM = "|";
static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM);
static final String SESSION_MAX_AGE_PARAM = "session-max-age";
static final String STATE_COOKIE_RESTORE_PATH = "restore-path";
static final Uni VOID_UNI = Uni.createFrom().voidItem();
static final Integer MAX_COOKIE_VALUE_LENGTH = 4096;
static final String NO_OIDC_COOKIES_AVAILABLE = "no_oidc_cookies";
private static final String INTERNAL_IDTOKEN_HEADER = "internal";
private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class);
private final BlockingTaskRunner createTokenStateRequestContext;
private final BlockingTaskRunner getTokenStateRequestContext;
private final SecureRandom secureRandom = new SecureRandom();
public CodeAuthenticationMechanism(BlockingSecurityExecutor blockingExecutor) {
this.createTokenStateRequestContext = new BlockingTaskRunner<>(blockingExecutor);
this.getTokenStateRequestContext = new BlockingTaskRunner<>(blockingExecutor);
}
public Uni authenticate(RoutingContext context,
IdentityProviderManager identityProviderManager, OidcTenantConfig oidcTenantConfig) {
final Map cookies = context.request().cookieMap();
final Cookie sessionCookie = cookies.get(getSessionCookieName(oidcTenantConfig));
// If the session is already established then try to re-authenticate
if (sessionCookie != null) {
LOG.debug("Session cookie is present, starting the reauthentication");
context.put(OidcUtils.SESSION_COOKIE_NAME, sessionCookie.getName());
Uni resolvedContext = resolver.resolveContext(context);
return resolvedContext.onItem()
.transformToUni(new Function>() {
@Override
public Uni apply(TenantConfigContext tenantContext) {
return reAuthenticate(sessionCookie, context, identityProviderManager, tenantContext);
}
});
}
// Check if the state cookie is available
if (isStateCookieAvailable(cookies)) {
// Authorization code flow is in progress, however it is not necessarily tied to the current request.
if (ResponseMode.FORM_POST == oidcTenantConfig.authentication.responseMode.orElse(ResponseMode.QUERY)) {
if (OidcUtils.isFormUrlEncodedRequest(context)) {
return OidcUtils.getFormUrlEncodedData(context).onItem()
.transformToUni(new Function>() {
@Override
public Uni extends SecurityIdentity> apply(MultiMap requestParams) {
return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager,
requestParams, cookies);
}
});
}
LOG.debug("HTTP POST and " + HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()
+ " content type must be used with the form_post response mode");
return Uni.createFrom().failure(new AuthenticationFailedException());
} else {
return processRedirectFromOidc(context, oidcTenantConfig, identityProviderManager,
context.queryParams(), cookies);
}
}
// return an empty identity - this will lead to a challenge redirecting the user to OpenId Connect provider
// unless it is detected it is a redirect from the provider in which case HTTP 401 will be returned.
context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE);
return Uni.createFrom().optional(Optional.empty());
}
private boolean isStateCookieAvailable(Map cookies) {
for (String name : cookies.keySet()) {
if (name.startsWith(OidcUtils.STATE_COOKIE_NAME)) {
return true;
}
}
return false;
}
private Uni processRedirectFromOidc(RoutingContext context, OidcTenantConfig oidcTenantConfig,
IdentityProviderManager identityProviderManager, MultiMap requestParams,
Map cookies) {
// At this point it has already been detected that some state cookie is available.
// If the state query parameter is not available or is available but no matching state cookie is found then if
// 1) the redirect path matches the current request path
// or
// 2) no parallel code flows from the same browser is allowed
// then 401 will be returned, otherwise a new authentication challenge will be created
//
// Once the state cookie matching the state query parameter has been found,
// the state cookie first part value must always match the state query value
List stateQueryParam = requestParams.getAll(OidcConstants.CODE_FLOW_STATE);
if (stateQueryParam.size() != 1) {
return stateParamIsMissing(oidcTenantConfig, context, cookies, stateQueryParam.size() > 1);
}
String stateCookieNameSuffix = oidcTenantConfig.authentication.allowMultipleCodeFlows ? "_" + stateQueryParam.get(0)
: "";
final Cookie stateCookie = context.request().getCookie(
getStateCookieName(oidcTenantConfig) + stateCookieNameSuffix);
if (stateCookie == null) {
return stateCookieIsMissing(oidcTenantConfig, context, cookies);
}
String[] parsedStateCookieValue = COOKIE_PATTERN.split(stateCookie.getValue());
OidcUtils.removeCookie(context, oidcTenantConfig, stateCookie.getName());
if (!parsedStateCookieValue[0].equals(stateQueryParam.get(0))) {
LOG.debug("State cookie value does not match the state query parameter value, "
+ "completing the code flow with HTTP status 401");
return Uni.createFrom().failure(new AuthenticationCompletionException());
}
// State cookie is available, try to complete the code flow and start a new session
LOG.debug("State cookie is present, processing an expected redirect from the OIDC provider");
if (requestParams.contains(OidcConstants.CODE_FLOW_CODE)) {
LOG.debug("Authorization code is present, completing the code flow");
Uni resolvedContext = resolver.resolveContext(context);
return resolvedContext.onItem()
.transformToUni(new Function>() {
@Override
public Uni apply(TenantConfigContext tenantContext) {
return performCodeFlow(identityProviderManager, context, tenantContext, requestParams,
parsedStateCookieValue);
}
});
} else if (requestParams.contains(OidcConstants.CODE_FLOW_ERROR)) {
OidcUtils.removeCookie(context, oidcTenantConfig, stateCookie.getName());
String error = requestParams.get(OidcConstants.CODE_FLOW_ERROR);
String errorDescription = requestParams.get(OidcConstants.CODE_FLOW_ERROR_DESCRIPTION);
LOG.debugf("Authentication has failed, error: %s, description: %s", error, errorDescription);
if (oidcTenantConfig.authentication.errorPath.isPresent()) {
Uni resolvedContext = resolver.resolveContext(context);
return resolvedContext.onItem()
.transformToUni(new Function>() {
@Override
public Uni apply(TenantConfigContext tenantContext) {
URI absoluteUri = URI.create(context.request().absoluteURI());
String userQuery = null;
// This is an original redirect from IDP, check if the original request path and query need to be restored
CodeAuthenticationStateBean stateBean = getCodeAuthenticationBean(parsedStateCookieValue,
tenantContext);
if (stateBean != null && stateBean.getRestorePath() != null) {
String restorePath = stateBean.getRestorePath();
int userQueryIndex = restorePath.indexOf("?");
if (userQueryIndex >= 0 && userQueryIndex + 1 < restorePath.length()) {
userQuery = restorePath.substring(userQueryIndex + 1);
}
}
StringBuilder errorUri = new StringBuilder(buildUri(context,
isForceHttps(oidcTenantConfig),
absoluteUri.getAuthority(),
oidcTenantConfig.authentication.errorPath.get()));
errorUri.append('?')
.append(getRequestParametersAsQuery(absoluteUri, requestParams, oidcTenantConfig));
if (userQuery != null) {
errorUri.append('&').append(userQuery);
}
String finalErrorUri = errorUri.toString();
LOG.debugf("Error URI: %s", finalErrorUri);
return Uni.createFrom().failure(new AuthenticationRedirectException(finalErrorUri));
}
});
} else {
LOG.error(
"Authentication has failed but no error handler is found, completing the code flow with HTTP status 401");
return Uni.createFrom().failure(new AuthenticationCompletionException());
}
} else {
LOG.error("State cookie is present but neither 'code' nor 'error' query parameter is returned");
return Uni.createFrom().failure(new AuthenticationCompletionException());
}
}
private Uni stateParamIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context,
Map cookies, boolean multipleStateQueryParams) {
if (multipleStateQueryParams) {
LOG.warn("State query parameter can not be multi-valued if the state cookie is present");
removeStateCookies(oidcTenantConfig, context, cookies);
return Uni.createFrom().failure(new AuthenticationCompletionException());
}
LOG.debug("State parameter can not be empty if the state cookie is present");
return stateCookieIsNotMatched(oidcTenantConfig, context, cookies);
}
private Uni stateCookieIsMissing(OidcTenantConfig oidcTenantConfig, RoutingContext context,
Map cookies) {
LOG.debug("Matching state cookie is not found");
return stateCookieIsNotMatched(oidcTenantConfig, context, cookies);
}
private Uni stateCookieIsNotMatched(OidcTenantConfig oidcTenantConfig, RoutingContext context,
Map cookies) {
if (!oidcTenantConfig.authentication.allowMultipleCodeFlows
|| context.request().path().equals(getRedirectPath(oidcTenantConfig, context))) {
if (oidcTenantConfig.authentication.failOnMissingStateParam) {
return Uni.createFrom().failure(new AuthenticationCompletionException());
} else {
removeStateCookies(oidcTenantConfig, context, cookies);
}
}
context.put(NO_OIDC_COOKIES_AVAILABLE, Boolean.TRUE);
return Uni.createFrom().optional(Optional.empty());
}
private void removeStateCookies(OidcTenantConfig oidcTenantConfig, RoutingContext context, Map cookies) {
for (String name : cookies.keySet()) {
if (name.startsWith(OidcUtils.STATE_COOKIE_NAME)) {
OidcUtils.removeCookie(context, oidcTenantConfig, name);
}
}
}
private String getRequestParametersAsQuery(URI requestUri, MultiMap requestParams, OidcTenantConfig oidcConfig) {
if (ResponseMode.FORM_POST == oidcConfig.authentication.responseMode.orElse(ResponseMode.QUERY)) {
return OidcCommonUtils.encodeForm(new io.vertx.mutiny.core.MultiMap(requestParams)).toString();
} else {
return requestUri.getRawQuery();
}
}
private Uni reAuthenticate(Cookie sessionCookie,
RoutingContext context,
IdentityProviderManager identityProviderManager,
TenantConfigContext configContext) {
context.put(TenantConfigContext.class.getName(), configContext);
return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig,
sessionCookie.getValue(), getTokenStateRequestContext)
.onFailure(AuthenticationCompletionException.class)
.recoverWithUni(
new Function>() {
@Override
public Uni apply(Throwable t) {
return removeSessionCookie(context, configContext.oidcConfig)
.replaceWith(Uni.createFrom().failure(t));
}
})
.chain(new Function>() {
@Override
public Uni extends SecurityIdentity> apply(AuthorizationCodeTokens session) {
context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken());
context.put(AuthorizationCodeTokens.class.getName(), session);
// Default token state manager may have encrypted ID token when it was saved in a cookie
final String currentIdToken = decryptIdTokenIfEncryptedByProvider(configContext, session.getIdToken());
return authenticate(identityProviderManager, context,
new IdTokenCredential(currentIdToken,
isInternalIdToken(currentIdToken, configContext)))
.call(new LogoutCall(context, configContext, session.getIdToken())).onFailure()
.recoverWithUni(new Function>() {
@Override
public Uni extends SecurityIdentity> apply(Throwable t) {
if (t instanceof AuthenticationRedirectException) {
LOG.debug("Redirecting after the reauthentication");
return Uni.createFrom().failure((AuthenticationRedirectException) t);
}
if (t instanceof LogoutException) {
LOG.debugf("User has been logged out, authentication challenge is required");
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
if (!(t instanceof TokenAutoRefreshException)) {
boolean expired = (t.getCause() instanceof InvalidJwtException)
&& ((InvalidJwtException) t.getCause())
.hasErrorCode(ErrorCodes.EXPIRED);
if (!expired) {
LOG.errorf("ID token verification failure: %s", errorMessage(t));
return removeSessionCookie(context, configContext.oidcConfig)
.replaceWith(Uni.createFrom()
.failure(t
.getCause() instanceof AuthenticationCompletionException
? t.getCause()
: new AuthenticationCompletionException(
t.getCause())));
}
// Token has expired, try to refresh
if (isRpInitiatedLogout(context, configContext)) {
LOG.debug("Session has expired, performing an RP initiated logout");
fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED_SESSION_EXPIRED,
Map.of(SecurityEvent.SESSION_TOKENS_PROPERTY, session));
return Uni.createFrom().item((SecurityIdentity) null)
.call(() -> buildLogoutRedirectUriUni(context, configContext,
currentIdToken));
}
if (session.getRefreshToken() == null) {
LOG.debug(
"Token has expired, token refresh is not possible because the refresh token is null");
return Uni.createFrom()
.failure(new AuthenticationFailedException(t.getCause()));
}
if (!configContext.oidcConfig.token.refreshExpired) {
LOG.debug("Token has expired, token refresh is not allowed");
return Uni.createFrom()
.failure(new AuthenticationFailedException(t.getCause()));
}
LOG.debug("Token has expired, trying to refresh it");
return refreshSecurityIdentity(configContext,
currentIdToken,
session.getRefreshToken(),
context,
identityProviderManager, false, null);
} else {
// Token auto-refresh, security identity is still valid
SecurityIdentity currentIdentity = ((TokenAutoRefreshException) t)
.getSecurityIdentity();
if (isLogout(context, configContext, currentIdentity)) {
// No need to refresh the token since the user is requesting a logout
return Uni.createFrom().item(currentIdentity).call(
new LogoutCall(context, configContext, session.getIdToken()));
}
if (session.getRefreshToken() != null) {
// Token has nearly expired, try to refresh
LOG.debug("Token auto-refresh is starting");
return refreshSecurityIdentity(configContext,
currentIdToken,
session.getRefreshToken(),
context,
identityProviderManager, true,
currentIdentity);
} else {
LOG.debug(
"Token auto-refresh is required but is not possible because the refresh token is null");
// Auto-refreshing is not possible, just continue with the current security identity
if (currentIdentity != null) {
return Uni.createFrom().item(currentIdentity);
} else {
return Uni.createFrom()
.failure(new AuthenticationFailedException(t.getCause()));
}
}
}
}
});
}
});
}
private static String decryptIdTokenIfEncryptedByProvider(TenantConfigContext resolvedContext, String token) {
if ((resolvedContext.provider.tokenDecryptionKey != null || resolvedContext.provider.client.getClientJwtKey() != null)
&& OidcUtils.isEncryptedToken(token)) {
try {
return OidcUtils.decryptString(token,
resolvedContext.provider.tokenDecryptionKey != null ? resolvedContext.provider.tokenDecryptionKey
: resolvedContext.provider.client.getClientJwtKey(),
KeyEncryptionAlgorithm.RSA_OAEP);
} catch (JoseException ex) {
Log.debugf("Failed to decrypt a token: %s, a token introspection will be attempted instead", ex.getMessage());
}
}
return token;
}
private boolean isLogout(RoutingContext context, TenantConfigContext configContext, SecurityIdentity identity) {
return isRpInitiatedLogout(context, configContext) || isBackChannelLogoutPending(configContext, identity)
|| isFrontChannelLogoutValid(context, configContext, identity);
}
private boolean isBackChannelLogoutPending(TenantConfigContext configContext, SecurityIdentity identity) {
if (configContext.oidcConfig.logout.backchannel.path.isEmpty()) {
return false;
}
BackChannelLogoutTokenCache tokens = resolver.getBackChannelLogoutTokens()
.get(configContext.oidcConfig.getTenantId().get());
if (tokens != null) {
JsonObject idTokenJson = OidcUtils.decodeJwtContent(((JsonWebToken) (identity.getPrincipal())).getRawToken());
String logoutTokenKeyValue = idTokenJson.getString(configContext.oidcConfig.logout.backchannel.getLogoutTokenKey());
return tokens.containsTokenVerification(logoutTokenKeyValue);
}
return false;
}
private boolean isBackChannelLogoutPendingAndValid(TenantConfigContext configContext, SecurityIdentity identity) {
if (configContext.oidcConfig.logout.backchannel.path.isEmpty()) {
return false;
}
BackChannelLogoutTokenCache tokens = resolver.getBackChannelLogoutTokens()
.get(configContext.oidcConfig.getTenantId().get());
if (tokens != null) {
JsonObject idTokenJson = OidcUtils.decodeJwtContent(((JsonWebToken) (identity.getPrincipal())).getRawToken());
String logoutTokenKeyValue = idTokenJson.getString(configContext.oidcConfig.logout.backchannel.getLogoutTokenKey());
TokenVerificationResult backChannelLogoutTokenResult = tokens.removeTokenVerification(logoutTokenKeyValue);
if (backChannelLogoutTokenResult == null) {
return false;
}
String idTokenIss = idTokenJson.getString(Claims.iss.name());
String logoutTokenIss = backChannelLogoutTokenResult.localVerificationResult.getString(Claims.iss.name());
if (logoutTokenIss != null && !logoutTokenIss.equals(idTokenIss)) {
LOG.debugf("Logout token issuer does not match the ID token issuer");
return false;
}
String idTokenSub = idTokenJson.getString(Claims.sub.name());
String logoutTokenSub = backChannelLogoutTokenResult.localVerificationResult.getString(Claims.sub.name());
if (logoutTokenSub != null && idTokenSub != null && !logoutTokenSub.equals(idTokenSub)) {
LOG.debugf("Logout token subject does not match the ID token subject");
return false;
}
String idTokenSid = idTokenJson.getString(OidcConstants.ID_TOKEN_SID_CLAIM);
String logoutTokenSid = backChannelLogoutTokenResult.localVerificationResult
.getString(OidcConstants.BACK_CHANNEL_LOGOUT_SID_CLAIM);
if (logoutTokenSid != null && idTokenSid != null && !logoutTokenSid.equals(idTokenSid)) {
LOG.debugf("Logout token session id does not match the ID token session id");
return false;
}
LOG.debugf("Backchannel logout request for the tenant %s has been completed",
configContext.oidcConfig.tenantId.get());
fireEvent(SecurityEvent.Type.OIDC_BACKCHANNEL_LOGOUT_COMPLETED, identity);
return true;
}
return false;
}
private boolean isFrontChannelLogoutValid(RoutingContext context, TenantConfigContext configContext,
SecurityIdentity identity) {
if (isEqualToRequestPath(configContext.oidcConfig.logout.frontchannel.path, context, configContext)) {
JsonObject idTokenJson = OidcUtils.decodeJwtContent(((JsonWebToken) (identity.getPrincipal())).getRawToken());
String idTokenIss = idTokenJson.getString(Claims.iss.name());
List frontChannelIss = context.queryParam(Claims.iss.name());
if (frontChannelIss != null && frontChannelIss.size() == 1 && !frontChannelIss.get(0).equals(idTokenIss)) {
LOG.debugf("Frontchannel issuer parameter does not match the ID token issuer");
return false;
}
String idTokenSid = idTokenJson.getString(OidcConstants.ID_TOKEN_SID_CLAIM);
List frontChannelSid = context.queryParam(OidcConstants.FRONT_CHANNEL_LOGOUT_SID_PARAM);
if (frontChannelSid != null && frontChannelSid.size() == 1 && !frontChannelSid.get(0).equals(idTokenSid)) {
LOG.debugf("Frontchannel session id parameter does not match the ID token session id");
return false;
}
LOG.debugf("Frontchannel logout request for the tenant %s has been completed",
configContext.oidcConfig.tenantId.get());
fireEvent(SecurityEvent.Type.OIDC_FRONTCHANNEL_LOGOUT_COMPLETED, identity);
return true;
}
return false;
}
private boolean isInternalIdToken(String idToken, TenantConfigContext configContext) {
if (!configContext.oidcConfig.authentication.idTokenRequired.orElse(true)) {
JsonObject headers = OidcUtils.decodeJwtHeaders(idToken);
if (headers != null) {
return headers.getBoolean(INTERNAL_IDTOKEN_HEADER, false);
}
}
return false;
}
private boolean isIdTokenRequired(TenantConfigContext configContext) {
return configContext.oidcConfig.authentication.isIdTokenRequired().orElse(true);
}
private boolean isJavaScript(RoutingContext context) {
JavaScriptRequestChecker checker = resolver.getJavaScriptRequestChecker();
if (checker != null) {
return checker.isJavaScriptRequest(context);
}
String value = context.request().getHeader("X-Requested-With");
return "JavaScript".equals(value) || "XMLHttpRequest".equals(value);
}
// This test determines if the default behavior of returning a 302 should go forward
// The only case that shouldn't return a 302 is if the call is a XHR and the
// user has set the auto direct application property to false indicating that
// the client application will manually handle the redirect to account for SPA behavior
private boolean shouldAutoRedirect(TenantConfigContext configContext, RoutingContext context) {
return isJavaScript(context) ? configContext.oidcConfig.authentication.javaScriptAutoRedirect : true;
}
public Uni getChallenge(RoutingContext context) {
Uni tenantContext = resolver.resolveContext(context);
return tenantContext.onItem().transformToUni(new Function>() {
@Override
public Uni apply(TenantConfigContext tenantContext) {
return getChallengeInternal(context, tenantContext);
}
});
}
public Uni getChallengeInternal(RoutingContext context, TenantConfigContext configContext) {
LOG.debug("Starting an authentication challenge");
return removeSessionCookie(context, configContext.oidcConfig)
.chain(new Function>() {
@Override
public Uni apply(Void t) {
if (context.get(NO_OIDC_COOKIES_AVAILABLE) != null
&& isRedirectFromProvider(context, configContext)) {
LOG.warn(
"The state cookie is missing after the redirect from OpenId Connect Provider, authentication has failed");
return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "OIDC"));
}
if (!shouldAutoRedirect(configContext, context)) {
// If the client (usually an SPA) wants to handle the redirect manually, then
// return status code 499 and WWW-Authenticate header with the 'OIDC' value.
return Uni.createFrom().item(new ChallengeData(499, "WWW-Authenticate", "OIDC"));
}
StringBuilder codeFlowParams = new StringBuilder(168); // experimentally determined to be a good size for preventing resizing and not wasting space
// response_type
codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_TYPE).append(EQ)
.append(OidcConstants.CODE_FLOW_CODE);
// response_mode
if (ResponseMode.FORM_POST == configContext.oidcConfig.authentication.responseMode
.orElse(ResponseMode.QUERY)) {
codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_RESPONSE_MODE).append(EQ)
.append(configContext.oidcConfig.authentication.responseMode.get().toString()
.toLowerCase());
}
// client_id
codeFlowParams.append(AMP).append(OidcConstants.CLIENT_ID).append(EQ)
.append(OidcCommonUtils.urlEncode(configContext.oidcConfig.clientId.get()));
// scope
List oidcConfigScopes = configContext.oidcConfig.getAuthentication().scopes.isPresent()
? configContext.oidcConfig.getAuthentication().scopes.get()
: Collections.emptyList();
List scopes = new ArrayList<>(oidcConfigScopes.size() + 1);
if (configContext.oidcConfig.getAuthentication().addOpenidScope.orElse(true)) {
scopes.add(OidcConstants.OPENID_SCOPE);
}
scopes.addAll(oidcConfigScopes);
// Extra scopes if any
String extraScopeValue = configContext.oidcConfig.getAuthentication().getExtraParams()
.get(OidcConstants.TOKEN_SCOPE);
if (extraScopeValue != null) {
String[] extraScopes = extraScopeValue.split(COMMA);
scopes.addAll(List.of(extraScopes));
}
codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ)
.append(OidcCommonUtils.urlEncode(String.join(" ", scopes)));
MultiMap requestQueryParams = null;
if (!configContext.oidcConfig.getAuthentication().forwardParams.isEmpty()) {
requestQueryParams = context.queryParams();
for (String forwardedParam : configContext.oidcConfig.getAuthentication().forwardParams.get()) {
if (requestQueryParams.contains(forwardedParam)) {
for (String requestQueryParamValue : requestQueryParams.getAll(forwardedParam))
codeFlowParams.append(AMP).append(forwardedParam).append(EQ)
.append(OidcCommonUtils.urlEncode(requestQueryParamValue));
requestQueryParams.remove(forwardedParam);
}
}
}
// redirect_uri
String redirectPath = getRedirectPath(configContext.oidcConfig, context);
String redirectUriParam = buildUri(context, isForceHttps(configContext.oidcConfig), redirectPath);
LOG.debugf("Authentication request redirect_uri parameter: %s", redirectUriParam);
codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_REDIRECT_URI).append(EQ)
.append(OidcCommonUtils.urlEncode(redirectUriParam));
// pkce
PkceStateBean pkceStateBean = createPkceStateBean(configContext);
// state
String nonce = configContext.oidcConfig.authentication.nonceRequired ? UUID.randomUUID().toString()
: null;
codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_STATE).append(EQ)
.append(generateCodeFlowState(context, configContext, redirectPath, requestQueryParams,
(pkceStateBean != null ? pkceStateBean.getCodeVerifier() : null), nonce));
if (pkceStateBean != null) {
codeFlowParams
.append(AMP).append(OidcConstants.PKCE_CODE_CHALLENGE).append(EQ)
.append(pkceStateBean.getCodeChallenge());
codeFlowParams
.append(AMP).append(OidcConstants.PKCE_CODE_CHALLENGE_METHOD).append(EQ)
.append(OidcConstants.PKCE_CODE_CHALLENGE_S256);
}
if (nonce != null) {
codeFlowParams.append(AMP).append(OidcConstants.NONCE).append(EQ).append(nonce);
}
// extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests
addExtraParamsToUri(codeFlowParams, configContext.oidcConfig.authentication.getExtraParams());
String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?"
+ codeFlowParams.toString();
LOG.debugf("Code flow redirect to: %s", authorizationURL);
return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION,
authorizationURL));
}
});
}
private boolean isRedirectFromProvider(RoutingContext context, TenantConfigContext configContext) {
// The referrer check is the best effort at attempting to avoid the redirect loop after
// the user has authenticated at the OpenId Connect Provider page but the state cookie has been lost
// during the redirect back to Quarkus.
String referer = context.request().getHeader(HttpHeaders.REFERER);
return referer != null && referer.startsWith(configContext.provider.getMetadata().getAuthorizationUri());
}
private PkceStateBean createPkceStateBean(TenantConfigContext configContext) {
if (configContext.oidcConfig.authentication.pkceRequired.orElse(false)) {
PkceStateBean bean = new PkceStateBean();
Encoder encoder = Base64.getUrlEncoder().withoutPadding();
// code verifier
byte[] codeVerifierBytes = new byte[32];
secureRandom.nextBytes(codeVerifierBytes);
String codeVerifier = encoder.encodeToString(codeVerifierBytes);
bean.setCodeVerifier(codeVerifier);
// code challenge
try {
byte[] codeChallengeBytes = OidcUtils.getSha256Digest(codeVerifier.getBytes(StandardCharsets.ISO_8859_1));
String codeChallenge = encoder.encodeToString(codeChallengeBytes);
bean.setCodeChallenge(codeChallenge);
} catch (Exception ex) {
LOG.errorf("Code challenge creation failure: %s", ex.getMessage());
throw new AuthenticationCompletionException(ex);
}
return bean;
}
return null;
}
private Uni performCodeFlow(IdentityProviderManager identityProviderManager,
RoutingContext context, TenantConfigContext configContext, MultiMap requestParams,
String[] parsedStateCookieValue) {
String userPath = null;
String userQuery = null;
// This is an original redirect from IDP, check if the original request path and query need to be restored
CodeAuthenticationStateBean stateBean = getCodeAuthenticationBean(parsedStateCookieValue, configContext);
if (stateBean != null && stateBean.getRestorePath() != null) {
String restorePath = stateBean.getRestorePath();
int userQueryIndex = restorePath.indexOf("?");
if (userQueryIndex >= 0) {
userPath = isRestorePath(configContext.oidcConfig.authentication) ? restorePath.substring(0, userQueryIndex)
: null;
if (userQueryIndex + 1 < restorePath.length()) {
userQuery = restorePath.substring(userQueryIndex + 1);
}
} else {
userPath = restorePath;
}
}
final String finalUserPath = userPath;
final String finalUserQuery = userQuery;
final String code = requestParams.get(OidcConstants.CODE_FLOW_CODE);
LOG.debug("Exchanging the authorization code for the tokens");
Uni codeFlowTokensUni = getCodeFlowTokensUni(context, configContext, code,
stateBean != null ? stateBean.getCodeVerifier() : null);
return codeFlowTokensUni
.onItemOrFailure()
.transformToUni(new BiFunction>() {
@Override
public Uni apply(final AuthorizationCodeTokens tokens, final Throwable tOuter) {
if (tOuter != null) {
LOG.errorf("Exception during the code to token exchange: %s", tOuter.getMessage());
return Uni.createFrom().failure(new AuthenticationCompletionException(tOuter));
}
final boolean internalIdToken;
if (tokens.getIdToken() == null) {
if (isIdTokenRequired(configContext)) {
LOG.errorf("ID token is not available in the authorization code grant response");
return Uni.createFrom().failure(new AuthenticationCompletionException());
} else {
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, null));
internalIdToken = true;
}
} else {
if (!prepareNonceForVerification(context, configContext.oidcConfig, stateBean,
tokens.getIdToken())) {
return Uni.createFrom().failure(new AuthenticationCompletionException());
}
internalIdToken = false;
}
context.put(NEW_AUTHENTICATION, Boolean.TRUE);
context.put(OidcConstants.ACCESS_TOKEN_VALUE, tokens.getAccessToken());
context.put(AuthorizationCodeTokens.class.getName(), tokens);
// Default token state manager may have encrypted ID token
final String idToken = decryptIdTokenIfEncryptedByProvider(configContext, tokens.getIdToken());
LOG.debug("Authorization code has been exchanged, verifying ID token");
return authenticate(identityProviderManager, context,
new IdTokenCredential(idToken, internalIdToken))
.call(new Function>() {
@Override
public Uni apply(SecurityIdentity identity) {
if (internalIdToken && configContext.oidcConfig.allowUserInfoCache
&& configContext.oidcConfig.cacheUserInfoInIdtoken) {
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig,
identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null));
}
return processSuccessfulAuthentication(context, configContext,
tokens, idToken, identity);
}
})
.map(new Function() {
@Override
public SecurityIdentity apply(SecurityIdentity identity) {
boolean removeRedirectParams = configContext.oidcConfig.authentication
.isRemoveRedirectParameters();
if (removeRedirectParams || finalUserPath != null
|| finalUserQuery != null) {
URI absoluteUri = URI.create(context.request().absoluteURI());
StringBuilder finalUriWithoutQuery = new StringBuilder(buildUri(context,
isForceHttps(configContext.oidcConfig),
absoluteUri.getAuthority(),
(finalUserPath != null ? finalUserPath
: absoluteUri.getRawPath())));
if (!removeRedirectParams) {
finalUriWithoutQuery.append('?')
.append(getRequestParametersAsQuery(absoluteUri, requestParams,
configContext.oidcConfig));
}
if (finalUserQuery != null) {
finalUriWithoutQuery.append(!removeRedirectParams ? "" : "?");
finalUriWithoutQuery.append(finalUserQuery);
}
String finalRedirectUri = finalUriWithoutQuery.toString();
LOG.debugf("Removing code flow redirect parameters, final redirect URI: %s",
finalRedirectUri);
throw new AuthenticationRedirectException(finalRedirectUri);
} else {
return identity;
}
}
}).onFailure().transform(new Function() {
@Override
public Throwable apply(Throwable tInner) {
if (tInner instanceof AuthenticationRedirectException) {
LOG.debugf("Starting the final redirect");
return tInner;
}
LOG.errorf("ID token verification has failed: %s", errorMessage(tInner));
return new AuthenticationCompletionException(tInner);
}
});
}
});
}
private static boolean prepareNonceForVerification(RoutingContext context, OidcTenantConfig oidcConfig,
CodeAuthenticationStateBean stateBean, String idToken) {
if (oidcConfig.authentication.nonceRequired) {
if (stateBean != null && stateBean.getNonce() != null) {
// Avoid parsing the token now
context.put(OidcConstants.NONCE, stateBean.getNonce());
return true;
}
LOG.errorf("ID token 'nonce' is required but the authentication request 'nonce' is not found in the state cookie");
return false;
} else {
return true;
}
}
private static String errorMessage(Throwable t) {
return t.getCause() != null ? t.getCause().getMessage() : t.getMessage();
}
private CodeAuthenticationStateBean getCodeAuthenticationBean(String[] parsedStateCookieValue,
TenantConfigContext configContext) {
if (parsedStateCookieValue.length == 2) {
CodeAuthenticationStateBean bean = new CodeAuthenticationStateBean();
Authentication authentication = configContext.oidcConfig.authentication;
boolean pkceRequired = authentication.pkceRequired.orElse(false);
if (!pkceRequired && !authentication.nonceRequired) {
bean.setRestorePath(parsedStateCookieValue[1]);
return bean;
}
JsonObject json = null;
try {
json = OidcUtils.decryptJson(parsedStateCookieValue[1], configContext.getStateEncryptionKey());
} catch (Exception ex) {
LOG.errorf("State cookie value can not be decrypted for the %s tenant",
configContext.oidcConfig.tenantId.get());
throw new AuthenticationCompletionException(ex);
}
bean.setRestorePath(json.getString(STATE_COOKIE_RESTORE_PATH));
bean.setCodeVerifier(json.getString(OidcConstants.PKCE_CODE_VERIFIER));
bean.setNonce(json.getString(OidcConstants.NONCE));
return bean;
}
return null;
}
private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo userInfo, String currentIdToken) {
JwtClaimsBuilder builder = Jwt.claims();
if (currentIdToken != null) {
AbstractJsonObjectResponse currentIdTokenJson = new AbstractJsonObjectResponse(
OidcUtils.decodeJwtContentAsString(currentIdToken)) {
};
for (String claim : currentIdTokenJson.getPropertyNames()) {
// Ignore "iat"(issued at) and "exp"(expiry) claims, new "iat" and "exp" claims will be generated
if (!claim.equals(Claims.iat.name()) && !claim.equals(Claims.exp.name())) {
builder.claim(claim, currentIdTokenJson.get(claim));
}
}
}
if (userInfo != null) {
builder.claim(OidcUtils.USER_INFO_ATTRIBUTE, userInfo.getJsonObject());
}
if (oidcConfig.authentication.internalIdTokenLifespan.isPresent()) {
builder.expiresIn(oidcConfig.authentication.internalIdTokenLifespan.get().getSeconds());
}
builder.audience(oidcConfig.getClientId().get());
return builder.jws().header(INTERNAL_IDTOKEN_HEADER, true)
.sign(KeyUtils.createSecretKeyFromSecret(OidcCommonUtils.clientSecret(oidcConfig.credentials)));
}
private Uni processSuccessfulAuthentication(RoutingContext context,
TenantConfigContext configContext,
AuthorizationCodeTokens tokens,
String idToken,
SecurityIdentity securityIdentity) {
LOG.debug("ID token has been verified, removing the existing session cookie if any and creating a new one");
return removeSessionCookie(context, configContext.oidcConfig)
.chain(new Function>() {
@Override
public Uni extends Void> apply(Void t) {
JsonObject idTokenJson = OidcUtils.decodeJwtContent(idToken);
if (!idTokenJson.containsKey("exp") || !idTokenJson.containsKey("iat")) {
LOG.error("ID Token is required to contain 'exp' and 'iat' claims");
throw new AuthenticationCompletionException();
}
long maxAge = idTokenJson.getLong("exp") - idTokenJson.getLong("iat");
LOG.debugf("ID token is valid for %d seconds", maxAge);
if (configContext.oidcConfig.token.lifespanGrace.isPresent()) {
maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt();
}
if (configContext.oidcConfig.token.refreshExpired) {
maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds();
}
final long sessionMaxAge = maxAge;
context.put(SESSION_MAX_AGE_PARAM, maxAge);
context.put(TenantConfigContext.class.getName(), configContext);
// Just in case, remove the stale Back-Channel Logout data if the previous session was not terminated correctly
resolver.getBackChannelLogoutTokens().remove(configContext.oidcConfig.tenantId.get());
return resolver.getTokenStateManager()
.createTokenState(context, configContext.oidcConfig, tokens, createTokenStateRequestContext)
.map(new Function() {
@Override
public Void apply(String cookieValue) {
String sessionCookie = createCookie(context, configContext.oidcConfig,
getSessionCookieName(configContext.oidcConfig),
cookieValue, sessionMaxAge, true).getValue();
if (sessionCookie.length() >= MAX_COOKIE_VALUE_LENGTH) {
LOG.warnf(
"Session cookie length for the tenant %s is equal or greater than %d bytes."
+ " Browsers may ignore this cookie which will cause a new challenge for the authenticated users."
+ " Recommendations: 1. Set 'quarkus.oidc.token-state-manager.split-tokens=true'"
+ " to have the ID, access and refresh tokens stored in separate cookies."
+ " 2. Set 'quarkus.oidc.token-state-manager.strategy=id-refresh-tokens' if you do not need to use the access token"
+ " as a source of roles or to request UserInfo or propagate it to the downstream services."
+ " 3. Decrease the session cookie's length by disabling its encryption with 'quarkus.oidc.token-state-manager.encryption-required=false'"
+ " but only if it is considered to be safe in your application's network."
+ " 4. Register a custom 'quarkus.oidc.TokenStateManager' CDI bean with the alternative priority set to 1.",
configContext.oidcConfig.tenantId.get(),
MAX_COOKIE_VALUE_LENGTH);
}
fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity);
return null;
}
});
}
});
}
private void fireEvent(SecurityEvent.Type eventType, SecurityIdentity securityIdentity) {
if (resolver.isSecurityEventObserved()) {
resolver.getSecurityEvent().fire(new SecurityEvent(eventType, securityIdentity));
}
}
private void fireEvent(SecurityEvent.Type eventType, Map properties) {
if (resolver.isSecurityEventObserved()) {
resolver.getSecurityEvent().fire(new SecurityEvent(eventType, properties));
}
}
private String getRedirectPath(OidcTenantConfig oidcConfig, RoutingContext context) {
Authentication auth = oidcConfig.getAuthentication();
return auth.getRedirectPath().isPresent() ? auth.getRedirectPath().get() : context.request().path();
}
private String generateCodeFlowState(RoutingContext context, TenantConfigContext configContext,
String redirectPath, MultiMap requestQueryWithoutForwardedParams, String pkceCodeVerifier, String nonce) {
String uuid = UUID.randomUUID().toString();
String cookieValue = uuid;
Authentication authentication = configContext.oidcConfig.getAuthentication();
boolean restorePath = isRestorePath(authentication);
if (restorePath || pkceCodeVerifier != null || nonce != null) {
CodeAuthenticationStateBean extraStateValue = new CodeAuthenticationStateBean();
if (restorePath) {
String requestQuery = context.request().query();
String requestPath = !redirectPath.equals(context.request().path()) || requestQuery != null
? context.request().path()
: "";
if (requestQuery != null) {
requestPath += "?";
if (requestQueryWithoutForwardedParams == null) {
requestPath += requestQuery;
} else {
StringBuilder sb = new StringBuilder();
for (String requestQueryParam : requestQueryWithoutForwardedParams.names()) {
for (String requestQueryParamValue : requestQueryWithoutForwardedParams.getAll(requestQueryParam)) {
if (sb.length() > 0) {
sb.append(AMP);
}
sb.append(requestQueryParam).append(EQ)
.append(OidcCommonUtils.urlEncode(requestQueryParamValue));
}
}
requestPath += sb.toString();
}
}
if (!requestPath.isEmpty()) {
extraStateValue.setRestorePath(requestPath);
}
}
extraStateValue.setCodeVerifier(pkceCodeVerifier);
extraStateValue.setNonce(nonce);
if (!extraStateValue.isEmpty()) {
cookieValue += (COOKIE_DELIM + encodeExtraStateValue(extraStateValue, configContext));
}
} else if (context.request().query() != null) {
CodeAuthenticationStateBean extraStateValue = new CodeAuthenticationStateBean();
extraStateValue.setRestorePath("?" + context.request().query());
cookieValue += (COOKIE_DELIM + encodeExtraStateValue(extraStateValue, configContext));
}
String stateCookieNameSuffix = configContext.oidcConfig.authentication.allowMultipleCodeFlows ? "_" + uuid : "";
createCookie(context, configContext.oidcConfig,
getStateCookieName(configContext.oidcConfig) + stateCookieNameSuffix, cookieValue, 60 * 30);
return uuid;
}
private boolean isRestorePath(Authentication auth) {
return auth.isRestorePathAfterRedirect() || !auth.redirectPath.isPresent();
}
private String encodeExtraStateValue(CodeAuthenticationStateBean extraStateValue, TenantConfigContext configContext) {
if (extraStateValue.getCodeVerifier() != null || extraStateValue.getNonce() != null) {
JsonObject json = new JsonObject();
if (extraStateValue.getCodeVerifier() != null) {
json.put(OidcConstants.PKCE_CODE_VERIFIER, extraStateValue.getCodeVerifier());
}
if (extraStateValue.getNonce() != null) {
json.put(OidcConstants.NONCE, extraStateValue.getNonce());
}
if (extraStateValue.getRestorePath() != null) {
json.put(STATE_COOKIE_RESTORE_PATH, extraStateValue.getRestorePath());
}
try {
return OidcUtils.encryptJson(json, configContext.getStateEncryptionKey());
} catch (Exception ex) {
LOG.errorf("State containing the code verifier can not be encrypted: %s", ex.getMessage());
throw new AuthenticationCompletionException(ex);
}
} else {
return extraStateValue.getRestorePath();
}
}
private String generatePostLogoutState(RoutingContext context, TenantConfigContext configContext) {
OidcUtils.removeCookie(context, configContext.oidcConfig, getPostLogoutCookieName(configContext.oidcConfig));
return createCookie(context, configContext.oidcConfig, getPostLogoutCookieName(configContext.oidcConfig),
UUID.randomUUID().toString(),
60 * 30).getValue();
}
static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig,
String name, String value, long maxAge) {
return createCookie(context, oidcConfig, name, value, maxAge, false);
}
static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcConfig,
String name, String value, long maxAge, boolean sessionCookie) {
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, auth, cookie);
if (auth.cookieDomain.isPresent()) {
cookie.setDomain(auth.getCookieDomain().get());
}
if (sessionCookie) {
cookie.setSameSite(CookieSameSite.valueOf(auth.cookieSameSite.name()));
}
context.response().addCookie(cookie);
return cookie;
}
private String buildUri(RoutingContext context, boolean forceHttps, String path) {
String authority = URI.create(context.request().absoluteURI()).getAuthority();
return buildUri(context, forceHttps, authority, path);
}
private String buildUri(RoutingContext context, boolean forceHttps, String authority, String path) {
final String scheme = forceHttps ? "https" : context.request().scheme();
String forwardedPrefix = "";
if (resolver.isEnableHttpForwardedPrefix()) {
String forwardedPrefixHeader = context.request().getHeader("X-Forwarded-Prefix");
if (forwardedPrefixHeader != null && !forwardedPrefixHeader.equals("/") && !forwardedPrefixHeader.equals("//")) {
forwardedPrefix = forwardedPrefixHeader;
if (forwardedPrefix.endsWith("/")) {
forwardedPrefix = forwardedPrefix.substring(0, forwardedPrefix.length() - 1);
}
}
}
return new StringBuilder(scheme).append("://")
.append(authority)
.append(forwardedPrefix)
.append(path)
.toString();
}
private boolean isRpInitiatedLogout(RoutingContext context, TenantConfigContext configContext) {
return isEqualToRequestPath(configContext.oidcConfig.logout.path, context, configContext);
}
private boolean isEqualToRequestPath(Optional path, RoutingContext context, TenantConfigContext configContext) {
if (path.isPresent()) {
return context.request().path().equals(path.get());
}
return false;
}
private Uni refreshSecurityIdentity(TenantConfigContext configContext, String currentIdToken,
String refreshToken,
RoutingContext context, IdentityProviderManager identityProviderManager, boolean autoRefresh,
SecurityIdentity fallback) {
Uni refreshedTokensUni = refreshTokensUni(configContext, currentIdToken, refreshToken,
autoRefresh);
return refreshedTokensUni
.onItemOrFailure()
.transformToUni(new BiFunction>() {
@Override
public Uni apply(final AuthorizationCodeTokens tokens, final Throwable t) {
if (t != null) {
LOG.debugf("ID token refresh has failed: %s", errorMessage(t));
if (autoRefresh && fallback != null) {
LOG.debug("Using the current SecurityIdentity since the ID token is still valid");
return Uni.createFrom().item(fallback);
} else {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
}
} else {
context.put(OidcConstants.ACCESS_TOKEN_VALUE, tokens.getAccessToken());
context.put(AuthorizationCodeTokens.class.getName(), tokens);
context.put(REFRESH_TOKEN_GRANT_RESPONSE, Boolean.TRUE);
// Default token state manager may have encrypted the refreshed ID token
final String idToken = decryptIdTokenIfEncryptedByProvider(configContext, tokens.getIdToken());
LOG.debug("Verifying the refreshed ID token");
return authenticate(identityProviderManager, context,
new IdTokenCredential(idToken,
isInternalIdToken(idToken, configContext)))
.call(new Function>() {
@Override
public Uni apply(SecurityIdentity identity) {
// after a successful refresh, rebuild the identity and update the cookie
return processSuccessfulAuthentication(context, configContext,
tokens, idToken, identity);
}
})
.map(new Function() {
@Override
public SecurityIdentity apply(SecurityIdentity identity) {
fireEvent(autoRefresh ? SecurityEvent.Type.OIDC_SESSION_REFRESHED
: SecurityEvent.Type.OIDC_SESSION_EXPIRED_AND_REFRESHED,
identity);
return identity;
}
}).onFailure().transform(new Function() {
@Override
public Throwable apply(Throwable tInner) {
LOG.debugf("Verifying the refreshed ID token failed %s", errorMessage(tInner));
return new AuthenticationFailedException(tInner);
}
});
}
}
});
}
private Uni refreshTokensUni(TenantConfigContext configContext,
String currentIdToken, String refreshToken, boolean autoRefresh) {
return configContext.provider.refreshTokens(refreshToken).onItem()
.transform(new Function() {
@Override
public AuthorizationCodeTokens apply(AuthorizationCodeTokens tokens) {
if (tokens.getRefreshToken() == null) {
tokens.setRefreshToken(refreshToken);
}
if (tokens.getIdToken() == null) {
if (isIdTokenRequired(configContext) || !isInternalIdToken(currentIdToken, configContext)) {
if (!autoRefresh) {
LOG.debugf(
"ID token is not returned in the refresh token grant response, re-authentication is required");
throw new AuthenticationFailedException();
} else {
// Auto-refresh is triggered while current ID token is still valid, continue using it.
tokens.setIdToken(currentIdToken);
}
} else {
tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, currentIdToken));
}
}
return tokens;
}
});
}
private Uni getCodeFlowTokensUni(RoutingContext context, TenantConfigContext configContext,
String code, String codeVerifier) {
// 'redirect_uri': typically it must match the 'redirect_uri' query parameter which was used during the code request.
String redirectPath = getRedirectPath(configContext.oidcConfig, context);
String redirectUriParam = buildUri(context, isForceHttps(configContext.oidcConfig), redirectPath);
LOG.debugf("Token request redirect_uri parameter: %s", redirectUriParam);
return configContext.provider.getCodeFlowTokens(code, redirectUriParam, codeVerifier);
}
private String buildLogoutRedirectUri(TenantConfigContext configContext, String idToken, RoutingContext context) {
String logoutPath = configContext.provider.getMetadata().getEndSessionUri();
StringBuilder logoutUri = new StringBuilder(logoutPath);
if (idToken != null || configContext.oidcConfig.logout.postLogoutPath.isPresent()) {
logoutUri.append("?");
}
if (idToken != null) {
logoutUri.append(OidcConstants.LOGOUT_ID_TOKEN_HINT).append(EQ).append(idToken);
}
if (configContext.oidcConfig.logout.postLogoutPath.isPresent()) {
logoutUri.append(AMP).append(configContext.oidcConfig.logout.getPostLogoutUriParam()).append(EQ).append(
OidcCommonUtils.urlEncode(buildUri(context, isForceHttps(configContext.oidcConfig),
configContext.oidcConfig.logout.postLogoutPath.get())));
logoutUri.append(AMP).append(OidcConstants.LOGOUT_STATE).append(EQ)
.append(generatePostLogoutState(context, configContext));
}
addExtraParamsToUri(logoutUri, configContext.oidcConfig.logout.extraParams);
return logoutUri.toString();
}
private static void addExtraParamsToUri(StringBuilder builder, Map extraParams) {
if (extraParams != null) {
for (Map.Entry entry : extraParams.entrySet()) {
if (entry.getKey().equals(OidcConstants.TOKEN_SCOPE)) {
continue;
}
builder.append(AMP).append(entry.getKey()).append(EQ).append(OidcCommonUtils.urlEncode(entry.getValue()));
}
}
}
private boolean isForceHttps(OidcTenantConfig oidcConfig) {
return oidcConfig.authentication.forceRedirectHttpsScheme.orElse(false);
}
private Uni buildLogoutRedirectUriUni(RoutingContext context, TenantConfigContext configContext,
String idToken) {
return removeSessionCookie(context, configContext.oidcConfig)
.map(new Function() {
@Override
public Void apply(Void t) {
String logoutUri = buildLogoutRedirectUri(configContext, idToken, context);
LOG.debugf("Logout uri: %s", logoutUri);
throw new AuthenticationRedirectException(logoutUri);
}
});
}
private static String getStateCookieName(OidcTenantConfig oidcConfig) {
return OidcUtils.STATE_COOKIE_NAME + getCookieSuffix(oidcConfig);
}
private static String getPostLogoutCookieName(OidcTenantConfig oidcConfig) {
return OidcUtils.POST_LOGOUT_COOKIE_NAME + getCookieSuffix(oidcConfig);
}
private static String getSessionCookieName(OidcTenantConfig oidcConfig) {
return OidcUtils.SESSION_COOKIE_NAME + getCookieSuffix(oidcConfig);
}
private Uni removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig) {
String cookieName = getSessionCookieName(oidcConfig);
return OidcUtils.removeSessionCookie(context, oidcConfig, cookieName, resolver.getTokenStateManager());
}
static String getCookieSuffix(OidcTenantConfig oidcConfig) {
String tenantId = oidcConfig.tenantId.get();
boolean cookieSuffixConfigured = oidcConfig.authentication.cookieSuffix.isPresent();
String tenantIdSuffix = (cookieSuffixConfigured || !"Default".equals(tenantId)) ? UNDERSCORE + tenantId : "";
return cookieSuffixConfigured
? (tenantIdSuffix + UNDERSCORE + oidcConfig.authentication.cookieSuffix.get())
: tenantIdSuffix;
}
private class LogoutCall implements Function> {
RoutingContext context;
TenantConfigContext configContext;
String idToken;
LogoutCall(RoutingContext context, TenantConfigContext configContext, String idToken) {
this.context = context;
this.configContext = configContext;
this.idToken = idToken;
}
@Override
public Uni apply(SecurityIdentity identity) {
if (isRpInitiatedLogout(context, configContext)) {
LOG.debug("Performing an RP initiated logout");
fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity);
return buildLogoutRedirectUriUni(context, configContext, idToken);
}
if (isBackChannelLogoutPendingAndValid(configContext, identity)
|| isFrontChannelLogoutValid(context, configContext,
identity)) {
return removeSessionCookie(context, configContext.oidcConfig)
.map(new Function() {
@Override
public Void apply(Void t) {
throw new LogoutException();
}
});
}
return VOID_UNI;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy