diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java index 5ad63870..ebc84f65 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java @@ -31,6 +31,12 @@ public class OIDCAuthenticatorConstants { public static final String ID_TOKEN_PARAM = "idToken"; public static final String SESSION_DATA_KEY_PARAM = "sessionDataKey"; public static final String CLIENT_ID_PARAM = "clientId"; + public static final String REFRESH_TOKEN = "refresh_token"; + public static final String EXPIRES_IN = "expires_in"; + public static final String SHARE_FEDERATED_TOKEN_CONFIG = "ShareFederatedToken"; + public static final String SHARE_FEDERATED_TOKEN_PARAM = "share_federated_token"; + public static final String FEDERATED_TOKEN_ALLOWED_SCOPE = "FederatedTokenAllowedScope"; + public static final String FEDERATED_TOKEN_SCOPE = "federated_token_scope"; private OIDCAuthenticatorConstants() { diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java index 6923a659..f266d1a4 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java @@ -23,6 +23,7 @@ import com.nimbusds.jwt.SignedJWT; import net.minidev.json.JSONArray; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; @@ -48,8 +49,10 @@ import org.wso2.carbon.identity.application.authentication.framework.exception.LogoutFailedException; import org.wso2.carbon.identity.application.authentication.framework.model.AdditionalData; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticationRequest; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatorData; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatorMessage; +import org.wso2.carbon.identity.application.authentication.framework.model.FederatedToken; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.authenticator.oidc.internal.OpenIDConnectAuthenticatorDataHolder; @@ -94,6 +97,7 @@ import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -112,6 +116,7 @@ import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.ACCESS_TOKEN_PARAM; import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.AUTHENTICATOR_OIDC; import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.Claim.NONCE; +import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_CONFIG; import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.ID_TOKEN_PARAM; import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.LogConstants.ActionIDs.INITIATE_OUTBOUND_AUTH_REQUEST; import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.LogConstants.ActionIDs.PROCESS_AUTHENTICATION_RESPONSE; @@ -136,6 +141,10 @@ public class OpenIDConnectAuthenticator extends AbstractApplicationAuthenticator private static final String DYNAMIC_PARAMETER_LOOKUP_REGEX = "\\$\\{(\\w+)\\}"; private static final String IS_API_BASED = "IS_API_BASED"; private static final String REDIRECT_URL = "REDIRECT_URL"; + private static final String SPACE_REGEX = "\\s+"; + private static final String SPACE = " "; + private static final String SEMI_COLON_DELIMITER = ";"; + private static final String COMMA_DELIMITER = ","; private static Pattern pattern = Pattern.compile(DYNAMIC_PARAMETER_LOOKUP_REGEX); private static final String[] NON_USER_ATTRIBUTES = new String[]{"at_hash", "iss", "iat", "exp", "aud", "azp"}; private static final String AUTHENTICATOR_MESSAGE = "authenticatorMessage"; @@ -481,6 +490,16 @@ protected void initiateAuthenticationRequest(HttpServletRequest request, HttpSer String scopes = getScope(authenticatorProperties); + /* + The scopes for the federated tokens are evaluated only if the authenticator + configuration ShareFederatedToken is enabled and the application has requested the federated token. + */ + if (Boolean.parseBoolean(authenticatorProperties.get(SHARE_FEDERATED_TOKEN_CONFIG)) && + requestedToShareFederatedToken(context)) { + // Adding the scopes requested by the application side for token sharing. + scopes = addValidScopesForFederatedTokenSharing(context, authenticatorProperties, scopes); + } + String queryString = getQueryString(authenticatorProperties); if (StringUtils.isNotBlank(scopes)) { if (LoggerUtils.isDiagnosticLogsEnabled() && diagnosticLogBuilder != null) { @@ -580,6 +599,263 @@ protected void initiateAuthenticationRequest(HttpServletRequest request, HttpSer return; } + /** + * This method is used to append the application side requested scopes after validating. + * The application can request the scopes for federated token sharing either via adaptive scripts + * or via the authorize request query parameters. The adaptive script has the first priority + * while the request query parameters will be evaluated later. + * i.e. Adaptive Script example: + * This will ignore any other definition (common, local) of the authenticatorParams. + * var onLoginRequest = function(context) { + * executeStep(1, { + * authenticatorParams: { + * federated: { + * "Google Calender": { + * federated_token_scope: "https://googleapis.calander.readonly https://google.calander.list" + * }}}}, {});} + * i.e Authorize request query param example: + * /authorize?response_type=id_token&client_id={ClientId}&redirect_uri={https://app/callback} + * &scope=email profile openid + * &federated_token_scope=Google Calender;read write,Microsoft Authenticator;https://googleapis.calender + * + * @param context The authentication context. + * @param authenticatorProperties The authenticator properties. + * @param scopes The scopes defined in the authenticator properties. + * @return The IDP defined scope and the validated scopes requested by the application. + */ + private String addValidScopesForFederatedTokenSharing(AuthenticationContext context, + Map authenticatorProperties, String scopes) { + + // Get the application requested scopes for the federated tokens. + String requestedScopesForTokenSharing = getRequestedScopesForTokenSharing(context); + + // Validating the application requested scopes by the authenticator allowed scopes for federated token sharing. + Set validScopesForTokenSharing = validateScopeForTokenSharing( + authenticatorProperties.get(OIDCAuthenticatorConstants.FEDERATED_TOKEN_ALLOWED_SCOPE), + requestedScopesForTokenSharing); + + if (CollectionUtils.isEmpty(validScopesForTokenSharing)) { + if (LOG.isDebugEnabled()) { + LOG.debug("No matching scopes found for federated token sharing."); + } + return scopes; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Valid scopes found for the IDP" + getFederatedAuthenticatorName(context) + + " in federated token sharing: " + validScopesForTokenSharing); + } + /* + Remove the duplicate scopes among the validated scopes for federated token sharing and the existing scopes + of the authenticators. + */ + scopes = removeDuplicateScopes(scopes, validScopesForTokenSharing); + if (LOG.isDebugEnabled()) { + LOG.debug("The scopes for the IDP: " + getFederatedAuthenticatorName(context) + " : " + scopes + + " after considering federated token sharing."); + } + return scopes; + } + + /** + * This method is used to remove the duplicate scopes. + * + * @param scopes The scopes defined in the authenticator. i.e. "openid email profile" + * @param validScopesForTokenSharing The validated scopes requested by the application and the allowed scopes + * for the token sharing. + * @return The scopes after removing the duplicate scopes. + */ + private String removeDuplicateScopes(String scopes, Set validScopesForTokenSharing) { + + if (StringUtils.isBlank(scopes)) { + scopes = StringUtils.join(validScopesForTokenSharing, SPACE); + } + + Set scopeSet = new HashSet<>(Arrays.asList(scopes.split(SPACE_REGEX))); + scopeSet.addAll(validScopesForTokenSharing); + + scopes = StringUtils.join(scopeSet, SPACE); + return scopes; + } + + /** + * This method returns the scopes requested by the application for the federated tokens. + * + * @param context The authentication context. + * @return The scopes requested by the application for token sharing. + */ + private String getRequestedScopesForTokenSharing(AuthenticationContext context) { + + // The first priority is given to the parameters passed from the adaptive script. Then the query parameters. + String requestedScopesViaAdaptiveScript = + getAdaptiveScriptValues(context, OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE); + // Checks if there exists scopes requested via adaptive script. + if (StringUtils.isNotBlank(requestedScopesViaAdaptiveScript)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Adaptive script parameter found for " + OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE + + " in federated token sharing, IDP: " + getFederatedAuthenticatorName(context)); + } + return requestedScopesViaAdaptiveScript; + } else { + String requestedScopesViaQueryParams = getRequestedScopesViaQueryParams(context); + if (LOG.isDebugEnabled() && StringUtils.isNotBlank(requestedScopesViaQueryParams)) { + LOG.debug("No adaptive script parameter: " + OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE + + " found. Query parameter: " + OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE + + " value: " + requestedScopesViaQueryParams + " found for federated token sharing, IDP: " + + getFederatedAuthenticatorName(context)); + } + return requestedScopesViaQueryParams; + } + } + + /** + * This method returns the adaptive script federated authenticator param value for a given parameter name. + * + * @param context The authentication context with federated authenticator params. + * @param param The federated authenticator parameter name. + * @return The adaptive script federated authenticator param value for the given parameter name. + */ + private String getAdaptiveScriptValues(AuthenticationContext context, String param) { + + Map runtimeParams = this.getRuntimeParams(context); + if (runtimeParams != null) { + return runtimeParams.get(param); + } + return StringUtils.EMPTY; + } + + /** + * The optional scope string cannot have scattered segments for the same authenticator. + * Only the very first segment is considered. + * i.e. A valid string: + * Google Calender has read write scopes, Microsoft Authenticator has https://googleapis.calender scope + * A valid string: + * federated_token_scope=Google Calander;read write,Microsoft Authenticator;https://googleapis.calender + * A valid string: + * federated_token_scope=Google Calender;read https://googleapis.calender.read + * + * @param context The authentication context with authentication request having the query parameters. + * @return The scopes requested by the application via the query parameters for federated token sharing. + */ + private String getRequestedScopesViaQueryParams(AuthenticationContext context) { + + String authenticatorName = getFederatedAuthenticatorName(context); + if (StringUtils.isBlank(authenticatorName)) { + if (LOG.isDebugEnabled()) { + LOG.debug("No external IDP name found in the authentication context for federated token sharing. " + + "Cannot retrieve the query parameters."); + } + return null; + } + + String scopeString = getQueryParameter(context, OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE); + if (StringUtils.isBlank(scopeString)) { + if (LOG.isDebugEnabled()) { + LOG.debug("No query parameter " + OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE + + " found in federated token sharing, IDP: " + authenticatorName); + } + return null; + } + /* + The requested scopes for particular authenticator should come with the authenticator name separated + by a semicolon. + i.e. A valid string: + When Google Calender has read write scopes and Microsoft Authenticator has https://googleapis.calender scope + A valid requested scopes string: + federated_token_scope=Google Calander;read write,Microsoft Authenticator;https://googleapis.calender + */ + if (!scopeString.contains(SEMI_COLON_DELIMITER)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Query parameter name: " + OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE + " value: " + + scopeString + " is missing " + SEMI_COLON_DELIMITER + + " delimiter in federated token sharing, IDP: " + authenticatorName); + } + return null; + } + + String[] scopeSegments = StringUtils.split(scopeString, COMMA_DELIMITER); + StringBuilder filteredScopes = new StringBuilder(); + + for (String scopesFollowedByAuthenticator : scopeSegments) { + String[] scopes = StringUtils.split(scopesFollowedByAuthenticator, SEMI_COLON_DELIMITER); + if (ArrayUtils.getLength(scopes) == 2 && + StringUtils.equals(authenticatorName, StringUtils.trim(scopes[0]))) { + filteredScopes.append(StringUtils.trim(scopes[1])).append(SPACE); + } + } + + String requestedScopes = filteredScopes.toString(); + if (LOG.isDebugEnabled() && StringUtils.isBlank(requestedScopes)) { + LOG.debug("No valid values found for the IDP: " + authenticatorName + " in the query parameter " + + OIDCAuthenticatorConstants.FEDERATED_TOKEN_SCOPE + " for federated token sharing"); + } + + return requestedScopes; + } + + /** + * This method evaluates whether application has requested to share the token. The first priority is given to the + * authenticator parameters set at the adaptive script. Then the query parameters. + * + * @param context The authentication context. + * @return Whether the application has requested to share the token. + */ + private boolean requestedToShareFederatedToken(AuthenticationContext context) { + + // The first priority is given to the parameters setup at the adaptive script. Then the query parameters. + String shareFederatedToken = + getAdaptiveScriptValues(context, OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_PARAM); + + if (LOG.isDebugEnabled() && StringUtils.isNotBlank(shareFederatedToken)) { + LOG.debug("Adaptive script parameter " + OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_PARAM + + " found for federated token sharing, IDP: " + getFederatedAuthenticatorName(context)); + } + + if (StringUtils.isBlank(shareFederatedToken)) { + // Checks if the token sharing is requested via authorize request query parameters. + shareFederatedToken = getQueryParameter(context, OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_PARAM); + if (LOG.isDebugEnabled()) { + LOG.debug("No adaptive script parameter: " + OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_PARAM + + " found. Query parameter: " + OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_PARAM + + " value: " + shareFederatedToken + " found for federated token sharing, IDP: " + + getFederatedAuthenticatorName(context)); + } + } + return Boolean.parseBoolean(shareFederatedToken); + } + + /** + * This method is used to retrieve the query parameters from the authentication request. + * + * @param context The authentication context with authentication request. + * @param queryParamName The required query parameter name. + * @return The query parameter value. + */ + private String getQueryParameter(AuthenticationContext context, String queryParamName) { + + AuthenticationRequest authenticationRequest = context.getAuthenticationRequest(); + if (authenticationRequest == null || StringUtils.isBlank(queryParamName)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Invalid authentication request or invalid query parameter name : " + queryParamName + + " for federated token sharing, IDP: " + getFederatedAuthenticatorName(context)); + } + return null; + } + String[] queryParamValues = authenticationRequest.getRequestQueryParam(queryParamName); + if (ArrayUtils.isNotEmpty(queryParamValues)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Query parameter found for, " + queryParamName + " in federated token sharing, IDP: " + + getFederatedAuthenticatorName(context)); + } + return queryParamValues[0]; + } + if (LOG.isDebugEnabled()) { + LOG.debug("No value found for the query parameter : " + queryParamName + + " in federated token sharing, IDP: " + getFederatedAuthenticatorName(context)); + } + return null; + } + private static void setAuthenticatorMessageToContext(ErrorMessages errorMessage, AuthenticationContext context) { @@ -630,6 +906,19 @@ protected void processAuthenticationResponse(HttpServletRequest request, HttpSer OAuthClientResponse oAuthResponse = requestAccessToken(request, context); // TODO : return access token and id token to framework mapAccessToken(request, context, oAuthResponse); + + /* + Federated tokens are added only if the authenticator configuration ShareFederatedToken is enabled and the + application has requested the federated token. + */ + if (context.getAuthenticatorProperties() != null && Boolean.parseBoolean( + context.getAuthenticatorProperties().get(OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_CONFIG)) && + requestedToShareFederatedToken(context)) { + // Adding the federated tokens to the context for token sharing. + addFederatedTokensToContext(context, oAuthResponse); + } + + String idToken = mapIdToken(context, request, oAuthResponse); Map authenticatorProperties = context.getAuthenticatorProperties(); @@ -779,6 +1068,69 @@ protected void mapAccessToken(HttpServletRequest request, AuthenticationContext context.setProperty(OIDCAuthenticatorConstants.ACCESS_TOKEN, accessToken); } + /** + * Add the federated tokens to the authentication context. This is used to share the tokens with the application. + * + * @param context The authentication context for the request on which the federated tokens are kept. + * @param oAuthResponse The OAuth client response. + */ + private void addFederatedTokensToContext(AuthenticationContext context, OAuthClientResponse oAuthResponse) { + + // If there is an existing list of federated tokens obtained in a previous step, utilizing the same list. + List federatedTokens; + Object federatedTokensObj = context.getProperty(FrameworkConstants.FEDERATED_TOKENS); + if (federatedTokensObj instanceof List) { + federatedTokens = (List) federatedTokensObj; + } else { + federatedTokens = new ArrayList<>(); + } + + String identityProviderName = getFederatedAuthenticatorName(context); + + FederatedToken federatedToken = new FederatedToken(identityProviderName, + oAuthResponse.getParam(OIDCAuthenticatorConstants.ACCESS_TOKEN)); + federatedToken.setRefreshToken(oAuthResponse.getParam(OIDCAuthenticatorConstants.REFRESH_TOKEN)); + federatedToken.setTokenValidityPeriod(oAuthResponse.getParam(OIDCAuthenticatorConstants.EXPIRES_IN)); + federatedToken.setScope(oAuthResponse.getParam(OIDCAuthenticatorConstants.SCOPE)); + federatedTokens.add(federatedToken); + + context.setProperty(FrameworkConstants.FEDERATED_TOKENS, federatedTokens); + if (LOG.isDebugEnabled()) { + LOG.debug("Federated tokens added to the authentication context, IDP: " + identityProviderName); + } + } + + /** + * This returns the intersection of the allowed scopes defined at the IDP configuration and the requested scopes + * from the application side for federated token sharing. + * + * @param allowedScope The administrator defined scopes in the IDP configuration for federated token sharing. + * @param requestedScope The application side requested scopes for federated token sharing. + * @return The intersection of the allowed and the requested scopes for federated token sharing as a set of list. + */ + private Set validateScopeForTokenSharing(String allowedScope, String requestedScope) { + + if (StringUtils.isBlank(allowedScope)) { + if (LOG.isDebugEnabled()) { + LOG.debug("No scopes are allowed for federated token sharing."); + } + return null; + } + if (StringUtils.isBlank(requestedScope)) { + if (LOG.isDebugEnabled()) { + LOG.debug("No scopes are requested for federated token sharing."); + } + return null; + } + Set allowedScopesSet = new HashSet<>(Arrays.asList(allowedScope.split(SPACE_REGEX))); + Set requestedScopesSet = new HashSet<>(Arrays.asList(requestedScope.split(SPACE_REGEX))); + + Set subset = new HashSet<>(requestedScopesSet); + subset.retainAll(allowedScopesSet); + + return subset; + } + /** * Generates OAuth client and returns the oAuthResponse according to the flow supported by the authenticator. * Overridden in Google Authenticator for Google one tap. @@ -1735,4 +2087,22 @@ private boolean isNativeSDKBasedFederationCall(HttpServletRequest request) { return request.getParameter(ACCESS_TOKEN_PARAM) != null && request.getParameter(ID_TOKEN_PARAM) != null; } + + /** + * This method returns the current federated authenticator name. If there is no external IdP, then the current + * authenticator name is returned. + * + * @param context Authentication context. + * @return Federated authenticator name. + */ + private String getFederatedAuthenticatorName(AuthenticationContext context) { + + if (context == null || context.getExternalIdP() == null || context.getExternalIdP().getIdPName() == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Cannot resolve the authenticator name from the authentication context."); + } + return StringUtils.EMPTY; + } + return context.getExternalIdP().getIdPName(); + } } diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/test/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticatorTest.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/test/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticatorTest.java index c8e33ec5..38d06d20 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/test/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticatorTest.java +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/test/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticatorTest.java @@ -49,6 +49,7 @@ import org.wso2.carbon.identity.application.authentication.framework.exception.FrameworkException; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticationRequest; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatorData; +import org.wso2.carbon.identity.application.authentication.framework.model.FederatedToken; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.authenticator.oidc.internal.OpenIDConnectAuthenticatorDataHolder; @@ -61,6 +62,7 @@ import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.core.ServiceURL; import org.wso2.carbon.identity.core.ServiceURLBuilder; +import org.wso2.carbon.identity.core.URLBuilderException; import org.wso2.carbon.identity.core.util.IdentityCoreConstants; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; @@ -81,6 +83,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -100,12 +103,13 @@ import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; -import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.OIDC_FEDERATION_NONCE; -import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.AUTHENTICATOR_OIDC; +import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.AUTHENTICATOR_FRIENDLY_NAME; import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.AUTHENTICATOR_NAME; -import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants. - AUTHENTICATOR_FRIENDLY_NAME; +import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.AUTHENTICATOR_OIDC; import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.Claim.NONCE; +import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_CONFIG; +import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.OIDC_FEDERATION_NONCE; +import static org.wso2.carbon.identity.application.authenticator.oidc.OIDCAuthenticatorConstants.SHARE_FEDERATED_TOKEN_PARAM; /*** * Unit test class for OpenIDConnectAuthenticator class. @@ -118,6 +122,9 @@ "org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException"}) public class OpenIDConnectAuthenticatorTest extends PowerMockTestCase { + private static final String OIDC_PARAM_MAP_STRING = "oidc:param.map"; + private static final String HTTPS_LOCALHOST_9443 = "https://localhost:9443"; + private static final String COMMA_SEPARATOR = ","; @Mock private HttpServletRequest mockServletRequest; @@ -187,6 +194,9 @@ public class OpenIDConnectAuthenticatorTest extends PowerMockTestCase { private static Map authenticatorParamProperties; private static String clientId = "u5FIfG5xzLvBGiamoAYzzcqpBqga"; private static String accessToken = "4952b467-86b2-31df-b63c-0bf25cec4f86s"; + private static String refreshToken = "6357238-86b2-31df-b63c-0bf25cec4f86s"; + private static String expiresIn = "3600"; + private String scope = "openid email profile"; private static String idToken = "eyJ4NXQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5" + "sWkRVMU9HRmtOakZpTVEiLCJraWQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9" + "HRmtOakZpTVEiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6WyJ1NUZJZkc1eHpMdkJHaWFtb0FZenpjc" + @@ -238,7 +248,7 @@ public void init() { public Object[][] getSeperator() { return new String[][]{ - {","}, + {COMMA_SEPARATOR}, {",,,"} }; } @@ -607,7 +617,7 @@ public void testPassProcessAuthenticationWithParamValue() throws Exception { authenticatorProperties.put("callbackUrl", "http://localhost:8080/playground2/oauth2client"); Map paramMap = new HashMap<>(); paramMap.put("redirect_uri", "http:/localhost:9443/oauth2/redirect"); - when(mockAuthenticationContext.getProperty("oidc:param.map")).thenReturn(paramMap); + when(mockAuthenticationContext.getProperty(OIDC_PARAM_MAP_STRING)).thenReturn(paramMap); setParametersForOAuthClientResponse(mockOAuthClientResponse, accessToken, idToken); when(openIDConnectAuthenticatorDataHolder.getClaimMetadataManagementService()).thenReturn (claimMetadataManagementService); @@ -959,10 +969,10 @@ private void setupTest() throws Exception { when(mockUserRealm.getUserStoreManager()).thenReturn(mockUserStoreManager); when(mockUserStoreManager.getRealmConfiguration()).thenReturn(mockRealmConfiguration); when(mockRealmConfiguration.getUserStoreProperty(IdentityCoreConstants.MULTI_ATTRIBUTE_SEPARATOR)) - .thenReturn(","); + .thenReturn(COMMA_SEPARATOR); mockStatic(IdentityUtil.class); when(IdentityUtil.getServerURL("", false, false)) - .thenReturn("https://localhost:9443"); + .thenReturn(HTTPS_LOCALHOST_9443); mockStatic(ServiceURLBuilder.class); when(ServiceURLBuilder.create()).thenReturn(serviceURLBuilder); @@ -985,7 +995,7 @@ private void mockAuthenticationRequestContext(AuthenticationContext mockAuthenti when(mockAuthenticationContext.getAuthenticatorProperties()).thenReturn(authenticatorProperties); paramValueMap = new HashMap<>(); - when(mockAuthenticationContext.getProperty("oidc:param.map")).thenReturn(paramValueMap); + when(mockAuthenticationContext.getProperty(OIDC_PARAM_MAP_STRING)).thenReturn(paramValueMap); when(mockAuthenticationContext.getContextIdentifier()).thenReturn(""); when(mockAuthenticationContext.getExternalIdP()).thenReturn(getDummyExternalIdPConfig()); when(mockAuthenticationContext.getAuthenticationRequest()).thenReturn(mockAuthenticationRequest); @@ -1081,4 +1091,266 @@ private ExternalIdPConfig getDummyExternalIdPConfig() { identityProvider.setIdentityProviderName("DummyIDPName"); return new ExternalIdPConfig(identityProvider); } + + private ExternalIdPConfig getExternalIdPConfig(String idpName) { + + IdentityProvider identityProvider = new IdentityProvider(); + identityProvider.setIdentityProviderName(idpName); + return new ExternalIdPConfig(identityProvider); + } + + /** + * This method generates test criteria on the query parameter change on sharing the federated token for + * different combination of the IDP configuration value. The adaptive script parameter values are same + * to the query parameter values. + * + * @return Object[][] The test criteria. + */ + @DataProvider(name = "shareFederatedTokenParams") + public Object[][] shareFederatedTokenParams() { + + return new Object[][]{ + {"IDP config is enabled, application requested the federated token, " + + "adaptive script requested the federated token", "true", "true", "true"}, + {"IDP config is disabled, application requested the federated token", "false", "true", "true"}, + {"IDP config is enabled, application did not request the federated token", "true", "false", "false"}, + {"IDP config is disabled, application did not request the federated token", "false", "false", + "false"}, + {"IDP config is disabled, application did not send the parameter", "true", null, null}, + {"IDP config is disabled, application did not send the parameter", "false", null, null}, + {"No IDP config found, application requested the federated token", null, "true", "true"}, + {"No IDP config found, application did not request the federated token", null, "false", "false"}, + {"No IDP config found, application did not send the parameter", null, null, null} + }; + } + + /** + * This method generates test criteria on the adaptive script parameter change on sharing the federated token. + * + * @return Object[][] The test criteria. + */ + @DataProvider(name = "shareFederatedTokenParamsForAdaptiveScriptConfigs") + public Object[][] shareFederatedTokenParamsForAdaptiveScriptConfigs() { + + return new Object[][]{ + {"IDP config is enabled, authorize call requested the federated token," + + " adaptive script requested the federated token", "true", "true", "true"}, + {"IDP config is enabled, authorize call requested the federated token," + + " adaptive script did not request the federated token", "true", "true", "false"}, + {"IDP config is enabled, authorize call requested the federated token," + + " no adaptive script config found", "true", "true", null}, + {"IDP config is disabled, authorize call requested the federated token", "false", "true", "true"}, + {"IDP config is enabled, authorize call did not request the federated token," + + " adaptive script did not request the federated token", "true", "false", "false"}, + {"IDP config is enabled, authorize call did not request the federated token," + + " adaptive script requested the federated token", "true", "false", "true"}, + {"IDP config is enabled, authorize call did not request the federated token," + + " no adaptive script config found", "true", "false", null}, + {"IDP config is disabled, authorize call did not request the federated token", + "false", "false", "false"}, + {"IDP config is disabled, authorize call did not send the parameter," + + " no adaptive script config found", "true", null, null}, + {"IDP config is disabled, authorize call did not send the parameter," + + " adaptive script requested the federated token", "true", null, "true"}, + {"IDP config is disabled, authorize call did not send the parameter," + + " adaptive script did not request the federated token", "true", null, "false"}, + {"IDP config is disabled, authorize call did not send the parameter", "false", null, null}, + {"No IDP config found, authorize call requested the federated token", null, "true", "true"}, + {"No IDP config found, authorize call did not request the federated token", null, "false", "false"}, + {"No IDP config found, authorize call did not send the parameter", null, null, null} + }; + } + + @Test(dataProvider = "shareFederatedTokenParamsForAdaptiveScriptConfigs") + public void testShareFederatedTokenParamsForAdaptiveScriptConfigs(String errorMessage, + String enableShareTokenIDPConfig, + String shareTokenQueryParameter, + String shareTokeAdaptiveScriptParam) + throws Exception { + + testShareFederatedTokenForIDPConfigAndQueryParameter(errorMessage, enableShareTokenIDPConfig, + shareTokenQueryParameter, shareTokeAdaptiveScriptParam); + } + + @Test(dataProvider = "shareFederatedTokenParams") + public void testShareFederatedTokenForIDPConfigAndQueryParameter(String errorMessage, + String enableShareTokenIDPConfig, + String shareTokenQueryParameter, + String shareTokeAdaptiveScriptParam) + throws Exception { + + String authenticator = "Google Calender"; + mockStaticClasses(); + mockServiceVariables(); + + AuthenticationRequest authenticationRequest = new AuthenticationRequest(); + mapQueryParamsToAuthenticationRequest(authenticationRequest, SHARE_FEDERATED_TOKEN_PARAM, + shareTokenQueryParameter); + AuthenticationContext authenticationContext = + getAuthenticationContext(authenticator, authenticationRequest, enableShareTokenIDPConfig); + addAdaptiveScriptParams(authenticationContext, SHARE_FEDERATED_TOKEN_PARAM, shareTokeAdaptiveScriptParam); + mockIDPAuthentication(); + + openIDConnectAuthenticator.processAuthenticationResponse(mockServletRequest, mockServletResponse, + authenticationContext); + + if (Boolean.parseBoolean(enableShareTokenIDPConfig) && ((StringUtils.isBlank(shareTokeAdaptiveScriptParam) && + Boolean.parseBoolean(shareTokenQueryParameter)) || + Boolean.parseBoolean(shareTokeAdaptiveScriptParam))) { + assertNotNull(authenticationContext.getProperty(FrameworkConstants.FEDERATED_TOKENS), errorMessage); + + if (authenticationContext.getProperty(FrameworkConstants.FEDERATED_TOKENS) != null && + authenticationContext.getProperty(FrameworkConstants.FEDERATED_TOKENS) instanceof List) { + List federatedToken = (List) authenticationContext.getProperty( + FrameworkConstants.FEDERATED_TOKENS); + assertEquals(federatedToken.get(0).getAccessToken(), accessToken, "No access token found"); + assertEquals(federatedToken.get(0).getRefreshToken(), refreshToken, "No refresh token found"); + assertEquals(federatedToken.get(0).getTokenValidityPeriod(), expiresIn, "No expiry time found"); + assertEquals(federatedToken.get(0).getScope(), scope, "No scope found"); + } + } else { + assertNull(authenticationContext.getProperty(FrameworkConstants.FEDERATED_TOKENS), errorMessage); + } + } + + /** + * This method generates an authentication instance for the federated token sharing. + * + * @param authenticator The federated authenticator name. + * @param authenticationRequest The authentication request. + * @param enableShareTokenIDPConfig The IDP config of the enabling the federated token sharing. + * @return AuthenticationContext The generated authentication context for the federated token sharing. + */ + private AuthenticationContext getAuthenticationContext(String authenticator, + AuthenticationRequest authenticationRequest, + String enableShareTokenIDPConfig) { + + AuthenticationContext authenticationContext = new AuthenticationContext(); + authenticationContext.setAuthenticationRequest(authenticationRequest); + authenticationContext.setExternalIdP(getExternalIdPConfig(authenticator)); + authenticationContext.setProperty(OIDC_PARAM_MAP_STRING, paramValueMap); + authenticationContext.setContextIdentifier(""); + + if (StringUtils.isNotBlank(enableShareTokenIDPConfig)) { + authenticatorProperties.put(SHARE_FEDERATED_TOKEN_CONFIG, enableShareTokenIDPConfig); + } + authenticationContext.setAuthenticatorProperties(authenticatorProperties); + + return authenticationContext; + } + + /** + * This method maps the authorize request query parameters for testing. + * + * @param authenticationRequest The authentication request to map the query parameters. + * @param queryParameterName The query parameter name. + * @param queryParameterValue The query parameter value. + */ + private void mapQueryParamsToAuthenticationRequest(AuthenticationRequest authenticationRequest, + String queryParameterName, String queryParameterValue) { + if (StringUtils.isNotBlank(queryParameterValue)) { + String[] queryParam = new String[]{queryParameterValue}; + authenticationRequest.addRequestQueryParam(queryParameterName, queryParam); + } + } + + /** + * This method mocks the adaptive parameters to the authentication context. + * + * @param authenticationContext The authentication context to be tested. + * @param shareTokeAdaptiveScriptParamName The adaptive scrip parameter name. + * @param shareTokeAdaptiveScriptParam The adaptive script parameter value. + */ + private void addAdaptiveScriptParams(AuthenticationContext authenticationContext, + String shareTokeAdaptiveScriptParamName, String shareTokeAdaptiveScriptParam) { + + Map adaptiveScriptParam = new HashMap<>(); + Map> runtimeParams = new HashMap<>(); + authenticationContext.addParameter("RUNTIME_PARAMS", runtimeParams); + runtimeParams.put(OIDCAuthenticatorConstants.AUTHENTICATOR_NAME, adaptiveScriptParam); + + if (StringUtils.isNotBlank(shareTokeAdaptiveScriptParam)) { + adaptiveScriptParam.put(shareTokeAdaptiveScriptParamName, shareTokeAdaptiveScriptParam); + } + } + + /** + * This method mocks the external IDP authentication flow and the value mappings. + * + * @throws Exception Throws the exceptions on error. + */ + private void mockIDPAuthentication() throws Exception { + + IdentityProviderProperty[] identityProviderProperties = getIdentityProviderProperties(); + when(externalIdPConfig.getIdentityProvider()).thenReturn(identityProvider); + when(identityProvider.getIdpProperties()).thenReturn(identityProviderProperties); + whenNew(OAuthClient.class).withAnyArguments().thenReturn(mockOAuthClient); + when(mockOAuthClient.accessToken(Matchers.anyObject())).thenReturn(mockOAuthJSONAccessTokenResponse); + when(mockOAuthJSONAccessTokenResponse.getParam(OIDCAuthenticatorConstants.ACCESS_TOKEN)).thenReturn( + accessToken); + when(mockOAuthJSONAccessTokenResponse.getParam(OIDCAuthenticatorConstants.ID_TOKEN)).thenReturn(idToken); + when(mockOAuthJSONAccessTokenResponse.getParam(OIDCAuthenticatorConstants.REFRESH_TOKEN)).thenReturn( + refreshToken); + when(mockOAuthJSONAccessTokenResponse.getParam(OIDCAuthenticatorConstants.EXPIRES_IN)).thenReturn(expiresIn); + when(mockOAuthJSONAccessTokenResponse.getParam(OIDCAuthenticatorConstants.SCOPE)).thenReturn(scope); + } + + /** + * This method mocks the service variable for the authorization call. + * + * @throws OAuthProblemException Throws on the oAuth flow exception. + * @throws UserStoreException Throws on the user store exception. + * @throws URLBuilderException Throws on the url builder exception. + */ + private void mockServiceVariables() throws OAuthProblemException, UserStoreException, URLBuilderException { + + when(OAuthAuthzResponse.oauthCodeAuthzResponse(mockServletRequest)).thenReturn(mockOAuthzResponse); + when(mockServletRequest.getParameter("domain")).thenReturn(superTenantDomain); + when(mockOAuthzResponse.getCode()).thenReturn("200"); + setParametersForOAuthClientResponse(mockOAuthClientResponse, accessToken, idToken); + when(OpenIDConnectAuthenticatorDataHolder.getInstance()).thenReturn(openIDConnectAuthenticatorDataHolder); + when(openIDConnectAuthenticatorDataHolder.getRealmService()).thenReturn(mockRealmService); + when(mockRealmService.getTenantManager()).thenReturn(mockTenantManger); + when(mockTenantManger.getTenantId(anyString())).thenReturn(TENANT_ID); + when(mockRealmService.getTenantUserRealm(anyInt())).thenReturn(mockUserRealm); + when(mockUserRealm.getUserStoreManager()).thenReturn(mockUserStoreManager); + when(mockUserStoreManager.getRealmConfiguration()).thenReturn(mockRealmConfiguration); + when(mockRealmConfiguration.getUserStoreProperty(IdentityCoreConstants.MULTI_ATTRIBUTE_SEPARATOR)) + .thenReturn(COMMA_SEPARATOR); + when(IdentityUtil.getServerURL("", false, false)) + .thenReturn(HTTPS_LOCALHOST_9443); + when(ServiceURLBuilder.create()).thenReturn(serviceURLBuilder); + when(serviceURLBuilder.addPath(anyString())).thenReturn(serviceURLBuilder); + when(serviceURLBuilder.addParameter(anyString(), anyString())).thenReturn(serviceURLBuilder); + when(serviceURLBuilder.build()).thenReturn(serviceURL); + when(LoggerUtils.isDiagnosticLogsEnabled()).thenReturn(true); + } + + /** + * This method do all the static level mocks. + */ + private void mockStaticClasses() { + + mockStatic(OAuthAuthzResponse.class); + mockStatic(OpenIDConnectAuthenticatorDataHolder.class); + mockStatic(IdentityUtil.class); + mockStatic(ServiceURLBuilder.class); + mockStatic(LoggerUtils.class); + } + + /** + * This method creates the external identity provider properties. + * + * @return IdentityProviderProperty[] The array of the idp properties. + */ + private IdentityProviderProperty[] getIdentityProviderProperties() { + + IdentityProviderProperty property = new IdentityProviderProperty(); + property.setName(IdPManagementConstants.IS_TRUSTED_TOKEN_ISSUER); + property.setValue("false"); + IdentityProviderProperty[] identityProviderProperties = new IdentityProviderProperty[1]; + identityProviderProperties[0] = property; + return identityProviderProperties; + } + } diff --git a/pom.xml b/pom.xml index 10b885de..d898ba78 100644 --- a/pom.xml +++ b/pom.xml @@ -304,7 +304,7 @@ ${project.version} - 5.25.560 + 7.0.93 1.0.0.wso2v3 2.4.7 3.0.0.wso2v4