diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java index e33c6ce70..59c6ebdec 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java @@ -45,11 +45,9 @@ import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.apache.guacamole.net.auth.IdentifierGenerator; import org.jose4j.jwt.JwtClaims; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.guacamole.net.auth.IdentifierGenerator; /** * Service that authenticates Guacamole users by processing OpenID tokens. @@ -81,12 +79,6 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS */ public static final String AUTH_SESSION_QUERY_PARAM = "state"; - /** - * The REST API used for a local rediect and callback for the IdP to - * return a code and access stored PKCE verifiers - */ - public static final String CODE_REDIRECT_API = "/api/ext/openid/redirect"; - /** * Service for retrieving OpenID configuration information. */ @@ -94,7 +86,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS private ConfigurationService confService; /** - * Manager of active SAML authentication attempts. + * Manager of active OpenID authentication attempts. */ @Inject private OpenIDAuthenticationSessionManager sessionManager; @@ -197,6 +189,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) })) ); + } @Override diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java.orig b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java.orig new file mode 100644 index 000000000..1513b9b18 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java.orig @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.openid; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.auth.openid.conf.ConfigurationService; +import org.apache.guacamole.auth.openid.OpenIDAuthenticationSessionManager; +import org.apache.guacamole.auth.openid.token.TokenValidationService; +import org.apache.guacamole.auth.openid.util.PKCEUtil; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; +import org.apache.guacamole.form.Field; +import org.apache.guacamole.form.RedirectField; +import org.apache.guacamole.language.TranslatableMessage; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.credentials.CredentialsInfo; +import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; +import org.apache.guacamole.net.auth.IdentifierGenerator; +import org.jose4j.jwt.JwtClaims; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.guacamole.net.auth.IdentifierGenerator; + +/** + * Service that authenticates Guacamole users by processing OpenID tokens. + */ +@Singleton +public class AuthenticationProviderService implements SSOAuthenticationProviderService { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class); + + /** + * The standard HTTP parameter which will be included within the URL by all + * OpenID services upon successful implicit flow authentication. + */ + public static final String IMPLICIT_TOKEN_PARAMETER_NAME = "id_token"; + + /** + * The standard HTTP parameter which will be included within the URL by all + * OpenID services upon successful code flow authentication. Used to recover + * the stored user state. + */ + public static final String CODE_TOKEN_PARAMETER_NAME = "code"; + + /** + * The name of the query parameter that identifies an active authentication + * session (in-progress OpenID authentication attempt). + */ + public static final String AUTH_SESSION_QUERY_PARAM = "state"; + + /** + * The REST API used for a local rediect and callback for the IdP to + * return a code and access stored PKCE verifiers + */ + public static final String CODE_REDIRECT_API = "/api/ext/openid/redirect"; + + /** + * Service for retrieving OpenID configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Manager of active SAML authentication attempts. + */ + @Inject + private OpenIDAuthenticationSessionManager sessionManager; + + /** + * Service for validating and generating unique nonce values. + */ + @Inject + private NonceService nonceService; + + /** + * Service for validating received ID tokens. + */ + @Inject + private TokenValidationService tokenService; + + /** + * Provider for AuthenticatedUser objects. + */ + @Inject + private Provider authenticatedUserProvider; + + /** + * Return the value of the session identifier associated with the given + * credentials, or null if no session identifier is found in the + * credentials. + * + * @param credentials + * The credentials from which to extract the session identifier. + * + * @return + * The session identifier associated with the given credentials, or + * null if no identifier is found. + */ + public static String getSessionIdentifier(Credentials credentials) { + + // Return the session identifier from the request params, if set, or + // null otherwise + return credentials != null ? credentials.getParameter(AUTH_SESSION_QUERY_PARAM) : null; + } + + @Override + public SSOAuthenticatedUser authenticateUser(Credentials credentials) + throws GuacamoleException { + + String username = null; + Set groups = null; + Map tokens = Collections.emptyMap(); + + logger.debug("OpenID authentication with {} flow (ID: {}, Secret: {}, PKCE: {})", + confService.getFlowType(), + confService.getClientID(), + confService.getClientSecret(), + confService.isPKCERequired()); + + if (confService.getFlowType().equals("implicit")) { + String token = credentials.getParameter(IMPLICIT_TOKEN_PARAMETER_NAME); + if (token != null) { + JwtClaims claims = tokenService.validateToken(token); + if (claims != null) { + username = tokenService.processUsername(claims); + groups = tokenService.processGroups(claims); + tokens = tokenService.processAttributes(claims); + } + } + } else { + String verifier = null; + if (confService.isPKCERequired()) { + // Recover session + String identifier = getSessionIdentifier(credentials); + if (identifier != null) { + verifier = sessionManager.getVerifier(identifier); + } + } + String code = credentials.getParameter("code"); + if (code != null && (confService.isPKCERequired() == false || verifier != null)) { + JwtClaims claims = tokenService.validateCode(code, verifier); + if (claims != null) { + username = tokenService.processUsername(claims); + groups = tokenService.processGroups(claims); + tokens = tokenService.processAttributes(claims); + } + } + } + + // If the username was successfully retrieved from the token, produce + // authenticated user + if (username != null) { + // Create corresponding authenticated user + SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); + authenticatedUser.init(username, credentials, groups, tokens); + return authenticatedUser; + } + + // Request OpenID token (will automatically redirect the user to the + // OpenID authorization page via JavaScript) + throw new GuacamoleInvalidCredentialsException("Invalid login.", + new CredentialsInfo(Arrays.asList(new Field[] { + new RedirectField(AUTH_SESSION_QUERY_PARAM, getLoginURI(), + new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) + })) + ); + } + + @Override + public URI getLoginURI() throws GuacamoleException { + if (confService.getFlowType().equals("implicit")) { + return UriBuilder.fromUri(confService.getAuthorizationEndpoint()) + .queryParam("scope", confService.getScope()) + .queryParam("response_type", IMPLICIT_TOKEN_PARAMETER_NAME) + .queryParam("client_id", confService.getClientID()) + .queryParam("redirect_uri", confService.getRedirectURI()) + .queryParam("nonce", nonceService.generate(confService.getMaxNonceValidity() * 60000L)) + .build(); + } else { + UriBuilder builder = UriBuilder.fromUri(confService.getAuthorizationEndpoint()) + .queryParam("scope", confService.getScope()) + .queryParam("response_type", CODE_TOKEN_PARAMETER_NAME) + .queryParam("client_id", confService.getClientID()) + .queryParam("redirect_uri", confService.getRedirectURI()); + + if (confService.isPKCERequired()) { + String codeVerifier = PKCEUtil.generateCodeVerifier(); + String codeChallenge; + + try { + codeChallenge = PKCEUtil.generateCodeChallenge(codeVerifier); + } + catch (Exception e) { + throw new GuacamoleException("Unable to compute PKCE challenge", e); + } + + // Store verifier for authenticateUser + OpenIDAuthenticationSession session = new OpenIDAuthenticationSession(codeVerifier, + confService.getAuthenticationTimeout() * 60000L); + String identifier = IdentifierGenerator.generateIdentifier(); + sessionManager.defer(session, identifier); + + builder.queryParam("code_challenge", codeChallenge) + .queryParam("code_challenge_method", "S256") + .queryParam(AUTH_SESSION_QUERY_PARAM, identifier); + } + + return builder.build(); + } + } + + @Override + public URI getLogoutURI(String idToken) throws GuacamoleException { + + // If no logout endpoint is configured, return null + URI logoutEndpoint = confService.getLogoutEndpoint(); + if (logoutEndpoint == null) + return null; + + // Build the logout URI with appropriate parameters + UriBuilder logoutUriBuilder = UriBuilder.fromUri(logoutEndpoint); + + // Add post_logout_redirect_uri parameter + logoutUriBuilder.queryParam("post_logout_redirect_uri", + confService.getPostLogoutRedirectURI()); + + // Add id_token_hint if available, otherwise add client_id + if (idToken != null && !idToken.isEmpty()) + logoutUriBuilder.queryParam("id_token_hint", idToken); + else + logoutUriBuilder.queryParam("client_id", confService.getClientID()); + + return logoutUriBuilder.build(); + } + + @Override + public void shutdown() { + sessionManager.shutdown(); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java.rej b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java.rej new file mode 100644 index 000000000..588486f9b --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java.rej @@ -0,0 +1,11 @@ +--- extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java 2026-04-08 19:37:41.518619970 +0200 ++++ extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/OpenIDAuthenticationProviderModule.java 2026-04-08 16:43:54.617087120 +0200 +@@ -37,7 +37,7 @@ + * The configuration environment for this server and extension. + */ + private final Environment environment = new OpenIDEnvironment(); +- ++ + @Override + protected void configure() { + bind(ConfigurationService.class); diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java index 2801281c0..481352450 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java @@ -461,7 +461,6 @@ public class ConfigurationService { return environment.getProperty(OPENID_AUTH_TIMEOUT, 5); } - /** * Returns the claim type which contains the authenticated user's username * within any valid JWT, as configured with guacamole.properties. By diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java.orig b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java.orig new file mode 100644 index 000000000..cf7bc1407 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java.orig @@ -0,0 +1,639 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.openid.conf; + +import com.google.inject.Inject; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.properties.BooleanGuacamoleProperty; +import org.apache.guacamole.properties.IntegerGuacamoleProperty; +import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.apache.guacamole.properties.URIGuacamoleProperty; + +/** + * Service for retrieving configuration information regarding the OpenID + * service. + */ +public class ConfigurationService { + + /** + * The default OICD flow type + */ + private static final String DEFAULT_FLOW_TYPE = "implicit"; + + /** + * The default claim type to use to retrieve an authenticated user's + * username. + */ + private static final String DEFAULT_USERNAME_CLAIM_TYPE = "email"; + + /** + * The default claim type to use to retrieve an authenticated user's + * groups. + */ + private static final String DEFAULT_GROUPS_CLAIM_TYPE = "groups"; + + /** + * The default JWT claims list to map to tokens. + */ + private static final List DEFAULT_ATTRIBUTES_CLAIM_TYPE = Collections.emptyList(); + + /** + * The default space-separated list of OpenID scopes to request. + */ + private static final String DEFAULT_SCOPE = "openid email profile"; + + /** + * The default amount of clock skew tolerated for timestamp comparisons + * between the Guacamole server and OpenID service clocks, in seconds. + */ + private static final int DEFAULT_ALLOWED_CLOCK_SKEW = 30; + + /** + * The default maximum amount of time that an OpenID token should remain + * valid, in minutes. + */ + private static final int DEFAULT_MAX_TOKEN_VALIDITY = 300; + + /** + * The default maximum amount of time that a nonce generated by the + * Guacamole server should remain valid, in minutes. + */ + private static final int DEFAULT_MAX_NONCE_VALIDITY = 10; + + /** + * The authorization endpoint (URI) of the OpenID service. + */ + private static final URIGuacamoleProperty OPENID_AUTHORIZATION_ENDPOINT = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "openid-authorization-endpoint"; } + + }; + + /** + * The endpoint (URI) of the JWKS service which defines how received ID + * tokens (JWTs) shall be validated. + */ + private static final URIGuacamoleProperty OPENID_JWKS_ENDPOINT = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "openid-jwks-endpoint"; } + + }; + + /** + * The issuer to expect for all received ID tokens. + */ + private static final StringGuacamoleProperty OPENID_ISSUER = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "openid-issuer"; } + + }; + + /** + * The token endpoint (URI) of the OIDC service. + */ + private static final URIGuacamoleProperty OPENID_TOKEN_ENDPOINT = + new URIGuacamoleProperty() { + @Override + public String getName() { + return "openid-token-endpoint"; + } + }; + + /** + * The flow type of the OpenID service. + */ + private static final StringGuacamoleProperty OPENID_FLOW_TYPE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "openid-flow-type"; } + + }; + + /** + * OIDC client secret which should be submitted to the OIDC service when + * validating tokens with code flow + */ + private static final StringGuacamoleProperty OPENID_CLIENT_SECRET = + new StringGuacamoleProperty() { + @Override + public String getName() { + return "openid-client-secret"; + } + }; + + /** + * True if "Proof Key for Code Exchange" (PKCE) must be used. + */ + private static final BooleanGuacamoleProperty OPENID_PKCE_REQUIRED = + new BooleanGuacamoleProperty() { + @Override + public String getName() { + return "openid-pkce-required"; + } + }; + + /** + * The maximum amount of time to allow for an in-progress OpenID + * authentication attempt to be completed, in minutes. A user that takes + * longer than this amount of time to complete authentication with their + * identity provider will be redirected back to the identity provider to + * try again. + */ + private static final IntegerGuacamoleProperty OPENID_AUTH_TIMEOUT = + new IntegerGuacamoleProperty() { + @Override + public String getName() { return "openid-auth-timeout"; } + + }; + + + /** + * The claim type which contains the authenticated user's username within + * any valid JWT. + */ + private static final StringGuacamoleProperty OPENID_USERNAME_CLAIM_TYPE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "openid-username-claim-type"; } + + }; + + /** + * The claim type which contains the authenticated user's groups within + * any valid JWT. + */ + private static final StringGuacamoleProperty OPENID_GROUPS_CLAIM_TYPE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "openid-groups-claim-type"; } + + }; + + /** + * The claims within any valid JWT that should be mapped to + * the authenticated user's tokens, as configured with guacamole.properties. + */ + private static final StringGuacamoleProperty OPENID_ATTRIBUTES_CLAIM_TYPE = + new StringGuacamoleProperty() { + @Override + public String getName() { return "openid-attributes-claim-type"; } + }; + + /** + * The space-separated list of OpenID scopes to request. + */ + private static final StringGuacamoleProperty OPENID_SCOPE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "openid-scope"; } + + }; + + /** + * The amount of clock skew tolerated for timestamp comparisons between the + * Guacamole server and OpenID service clocks, in seconds. + */ + private static final IntegerGuacamoleProperty OPENID_ALLOWED_CLOCK_SKEW = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "openid-allowed-clock-skew"; } + + }; + + /** + * The maximum amount of time that an OpenID token should remain valid, in + * minutes. + */ + private static final IntegerGuacamoleProperty OPENID_MAX_TOKEN_VALIDITY = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "openid-max-token-validity"; } + + }; + + /** + * The maximum amount of time that a nonce generated by the Guacamole server + * should remain valid, in minutes. As each OpenID request has a unique + * nonce value, this imposes an upper limit on the amount of time any + * particular OpenID request can result in successful authentication within + * Guacamole. + */ + private static final IntegerGuacamoleProperty OPENID_MAX_NONCE_VALIDITY = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "openid-max-nonce-validity"; } + + }; + + /** + * OpenID client ID which should be submitted to the OpenID service when + * necessary. This value is typically provided by the OpenID service when + * OpenID credentials are generated for your application. + */ + private static final StringGuacamoleProperty OPENID_CLIENT_ID = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "openid-client-id"; } + + }; + + /** + * The URI that the OpenID service should redirect to after the + * authentication process is complete. This must be the full URL that a + * user would enter into their browser to access Guacamole. + */ + private static final URIGuacamoleProperty OPENID_REDIRECT_URI = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "openid-redirect-uri"; } + + }; + + /** + * The logout endpoint (URI) of the OpenID service. If specified, users + * will be redirected to this endpoint when they log out, allowing them + * to log out from the OpenID provider as well. + */ + private static final URIGuacamoleProperty OPENID_LOGOUT_ENDPOINT = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "openid-logout-endpoint"; } + + }; + + /** + * The URI that the OpenID service should redirect to after logout is + * complete. If not specified, the main redirect URI will be used. + */ + private static final URIGuacamoleProperty OPENID_POST_LOGOUT_REDIRECT_URI = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "openid-post-logout-redirect-uri"; } + + }; + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /** + * Returns the authorization endpoint (URI) of the OpenID service as + * configured with guacamole.properties. + * + * @return + * The authorization endpoint of the OpenID service, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the authorization + * endpoint property is missing. + */ + public URI getAuthorizationEndpoint() throws GuacamoleException { + return environment.getRequiredProperty(OPENID_AUTHORIZATION_ENDPOINT); + } + + /** + * Returns the OpenID client ID which should be submitted to the OpenID + * service when necessary, as configured with guacamole.properties. This + * value is typically provided by the OpenID service when OpenID credentials + * are generated for your application. + * + * @return + * The client ID to use when communicating with the OpenID service, + * as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the client ID + * property is missing. + */ + public String getClientID() throws GuacamoleException { + return environment.getRequiredProperty(OPENID_CLIENT_ID); + } + + /** + * Returns the URI that the OpenID service should redirect to after + * the authentication process is complete, as configured with + * guacamole.properties. This must be the full URL that a user would enter + * into their browser to access Guacamole. + * + * @return + * The client secret to use when communicating with the OpenID service, + * as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the redirect URI + * property is missing. + */ + public URI getRedirectURI() throws GuacamoleException { + return environment.getRequiredProperty(OPENID_REDIRECT_URI); + } + + /** + * Returns the issuer to expect for all received ID tokens, as configured + * with guacamole.properties. + * + * @return + * The issuer to expect for all received ID tokens, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the issuer property + * is missing. + */ + public String getIssuer() throws GuacamoleException { + return environment.getRequiredProperty(OPENID_ISSUER); + } + + /** + * Returns the endpoint (URI) of the JWKS service which defines how + * received ID tokens (JWTs) shall be validated, as configured with + * guacamole.properties. + * + * @return + * The endpoint (URI) of the JWKS service which defines how received ID + * tokens (JWTs) shall be validated, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the JWKS endpoint + * property is missing. + */ + public URI getJWKSEndpoint() throws GuacamoleException { + return environment.getRequiredProperty(OPENID_JWKS_ENDPOINT); + } + + /** + * Returns the token endpoint (URI) of the OIDC service as + * configured with guacamole.properties. + * + * @return + * The token endpoint of the OIDC service, as configured with + * guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the authorization + * endpoint property is missing. + */ + public URI getTokenEndpoint() throws GuacamoleException { + return environment.getRequiredProperty(OPENID_TOKEN_ENDPOINT); + } + + /** + * Returns the flow type of the OpenID service as configured with guacamole.properties. + * + * @return + * The flow type of the OpenID service, as configured with guacamole.properties. Can + * be either 'implicit' or 'code'. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public String getFlowType() throws GuacamoleException { + return environment.getProperty(OPENID_FLOW_TYPE, DEFAULT_FLOW_TYPE); + } + + /** + * Returns the OIDC client secret used for toen validation, as configured + * with guacamole.properties. This value is typically provided by the OIDC + * service when OIDC credentials are generated for your application, and + * may be null. + * + * @return + * The client secret to use when communicating with the OIDC service, + * as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed, or if the client ID + * property is missing. + */ + public String getClientSecret() throws GuacamoleException { + return environment.getProperty(OPENID_CLIENT_SECRET); + } + + /** + * Returns a boolean value of whether "Proof Key for Code Exchange (PKCE)" + * will be used, as configured with guacamole.properties. The choice of + * whether to use PKCE is up to you, but the OIDC service must support + * it. By default will be false. + * + * @return + * The whether to use PKCE or not, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public boolean isPKCERequired() throws GuacamoleException { + Boolean enabled = environment.getProperty(OPENID_PKCE_REQUIRED); + return enabled != null && enabled; + } + + /** + * Returns the maximum amount of time to allow for an in-progress OpenID + * authentication attempt to be completed, in minutes. A user that takes + * longer than this amount of time to complete authentication with their + * identity provider will be redirected back to the identity provider to + * try again. + * + * @return + * The maximum amount of time to allow for an in-progress SAML + * authentication attempt to be completed, in minutes. + * + * @throws GuacamoleException + * If the authentication timeout cannot be parsed. + */ + public int getAuthenticationTimeout() throws GuacamoleException { + return environment.getProperty(OPENID_AUTH_TIMEOUT, 5); + } + + + /** + * Returns the claim type which contains the authenticated user's username + * within any valid JWT, as configured with guacamole.properties. By + * default, this will be "email". + * + * @return + * The claim type which contains the authenticated user's username + * within any valid JWT, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public String getUsernameClaimType() throws GuacamoleException { + return environment.getProperty(OPENID_USERNAME_CLAIM_TYPE, DEFAULT_USERNAME_CLAIM_TYPE); + } + + /** + * Returns the claim type which contains the authenticated user's groups + * within any valid JWT, as configured with guacamole.properties. By + * default, this will be "groups". + * + * @return + * The claim type which contains the authenticated user's groups + * within any valid JWT, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public String getGroupsClaimType() throws GuacamoleException { + return environment.getProperty(OPENID_GROUPS_CLAIM_TYPE, DEFAULT_GROUPS_CLAIM_TYPE); + } + + /** + * Returns the claims list within any valid JWT that should be mapped to + * the authenticated user's tokens, as configured with guacamole.properties. + * Empty by default. + * + * @return + * The claims list within any valid JWT that should be mapped to + * the authenticated user's tokens, as configured with guacamole.properties. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public Collection getAttributesClaimType() throws GuacamoleException { + return environment.getPropertyCollection(OPENID_ATTRIBUTES_CLAIM_TYPE, DEFAULT_ATTRIBUTES_CLAIM_TYPE); + } + + /** + * Returns the space-separated list of OpenID scopes to request. By default, + * this will be "openid email profile". The OpenID scopes determine the + * information returned within the OpenID token, and thus affect what + * values can be used as an authenticated user's username. + * + * @return + * The space-separated list of OpenID scopes to request when identifying + * a user. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public String getScope() throws GuacamoleException { + return environment.getProperty(OPENID_SCOPE, DEFAULT_SCOPE); + } + + /** + * Returns the amount of clock skew tolerated for timestamp comparisons + * between the Guacamole server and OpenID service clocks, in seconds. Too + * much clock skew will affect token expiration calculations, possibly + * allowing old tokens to be used. By default, this will be 30. + * + * @return + * The amount of clock skew tolerated for timestamp comparisons, in + * seconds. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getAllowedClockSkew() throws GuacamoleException { + return environment.getProperty(OPENID_ALLOWED_CLOCK_SKEW, DEFAULT_ALLOWED_CLOCK_SKEW); + } + + /** + * Returns the maximum amount of time that an OpenID token should remain + * valid, in minutes. A token received from an OpenID service which is + * older than this amount of time will be rejected, even if it is otherwise + * valid. By default, this will be 300 (5 hours). + * + * @return + * The maximum amount of time that an OpenID token should remain valid, + * in minutes. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getMaxTokenValidity() throws GuacamoleException { + return environment.getProperty(OPENID_MAX_TOKEN_VALIDITY, DEFAULT_MAX_TOKEN_VALIDITY); + } + + /** + * Returns the maximum amount of time that a nonce generated by the + * Guacamole server should remain valid, in minutes. As each OpenID request + * has a unique nonce value, this imposes an upper limit on the amount of + * time any particular OpenID request can result in successful + * authentication within Guacamole. By default, this will be 10. + * + * @return + * The maximum amount of time that a nonce generated by the Guacamole + * server should remain valid, in minutes. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getMaxNonceValidity() throws GuacamoleException { + return environment.getProperty(OPENID_MAX_NONCE_VALIDITY, DEFAULT_MAX_NONCE_VALIDITY); + } + + /** + * Returns the logout endpoint (URI) of the OpenID service, as configured + * with guacamole.properties. If configured, users will be redirected to + * this endpoint when they log out from Guacamole. + * + * @return + * The logout endpoint of the OpenID service, as configured with + * guacamole.properties, or null if not configured. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public URI getLogoutEndpoint() throws GuacamoleException { + return environment.getProperty(OPENID_LOGOUT_ENDPOINT); + } + + /** + * Returns the URI that the OpenID service should redirect to after logout + * is complete, as configured with guacamole.properties. If not configured, + * the main redirect URI will be used. + * + * @return + * The post-logout redirect URI, as configured with + * guacamole.properties, or the main redirect URI if not specified. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public URI getPostLogoutRedirectURI() throws GuacamoleException { + return environment.getProperty(OPENID_POST_LOGOUT_REDIRECT_URI, getRedirectURI()); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDEnvironment.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDEnvironment.java index cb80689a2..440463081 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDEnvironment.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/conf/OpenIDEnvironment.java @@ -27,7 +27,7 @@ import org.apache.guacamole.environment.LocalEnvironment; * configuration. */ public class OpenIDEnvironment extends DelegatingEnvironment { - + /** * Create a new instance of the configuration environment for the * OpenID SSO module, pulling the default instance of the LocalEnvironment. @@ -35,5 +35,5 @@ public class OpenIDEnvironment extends DelegatingEnvironment { public OpenIDEnvironment() { super(LocalEnvironment.getInstance()); } - + } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java index 78afddd4c..c0f4dfae4 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java @@ -35,7 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import javax.servlet.http.HttpServletRequest; + import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.openid.conf.ConfigurationService; @@ -191,7 +191,7 @@ public class TokenValidationService { return null; } - + /** * URLEncodes a key/value pair * @@ -297,7 +297,7 @@ public class TokenValidationService { /** * Parses the given JwtClaims, returning the username contained * therein, as defined by the username claim type given in - * guacamole.properties. If the username claim type is missing or + * guacamole.properties. If the username claim type is missing or * is invalid, null is returned. * * @param claims @@ -362,7 +362,7 @@ public class TokenValidationService { List oidcGroups = claims.getStringListClaimValue(groupsClaim); if (oidcGroups != null && !oidcGroups.isEmpty()) return Collections.unmodifiableSet(new HashSet<>(oidcGroups)); - } + } catch (MalformedClaimException e) { logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage()); logger.debug("Malformed claim within received JWT.", e); diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java.orig b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java.orig new file mode 100644 index 000000000..fd9f4c16a --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java.orig @@ -0,0 +1,430 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.openid.token; + +import com.google.inject.Inject; +import java.io.BufferedReader; +import java.io.OutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.openid.conf.ConfigurationService; +import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.token.TokenName; +import org.jose4j.json.JsonUtil; +import org.jose4j.jwk.HttpsJwks; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Service for validating ID tokens forwarded to us by the client, verifying + * that they did indeed come from the OpenID service. + */ +public class TokenValidationService { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(TokenValidationService.class); + + /** + * The prefix to use when generating token names. + */ + public static final String OIDC_ATTRIBUTE_TOKEN_PREFIX = "OIDC_"; + + /** + * Service for retrieving OpenID configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Service for validating and generating unique nonce values. + */ + @Inject + private NonceService nonceService; + + /** + * Validates the given ID token, using implicit flow, returning the JwtClaims + * contained therein. If the ID token is invalid, null is returned. + * + * @param token + * The ID token to validate. + * + * @return + * The JWT claims contained within the given ID token if it passes tests, + * or null if the token is not valid. + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public JwtClaims validateToken(String token) throws GuacamoleException { + // Validating the token requires a JWKS key resolver + HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint().toString()); + HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks); + + // Create JWT consumer for validating received token + JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setRequireExpirationTime() + .setMaxFutureValidityInMinutes(confService.getMaxTokenValidity()) + .setAllowedClockSkewInSeconds(confService.getAllowedClockSkew()) + .setRequireSubject() + .setExpectedIssuer(confService.getIssuer()) + .setExpectedAudience(confService.getClientID()) + .setVerificationKeyResolver(resolver) + .build(); + + try { + // Validate JWT + JwtClaims claims = jwtConsumer.processToClaims(token); + + // Verify a nonce is present + String nonce = claims.getStringClaimValue("nonce"); + if (nonce != null) { + // Verify that we actually generated the nonce, and that it has not + // already been used + if (nonceService.isValid(nonce)) { + // nonce is valid, consider claims valid + return claims; + } + else { + logger.info("Rejected OpenID token with invalid/old nonce."); + } + } + else { + logger.info("Rejected OpenID token without nonce."); + } + } + // Log any failures to validate/parse the JWT + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage(), e); + } + catch (InvalidJwtException e) { + logger.info("Rejected invalid OpenID token: {}", e.getMessage(), e); + } + + return null; + } + + /** + * Validates the given ID token, using code flow, returning the JwtClaims + * contained therein. If the ID token is invalid, null is returned. + * + * @param code + * The code to validate and receive the id_token. + * + * @param verifier + * A PKCE verifier or null if not used. + * + * @return + * The JWT claims contained within the given ID token if it passes tests, + * or null if the token is not valid. + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public JwtClaims validateCode(String code, String verifier) throws GuacamoleException { + // Validating the token requires a JWKS key resolver + HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint().toString()); + HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks); + + /* Exchange code → token */ + String token = exchangeCode(code, verifier); + + // Create JWT consumer for validating received token + JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setRequireExpirationTime() + .setMaxFutureValidityInMinutes(confService.getMaxTokenValidity()) + .setAllowedClockSkewInSeconds(confService.getAllowedClockSkew()) + .setRequireSubject() + .setExpectedIssuer(confService.getIssuer()) + .setExpectedAudience(confService.getClientID()) + .setVerificationKeyResolver(resolver) + .build(); + + try { + // Validate JWT + return jwtConsumer.processToClaims(token); + } + // Log any failures to validate/parse the JWT + catch (InvalidJwtException e) { + logger.info("Rejected invalid OpenID token: {}", e.getMessage(), e); + } + + return null; + } + + /** + * URLEncodes a key/value pair + * + * @param key + * The key to encode + * + * @param value + * The value to encode + * + * @return + * The urlencoded kay/value pair + */ + private String urlencode(String key, String value) { + StringBuilder builder = new StringBuilder(); + return builder.append(URLEncoder.encode(key, StandardCharsets.UTF_8)) + .append("=") + .append(URLEncoder.encode(value, StandardCharsets.UTF_8)) + .toString(); + } + + /** + * Exchanges the authorization code for tokens. + * + * @param code + * The authorization code received from the IdP. + * @param codeVerifier + * The PKCE verifier (or null if PKCE is disabled). + * + * @return + * The token string returned. + * + * @throws GuacamoleException + * If a valid token is not returned. + */ + private String exchangeCode(String code, String verifier) throws GuacamoleException { + + try { + StringBuilder bodyBuilder = new StringBuilder(); + bodyBuilder.append(urlencode("grant_type", "authorization_code")).append("&"); + bodyBuilder.append(urlencode("code", code)).append("&"); + bodyBuilder.append(urlencode("redirect_uri", confService.getRedirectURI().toString())).append("&"); + bodyBuilder.append(urlencode("scope", confService.getScope())).append("&"); + bodyBuilder.append(urlencode("client_id", confService.getClientID())); + + String clientSecret = confService.getClientSecret(); + if (clientSecret != null && !clientSecret.trim().isEmpty()) { + bodyBuilder.append("&").append(urlencode("client_secret", clientSecret)); + } + + if (confService.isPKCERequired()) { + bodyBuilder.append("&").append(urlencode("code_verifier", verifier)); + } + + // Build the final URI and convert to a URL + URL url = confService.getTokenEndpoint().toURL(); + + // Open connection, using HttpURLConnection + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8" + ); + + try (OutputStream out = conn.getOutputStream()) { + byte [] body = bodyBuilder.toString().getBytes(StandardCharsets.UTF_8); + out.write(body, 0, body.length); + } + + // Read response + int status = conn.getResponseCode(); + + BufferedReader reader = new BufferedReader( + new InputStreamReader( + status >= 200 && status < 300 + ? conn.getInputStream() + : conn.getErrorStream(), + StandardCharsets.UTF_8 + ) + ); + + StringBuilder responseBody = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + responseBody.append(line); + } + reader.close(); + + Map json = JsonUtil.parseJson(responseBody.toString()); + + if (status < 200 || status >= 300) { + throw new GuacamoleException("Token endpoint error (" + status + "): " + json.toString()); + } + + return (String) json.get("id_token"); + + } catch (Exception e) { + logger.info("Rejected invalid OpenID code exchange: {}", e.getMessage(), e); + } + return null; + } + + /** + * Parses the given JwtClaims, returning the username contained + * therein, as defined by the username claim type given in + * guacamole.properties. If the username claim type is missing or + * is invalid, null is returned. + * + * @param claims + * A valid JwtClaims to extract the username from. + * + * @return + * The username contained within the given JwtClaims, or null if the + * claim is not valid or the username claim type is missing, + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public String processUsername(JwtClaims claims) throws GuacamoleException { + String usernameClaim = confService.getUsernameClaimType(); + + if (claims != null) { + try { + // Pull username from claims + String username = claims.getStringClaimValue(usernameClaim); + if (username != null) + return username; + } + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage(), e); + } + + // Warn if username was not present in token, as it likely means + // the system is not set up correctly + logger.warn("Username claim \"{}\" missing from token. Perhaps the " + + "OpenID scope and/or username claim type are " + + "misconfigured?", usernameClaim); + } + + // Could not retrieve username from JWT + return null; + } + + /** + * Parses the given JwtClaims, returning the groups contained + * therein, as defined by the groups claim type given in + * guacamole.properties. If the groups claim type is missing or + * is invalid, an empty set is returned. + * + * @param claims + * A valid JwtClaims to extract groups from. + * + * @return + * A Set of String representing the groups the user is member of + * from the OpenID provider point of view, or an empty Set if + * claim is not valid or the groups claim type is missing, + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public Set processGroups(JwtClaims claims) throws GuacamoleException { + String groupsClaim = confService.getGroupsClaimType(); + + if (claims != null) { + try { + // Pull groups from claims + List oidcGroups = claims.getStringListClaimValue(groupsClaim); + if (oidcGroups != null && !oidcGroups.isEmpty()) + return Collections.unmodifiableSet(new HashSet<>(oidcGroups)); + } + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage(), e); + } + } + + // Could not retrieve groups from JWT + return Collections.emptySet(); + } + + /** + * Parses the given JwtClaims, returning the attributes contained + * therein, as defined by the attributes claim type given in + * guacamole.properties. If the attributes claim type is missing or + * is invalid, an empty set is returned. + * + * @param claims + * A valid JwtClaims to extract attributes from. + * + * @return + * A Map of String,String representing the attributes and values + * from the OpenID provider point of view, or an empty Map if + * claim is not valid or the attributes claim type is missing. + * + * @throws GuacamoleException + * If guacamole.properties could not be parsed. + */ + public Map processAttributes(JwtClaims claims) throws GuacamoleException { + Collection attributesClaim = confService.getAttributesClaimType(); + + if (claims != null && !attributesClaim.isEmpty()) { + try { + logger.debug("Iterating over attributes claim list : {}", attributesClaim); + + // We suppose all claims are resolved, so the hashmap is initialised to + // the size of the configuration list + Map tokens = new HashMap(attributesClaim.size()); + + // We iterate over the configured attributes + for (String key: attributesClaim) { + // Retrieve the corresponding claim + String oidcAttr = claims.getStringClaimValue(key); + + // We do have a matching claim and it is not empty + if (oidcAttr != null && !oidcAttr.isEmpty()) { + // append the prefixed claim value to the token map with its value + String tokenName = TokenName.canonicalize(key, OIDC_ATTRIBUTE_TOKEN_PREFIX); + tokens.put(tokenName, oidcAttr); + logger.debug("Claim {} found and set to {}", key, tokenName); + } + else { + // wanted attribute is not found in the claim + logger.debug("Claim {} not found in JWT.", key); + } + } + + // We did process all the expected claims + return Collections.unmodifiableMap(tokens); + } + catch (MalformedClaimException e) { + logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage(), e); + } + } + + // Could not retrieve attributes from JWT + logger.debug("Attributes claim not defined. Returning empty map."); + return Collections.emptyMap(); + } +}