diff --git a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties index 43c0b8b4c2cc0..1caaf30523405 100644 --- a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties +++ b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties @@ -9,7 +9,7 @@ quarkus.oidc.application-type=web-app quarkus.oidc.logout.path=/protected/logout quarkus.oidc.authentication.pkce-required=true quarkus.log.category."org.htmlunit".level=ERROR -quarkus.log.category."io.quarkus.oidc.runtime.TenantConfigContext".level=DEBUG +quarkus.log.category."io.quarkus.oidc.runtime.TenantConfigContextImpl".level=DEBUG quarkus.log.file.enable=true # use blocking DNS lookup so that we have it tested somewhere diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java index 9c204af34a12a..7e1a7ebf91006 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java @@ -87,7 +87,7 @@ public void accept(MultiMap form) { try { // Do the general validation of the logout token now, compare with the IDToken later // Check the signature, as well the issuer and audience if it is configured - TokenVerificationResult result = tenantContext.provider + TokenVerificationResult result = tenantContext.provider() .verifyLogoutJwtToken(encodedLogoutToken); if (verifyLogoutTokenClaims(result)) { @@ -162,9 +162,9 @@ private TenantConfigContext getTenantConfigContext(final String requestPath) { } private boolean isMatchingTenant(String requestPath, TenantConfigContext tenant) { - return tenant.oidcConfig.isTenantEnabled() - && tenant.oidcConfig.getTenantId().get().equals(oidcTenantConfig.getTenantId().get()) - && requestPath.equals(getRootPath() + tenant.oidcConfig.logout.backchannel.path.orElse(null)); + return tenant.oidcConfig().isTenantEnabled() + && tenant.oidcConfig().getTenantId().get().equals(oidcTenantConfig.getTenantId().get()) + && requestPath.equals(getRootPath() + tenant.oidcConfig().logout.backchannel.path.orElse(null)); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java index b93c506c18ed2..2be2a0dc44567 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BearerAuthenticationMechanism.java @@ -37,7 +37,7 @@ public Uni getChallenge(RoutingContext context) { @Override public Uni apply(TenantConfigContext tenantContext) { return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), - HttpHeaderNames.WWW_AUTHENTICATE, tenantContext.oidcConfig.token.authorizationScheme)); + HttpHeaderNames.WWW_AUTHENTICATE, tenantContext.oidcConfig().token.authorizationScheme)); } }); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index de2d31f987103..97e002f4b65f1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -335,14 +335,14 @@ private Uni reAuthenticate(String sessionCookie, TenantConfigContext configContext) { context.put(TenantConfigContext.class.getName(), configContext); - return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, + return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig(), sessionCookie, getTokenStateRequestContext) .onFailure(AuthenticationCompletionException.class) .recoverWithUni( new Function>() { @Override public Uni apply(Throwable t) { - return removeSessionCookie(context, configContext.oidcConfig) + return removeSessionCookie(context, configContext.oidcConfig()) .replaceWith(Uni.createFrom().failure(t)); } }) @@ -376,7 +376,7 @@ public Uni apply(Throwable t) { if (!expired) { String error = logAuthenticationError(context, t); - return removeSessionCookie(context, configContext.oidcConfig) + return removeSessionCookie(context, configContext.oidcConfig()) .replaceWith(Uni.createFrom() .failure(t .getCause() instanceof AuthenticationCompletionException @@ -393,7 +393,7 @@ public Uni apply(Throwable t) { .call(() -> buildLogoutRedirectUriUni(context, configContext, currentIdToken)); } - if (!configContext.oidcConfig.token.refreshExpired) { + if (!configContext.oidcConfig().token.refreshExpired) { LOG.debug( "Token has expired, token refresh is not allowed, redirecting to re-authenticate"); return refreshIsNotPossible(context, configContext, t); @@ -455,7 +455,7 @@ public Uni apply(Throwable t) { private Uni refreshIsNotPossible(RoutingContext context, TenantConfigContext configContext, Throwable t) { - if (configContext.oidcConfig.authentication.getSessionExpiredPath() + if (configContext.oidcConfig().authentication.getSessionExpiredPath() .isPresent()) { return redirectToSessionExpiredPage(context, configContext); } @@ -476,23 +476,24 @@ private Uni autoRefreshIsNotPossible(RoutingContext context, T private Uni redirectToSessionExpiredPage(RoutingContext context, TenantConfigContext configContext) { URI absoluteUri = URI.create(context.request().absoluteURI()); StringBuilder sessionExpired = new StringBuilder(buildUri(context, - isForceHttps(configContext.oidcConfig), + isForceHttps(configContext.oidcConfig()), absoluteUri.getAuthority(), - configContext.oidcConfig.authentication.getSessionExpiredPath().get())); + configContext.oidcConfig().authentication.getSessionExpiredPath().get())); String sessionExpiredUri = sessionExpired.toString(); LOG.debugf("Session Expired URI: %s", sessionExpiredUri); - return removeSessionCookie(context, configContext.oidcConfig) + return removeSessionCookie(context, configContext.oidcConfig()) .chain(() -> Uni.createFrom().failure(new AuthenticationRedirectException( filterRedirect(context, configContext, sessionExpiredUri, Redirect.Location.SESSION_EXPIRED_PAGE)))); } private static String decryptIdTokenIfEncryptedByProvider(TenantConfigContext resolvedContext, String token) { - if ((resolvedContext.provider.tokenDecryptionKey != null || resolvedContext.provider.client.getClientJwtKey() != null) + 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(), + 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()); @@ -507,15 +508,16 @@ private boolean isLogout(RoutingContext context, TenantConfigContext configConte } private boolean isBackChannelLogoutPending(TenantConfigContext configContext, SecurityIdentity identity) { - if (configContext.oidcConfig.logout.backchannel.path.isEmpty()) { + if (configContext.oidcConfig().logout.backchannel.path.isEmpty()) { return false; } BackChannelLogoutTokenCache tokens = resolver.getBackChannelLogoutTokens() - .get(configContext.oidcConfig.getTenantId().get()); + .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()); + String logoutTokenKeyValue = idTokenJson + .getString(configContext.oidcConfig().logout.backchannel.getLogoutTokenKey()); return tokens.containsTokenVerification(logoutTokenKeyValue); } @@ -523,15 +525,16 @@ private boolean isBackChannelLogoutPending(TenantConfigContext configContext, Se } private boolean isBackChannelLogoutPendingAndValid(TenantConfigContext configContext, SecurityIdentity identity) { - if (configContext.oidcConfig.logout.backchannel.path.isEmpty()) { + if (configContext.oidcConfig().logout.backchannel.path.isEmpty()) { return false; } BackChannelLogoutTokenCache tokens = resolver.getBackChannelLogoutTokens() - .get(configContext.oidcConfig.getTenantId().get()); + .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()); + String logoutTokenKeyValue = idTokenJson + .getString(configContext.oidcConfig().logout.backchannel.getLogoutTokenKey()); TokenVerificationResult backChannelLogoutTokenResult = tokens.removeTokenVerification(logoutTokenKeyValue); if (backChannelLogoutTokenResult == null) { @@ -558,7 +561,7 @@ private boolean isBackChannelLogoutPendingAndValid(TenantConfigContext configCon return false; } LOG.debugf("Backchannel logout request for the tenant %s has been completed", - configContext.oidcConfig.tenantId.get()); + configContext.oidcConfig().tenantId.get()); fireEvent(SecurityEvent.Type.OIDC_BACKCHANNEL_LOGOUT_COMPLETED, identity); @@ -569,7 +572,7 @@ private boolean isBackChannelLogoutPendingAndValid(TenantConfigContext configCon private boolean isFrontChannelLogoutValid(RoutingContext context, TenantConfigContext configContext, SecurityIdentity identity) { - if (isEqualToRequestPath(configContext.oidcConfig.logout.frontchannel.path, context, configContext)) { + if (isEqualToRequestPath(configContext.oidcConfig().logout.frontchannel.path, context, configContext)) { JsonObject idTokenJson = OidcUtils.decodeJwtContent(((JsonWebToken) (identity.getPrincipal())).getRawToken()); String idTokenIss = idTokenJson.getString(Claims.iss.name()); @@ -585,7 +588,7 @@ private boolean isFrontChannelLogoutValid(RoutingContext context, TenantConfigCo return false; } LOG.debugf("Frontchannel logout request for the tenant %s has been completed", - configContext.oidcConfig.tenantId.get()); + configContext.oidcConfig().tenantId.get()); fireEvent(SecurityEvent.Type.OIDC_FRONTCHANNEL_LOGOUT_COMPLETED, identity); return true; } @@ -593,7 +596,7 @@ private boolean isFrontChannelLogoutValid(RoutingContext context, TenantConfigCo } private boolean isInternalIdToken(String idToken, TenantConfigContext configContext) { - if (!configContext.oidcConfig.authentication.idTokenRequired.orElse(true)) { + if (!configContext.oidcConfig().authentication.idTokenRequired.orElse(true)) { JsonObject headers = OidcUtils.decodeJwtHeaders(idToken); if (headers != null) { return headers.getBoolean(INTERNAL_IDTOKEN_HEADER, false); @@ -603,7 +606,7 @@ private boolean isInternalIdToken(String idToken, TenantConfigContext configCont } private boolean isIdTokenRequired(TenantConfigContext configContext) { - return configContext.oidcConfig.authentication.isIdTokenRequired().orElse(true); + return configContext.oidcConfig().authentication.isIdTokenRequired().orElse(true); } private boolean isJavaScript(RoutingContext context) { @@ -620,7 +623,7 @@ private boolean isJavaScript(RoutingContext context) { // 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; + return isJavaScript(context) ? configContext.oidcConfig().authentication.javaScriptAutoRedirect : true; } public Uni getChallenge(RoutingContext context) { @@ -635,12 +638,12 @@ public Uni apply(TenantConfigContext tenantContext) { } public Uni getChallengeInternal(RoutingContext context, TenantConfigContext configContext) { - LOG.debugf("Starting an authentication challenge for tenant %s.", configContext.oidcConfig.tenantId.get()); - if (configContext.oidcConfig.clientName.isPresent()) { - LOG.debugf(" Client name: %s", configContext.oidcConfig.clientName.get()); + LOG.debugf("Starting an authentication challenge for tenant %s.", configContext.oidcConfig().tenantId.get()); + if (configContext.oidcConfig().clientName.isPresent()) { + LOG.debugf(" Client name: %s", configContext.oidcConfig().clientName.get()); } - OidcTenantConfig sessionCookieConfig = configContext.oidcConfig; + OidcTenantConfig sessionCookieConfig = configContext.oidcConfig(); String sessionTenantIdSetByCookie = context.get(OidcUtils.TENANT_ID_SET_BY_SESSION_COOKIE); if (sessionTenantIdSetByCookie != null @@ -680,25 +683,25 @@ && isRedirectFromProvider(context, configContext)) { .append(OidcConstants.CODE_FLOW_CODE); // response_mode - if (ResponseMode.FORM_POST == configContext.oidcConfig.authentication.responseMode + 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() + .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())); + .append(OidcCommonUtils.urlEncode(configContext.oidcConfig().clientId.get())); // scope codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ) - .append(OidcUtils.encodeScopes(configContext.oidcConfig)); + .append(OidcUtils.encodeScopes(configContext.oidcConfig())); MultiMap requestQueryParams = null; - if (!configContext.oidcConfig.getAuthentication().forwardParams.isEmpty()) { + if (!configContext.oidcConfig().getAuthentication().forwardParams.isEmpty()) { requestQueryParams = context.queryParams(); - for (String forwardedParam : configContext.oidcConfig.getAuthentication().forwardParams.get()) { + 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) @@ -709,8 +712,8 @@ && isRedirectFromProvider(context, configContext)) { } // redirect_uri - String redirectPath = getRedirectPath(configContext.oidcConfig, context); - String redirectUriParam = buildUri(context, isForceHttps(configContext.oidcConfig), redirectPath); + 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) @@ -720,7 +723,7 @@ && isRedirectFromProvider(context, configContext)) { PkceStateBean pkceStateBean = createPkceStateBean(configContext); // state - String nonce = configContext.oidcConfig.authentication.nonceRequired ? UUID.randomUUID().toString() + String nonce = configContext.oidcConfig().authentication.nonceRequired ? UUID.randomUUID().toString() : null; codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_STATE).append(EQ) @@ -741,9 +744,9 @@ && isRedirectFromProvider(context, configContext)) { } // extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests - addExtraParamsToUri(codeFlowParams, configContext.oidcConfig.authentication.getExtraParams()); + addExtraParamsToUri(codeFlowParams, configContext.oidcConfig().authentication.getExtraParams()); - String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?" + String authorizationURL = configContext.provider().getMetadata().getAuthorizationUri() + "?" + codeFlowParams.toString(); authorizationURL = filterRedirect(context, configContext, authorizationURL, @@ -763,11 +766,11 @@ private boolean isRedirectFromProvider(RoutingContext context, TenantConfigConte // during the redirect back to Quarkus. String referer = context.request().getHeader(HttpHeaders.REFERER); - return referer != null && referer.startsWith(configContext.provider.getMetadata().getAuthorizationUri()); + return referer != null && referer.startsWith(configContext.provider().getMetadata().getAuthorizationUri()); } private PkceStateBean createPkceStateBean(TenantConfigContext configContext) { - if (configContext.oidcConfig.authentication.pkceRequired.orElse(false)) { + if (configContext.oidcConfig().authentication.pkceRequired.orElse(false)) { PkceStateBean bean = new PkceStateBean(); Encoder encoder = Base64.getUrlEncoder().withoutPadding(); @@ -806,7 +809,7 @@ private Uni performCodeFlow(IdentityProviderManager identityPr String restorePath = stateBean.getRestorePath(); int userQueryIndex = restorePath.indexOf("?"); if (userQueryIndex >= 0) { - userPath = isRestorePath(configContext.oidcConfig.authentication) ? restorePath.substring(0, userQueryIndex) + userPath = isRestorePath(configContext.oidcConfig().authentication) ? restorePath.substring(0, userQueryIndex) : null; if (userQueryIndex + 1 < restorePath.length()) { userQuery = restorePath.substring(userQueryIndex + 1); @@ -846,7 +849,7 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T internalIdToken = true; } } else { - if (!prepareNonceForVerification(context, configContext.oidcConfig, stateBean, + if (!prepareNonceForVerification(context, configContext.oidcConfig(), stateBean, tokens.getIdToken())) { return Uni.createFrom().failure(new AuthenticationCompletionException()); } @@ -867,7 +870,7 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T @Override public Uni apply(SecurityIdentity identity) { if (internalIdToken - && OidcUtils.cacheUserInfoInIdToken(resolver, configContext.oidcConfig)) { + && OidcUtils.cacheUserInfoInIdToken(resolver, configContext.oidcConfig())) { tokens.setIdToken(generateInternalIdToken(configContext, identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null, tokens.getAccessTokenExpiresIn())); @@ -879,7 +882,7 @@ public Uni apply(SecurityIdentity identity) { .map(new Function() { @Override public SecurityIdentity apply(SecurityIdentity identity) { - boolean removeRedirectParams = configContext.oidcConfig.authentication + boolean removeRedirectParams = configContext.oidcConfig().authentication .isRemoveRedirectParameters(); if (removeRedirectParams || finalUserPath != null || finalUserQuery != null) { @@ -887,7 +890,7 @@ public SecurityIdentity apply(SecurityIdentity identity) { URI absoluteUri = URI.create(context.request().absoluteURI()); StringBuilder finalUriWithoutQuery = new StringBuilder(buildUri(context, - isForceHttps(configContext.oidcConfig), + isForceHttps(configContext.oidcConfig()), absoluteUri.getAuthority(), (finalUserPath != null ? finalUserPath : absoluteUri.getRawPath()))); @@ -895,7 +898,7 @@ public SecurityIdentity apply(SecurityIdentity identity) { if (!removeRedirectParams) { finalUriWithoutQuery.append('?') .append(getRequestParametersAsQuery(absoluteUri, requestParams, - configContext.oidcConfig)); + configContext.oidcConfig())); } if (finalUserQuery != null) { finalUriWithoutQuery.append(!removeRedirectParams ? "" : "?"); @@ -969,7 +972,7 @@ private CodeAuthenticationStateBean getCodeAuthenticationBean(String[] parsedSta TenantConfigContext configContext) { if (parsedStateCookieValue.length == 2) { CodeAuthenticationStateBean bean = new CodeAuthenticationStateBean(); - Authentication authentication = configContext.oidcConfig.authentication; + Authentication authentication = configContext.oidcConfig().authentication; boolean pkceRequired = authentication.pkceRequired.orElse(false); if (!pkceRequired && !authentication.nonceRequired) { @@ -984,7 +987,7 @@ private CodeAuthenticationStateBean getCodeAuthenticationBean(String[] parsedSta 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()); + configContext.oidcConfig().tenantId.get()); throw new AuthenticationCompletionException(ex); } bean.setRestorePath(json.getString(OidcUtils.STATE_COOKIE_RESTORE_PATH)); @@ -1012,22 +1015,23 @@ private String generateInternalIdToken(TenantConfigContext context, UserInfo use if (userInfo != null) { builder.claim(OidcUtils.USER_INFO_ATTRIBUTE, userInfo.getJsonObject()); } - if (context.oidcConfig.authentication.internalIdTokenLifespan.isPresent()) { - builder.expiresIn(context.oidcConfig.authentication.internalIdTokenLifespan.get().getSeconds()); + if (context.oidcConfig().authentication.internalIdTokenLifespan.isPresent()) { + builder.expiresIn(context.oidcConfig().authentication.internalIdTokenLifespan.get().getSeconds()); } else if (accessTokenExpiresInSecs != null) { builder.expiresIn(accessTokenExpiresInSecs); } - builder.audience(context.oidcConfig.getClientId().get()); + builder.audience(context.oidcConfig().getClientId().get()); JwtSignatureBuilder sigBuilder = builder.jws().header(INTERNAL_IDTOKEN_HEADER, true); - String clientOrJwtSecret = OidcCommonUtils.getClientOrJwtSecret(context.oidcConfig.credentials); + String clientOrJwtSecret = OidcCommonUtils.getClientOrJwtSecret(context.oidcConfig().credentials); if (clientOrJwtSecret != null) { LOG.debug("Signing internal ID token with a configured client secret"); return sigBuilder.sign(KeyUtils.createSecretKeyFromSecret(clientOrJwtSecret)); - } else if (context.provider.client.getClientJwtKey() instanceof PrivateKey) { + } else if (context.provider().client.getClientJwtKey() instanceof PrivateKey) { LOG.debug("Signing internal ID token with a configured JWT private key"); - return sigBuilder.sign(OidcUtils.createSecretKeyFromDigest(((PrivateKey) context.provider.client.getClientJwtKey()) - .getEncoded())); + return sigBuilder + .sign(OidcUtils.createSecretKeyFromDigest(((PrivateKey) context.provider().client.getClientJwtKey()) + .getEncoded())); } else { LOG.debug("Signing internal ID token with a generated secret key"); return sigBuilder.sign(context.getInternalIdTokenSecretKey()); @@ -1040,7 +1044,7 @@ private Uni processSuccessfulAuthentication(RoutingContext context, 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) + return removeSessionCookie(context, configContext.oidcConfig()) .chain(new Function>() { @Override @@ -1054,27 +1058,27 @@ public Uni apply(Void t) { } 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.lifespanGrace.isPresent()) { + maxAge += configContext.oidcConfig().token.lifespanGrace.getAsInt(); } - if (configContext.oidcConfig.token.refreshExpired && tokens.getRefreshToken() != null) { - maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds(); + if (configContext.oidcConfig().token.refreshExpired && tokens.getRefreshToken() != null) { + 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()); + resolver.getBackChannelLogoutTokens().remove(configContext.oidcConfig().tenantId.get()); return resolver.getTokenStateManager() - .createTokenState(context, configContext.oidcConfig, tokens, createTokenStateRequestContext) + .createTokenState(context, configContext.oidcConfig(), tokens, createTokenStateRequestContext) .map(new Function() { @Override public Void apply(String cookieValue) { - String sessionName = OidcUtils.getSessionCookieName(configContext.oidcConfig); + String sessionName = OidcUtils.getSessionCookieName(configContext.oidcConfig()); LOG.debugf("Session cookie length for the tenant %s is %d bytes.", - configContext.oidcConfig.tenantId.get(), cookieValue.length()); + configContext.oidcConfig().tenantId.get(), cookieValue.length()); if (cookieValue.length() > OidcUtils.MAX_COOKIE_VALUE_LENGTH) { LOG.debugf( "Session cookie length for the tenant %s is greater than %d bytes." @@ -1089,7 +1093,7 @@ public Void apply(String cookieValue) { + " but only if it is considered to be safe in your application's network." + " 5. Use the 'quarkus-oidc-db-token-state-manager' extension or register a custom 'quarkus.oidc.TokenStateManager'" + " CDI bean with the alternative priority set to 1 and save the tokens on the server.", - configContext.oidcConfig.tenantId.get(), + configContext.oidcConfig().tenantId.get(), OidcUtils.MAX_COOKIE_VALUE_LENGTH); for (int sessionIndex = 1, currentPos = 0; currentPos < cookieValue.length(); sessionIndex++) { @@ -1101,12 +1105,12 @@ public Void apply(String cookieValue) { String nextName = sessionName + OidcUtils.SESSION_COOKIE_CHUNK + sessionIndex; LOG.debugf("Creating the %s session cookie chunk, size: %d", nextName, nextValue.length()); - createCookie(context, configContext.oidcConfig, nextName, nextValue, + createCookie(context, configContext.oidcConfig(), nextName, nextValue, sessionMaxAge, true); currentPos = nextPos; } } else { - createCookie(context, configContext.oidcConfig, sessionName, cookieValue, + createCookie(context, configContext.oidcConfig(), sessionName, cookieValue, sessionMaxAge, true); } fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity); @@ -1142,7 +1146,7 @@ private String generateCodeFlowState(RoutingContext context, TenantConfigContext String uuid = UUID.randomUUID().toString(); String cookieValue = uuid; - Authentication authentication = configContext.oidcConfig.getAuthentication(); + Authentication authentication = configContext.oidcConfig().getAuthentication(); boolean restorePath = isRestorePath(authentication); if (restorePath || pkceCodeVerifier != null || nonce != null) { CodeAuthenticationStateBean extraStateValue = new CodeAuthenticationStateBean(); @@ -1183,10 +1187,10 @@ private String generateCodeFlowState(RoutingContext context, TenantConfigContext 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, - configContext.oidcConfig.authentication.stateCookieAge.toSeconds()); + String stateCookieNameSuffix = configContext.oidcConfig().authentication.allowMultipleCodeFlows ? "_" + uuid : ""; + createCookie(context, configContext.oidcConfig(), + getStateCookieName(configContext.oidcConfig()) + stateCookieNameSuffix, cookieValue, + configContext.oidcConfig().authentication.stateCookieAge.toSeconds()); return uuid; } @@ -1222,8 +1226,8 @@ private String encodeExtraStateValue(CodeAuthenticationStateBean extraStateValue } private String generatePostLogoutState(RoutingContext context, TenantConfigContext configContext) { - OidcUtils.removeCookie(context, configContext.oidcConfig, getPostLogoutCookieName(configContext.oidcConfig)); - return createCookie(context, configContext.oidcConfig, getPostLogoutCookieName(configContext.oidcConfig), + OidcUtils.removeCookie(context, configContext.oidcConfig(), getPostLogoutCookieName(configContext.oidcConfig())); + return createCookie(context, configContext.oidcConfig(), getPostLogoutCookieName(configContext.oidcConfig()), UUID.randomUUID().toString(), 60 * 30).getValue(); } @@ -1271,7 +1275,7 @@ private String buildUri(RoutingContext context, boolean forceHttps, String autho } private boolean isRpInitiatedLogout(RoutingContext context, TenantConfigContext configContext) { - return isEqualToRequestPath(configContext.oidcConfig.logout.path, context, configContext); + return isEqualToRequestPath(configContext.oidcConfig().logout.path, context, configContext); } private boolean isEqualToRequestPath(Optional path, RoutingContext context, TenantConfigContext configContext) { @@ -1305,7 +1309,7 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T } else { return Uni.createFrom().failure(new AuthenticationFailedException(t)); } - } else if (configContext.oidcConfig.authentication.getSessionExpiredPath().isPresent()) { + } else if (configContext.oidcConfig().authentication.getSessionExpiredPath().isPresent()) { // Token has expired but the refresh does not work, check if the session expired page is available return redirectToSessionExpiredPage(context, configContext); } @@ -1354,7 +1358,7 @@ public Throwable apply(Throwable tInner) { private Uni refreshTokensUni(TenantConfigContext configContext, String currentIdToken, String refreshToken, boolean autoRefresh) { - return configContext.provider.refreshTokens(refreshToken).onItem() + return configContext.provider().refreshTokens(refreshToken).onItem() .transform(new Function() { @Override public AuthorizationCodeTokens apply(AuthorizationCodeTokens tokens) { @@ -1389,10 +1393,10 @@ private Uni getCodeFlowTokensUni(RoutingContext context String code, String codeVerifier) { // 'redirect_uri': it must match the 'redirect_uri' query parameter which was used during the code request. - Optional configuredRedirectPath = configContext.oidcConfig.authentication.redirectPath; + Optional configuredRedirectPath = configContext.oidcConfig().authentication.redirectPath; if (configuredRedirectPath.isPresent()) { String requestPath = configuredRedirectPath.get().startsWith(HTTP_SCHEME) - ? buildUri(context, configContext.oidcConfig.authentication.forceRedirectHttpsScheme.orElse(false), + ? buildUri(context, configContext.oidcConfig().authentication.forceRedirectHttpsScheme.orElse(false), context.request().path()) : context.request().path(); if (!configuredRedirectPath.get().equals(requestPath)) { @@ -1401,32 +1405,32 @@ private Uni getCodeFlowTokensUni(RoutingContext context } } - String redirectPath = getRedirectPath(configContext.oidcConfig, context); - String redirectUriParam = buildUri(context, isForceHttps(configContext.oidcConfig), redirectPath); + 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); + return configContext.provider().getCodeFlowTokens(code, redirectUriParam, codeVerifier); } private String buildLogoutRedirectUri(TenantConfigContext configContext, String idToken, RoutingContext context) { - String logoutPath = configContext.provider.getMetadata().getEndSessionUri(); + String logoutPath = configContext.provider().getMetadata().getEndSessionUri(); StringBuilder logoutUri = new StringBuilder(logoutPath); - if (idToken != null || configContext.oidcConfig.logout.postLogoutPath.isPresent()) { + 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()))); + 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); + addExtraParamsToUri(logoutUri, configContext.oidcConfig().logout.extraParams); return logoutUri.toString(); } @@ -1448,7 +1452,7 @@ private boolean isForceHttps(OidcTenantConfig oidcConfig) { private Uni buildLogoutRedirectUriUni(RoutingContext context, TenantConfigContext configContext, String idToken) { - return removeSessionCookie(context, configContext.oidcConfig) + return removeSessionCookie(context, configContext.oidcConfig()) .map(new Function() { @Override public Void apply(Void t) { @@ -1493,7 +1497,7 @@ public Uni apply(SecurityIdentity identity) { if (isBackChannelLogoutPendingAndValid(configContext, identity) || isFrontChannelLogoutValid(context, configContext, identity)) { - return removeSessionCookie(context, configContext.oidcConfig) + return removeSessionCookie(context, configContext.oidcConfig()) .map(new Function() { @Override public Void apply(Void t) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java index 28d79053b5132..15ae16cca5ece 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java @@ -1,14 +1,8 @@ package io.quarkus.oidc.runtime; -import static io.quarkus.oidc.runtime.OidcProvider.ANY_ISSUER; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import java.util.function.Supplier; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; @@ -18,7 +12,6 @@ import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.jwt.Claims; import org.jboss.logging.Logger; import io.quarkus.oidc.JavaScriptRequestChecker; @@ -34,7 +27,6 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; import io.quarkus.security.spi.runtime.SecurityEventHelper; -import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -49,9 +41,9 @@ public class DefaultTenantConfigResolver { private final BlockingTaskRunner blockingRequestContext; private final boolean securityEventObserved; private final TenantConfigBean tenantConfigBean; - private final TenantResolver[] staticTenantResolvers; private final boolean annotationBasedTenantResolutionEnabled; private final String rootPath; + private final StaticTenantResolver staticTenantResolver; @Inject Instance tenantConfigResolver; @@ -84,10 +76,10 @@ public class DefaultTenantConfigResolver { this.securityEventObserved = SecurityEventHelper.isEventObserved(new SecurityEvent(null, (SecurityIdentity) null), beanManager, securityEventsEnabled); this.tenantConfigBean = tenantConfigBean; - this.staticTenantResolvers = prepareStaticTenantResolvers(tenantConfigBean, rootPath, tenantResolverInstance, - resolveTenantsWithIssuer, new DefaultStaticTenantResolver()); this.annotationBasedTenantResolutionEnabled = Boolean.getBoolean(OidcUtils.ANNOTATION_BASED_TENANT_RESOLUTION_ENABLED); this.rootPath = rootPath; + this.staticTenantResolver = new StaticTenantResolver(tenantConfigBean, rootPath, resolveTenantsWithIssuer, + tenantResolverInstance); } @PostConstruct @@ -111,30 +103,23 @@ public void verifyResolvers() { Uni resolveConfig(RoutingContext context) { return getDynamicTenantConfig(context) - .map(new Function() { + .flatMap(new Function>() { @Override - public OidcTenantConfig apply(OidcTenantConfig tenantConfig) { - if (tenantConfig == null) { - - final String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE); - - if (tenantId != null && !isTenantSetByAnnotation(context, tenantId)) { - TenantConfigContext tenantContext = tenantConfigBean.getDynamicTenantsConfig().get(tenantId); - if (tenantContext != null) { - // Dynamic map may contain the static contexts initialized on demand, - if (tenantConfigBean.getStaticTenantsConfig().containsKey(tenantId)) { - context.put(CURRENT_STATIC_TENANT_ID, tenantId); - } - return tenantContext.getOidcTenantConfig(); - } - } + public Uni apply(OidcTenantConfig oidcTenantConfig) { + if (oidcTenantConfig != null) { + return Uni.createFrom().item(oidcTenantConfig); + } + final String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE); - TenantConfigContext tenant = getStaticTenantContext(context); - if (tenant != null) { - tenantConfig = tenant.oidcConfig; + if (tenantId != null && !isTenantSetByAnnotation(context, tenantId)) { + TenantConfigContext tenantContext = tenantConfigBean.getDynamicTenantsConfig().get(tenantId); + if (tenantContext != null) { + return Uni.createFrom().item(tenantContext.getOidcTenantConfig()); } } - return tenantConfig; + + return getStaticTenantContext(context) + .onItem().ifNotNull().transform(TenantConfigContext::getOidcTenantConfig); } }); } @@ -144,60 +129,66 @@ Uni resolveContext(String tenantId) { } Uni resolveContext(RoutingContext context) { - return getDynamicTenantContext(context).onItem().ifNull().switchTo(new Supplier>() { - @Override - public Uni get() { - return initializeStaticTenantIfContextNotReady(getStaticTenantContext(context)); - } - }); + return getDynamicTenantContext(context) + .flatMap(new Function>() { + @Override + public Uni apply(TenantConfigContext tenantConfigContext) { + if (tenantConfigContext != null) { + return Uni.createFrom().item(tenantConfigContext); + } + return getStaticTenantContext(context) + .flatMap(DefaultTenantConfigResolver.this::initializeStaticTenantIfContextNotReady); + } + }); } private Uni initializeStaticTenantIfContextNotReady(TenantConfigContext tenantContext) { - if (tenantContext != null && !tenantContext.ready) { - - // check if the connection has already been created - TenantConfigContext readyTenantContext = tenantConfigBean.getDynamicTenantsConfig() - .get(tenantContext.oidcConfig.tenantId.get()); - if (readyTenantContext == null) { - LOG.debugf("Tenant '%s' is not initialized yet, trying to create OIDC connection now", - tenantContext.oidcConfig.tenantId.get()); - return tenantConfigBean.getTenantConfigContextFactory().apply(tenantContext.oidcConfig); - } else { - tenantContext = readyTenantContext; - } + if (tenantContext != null && !tenantContext.ready()) { + return tenantContext.initialize(); } return Uni.createFrom().item(tenantContext); } - private TenantConfigContext getStaticTenantContext(RoutingContext context) { + private Uni getStaticTenantContext(RoutingContext context) { String tenantId = context.get(CURRENT_STATIC_TENANT_ID); - if (tenantId == null && context.get(CURRENT_STATIC_TENANT_ID_NULL) == null) { - tenantId = resolveStaticTenantId(context); - if (tenantId != null) { - context.put(CURRENT_STATIC_TENANT_ID, tenantId); - } else { - context.put(CURRENT_STATIC_TENANT_ID_NULL, true); - } + if (tenantId != null) { + return Uni.createFrom().item(getStaticTenantContext(tenantId)); + } + + if (context.get(CURRENT_STATIC_TENANT_ID_NULL) == null) { + return resolveStaticTenantId(context) + .map(new Function() { + @Override + public TenantConfigContext apply(String tenantId) { + if (tenantId != null) { + context.put(CURRENT_STATIC_TENANT_ID, tenantId); + } else { + context.put(CURRENT_STATIC_TENANT_ID_NULL, true); + } + return getStaticTenantContext(tenantId); + } + }); } - return getStaticTenantContext(tenantId); + return Uni.createFrom().item(getStaticTenantContext((String) null)); } - private String resolveStaticTenantId(RoutingContext context) { + private Uni resolveStaticTenantId(RoutingContext context) { String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE); if (isTenantSetByAnnotation(context, tenantId)) { - return tenantId; + return Uni.createFrom().item(tenantId); } - for (var staticTenantResolver : staticTenantResolvers) { - tenantId = staticTenantResolver.resolve(context); - if (tenantId != null) { + return staticTenantResolver.resolve(context).map(new Function() { + @Override + public String apply(String tenantId) { + if (tenantId == null) { + return context.get(OidcUtils.TENANT_ID_ATTRIBUTE); + } return tenantId; } - } - - return context.get(OidcUtils.TENANT_ID_ATTRIBUTE); + }); } private boolean isTenantSetByAnnotation(RoutingContext context, String tenantId) { @@ -308,99 +299,6 @@ public JavaScriptRequestChecker getJavaScriptRequestChecker() { return javaScriptRequestChecker.isResolvable() ? javaScriptRequestChecker.get() : null; } - private static TenantResolver[] prepareStaticTenantResolvers(TenantConfigBean tenantConfigBean, String rootPath, - Instance tenantResolverInstance, boolean resolveTenantsWithIssuer, - TenantResolver defaultStaticTenantResolver) { - List staticTenantResolvers = new ArrayList<>(); - // STATIC TENANT RESOLVERS BY PRIORITY: - // 0. annotation based resolver - - // 1. custom tenant resolver - if (tenantResolverInstance.isResolvable()) { - if (tenantResolverInstance.isAmbiguous()) { - throw new IllegalStateException("Multiple " + TenantResolver.class + " beans registered"); - } - staticTenantResolvers.add(tenantResolverInstance.get()); - } - - // 2. path-matching tenant resolver - var pathMatchingTenantResolver = PathMatchingTenantResolver.of(tenantConfigBean.getStaticTenantsConfig(), rootPath, - tenantConfigBean.getDefaultTenant()); - if (pathMatchingTenantResolver != null) { - staticTenantResolvers.add(pathMatchingTenantResolver); - } - - // 3. default static tenant resolver - if (!tenantConfigBean.getStaticTenantsConfig().isEmpty()) { - staticTenantResolvers.add(defaultStaticTenantResolver); - } - - // 4. issuer-based tenant resolver - if (resolveTenantsWithIssuer) { - IssuerBasedTenantResolver.addIssuerBasedTenantResolver(staticTenantResolvers, - tenantConfigBean.getStaticTenantsConfig(), tenantConfigBean.getDefaultTenant()); - } - - return staticTenantResolvers.toArray(new TenantResolver[0]); - } - - private class DefaultStaticTenantResolver implements TenantResolver { - - @Override - public String resolve(RoutingContext context) { - String[] pathSegments = context.request().path().split("/"); - if (pathSegments.length > 0) { - String lastPathSegment = pathSegments[pathSegments.length - 1]; - if (tenantConfigBean.getStaticTenantsConfig().containsKey(lastPathSegment)) { - LOG.debugf( - "Tenant id '%s' is selected on the '%s' request path", lastPathSegment, context.normalizedPath()); - return lastPathSegment; - } - } - return null; - } - } - - private static class PathMatchingTenantResolver implements TenantResolver { - private static final String DEFAULT_TENANT = "PathMatchingTenantResolver#DefaultTenant"; - private final ImmutablePathMatcher staticTenantPaths; - - private PathMatchingTenantResolver(ImmutablePathMatcher staticTenantPaths) { - this.staticTenantPaths = staticTenantPaths; - } - - private static PathMatchingTenantResolver of(Map staticTenantsConfig, String rootPath, - TenantConfigContext defaultTenant) { - final var builder = ImmutablePathMatcher. builder().rootPath(rootPath); - addPath(DEFAULT_TENANT, defaultTenant.oidcConfig, builder); - for (Map.Entry e : staticTenantsConfig.entrySet()) { - addPath(e.getKey(), e.getValue().oidcConfig, builder); - } - return builder.hasPaths() ? new PathMatchingTenantResolver(builder.build()) : null; - } - - @Override - public String resolve(RoutingContext context) { - String tenantId = staticTenantPaths.match(context.normalizedPath()).getValue(); - if (tenantId != null) { - LOG.debugf( - "Tenant id '%s' is selected on the '%s' request path", tenantId, context.normalizedPath()); - return tenantId; - } - return null; - } - - private static ImmutablePathMatcher.ImmutablePathMatcherBuilder addPath(String tenant, OidcTenantConfig config, - ImmutablePathMatcher.ImmutablePathMatcherBuilder builder) { - if (config != null && config.tenantPaths.isPresent()) { - for (String path : config.tenantPaths.get()) { - builder.addPath(path, tenant); - } - } - return builder; - } - } - public OidcTenantConfig getResolvedConfig(String sessionTenantId) { if (OidcUtils.DEFAULT_TENANT_ID.equals(sessionTenantId)) { return tenantConfigBean.getDefaultTenant().getOidcTenantConfig(); @@ -420,82 +318,4 @@ public String getRootPath() { return rootPath; } - private static final class IssuerBasedTenantResolver implements TenantResolver { - - private final TenantConfigContext[] tenantConfigContexts; - private final boolean detectedTenantWithoutMetadata; - - private IssuerBasedTenantResolver(TenantConfigContext[] tenantConfigContexts, boolean detectedTenantWithoutMetadata) { - this.tenantConfigContexts = tenantConfigContexts; - this.detectedTenantWithoutMetadata = detectedTenantWithoutMetadata; - } - - @Override - public String resolve(RoutingContext context) { - for (var tenantContext : tenantConfigContexts) { - if (detectedTenantWithoutMetadata - && (tenantContext.getOidcMetadata() == null || tenantContext.getOidcMetadata().getIssuer() == null - || ANY_ISSUER.equals(tenantContext.getOidcMetadata().getIssuer()))) { - // this is static tenant that didn't have OIDC metadata available at startup - continue; - } - - final String token = OidcUtils.extractBearerToken(context, tenantContext.oidcConfig); - if (token != null && !OidcUtils.isOpaqueToken(token)) { - final var tokenJson = OidcUtils.decodeJwtContent(token); - if (tokenJson != null) { - - final String iss = tokenJson.getString(Claims.iss.name()); - if (tenantContext.getOidcMetadata().getIssuer().equals(iss)) { - OidcUtils.storeExtractedBearerToken(context, token); - - final String tenantId = tenantContext.oidcConfig.tenantId.get(); - LOG.debugf("Resolved the '%s' OIDC tenant based on the matching issuer '%s'", tenantId, iss); - return tenantId; - } - } - } - } - return null; - } - - private static TenantResolver of(Map tenantConfigContexts) { - var contextsWithIssuer = new ArrayList(); - boolean detectedTenantWithoutMetadata = false; - for (TenantConfigContext context : tenantConfigContexts.values()) { - if (context.oidcConfig.tenantEnabled && !OidcUtils.isWebApp(context.oidcConfig)) { - if (context.getOidcMetadata() == null) { - // if the tenant metadata are not available, we can't decide now - detectedTenantWithoutMetadata = true; - contextsWithIssuer.add(context); - } else if (context.getOidcMetadata().getIssuer() != null - && !ANY_ISSUER.equals(context.getOidcMetadata().getIssuer())) { - contextsWithIssuer.add(context); - } - } - } - if (contextsWithIssuer.isEmpty()) { - return null; - } else { - return new IssuerBasedTenantResolver(contextsWithIssuer.toArray(new TenantConfigContext[0]), - detectedTenantWithoutMetadata); - } - } - - private static void addIssuerBasedTenantResolver(List resolvers, - Map staticTenantsConfig, TenantConfigContext defaultTenant) { - Map tenantConfigContexts = new HashMap<>(staticTenantsConfig); - tenantConfigContexts.put(OidcUtils.DEFAULT_TENANT_ID, defaultTenant); - var issuerTenantResolver = IssuerBasedTenantResolver.of(tenantConfigContexts); - if (issuerTenantResolver != null) { - resolvers.add(issuerTenantResolver); - } else { - LOG.debug("The 'quarkus.oidc.resolve-tenants-with-issuer' configuration property is set to true, " - + "but no static tenant supports this feature. To use this feature, please configure at least " - + "one static tenant with the discovered or configured issuer and set either 'service' or " - + "'hybrid' application type"); - } - } - } - } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/LazyTenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/LazyTenantConfigContext.java new file mode 100644 index 0000000000000..8e078fe097111 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/LazyTenantConfigContext.java @@ -0,0 +1,87 @@ +package io.quarkus.oidc.runtime; + +import java.util.List; +import java.util.function.Supplier; + +import javax.crypto.SecretKey; + +import org.jboss.logging.Logger; + +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.OidcRedirectFilter; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.Redirect; +import io.smallrye.mutiny.Uni; + +final class LazyTenantConfigContext implements TenantConfigContext { + + private static final Logger LOG = Logger.getLogger(LazyTenantConfigContext.class); + + private final Supplier> staticTenantCreator; + private volatile TenantConfigContext delegate; + + LazyTenantConfigContext(TenantConfigContext delegate, Supplier> staticTenantCreator) { + this.staticTenantCreator = staticTenantCreator; + this.delegate = delegate; + } + + @Override + public Uni initialize() { + if (!delegate.ready()) { + LOG.debugf("Tenant '%s' is not initialized yet, trying to create OIDC connection now", + delegate.oidcConfig().tenantId.get()); + return staticTenantCreator.get().invoke(ctx -> LazyTenantConfigContext.this.delegate = ctx); + } + return Uni.createFrom().item(delegate); + } + + @Override + public OidcTenantConfig oidcConfig() { + return delegate.oidcConfig(); + } + + @Override + public OidcProvider provider() { + return delegate.provider(); + } + + @Override + public boolean ready() { + return delegate.ready(); + } + + @Override + public OidcTenantConfig getOidcTenantConfig() { + return delegate.getOidcTenantConfig(); + } + + @Override + public OidcConfigurationMetadata getOidcMetadata() { + return delegate.getOidcMetadata(); + } + + @Override + public OidcProviderClient getOidcProviderClient() { + return delegate.getOidcProviderClient(); + } + + @Override + public SecretKey getStateEncryptionKey() { + return delegate.getStateEncryptionKey(); + } + + @Override + public SecretKey getTokenEncSecretKey() { + return delegate.getTokenEncSecretKey(); + } + + @Override + public SecretKey getInternalIdTokenSecretKey() { + return delegate.getInternalIdTokenSecretKey(); + } + + @Override + public List getOidcRedirectFilters(Redirect.Location loc) { + return delegate.getOidcRedirectFilters(loc); + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java index 85a92a7b2c90f..bb511db4ea6c3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigurationMetadataProducer.java @@ -20,8 +20,8 @@ public class OidcConfigurationMetadataProducer { OidcConfigurationMetadata produce() { OidcConfigurationMetadata configMetadata = OidcUtils.getAttribute(identity, OidcUtils.CONFIG_METADATA_ATTRIBUTE); - if (configMetadata == null && tenantConfig.getDefaultTenant().oidcConfig.tenantEnabled) { - configMetadata = tenantConfig.getDefaultTenant().provider.getMetadata(); + if (configMetadata == null && tenantConfig.getDefaultTenant().oidcConfig().tenantEnabled) { + configMetadata = tenantConfig.getDefaultTenant().provider().getMetadata(); } if (configMetadata == null) { throw new OIDCException("OidcConfigurationMetadata can not be injected"); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 83aa12b01af3d..68b7812178be5 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -99,12 +99,12 @@ protected Map getRequestData(TokenAuthenticationRequest request) private Uni authenticate(TokenAuthenticationRequest request, Map requestData, TenantConfigContext resolvedContext) { - if (resolvedContext.oidcConfig.authServerUrl.isPresent()) { + if (resolvedContext.oidcConfig().authServerUrl.isPresent()) { return validateAllTokensWithOidcServer(requestData, request, resolvedContext); - } else if (resolvedContext.oidcConfig.getCertificateChain().trustStoreFile.isPresent()) { + } else if (resolvedContext.oidcConfig().getCertificateChain().trustStoreFile.isPresent()) { LOG.debug("Performing token verification with a public key inlined in the certificate chain"); return validateTokenWithoutOidcServer(request, resolvedContext); - } else if (resolvedContext.oidcConfig.publicKey.isPresent()) { + } else if (resolvedContext.oidcConfig().publicKey.isPresent()) { LOG.debug("Performing token verification with a configured public key"); return validateTokenWithoutOidcServer(request, resolvedContext); } else { @@ -115,13 +115,13 @@ private Uni authenticate(TokenAuthenticationRequest request, M private Uni validateAllTokensWithOidcServer(Map requestData, TokenAuthenticationRequest request, TenantConfigContext resolvedContext) { - if (resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false) + if (resolvedContext.oidcConfig().token.verifyAccessTokenWithUserInfo.orElse(false) && isOpaqueAccessToken(requestData, request, resolvedContext)) { // UserInfo has to be acquired first as a precondition for verifying opaque access tokens. // Typically it will be done for bearer access tokens therefore even if the access token has expired // the client will be able to refresh if needed, no refresh token is available to Quarkus during the // bearer access token verification - if (resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false)) { + if (resolvedContext.oidcConfig().authentication.isUserInfoRequired().orElse(false)) { return getUserInfoUni(requestData, request, resolvedContext).onItemOrFailure().transformToUni( new BiFunction>() { @Override @@ -224,13 +224,13 @@ public Uni apply(TokenVerificationResult codeAccessTokenResult } if (codeAccessTokenResult != null) { if (tokenAutoRefreshPrepared(codeAccessTokenResult, requestData, - resolvedContext.oidcConfig)) { + resolvedContext.oidcConfig())) { return Uni.createFrom().failure(new TokenAutoRefreshException(null)); } requestData.put(OidcUtils.CODE_ACCESS_TOKEN_RESULT, codeAccessTokenResult); } - if (resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false)) { + if (resolvedContext.oidcConfig().authentication.isUserInfoRequired().orElse(false)) { return getUserInfoUni(requestData, request, resolvedContext).onItemOrFailure() .transformToUni( new BiFunction>() { @@ -263,8 +263,8 @@ private boolean isOpaqueAccessToken(Map requestData, TokenAuthen if (request.getToken() instanceof AccessTokenCredential) { return ((AccessTokenCredential) request.getToken()).isOpaque(); } else if (request.getToken() instanceof IdTokenCredential - && (resolvedContext.oidcConfig.authentication.verifyAccessToken - || resolvedContext.oidcConfig.roles.source.orElse(null) == Source.accesstoken)) { + && (resolvedContext.oidcConfig().authentication.verifyAccessToken + || resolvedContext.oidcConfig().roles.source.orElse(null) == Source.accesstoken)) { final String codeAccessToken = (String) requestData.get(OidcConstants.ACCESS_TOKEN_VALUE); return OidcUtils.isOpaqueToken(codeAccessToken); } @@ -288,8 +288,8 @@ private Uni createSecurityIdentityWithOidcServer(TokenVerifica } if (tokenJson != null) { try { - OidcUtils.validatePrimaryJwtTokenType(resolvedContext.oidcConfig.token, tokenJson); - if (userInfo != null && resolvedContext.oidcConfig.token.isSubjectRequired() + OidcUtils.validatePrimaryJwtTokenType(resolvedContext.oidcConfig().token, tokenJson); + if (userInfo != null && resolvedContext.oidcConfig().token.isSubjectRequired() && !tokenJson.getString(Claims.sub.name()).equals(userInfo.getString(Claims.sub.name()))) { String errorMessage = String .format("Token and UserInfo do not have matching `sub` claims"); @@ -303,7 +303,7 @@ private Uni createSecurityIdentityWithOidcServer(TokenVerifica // If the primary token is a bearer access token then there's no point of checking if // it should be refreshed as RT is only available for the code flow tokens if (isIdToken(request) - && tokenAutoRefreshPrepared(result, requestData, resolvedContext.oidcConfig)) { + && tokenAutoRefreshPrepared(result, requestData, resolvedContext.oidcConfig())) { return Uni.createFrom().failure(new TokenAutoRefreshException(securityIdentity)); } else { return Uni.createFrom().item(securityIdentity); @@ -324,10 +324,10 @@ && tokenAutoRefreshPrepared(result, requestData, resolvedContext.oidcConfig)) { OidcUtils.setSecurityIdentityConfigMetadata(builder, resolvedContext); final String userName; if (result.introspectionResult == null) { - if (resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection && - resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false)) { - if (resolvedContext.oidcConfig.token.principalClaim.isPresent() && userInfo != null) { - userName = userInfo.getString(resolvedContext.oidcConfig.token.principalClaim.get()); + if (resolvedContext.oidcConfig().token.allowOpaqueTokenIntrospection && + resolvedContext.oidcConfig().token.verifyAccessTokenWithUserInfo.orElse(false)) { + if (resolvedContext.oidcConfig().token.principalClaim.isPresent() && userInfo != null) { + userName = userInfo.getString(resolvedContext.oidcConfig().token.principalClaim.get()); } else { userName = ""; } @@ -358,10 +358,10 @@ public String getName() { }); if (userInfo != null) { var rolesJson = new JsonObject(userInfo.getJsonObject().toString()); - OidcUtils.setSecurityIdentityRoles(builder, resolvedContext.oidcConfig, rolesJson); - OidcUtils.setSecurityIdentityPermissions(builder, resolvedContext.oidcConfig, rolesJson); + OidcUtils.setSecurityIdentityRoles(builder, resolvedContext.oidcConfig(), rolesJson); + OidcUtils.setSecurityIdentityPermissions(builder, resolvedContext.oidcConfig(), rolesJson); } - OidcUtils.setTenantIdAttribute(builder, resolvedContext.oidcConfig); + OidcUtils.setTenantIdAttribute(builder, resolvedContext.oidcConfig()); var vertxContext = getRoutingContextAttribute(request); OidcUtils.setBlockingApiAttribute(builder, vertxContext); OidcUtils.setRoutingContextAttribute(builder, vertxContext); @@ -369,7 +369,7 @@ public String getName() { // If the primary token is a bearer access token then there's no point of checking if // it should be refreshed as RT is only available for the code flow tokens if (isIdToken(request) - && tokenAutoRefreshPrepared(result, requestData, resolvedContext.oidcConfig)) { + && tokenAutoRefreshPrepared(result, requestData, resolvedContext.oidcConfig())) { return Uni.createFrom().failure(new TokenAutoRefreshException(identity)); } return Uni.createFrom().item(identity); @@ -410,11 +410,11 @@ private static JsonObject getRolesJson(Map requestData, TenantCo TokenCredential tokenCred, JsonObject tokenJson, UserInfo userInfo) { JsonObject rolesJson = tokenJson; - if (resolvedContext.oidcConfig.roles.source.isPresent()) { - if (resolvedContext.oidcConfig.roles.source.get() == Source.userinfo) { + if (resolvedContext.oidcConfig().roles.source.isPresent()) { + if (resolvedContext.oidcConfig().roles.source.get() == Source.userinfo) { rolesJson = new JsonObject(userInfo.getJsonObject().toString()); } else if (tokenCred instanceof IdTokenCredential - && resolvedContext.oidcConfig.roles.source.get() == Source.accesstoken) { + && resolvedContext.oidcConfig().roles.source.get() == Source.accesstoken) { rolesJson = ((TokenVerificationResult) requestData .get(OidcUtils.CODE_ACCESS_TOKEN_RESULT)).localVerificationResult; if (rolesJson == null) { @@ -432,8 +432,8 @@ private Uni verifyCodeFlowAccessTokenUni(Map verifyTokenUni(Map requestD TokenCredential tokenCred, boolean enforceAudienceVerification, UserInfo userInfo) { final String token = tokenCred.getToken(); if (OidcUtils.isOpaqueToken(token)) { - if (!resolvedContext.oidcConfig.token.allowOpaqueTokenIntrospection) { + if (!resolvedContext.oidcConfig().token.allowOpaqueTokenIntrospection) { LOG.debug("Token is opaque but the opaque token introspection is not allowed"); throw new AuthenticationFailedException(); } // verify opaque access token with UserInfo if enabled and introspection URI is absent - if (resolvedContext.oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false) - && resolvedContext.provider.getMetadata().getIntrospectionUri() == null) { + if (resolvedContext.oidcConfig().token.verifyAccessTokenWithUserInfo.orElse(false) + && resolvedContext.provider().getMetadata().getIntrospectionUri() == null) { if (userInfo == null) { return Uni.createFrom().failure( new AuthenticationFailedException("Opaque access token verification failed as user info is null.")); @@ -462,24 +462,24 @@ private Uni verifyTokenUni(Map requestD } LOG.debug("Starting the opaque token introspection"); return introspectTokenUni(resolvedContext, token, false); - } else if (resolvedContext.provider.getMetadata().getJsonWebKeySetUri() == null - || resolvedContext.oidcConfig.token.requireJwtIntrospectionOnly) { + } else if (resolvedContext.provider().getMetadata().getJsonWebKeySetUri() == null + || resolvedContext.oidcConfig().token.requireJwtIntrospectionOnly) { // Verify JWT token with the remote introspection LOG.debug("Starting the JWT token introspection"); return introspectTokenUni(resolvedContext, token, false); - } else if (resolvedContext.oidcConfig.jwks.resolveEarly) { + } else if (resolvedContext.oidcConfig().jwks.resolveEarly) { // Verify JWT token with the local JWK keys with a possible remote introspection fallback final String nonce = tokenCred instanceof IdTokenCredential ? (String) requestData.get(OidcConstants.NONCE) : null; try { LOG.debug("Verifying the JWT token with the local JWK keys"); return Uni.createFrom() - .item(resolvedContext.provider.verifyJwtToken(token, enforceAudienceVerification, - resolvedContext.oidcConfig.token.isSubjectRequired(), nonce)); + .item(resolvedContext.provider().verifyJwtToken(token, enforceAudienceVerification, + resolvedContext.oidcConfig().token.isSubjectRequired(), nonce)); } catch (Throwable t) { if (t.getCause() instanceof UnresolvableKeyException) { LOG.debug("No matching JWK key is found, refreshing and repeating the token verification"); return refreshJwksAndVerifyTokenUni(resolvedContext, token, enforceAudienceVerification, - resolvedContext.oidcConfig.token.isSubjectRequired(), nonce); + resolvedContext.oidcConfig().token.isSubjectRequired(), nonce); } else { LOG.debugf("Token verification has failed: %s", t.getMessage()); return Uni.createFrom().failure(t); @@ -488,14 +488,14 @@ private Uni verifyTokenUni(Map requestD } else { final String nonce = (String) requestData.get(OidcConstants.NONCE); return resolveJwksAndVerifyTokenUni(resolvedContext, tokenCred, enforceAudienceVerification, - resolvedContext.oidcConfig.token.isSubjectRequired(), nonce); + resolvedContext.oidcConfig().token.isSubjectRequired(), nonce); } } private Uni verifySelfSignedTokenUni(TenantConfigContext resolvedContext, String token) { try { return Uni.createFrom().item( - resolvedContext.provider.verifySelfSignedJwtToken(token, resolvedContext.getInternalIdTokenSecretKey())); + resolvedContext.provider().verifySelfSignedJwtToken(token, resolvedContext.getInternalIdTokenSecretKey())); } catch (Throwable t) { return Uni.createFrom().failure(t); } @@ -503,7 +503,8 @@ private Uni verifySelfSignedTokenUni(TenantConfigContex private Uni refreshJwksAndVerifyTokenUni(TenantConfigContext resolvedContext, String token, boolean enforceAudienceVerification, boolean subjectRequired, String nonce) { - return resolvedContext.provider.refreshJwksAndVerifyJwtToken(token, enforceAudienceVerification, subjectRequired, nonce) + return resolvedContext.provider() + .refreshJwksAndVerifyJwtToken(token, enforceAudienceVerification, subjectRequired, nonce) .onFailure(f -> fallbackToIntrospectionIfNoMatchingKey(f, resolvedContext)) .recoverWithUni(f -> introspectTokenUni(resolvedContext, token, true)); } @@ -511,7 +512,7 @@ private Uni refreshJwksAndVerifyTokenUni(TenantConfigCo private Uni resolveJwksAndVerifyTokenUni(TenantConfigContext resolvedContext, TokenCredential tokenCred, boolean enforceAudienceVerification, boolean subjectRequired, String nonce) { - return resolvedContext.provider + return resolvedContext.provider() .getKeyResolverAndVerifyJwtToken(tokenCred, enforceAudienceVerification, subjectRequired, nonce, (tokenCred instanceof IdTokenCredential)) .onFailure(f -> fallbackToIntrospectionIfNoMatchingKey(f, resolvedContext)) @@ -522,7 +523,7 @@ private static boolean fallbackToIntrospectionIfNoMatchingKey(Throwable f, Tenan if (!(f.getCause() instanceof UnresolvableKeyException)) { LOG.debug("Local JWT token verification has failed, skipping the token introspection"); return false; - } else if (!resolvedContext.oidcConfig.token.allowJwtIntrospection) { + } else if (!resolvedContext.oidcConfig().token.allowJwtIntrospection) { LOG.debug("JWT token does not have a matching verification key but JWT token introspection is disabled"); return false; } else { @@ -537,7 +538,7 @@ private Uni introspectTokenUni(TenantConfigContext reso TokenIntrospectionCache tokenIntrospectionCache = tenantResolver.getTokenIntrospectionCache(); Uni tokenIntrospectionUni = tokenIntrospectionCache == null ? null : tokenIntrospectionCache - .getIntrospection(token, resolvedContext.oidcConfig, getIntrospectionRequestContext); + .getIntrospection(token, resolvedContext.oidcConfig(), getIntrospectionRequestContext); if (tokenIntrospectionUni == null) { tokenIntrospectionUni = newTokenIntrospectionUni(resolvedContext, token, fallbackFromJwkMatch); } else { @@ -554,8 +555,8 @@ public Uni get() { private Uni newTokenIntrospectionUni(TenantConfigContext resolvedContext, String token, boolean fallbackFromJwkMatch) { - Uni tokenIntrospectionUni = resolvedContext.provider.introspectToken(token, fallbackFromJwkMatch); - if (tenantResolver.getTokenIntrospectionCache() == null || !resolvedContext.oidcConfig.allowTokenIntrospectionCache) { + Uni tokenIntrospectionUni = resolvedContext.provider().introspectToken(token, fallbackFromJwkMatch); + if (tenantResolver.getTokenIntrospectionCache() == null || !resolvedContext.oidcConfig().allowTokenIntrospectionCache) { return tokenIntrospectionUni; } else { return tokenIntrospectionUni.call(new Function>() { @@ -563,7 +564,7 @@ private Uni newTokenIntrospectionUni(TenantConfigContext res @Override public Uni apply(TokenIntrospection introspection) { return tenantResolver.getTokenIntrospectionCache().addIntrospection(token, introspection, - resolvedContext.oidcConfig, uniVoidOidcContext); + resolvedContext.oidcConfig(), uniVoidOidcContext); } }); } @@ -573,8 +574,8 @@ private static Uni validateTokenWithoutOidcServer(TokenAuthent TenantConfigContext resolvedContext) { try { - TokenVerificationResult result = resolvedContext.provider.verifyJwtToken(request.getToken().getToken(), - resolvedContext.oidcConfig.token.subjectRequired, + TokenVerificationResult result = resolvedContext.provider().verifyJwtToken(request.getToken().getToken(), + resolvedContext.oidcConfig().token.subjectRequired, false, null); return Uni.createFrom() .item(validateAndCreateIdentity(Map.of(), request.getToken(), resolvedContext, @@ -586,7 +587,7 @@ private static Uni validateTokenWithoutOidcServer(TokenAuthent private Uni getUserInfoUni(Map requestData, TokenAuthenticationRequest request, TenantConfigContext resolvedContext) { - if (isInternalIdToken(request) && OidcUtils.cacheUserInfoInIdToken(tenantResolver, resolvedContext.oidcConfig)) { + if (isInternalIdToken(request) && OidcUtils.cacheUserInfoInIdToken(tenantResolver, resolvedContext.oidcConfig())) { JsonObject userInfo = OidcUtils.decodeJwtContent(request.getToken().getToken()) .getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE); if (userInfo != null) { @@ -600,7 +601,7 @@ private Uni getUserInfoUni(Map requestData, TokenAuthe UserInfoCache userInfoCache = tenantResolver.getUserInfoCache(); Uni userInfoUni = userInfoCache == null ? null - : userInfoCache.getUserInfo(accessToken, resolvedContext.oidcConfig, getUserInfoRequestContext); + : userInfoCache.getUserInfo(accessToken, resolvedContext.oidcConfig(), getUserInfoRequestContext); if (userInfoUni == null) { userInfoUni = newUserInfoUni(resolvedContext, accessToken); } else { @@ -616,9 +617,9 @@ public Uni get() { } private Uni newUserInfoUni(TenantConfigContext resolvedContext, String accessToken) { - Uni userInfoUni = resolvedContext.provider.getUserInfo(accessToken); - if (tenantResolver.getUserInfoCache() == null || !resolvedContext.oidcConfig.allowUserInfoCache - || OidcUtils.cacheUserInfoInIdToken(tenantResolver, resolvedContext.oidcConfig)) { + Uni userInfoUni = resolvedContext.provider().getUserInfo(accessToken); + if (tenantResolver.getUserInfoCache() == null || !resolvedContext.oidcConfig().allowUserInfoCache + || OidcUtils.cacheUserInfoInIdToken(tenantResolver, resolvedContext.oidcConfig())) { return userInfoUni; } else { return userInfoUni.call(new Function>() { @@ -626,7 +627,7 @@ private Uni newUserInfoUni(TenantConfigContext resolvedContext, String @Override public Uni apply(UserInfo userInfo) { return tenantResolver.getUserInfoCache().addUserInfo(accessToken, userInfo, - resolvedContext.oidcConfig, uniVoidOidcContext); + resolvedContext.oidcConfig(), uniVoidOidcContext); } }); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index 65518fef5942f..fc968952aca15 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -113,14 +113,19 @@ public TenantConfigBean setup(OidcConfig config, Vertx vertxValue, OidcTlsSuppor OidcRecorder.userInfoInjectionPointDetected = userInfoInjectionPointDetected; String defaultTenantId = config.defaultTenant.getTenantId().orElse(DEFAULT_TENANT_ID); - TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, config.defaultTenant, + var defaultTenantInitializer = createStaticTenantContextCreator(vertxValue, config.defaultTenant, !config.namedTenants.isEmpty(), defaultTenantId, tlsSupport); + TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, config.defaultTenant, + !config.namedTenants.isEmpty(), defaultTenantId, tlsSupport, defaultTenantInitializer); Map staticTenantsConfig = new HashMap<>(); for (Map.Entry tenant : config.namedTenants.entrySet()) { OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenant.getKey(), tenant.getValue().getTenantId()); + var staticTenantInitializer = createStaticTenantContextCreator(vertxValue, tenant.getValue(), false, + tenant.getKey(), tlsSupport); staticTenantsConfig.put(tenant.getKey(), - createStaticTenantContext(vertxValue, tenant.getValue(), false, tenant.getKey(), tlsSupport)); + createStaticTenantContext(vertxValue, tenant.getValue(), false, tenant.getKey(), tlsSupport, + staticTenantInitializer)); } return new TenantConfigBean(staticTenantsConfig, dynamicTenantsConfig, defaultTenantContext, @@ -163,7 +168,7 @@ public TenantConfigContext apply(TenantConfigContext t) { private TenantConfigContext createStaticTenantContext(Vertx vertx, OidcTenantConfig oidcConfig, boolean checkNamedTenants, String tenantId, - OidcTlsSupport tlsSupport) { + OidcTlsSupport tlsSupport, Supplier> staticTenantCreator) { Uni uniContext = createTenantContext(vertx, oidcConfig, checkNamedTenants, tenantId, tlsSupport); try { @@ -177,14 +182,14 @@ public TenantConfigContext apply(Throwable t) { + " Access to resources protected by this tenant may fail" + " if OIDC server will not become available", tenantId, t.getMessage()); - return new TenantConfigContext(null, oidcConfig, false); + return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); } logTenantConfigContextFailure(t, tenantId); if (t instanceof ConfigurationException && !oidcConfig.authServerUrl.isPresent() && LaunchMode.DEVELOPMENT == LaunchMode.current()) { // Let it start if it is a DEV mode and auth-server-url has not been configured yet - return new TenantConfigContext(null, oidcConfig, false); + return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); } // fail in all other cases throw new OIDCException(t); @@ -195,10 +200,26 @@ public TenantConfigContext apply(Throwable t) { LOG.warnf("Tenant '%s': OIDC server is not available after a %d seconds timeout, an attempt to connect will be made" + " during the first request. Access to resources protected by this tenant may fail if OIDC server" + " will not become available", tenantId, oidcConfig.getConnectionTimeout().getSeconds()); - return new TenantConfigContext(null, oidcConfig, false); + return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); } } + private Supplier> createStaticTenantContextCreator(Vertx vertx, OidcTenantConfig oidcConfig, + boolean checkNamedTenants, String tenantId, OidcTlsSupport tlsSupport) { + return new Supplier>() { + @Override + public Uni get() { + return createTenantContext(vertx, oidcConfig, checkNamedTenants, tenantId, tlsSupport) + .onFailure().transform(new Function() { + @Override + public Throwable apply(Throwable t) { + return logTenantConfigContextFailure(t, tenantId); + } + }); + } + }; + } + private static Throwable logTenantConfigContextFailure(Throwable t, String tenantId) { LOG.debugf( "'%s' tenant is not initialized: '%s'. Access to resources protected by this tenant will fail.", @@ -217,7 +238,7 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf if (!oidcConfig.tenantEnabled) { LOG.debugf("'%s' tenant configuration is disabled", tenantId); - return Uni.createFrom().item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig)); + return Uni.createFrom().item(TenantConfigContext.createReady(new OidcProvider(null, null, null, null), oidcConfig)); } if (!oidcConfig.getAuthServerUrl().isPresent()) { @@ -244,7 +265,7 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf + " or named tenants are configured."); oidcConfig.setTenantEnabled(false); return Uni.createFrom() - .item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig)); + .item(TenantConfigContext.createReady(new OidcProvider(null, null, null, null), oidcConfig)); } } throw new ConfigurationException( @@ -370,7 +391,7 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf .onItem().transform(new Function() { @Override public TenantConfigContext apply(OidcProvider p) { - return new TenantConfigContext(p, oidcConfig); + return TenantConfigContext.createReady(p, oidcConfig); } }); } @@ -402,7 +423,7 @@ private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantCo LOG.debug("'public-key' property for the local token verification is set," + " no connection to the OIDC server will be created"); - return new TenantConfigContext( + return TenantConfigContext.createReady( new OidcProvider(oidcConfig.publicKey.get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); } @@ -412,7 +433,7 @@ private static TenantConfigContext createTenantContextToVerifyCertChain(OidcTena "Currently only 'service' applications can be used to verify tokens with inlined certificate chains"); } - return new TenantConfigContext( + return TenantConfigContext.createReady( new OidcProvider(null, oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index d8c6c28f08ae8..3444f128ac770 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -350,7 +350,7 @@ static QuarkusSecurityIdentity validateAndCreateIdentity(Map req TenantConfigContext resolvedContext, JsonObject tokenJson, JsonObject rolesJson, UserInfo userInfo, TokenIntrospection introspectionResult, TokenAuthenticationRequest request) { - OidcTenantConfig config = resolvedContext.oidcConfig; + OidcTenantConfig config = resolvedContext.oidcConfig(); QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); builder.addCredential(credential); AuthorizationCodeTokens codeTokens = (AuthorizationCodeTokens) requestData.get(AuthorizationCodeTokens.class.getName()); @@ -464,8 +464,8 @@ public static void setSecurityIdentityIntrospection(Builder builder, TokenIntros public static void setSecurityIdentityConfigMetadata(QuarkusSecurityIdentity.Builder builder, TenantConfigContext resolvedContext) { - if (resolvedContext.provider.client != null) { - builder.addAttribute(CONFIG_METADATA_ATTRIBUTE, resolvedContext.provider.client.getMetadata()); + if (resolvedContext.provider().client != null) { + builder.addAttribute(CONFIG_METADATA_ATTRIBUTE, resolvedContext.provider().client.getMetadata()); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/StaticTenantResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/StaticTenantResolver.java new file mode 100644 index 0000000000000..dbe520988612e --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/StaticTenantResolver.java @@ -0,0 +1,287 @@ +package io.quarkus.oidc.runtime; + +import static io.quarkus.oidc.runtime.OidcProvider.ANY_ISSUER; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; + +import jakarta.enterprise.inject.Instance; + +import org.eclipse.microprofile.jwt.Claims; +import org.jboss.logging.Logger; + +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TenantResolver; +import io.quarkus.vertx.http.runtime.security.ImmutablePathMatcher; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +final class StaticTenantResolver { + + private static final Logger LOG = Logger.getLogger(StaticTenantResolver.class); + + private final TenantResolver[] staticTenantResolvers; + private final IssuerBasedTenantResolver issuerBasedTenantResolver; + + StaticTenantResolver(TenantConfigBean tenantConfigBean, String rootPath, boolean resolveTenantsWithIssuer, + Instance tenantResolverInstance) { + List staticTenantResolvers = new ArrayList<>(); + // STATIC TENANT RESOLVERS BY PRIORITY: + // 0. annotation based resolver + + // 1. custom tenant resolver + if (tenantResolverInstance.isResolvable()) { + if (tenantResolverInstance.isAmbiguous()) { + throw new IllegalStateException("Multiple " + TenantResolver.class + " beans registered"); + } + staticTenantResolvers.add(tenantResolverInstance.get()); + } + + // 2. path-matching tenant resolver + var pathMatchingTenantResolver = PathMatchingTenantResolver.of(tenantConfigBean.getStaticTenantsConfig(), rootPath, + tenantConfigBean.getDefaultTenant()); + if (pathMatchingTenantResolver != null) { + staticTenantResolvers.add(pathMatchingTenantResolver); + } + + // 3. default static tenant resolver + if (!tenantConfigBean.getStaticTenantsConfig().isEmpty()) { + staticTenantResolvers.add(new DefaultStaticTenantResolver(tenantConfigBean)); + } + + this.staticTenantResolvers = staticTenantResolvers.toArray(new TenantResolver[0]); + + // 4. issuer-based tenant resolver + if (resolveTenantsWithIssuer) { + this.issuerBasedTenantResolver = IssuerBasedTenantResolver.of( + tenantConfigBean.getStaticTenantsConfig(), tenantConfigBean.getDefaultTenant()); + } else { + this.issuerBasedTenantResolver = null; + } + } + + Uni resolve(RoutingContext context) { + for (TenantResolver resolver : staticTenantResolvers) { + final String tenantId = resolver.resolve(context); + if (tenantId != null) { + return Uni.createFrom().item(tenantId); + } + } + + if (issuerBasedTenantResolver != null) { + return issuerBasedTenantResolver.resolveTenant(context); + } + + return Uni.createFrom().nullItem(); + } + + private static final class DefaultStaticTenantResolver implements TenantResolver { + + private final TenantConfigBean tenantConfigBean; + + private DefaultStaticTenantResolver(TenantConfigBean tenantConfigBean) { + this.tenantConfigBean = tenantConfigBean; + } + + @Override + public String resolve(RoutingContext context) { + String[] pathSegments = context.request().path().split("/"); + if (pathSegments.length > 0) { + String lastPathSegment = pathSegments[pathSegments.length - 1]; + if (tenantConfigBean.getStaticTenantsConfig().containsKey(lastPathSegment)) { + LOG.debugf( + "Tenant id '%s' is selected on the '%s' request path", lastPathSegment, context.normalizedPath()); + return lastPathSegment; + } + } + return null; + } + } + + private static final class PathMatchingTenantResolver implements TenantResolver { + private static final String DEFAULT_TENANT = "PathMatchingTenantResolver#DefaultTenant"; + private final ImmutablePathMatcher staticTenantPaths; + + private PathMatchingTenantResolver(ImmutablePathMatcher staticTenantPaths) { + this.staticTenantPaths = staticTenantPaths; + } + + private static PathMatchingTenantResolver of(Map staticTenantsConfig, String rootPath, + TenantConfigContext defaultTenant) { + final var builder = ImmutablePathMatcher. builder().rootPath(rootPath); + addPath(DEFAULT_TENANT, defaultTenant.oidcConfig(), builder); + for (Map.Entry e : staticTenantsConfig.entrySet()) { + addPath(e.getKey(), e.getValue().oidcConfig(), builder); + } + return builder.hasPaths() ? new PathMatchingTenantResolver(builder.build()) : null; + } + + @Override + public String resolve(RoutingContext context) { + String tenantId = staticTenantPaths.match(context.normalizedPath()).getValue(); + if (tenantId != null) { + LOG.debugf( + "Tenant id '%s' is selected on the '%s' request path", tenantId, context.normalizedPath()); + return tenantId; + } + return null; + } + + private static ImmutablePathMatcher.ImmutablePathMatcherBuilder addPath(String tenant, OidcTenantConfig config, + ImmutablePathMatcher.ImmutablePathMatcherBuilder builder) { + if (config != null && config.tenantPaths.isPresent()) { + for (String path : config.tenantPaths.get()) { + builder.addPath(path, tenant); + } + } + return builder; + } + } + + private static final class IssuerBasedTenantResolver { + + private final TenantConfigContext[] tenantConfigContexts; + private final boolean detectedTenantWithoutMetadata; + private final Map tenantToRetry; + + private IssuerBasedTenantResolver(TenantConfigContext[] tenantConfigContexts, boolean detectedTenantWithoutMetadata, + Map tenantToRetry) { + this.tenantConfigContexts = tenantConfigContexts; + this.detectedTenantWithoutMetadata = detectedTenantWithoutMetadata; + this.tenantToRetry = tenantToRetry; + } + + private Uni resolveTenant(RoutingContext context) { + return resolveTenant(context, 0); + } + + private Uni resolveTenant(RoutingContext context, int index) { + if (index == tenantConfigContexts.length) { + return Uni.createFrom().nullItem(); + } + var tenantContext = tenantConfigContexts[index]; + if (detectedTenantWithoutMetadata) { + // this is static tenant that didn't have OIDC metadata available at startup + + if (tenantContext.getOidcMetadata() == null) { + if (tenantContext.ready()) { + return resolveTenant(context, index + 1); + } + + if (!tryToInitialize(tenantContext)) { + return resolveTenant(context, index + 1); + } + + return tenantContext.initialize() + .onItemOrFailure() + .transformToUni(new BiFunction>() { + @Override + public Uni apply(TenantConfigContext newContext, Throwable throwable) { + if (throwable != null) { + return resolveTenant(context, index + 1); + } + if (newContext.ready() && !isTenantWithoutIssuer(newContext)) { + return getTenantId(newContext, context, index); + } + return resolveTenant(context, index + 1); + } + }); + } + + if (isTenantWithoutIssuer(tenantContext)) { + return resolveTenant(context, index + 1); + } + } + + return getTenantId(tenantContext, context, index); + } + + private Uni getTenantId(TenantConfigContext tenantContext, RoutingContext context, int index) { + var tenantId = getTenantId(context, tenantContext); + if (tenantId == null) { + return resolveTenant(context, index + 1); + } + return Uni.createFrom().item(tenantId); + } + + /** + * When static tenant couldn't be initialized on Quarkus application startup, + * this strategy permits one more attempt on the first request when the issuer-based tenant resolver is applied. + */ + private boolean tryToInitialize(TenantConfigContext context) { + var tenantId = context.oidcConfig().tenantId.get(); + return this.tenantToRetry.get(tenantId).compareAndExchange(true, false); + } + + private static String getTenantId(RoutingContext context, TenantConfigContext tenantContext) { + final String token = OidcUtils.extractBearerToken(context, tenantContext.oidcConfig()); + if (token != null && !OidcUtils.isOpaqueToken(token)) { + final var tokenJson = OidcUtils.decodeJwtContent(token); + if (tokenJson != null) { + + final String iss = tokenJson.getString(Claims.iss.name()); + if (tenantContext.getOidcMetadata().getIssuer().equals(iss)) { + OidcUtils.storeExtractedBearerToken(context, token); + + final String tenantId = tenantContext.oidcConfig().tenantId.get(); + LOG.debugf("Resolved the '%s' OIDC tenant based on the matching issuer '%s'", tenantId, iss); + return tenantId; + } + } + } + return null; + } + + private static boolean isTenantWithoutIssuer(TenantConfigContext tenantContext) { + return tenantContext.getOidcMetadata().getIssuer() == null + || ANY_ISSUER.equals(tenantContext.getOidcMetadata().getIssuer()); + } + + private static IssuerBasedTenantResolver of(Map tenantConfigContexts) { + var contextsWithIssuer = new ArrayList(); + boolean detectedTenantWithoutMetadata = false; + Map tenantToRetry = new HashMap<>(); + for (TenantConfigContext context : tenantConfigContexts.values()) { + if (context.oidcConfig().tenantEnabled && !OidcUtils.isWebApp(context.oidcConfig())) { + if (context.getOidcMetadata() == null) { + // if the tenant metadata are not available, we can't decide now + detectedTenantWithoutMetadata = true; + contextsWithIssuer.add(context); + tenantToRetry.put(context.oidcConfig().tenantId.get(), new AtomicBoolean(true)); + } else if (context.getOidcMetadata().getIssuer() != null + && !ANY_ISSUER.equals(context.getOidcMetadata().getIssuer())) { + contextsWithIssuer.add(context); + } + } + } + if (contextsWithIssuer.isEmpty()) { + return null; + } else { + var tenantInitStrategy = detectedTenantWithoutMetadata ? Map.copyOf(tenantToRetry) : null; + return new IssuerBasedTenantResolver(contextsWithIssuer.toArray(new TenantConfigContext[0]), + detectedTenantWithoutMetadata, tenantInitStrategy); + } + } + + private static IssuerBasedTenantResolver of(Map staticTenantsConfig, + TenantConfigContext defaultTenant) { + Map tenantConfigContexts = new HashMap<>(staticTenantsConfig); + tenantConfigContexts.put(OidcUtils.DEFAULT_TENANT_ID, defaultTenant); + var issuerTenantResolver = IssuerBasedTenantResolver.of(tenantConfigContexts); + if (issuerTenantResolver != null) { + return issuerTenantResolver; + } else { + LOG.debug("The 'quarkus.oidc.resolve-tenants-with-issuer' configuration property is set to true, " + + "but no static tenant supports this feature. To use this feature, please configure at least " + + "one static tenant with the discovered or configured issuer and set either 'service' or " + + "'hybrid' application type"); + return null; + } + } + } + +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java index 535c17814c21e..073e6432fa8c1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java @@ -21,7 +21,7 @@ public TenantConfigBean( Map dynamicTenantsConfig, TenantConfigContext defaultTenant, Function> tenantConfigContextFactory) { - this.staticTenantsConfig = staticTenantsConfig; + this.staticTenantsConfig = Map.copyOf(staticTenantsConfig); this.dynamicTenantsConfig = dynamicTenantsConfig; this.defaultTenant = defaultTenant; this.tenantConfigContextFactory = tenantConfigContextFactory; @@ -48,17 +48,17 @@ public static class Destroyer implements BeanDestroyer { @Override public void destroy(TenantConfigBean instance, CreationalContext creationalContext, Map params) { - if (instance.defaultTenant != null && instance.defaultTenant.provider != null) { - instance.defaultTenant.provider.close(); + if (instance.defaultTenant != null && instance.defaultTenant.provider() != null) { + instance.defaultTenant.provider().close(); } for (var i : instance.staticTenantsConfig.values()) { - if (i.provider != null) { - i.provider.close(); + if (i.provider() != null) { + i.provider().close(); } } for (var i : instance.dynamicTenantsConfig.values()) { - if (i.provider != null) { - i.provider.close(); + if (i.provider() != null) { + i.provider().close(); } } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java index 01fa617414923..ee234563ba08a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java @@ -1,234 +1,60 @@ package io.quarkus.oidc.runtime; -import java.nio.charset.StandardCharsets; -import java.security.PrivateKey; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.function.Supplier; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import org.jboss.logging.Logger; - -import io.quarkus.arc.ClientProxy; -import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.OidcRedirectFilter; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.Redirect; -import io.quarkus.oidc.common.runtime.OidcCommonUtils; -import io.quarkus.runtime.configuration.ConfigurationException; +import io.smallrye.mutiny.Uni; -public class TenantConfigContext { - private static final Logger LOG = Logger.getLogger(TenantConfigContext.class); - - /** - * OIDC Provider - */ - final OidcProvider provider; +public sealed interface TenantConfigContext permits TenantConfigContextImpl, LazyTenantConfigContext { /** * Tenant configuration */ - final OidcTenantConfig oidcConfig; - - final Map> redirectFilters; - - /** - * PKCE Secret Key - */ - private final SecretKey stateSecretKey; + OidcTenantConfig oidcConfig(); /** - * Token Encryption Secret Key - */ - private final SecretKey tokenEncSecretKey; - - /** - * Internal ID token generated key + * OIDC Provider */ - private final SecretKey internalIdTokenGeneratedKey; - - final boolean ready; + OidcProvider provider(); - public TenantConfigContext(OidcProvider client, OidcTenantConfig config) { - this(client, config, true); - } - - public TenantConfigContext(OidcProvider provider, OidcTenantConfig config, boolean ready) { - this.provider = provider; - this.oidcConfig = config; - this.redirectFilters = getRedirectFiltersMap(TenantFeatureFinder.find(config, OidcRedirectFilter.class)); - this.ready = ready; - - boolean isService = OidcUtils.isServiceApp(config); - stateSecretKey = !isService && provider != null && provider.client != null ? createStateSecretKey(config) : null; - tokenEncSecretKey = !isService && provider != null && provider.client != null - ? createTokenEncSecretKey(config, provider) - : null; - internalIdTokenGeneratedKey = !isService && provider != null && provider.client != null - ? generateIdTokenSecretKey(config, provider) - : null; - } + boolean ready(); - private static SecretKey createStateSecretKey(OidcTenantConfig config) { - if (config.authentication.pkceRequired.orElse(false) || config.authentication.nonceRequired) { - String stateSecret = null; - if (config.authentication.pkceSecret.isPresent() && config.authentication.getStateSecret().isPresent()) { - throw new ConfigurationException( - "Both 'quarkus.oidc.authentication.state-secret' and 'quarkus.oidc.authentication.pkce-secret' are configured"); - } - if (config.authentication.getStateSecret().isPresent()) { - stateSecret = config.authentication.getStateSecret().get(); - } else if (config.authentication.pkceSecret.isPresent()) { - stateSecret = config.authentication.pkceSecret.get(); - } - - if (stateSecret == null) { - LOG.debug("'quarkus.oidc.authentication.state-secret' is not configured"); - String possiblePkceSecret = OidcCommonUtils.getClientOrJwtSecret(config.credentials); - if (possiblePkceSecret != null && possiblePkceSecret.length() < 32) { - LOG.debug("Client secret is less than 32 characters long, the state secret will be generated"); - } else { - stateSecret = possiblePkceSecret; - } - } - try { - if (stateSecret == null) { - LOG.debug("Secret key for encrypting state cookie is missing, auto-generating it"); - SecretKey key = OidcCommonUtils.generateSecretKey(); - return key; - } - byte[] secretBytes = stateSecret.getBytes(StandardCharsets.UTF_8); - if (secretBytes.length < 32) { - String errorMessage = "Secret key for encrypting state cookie should be at least 32 characters long" - + " for the strongest state cookie encryption to be produced." - + " Please update 'quarkus.oidc.authentication.state-secret' or update the configured client secret."; - if (secretBytes.length < 16) { - throw new ConfigurationException( - "Secret key for encrypting state cookie is less than 16 characters long"); - } else { - LOG.debug(errorMessage); - } - } - return new SecretKeySpec(OidcUtils.getSha256Digest(secretBytes), "AES"); - } catch (Exception ex) { - throw new OIDCException(ex); - } - } - return null; - } + OidcTenantConfig getOidcTenantConfig(); - private static SecretKey createTokenEncSecretKey(OidcTenantConfig config, OidcProvider provider) { - if (config.tokenStateManager.encryptionRequired) { - String encSecret = null; - if (config.tokenStateManager.encryptionSecret.isPresent()) { - encSecret = config.tokenStateManager.encryptionSecret.get(); - } else { - LOG.debug("'quarkus.oidc.token-state-manager.encryption-secret' is not configured"); - encSecret = OidcCommonUtils.getClientOrJwtSecret(config.credentials); - } - try { - if (encSecret != null) { - byte[] secretBytes = encSecret.getBytes(StandardCharsets.UTF_8); - if (secretBytes.length < 32) { - String errorMessage = "Secret key for encrypting tokens in a session cookie should be at least 32 characters long" - + " for the strongest cookie encryption to be produced." - + " Please configure 'quarkus.oidc.token-state-manager.encryption-secret'" - + " or update the configured client secret. You can disable the session cookie" - + " encryption with 'quarkus.oidc.token-state-manager.encryption-required=false'" - + " but only if it is considered to be safe in your application's network."; - if (secretBytes.length < 16) { - LOG.warn(errorMessage); - } else { - LOG.debug(errorMessage); - } - } - return OidcUtils.createSecretKeyFromDigest(secretBytes); - } else if (provider.client.getClientJwtKey() instanceof PrivateKey) { - return OidcUtils.createSecretKeyFromDigest(((PrivateKey) provider.client.getClientJwtKey()).getEncoded()); - } - - LOG.warn( - "Secret key for encrypting OIDC authorization code flow tokens in a session cookie is not configured, auto-generating it." - + " Note that a new secret will be generated after a restart, thus making it impossible to decrypt the session cookie and requiring a user re-authentication." - + " Use 'quarkus.oidc.token-state-manager.encryption-secret' to configure an encryption secret." - + " Alternatively, disable session cookie encryption with 'quarkus.oidc.token-state-manager.encryption-required=false'" - + " but only if it is considered to be safe in your application's network."); - return OidcCommonUtils.generateSecretKey(); - } catch (Exception ex) { - throw new OIDCException(ex); - } - } - return null; - } - - private static SecretKey generateIdTokenSecretKey(OidcTenantConfig config, OidcProvider provider) { - try { - return (!config.authentication.idTokenRequired.orElse(true) - && OidcCommonUtils.getClientOrJwtSecret(config.credentials) == null - && provider.client.getClientJwtKey() == null) ? OidcCommonUtils.generateSecretKey() : null; - } catch (Exception ex) { - throw new OIDCException(ex); - } - } + OidcConfigurationMetadata getOidcMetadata(); - public OidcTenantConfig getOidcTenantConfig() { - return oidcConfig; - } + OidcProviderClient getOidcProviderClient(); - public OidcConfigurationMetadata getOidcMetadata() { - return provider != null ? provider.getMetadata() : null; - } + SecretKey getStateEncryptionKey(); - public OidcProviderClient getOidcProviderClient() { - return provider != null ? provider.client : null; - } + SecretKey getTokenEncSecretKey(); - public SecretKey getStateEncryptionKey() { - return stateSecretKey; - } + SecretKey getInternalIdTokenSecretKey(); - public SecretKey getTokenEncSecretKey() { - return tokenEncSecretKey; - } + List getOidcRedirectFilters(Redirect.Location loc); - public SecretKey getInternalIdTokenSecretKey() { - return this.internalIdTokenGeneratedKey; + /** + * Only static tenants that are not {@link #ready()} can and need to be initialized. + * + * @return self, or in case of not {@link #ready()}, possibly ready self + */ + default Uni initialize() { + return Uni.createFrom().item(this); } - private static Map> getRedirectFiltersMap(List filters) { - Map> map = new HashMap<>(); - for (OidcRedirectFilter filter : filters) { - Redirect redirect = ClientProxy.unwrap(filter).getClass().getAnnotation(Redirect.class); - if (redirect != null) { - for (Redirect.Location loc : redirect.value()) { - map.computeIfAbsent(loc, k -> new ArrayList()).add(filter); - } - } else { - map.computeIfAbsent(Redirect.Location.ALL, k -> new ArrayList()).add(filter); - } - } - return map; + static TenantConfigContext createReady(OidcProvider provider, OidcTenantConfig config) { + return new TenantConfigContextImpl(provider, config); } - List getOidcRedirectFilters(Redirect.Location loc) { - List typeSpecific = redirectFilters.get(loc); - List all = redirectFilters.get(Redirect.Location.ALL); - if (typeSpecific == null && all == null) { - return List.of(); - } - if (typeSpecific != null && all == null) { - return typeSpecific; - } else if (typeSpecific == null && all != null) { - return all; - } else { - List combined = new ArrayList<>(typeSpecific.size() + all.size()); - combined.addAll(typeSpecific); - combined.addAll(all); - return combined; - } + static TenantConfigContext createNotReady(OidcProvider provider, OidcTenantConfig config, + Supplier> staticTenantCreator) { + var notReadyContext = new TenantConfigContextImpl(provider, config, false); + return new LazyTenantConfigContext(notReadyContext, staticTenantCreator); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContextImpl.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContextImpl.java new file mode 100644 index 0000000000000..0433a16e4b417 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContextImpl.java @@ -0,0 +1,256 @@ +package io.quarkus.oidc.runtime; + +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.ClientProxy; +import io.quarkus.oidc.OIDCException; +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.OidcRedirectFilter; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.Redirect; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; +import io.quarkus.runtime.configuration.ConfigurationException; + +final class TenantConfigContextImpl implements TenantConfigContext { + private static final Logger LOG = Logger.getLogger(TenantConfigContextImpl.class); + + /** + * OIDC Provider + */ + private final OidcProvider provider; + + /** + * Tenant configuration + */ + private final OidcTenantConfig oidcConfig; + + private final Map> redirectFilters; + + /** + * PKCE Secret Key + */ + private final SecretKey stateSecretKey; + + /** + * Token Encryption Secret Key + */ + private final SecretKey tokenEncSecretKey; + + /** + * Internal ID token generated key + */ + private final SecretKey internalIdTokenGeneratedKey; + + private final boolean ready; + + TenantConfigContextImpl(OidcProvider client, OidcTenantConfig config) { + this(client, config, true); + } + + TenantConfigContextImpl(OidcProvider provider, OidcTenantConfig config, boolean ready) { + this.provider = provider; + this.oidcConfig = config; + this.redirectFilters = getRedirectFiltersMap(TenantFeatureFinder.find(config, OidcRedirectFilter.class)); + this.ready = ready; + + boolean isService = OidcUtils.isServiceApp(config); + stateSecretKey = !isService && provider != null && provider.client != null ? createStateSecretKey(config) : null; + tokenEncSecretKey = !isService && provider != null && provider.client != null + ? createTokenEncSecretKey(config, provider) + : null; + internalIdTokenGeneratedKey = !isService && provider != null && provider.client != null + ? generateIdTokenSecretKey(config, provider) + : null; + } + + private static SecretKey createStateSecretKey(OidcTenantConfig config) { + if (config.authentication.pkceRequired.orElse(false) || config.authentication.nonceRequired) { + String stateSecret = null; + if (config.authentication.pkceSecret.isPresent() && config.authentication.getStateSecret().isPresent()) { + throw new ConfigurationException( + "Both 'quarkus.oidc.authentication.state-secret' and 'quarkus.oidc.authentication.pkce-secret' are configured"); + } + if (config.authentication.getStateSecret().isPresent()) { + stateSecret = config.authentication.getStateSecret().get(); + } else if (config.authentication.pkceSecret.isPresent()) { + stateSecret = config.authentication.pkceSecret.get(); + } + + if (stateSecret == null) { + LOG.debug("'quarkus.oidc.authentication.state-secret' is not configured"); + String possiblePkceSecret = OidcCommonUtils.getClientOrJwtSecret(config.credentials); + if (possiblePkceSecret != null && possiblePkceSecret.length() < 32) { + LOG.debug("Client secret is less than 32 characters long, the state secret will be generated"); + } else { + stateSecret = possiblePkceSecret; + } + } + try { + if (stateSecret == null) { + LOG.debug("Secret key for encrypting state cookie is missing, auto-generating it"); + SecretKey key = OidcCommonUtils.generateSecretKey(); + return key; + } + byte[] secretBytes = stateSecret.getBytes(StandardCharsets.UTF_8); + if (secretBytes.length < 32) { + String errorMessage = "Secret key for encrypting state cookie should be at least 32 characters long" + + " for the strongest state cookie encryption to be produced." + + " Please update 'quarkus.oidc.authentication.state-secret' or update the configured client secret."; + if (secretBytes.length < 16) { + throw new ConfigurationException( + "Secret key for encrypting state cookie is less than 16 characters long"); + } else { + LOG.debug(errorMessage); + } + } + return new SecretKeySpec(OidcUtils.getSha256Digest(secretBytes), "AES"); + } catch (Exception ex) { + throw new OIDCException(ex); + } + } + return null; + } + + private static SecretKey createTokenEncSecretKey(OidcTenantConfig config, OidcProvider provider) { + if (config.tokenStateManager.encryptionRequired) { + String encSecret = null; + if (config.tokenStateManager.encryptionSecret.isPresent()) { + encSecret = config.tokenStateManager.encryptionSecret.get(); + } else { + LOG.debug("'quarkus.oidc.token-state-manager.encryption-secret' is not configured"); + encSecret = OidcCommonUtils.getClientOrJwtSecret(config.credentials); + } + try { + if (encSecret != null) { + byte[] secretBytes = encSecret.getBytes(StandardCharsets.UTF_8); + if (secretBytes.length < 32) { + String errorMessage = "Secret key for encrypting tokens in a session cookie should be at least 32 characters long" + + " for the strongest cookie encryption to be produced." + + " Please configure 'quarkus.oidc.token-state-manager.encryption-secret'" + + " or update the configured client secret. You can disable the session cookie" + + " encryption with 'quarkus.oidc.token-state-manager.encryption-required=false'" + + " but only if it is considered to be safe in your application's network."; + if (secretBytes.length < 16) { + LOG.warn(errorMessage); + } else { + LOG.debug(errorMessage); + } + } + return OidcUtils.createSecretKeyFromDigest(secretBytes); + } else if (provider.client.getClientJwtKey() instanceof PrivateKey) { + return OidcUtils.createSecretKeyFromDigest(((PrivateKey) provider.client.getClientJwtKey()).getEncoded()); + } + + LOG.warn( + "Secret key for encrypting OIDC authorization code flow tokens in a session cookie is not configured, auto-generating it." + + " Note that a new secret will be generated after a restart, thus making it impossible to decrypt the session cookie and requiring a user re-authentication." + + " Use 'quarkus.oidc.token-state-manager.encryption-secret' to configure an encryption secret." + + " Alternatively, disable session cookie encryption with 'quarkus.oidc.token-state-manager.encryption-required=false'" + + " but only if it is considered to be safe in your application's network."); + return OidcCommonUtils.generateSecretKey(); + } catch (Exception ex) { + throw new OIDCException(ex); + } + } + return null; + } + + private static SecretKey generateIdTokenSecretKey(OidcTenantConfig config, OidcProvider provider) { + try { + return (!config.authentication.idTokenRequired.orElse(true) + && OidcCommonUtils.getClientOrJwtSecret(config.credentials) == null + && provider.client.getClientJwtKey() == null) ? OidcCommonUtils.generateSecretKey() : null; + } catch (Exception ex) { + throw new OIDCException(ex); + } + } + + @Override + public OidcTenantConfig oidcConfig() { + return oidcConfig; + } + + @Override + public OidcProvider provider() { + return provider; + } + + @Override + public boolean ready() { + return ready; + } + + @Override + public OidcTenantConfig getOidcTenantConfig() { + return oidcConfig; + } + + @Override + public OidcConfigurationMetadata getOidcMetadata() { + return provider != null ? provider.getMetadata() : null; + } + + @Override + public OidcProviderClient getOidcProviderClient() { + return provider != null ? provider.client : null; + } + + @Override + public SecretKey getStateEncryptionKey() { + return stateSecretKey; + } + + @Override + public SecretKey getTokenEncSecretKey() { + return tokenEncSecretKey; + } + + @Override + public SecretKey getInternalIdTokenSecretKey() { + return this.internalIdTokenGeneratedKey; + } + + private static Map> getRedirectFiltersMap(List filters) { + Map> map = new HashMap<>(); + for (OidcRedirectFilter filter : filters) { + Redirect redirect = ClientProxy.unwrap(filter).getClass().getAnnotation(Redirect.class); + if (redirect != null) { + for (Redirect.Location loc : redirect.value()) { + map.computeIfAbsent(loc, k -> new ArrayList()).add(filter); + } + } else { + map.computeIfAbsent(Redirect.Location.ALL, k -> new ArrayList()).add(filter); + } + } + return map; + } + + @Override + public List getOidcRedirectFilters(Redirect.Location loc) { + List typeSpecific = redirectFilters.get(loc); + List all = redirectFilters.get(Redirect.Location.ALL); + if (typeSpecific == null && all == null) { + return List.of(); + } + if (typeSpecific != null && all == null) { + return typeSpecific; + } else if (typeSpecific == null && all != null) { + return all; + } else { + List combined = new ArrayList<>(typeSpecific.size() + all.size()); + combined.addAll(typeSpecific); + combined.addAll(all); + return combined; + } + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java index 866a6d9ea0a40..0d11fb5bd819f 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java @@ -11,6 +11,7 @@ import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; +import io.vertx.ext.web.RoutingContext; /** * @author Pedro Igor @@ -21,6 +22,9 @@ public class AdminResource { @Inject SecurityIdentity identity; + @Inject + RoutingContext routingContext; + @Path("bearer") @GET @RolesAllowed("admin") @@ -53,6 +57,14 @@ public String adminNoIntrospection() { return "granted:" + identity.getRoles(); } + @Path("bearer-issuer-resolver/issuer") // don't change the path, avoid default tenant resolver + @GET + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + public String adminIssuerTest() { + return "static.tenant.id=" + routingContext.get("static.tenant.id"); + } + @Path("bearer-certificate-full-chain") @GET @RolesAllowed("admin") diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 36c0bd85cdb4e..4dab8862ff9b8 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -254,3 +254,10 @@ quarkus.native.additional-build-args=-H:IncludeResources=private.*\\.*,-H:Includ quarkus.grpc.clients.hello.host=localhost quarkus.grpc.clients.hello.port=8081 quarkus.grpc.server.use-separate-server=false + +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver.auth-server-url=http://localhost:8185/auth/realms/quarkus2 +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver.client-id=quarkus-app +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver.credentials.secret=secret +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver.token.audience=https://correct-issuer.edu +%issuer-based-resolver.quarkus.oidc.bearer-issuer-resolver.token.allow-jwt-introspection=false +%issuer-based-resolver.quarkus.oidc.resolve-tenants-with-issuer=true diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/StaticTenantIssuerResolverTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/StaticTenantIssuerResolverTest.java new file mode 100644 index 0000000000000..89c3365b7b944 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/StaticTenantIssuerResolverTest.java @@ -0,0 +1,55 @@ +package io.quarkus.it.keycloak; + +import java.util.Set; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; +import io.restassured.response.ValidatableResponse; +import io.smallrye.jwt.build.Jwt; + +@TestProfile(StaticTenantIssuerResolverTest.IssuerResolverProfile.class) +@QuarkusTest +public class StaticTenantIssuerResolverTest { + + @Test + public void testOidcServerUnavailableOnAppStartup() { + WiremockTestResource server = new WiremockTestResource("https://correct-issuer.edu", 8185); + server.start(); + try { + // 500 because default tenant has unavailable OIDC server (otherwise it assumes our issuer) + requestAdminRoles("https://wrong-issuer.edu").statusCode(500); + + requestAdminRoles("https://correct-issuer.edu").statusCode(200) + .body(Matchers.is("static.tenant.id=bearer-issuer-resolver")); + } finally { + server.stop(); + } + } + + private static ValidatableResponse requestAdminRoles(String issuer) { + return RestAssured.given().auth().oauth2(getAdminTokenWithRole(issuer)) + .when().get("/api/admin/bearer-issuer-resolver/issuer").then(); + } + + public static class IssuerResolverProfile implements QuarkusTestProfile { + @Override + public String getConfigProfile() { + return "issuer-based-resolver"; + } + } + + private static String getAdminTokenWithRole(String issuer) { + return Jwt.preferredUserName("alice") + .groups(Set.of("admin")) + .issuer(issuer) + .audience(issuer) + .jws() + .keyId("1") + .sign("privateKey.jwk"); + } +} diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java index 0ea4f558370de..51e3b088fd874 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/WiremockTestResource.java @@ -7,6 +7,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.not; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static io.quarkus.oidc.OidcConfigurationMetadata.ISSUER; import org.jboss.logging.Logger; @@ -16,13 +17,25 @@ public class WiremockTestResource { private static final Logger LOG = Logger.getLogger(WiremockTestResource.class); + private final String issuer; + private final int port; private WireMockServer server; + public WiremockTestResource() { + this.issuer = null; + this.port = 8180; + } + + public WiremockTestResource(String issuer, int port) { + this.issuer = issuer; + this.port = port; + } + public void start() { server = new WireMockServer( wireMockConfig() - .port(8180)); + .port(port)); server.start(); server.stubFor( @@ -32,6 +45,7 @@ public void start() { .willReturn(aResponse() .withHeader("Content-Type", "application/json") .withBody("{\n" + + (issuer == null ? "" : " \"" + ISSUER + "\": " + "\"" + issuer + "\",\n") + " \"jwks_uri\": \"" + server.baseUrl() + "/auth/realms/quarkus2/protocol/openid-connect/certs\"" + "}")));