diff --git a/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCode.java b/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCode.java index 4cda666fff..615028d129 100644 --- a/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCode.java +++ b/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCode.java @@ -21,8 +21,10 @@ import static org.forgerock.openam.utils.CollectionUtils.newList; import static org.forgerock.openam.utils.Time.*; +import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.Set; @@ -115,6 +117,21 @@ public void setResourceOwnerId(String resourceOwnerId) { setStringProperty(OAuth2Constants.CoreTokenParams.USERNAME, resourceOwnerId); } + + + public void setAuthModules(String authModules) { + setStringProperty(AUTH_MODULES, authModules); + } + + + /** + * Get the auth modules string. + * @return The pipe-separated list of auth modules. + */ + public String getAuthModules() { + return getStringProperty(AUTH_MODULES); + } + /** * Gets the Client ID parameter. * @return The Client ID. @@ -138,6 +155,14 @@ public String getNonce() { public String getAcrValues() { return getStringProperty(OAuth2Constants.Params.ACR_VALUES); } + + /** + * Sets the ACR Values for device code object. + */ + public void setAcrValues(String acrValues) { + setStringProperty(OAuth2Constants.Params.ACR_VALUES, acrValues); + } + /** * Gets the Code Challenge Method parameter. @@ -338,6 +363,7 @@ public boolean isAuthorized() { return Boolean.valueOf(getStringProperty("AUTHORIZED")); } + /** * {@inheritDoc} */ diff --git a/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCodeGrantTypeHandler.java b/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCodeGrantTypeHandler.java index ec67c700ef..a986b2a040 100644 --- a/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCodeGrantTypeHandler.java +++ b/openam-oauth2/src/main/java/org/forgerock/oauth2/core/DeviceCodeGrantTypeHandler.java @@ -85,13 +85,14 @@ protected AccessToken handle(OAuth2Request request, ClientRegistration client, String clientId = client.getClientId(); DeviceCode deviceCode = tokenStore.readDeviceCode(clientId, code, request); - + if (deviceCode == null || !clientId.equals(deviceCode.getClientId()) || !request.getParameter(REALM).equals(deviceCode.getRealm())) { throw new AuthorizationDeclinedException(); } + AccessToken accessToken; try { if (deviceCode.isAuthorized()) { String grantType = request.getParameter(OAuth2Constants.Params.GRANT_TYPE); @@ -99,8 +100,16 @@ protected AccessToken handle(OAuth2Request request, ClientRegistration client, String resourceOwnerId = deviceCode.getResourceOwnerId(); String validatedClaims = providerSettings.validateRequestedClaims( deviceCode.getStringProperty(OAuth2Constants.Custom.CLAIMS)); - return generateAccessToken(providerSettings, grantType, clientId, resourceOwnerId, scope, - validatedClaims, request); + final String nonce = deviceCode.getNonce(); + + accessToken = generateAccessToken(providerSettings, grantType, clientId, resourceOwnerId, scope, + validatedClaims, nonce, request); + + providerSettings.additionalDataToReturnFromTokenEndpoint( + accessToken, + request); + + return accessToken; } if (deviceCode.getExpiryTime() < currentTimeMillis()) { @@ -130,9 +139,9 @@ protected AccessToken handle(OAuth2Request request, ClientRegistration client, } private AccessToken generateAccessToken(OAuth2ProviderSettings providerSettings, String grantType, String clientId, - String resourceOwnerId, Set scope, String validatedClaims, OAuth2Request request) + String resourceOwnerId, Set scope, String validatedClaims, String nonce, OAuth2Request request) throws ServerException, NotFoundException { return accessTokenGenerator.generateAccessToken(providerSettings, grantType, clientId, resourceOwnerId, null, - scope, validatedClaims, null, null, request); + scope, validatedClaims, null, nonce, request); } } diff --git a/openam-oauth2/src/main/java/org/forgerock/oauth2/core/ResourceOwnerSessionValidator.java b/openam-oauth2/src/main/java/org/forgerock/oauth2/core/ResourceOwnerSessionValidator.java index 32bffaceeb..d6caa62789 100644 --- a/openam-oauth2/src/main/java/org/forgerock/oauth2/core/ResourceOwnerSessionValidator.java +++ b/openam-oauth2/src/main/java/org/forgerock/oauth2/core/ResourceOwnerSessionValidator.java @@ -199,7 +199,8 @@ public ResourceOwner validate(OAuth2Request request) throws ResourceOwnerAuthent throw new LoginRequiredException(); } } else if (OAuth2Constants.TokenEndpoint.PASSWORD.equals(request.getParameter(GRANT_TYPE)) - || OAuth2Constants.TokenEndpoint.CLIENT_CREDENTIALS.equals(request.getParameter(GRANT_TYPE))) { + || OAuth2Constants.TokenEndpoint.CLIENT_CREDENTIALS.equals(request.getParameter(GRANT_TYPE)) + || OAuth2Constants.TokenEndpoint.DEVICE_CODE.equals(request.getParameter(GRANT_TYPE))) { // If we're doing password grant type, the SSOToken will have been created and deleted again within // OpenAMResourceOwnerAuthenticator. The request will not have a session, and so the token will have // been null from the attempted creation in L148. diff --git a/openam-oauth2/src/main/java/org/forgerock/oauth2/restlet/DeviceCodeVerificationResource.java b/openam-oauth2/src/main/java/org/forgerock/oauth2/restlet/DeviceCodeVerificationResource.java index a8b5789da2..855c65fa0d 100644 --- a/openam-oauth2/src/main/java/org/forgerock/oauth2/restlet/DeviceCodeVerificationResource.java +++ b/openam-oauth2/src/main/java/org/forgerock/oauth2/restlet/DeviceCodeVerificationResource.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Set; +import com.iplanet.sso.SSOException; import com.iplanet.sso.SSOToken; import org.forgerock.oauth2.core.AuthorizationService; import org.forgerock.oauth2.core.ClientRegistration; @@ -71,6 +72,7 @@ import org.restlet.routing.Router; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.sun.identity.authentication.util.ISAuthConstants; /** * A restlet resource for user codes @@ -128,6 +130,7 @@ public DeviceCodeVerificationResource(XUIState xuiState, @Named("OAuth2Router") @Post public Representation verify(Representation body) throws ServerException, NotFoundException, InvalidGrantException, OAuth2RestletException { + final Request restletRequest = getRequest(); OAuth2Request request = requestFactory.create(restletRequest); @@ -168,7 +171,13 @@ public Representation verify(Representation body) throws ServerException, NotFou saveConsent(request); } if (consentGiven) { + ResourceOwner resourceOwner = resourceOwnerSessionValidator.validate(request); + deviceCode.setAcrValues(getAuthenticationContextClassReferenceFromRequest(request)); + SSOToken token = resourceOwnerSessionValidator.getResourceOwnerSession(request); + if (token != null) { + populateAuthenticationInfo(deviceCode, token); + } deviceCode.setResourceOwnerId(resourceOwner.getId()); deviceCode.setAuthorized(true); tokenStore.updateDeviceCode(deviceCode, request); @@ -180,10 +189,16 @@ public Representation verify(Representation body) throws ServerException, NotFou } } else { ResourceOwner resourceOwner = resourceOwnerSessionValidator.validate(request); + deviceCode.setAcrValues(getAuthenticationContextClassReferenceFromRequest(request)); + SSOToken token = resourceOwnerSessionValidator.getResourceOwnerSession(request); + if (token != null) { + populateAuthenticationInfo(deviceCode, token); + } deviceCode.setResourceOwnerId(resourceOwner.getId()); deviceCode.setAuthorized(true); tokenStore.updateDeviceCode(deviceCode, request); } + } catch (IllegalArgumentException e) { if (e.getMessage().contains("client_id")) { throw new OAuth2RestletException(400, "invalid_request", e.getMessage(), @@ -300,4 +315,21 @@ private TemplateFactory getTemplateFactory(Context context) { protected void doCatch(Throwable throwable) { exceptionHandler.handle(throwable, getContext(), getRequest(), getResponse()); } + + + private void populateAuthenticationInfo(DeviceCode deviceCode, SSOToken token) { + if (token == null) { + return; + } + + try { + deviceCode.setAuthModules(token.getProperty(ISAuthConstants.AUTH_TYPE)); + } catch (SSOException e) { + logger.warn("Could not get list of auth modules from authentication", e); + } + } + + private String getAuthenticationContextClassReferenceFromRequest(OAuth2Request request) { + return (String) request.getRequest().getAttributes().get(OAuth2Constants.JWTTokenParams.ACR); + } } diff --git a/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatefulTokenStore.java b/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatefulTokenStore.java index 725cab3a47..153e7a3a82 100644 --- a/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatefulTokenStore.java +++ b/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatefulTokenStore.java @@ -407,6 +407,8 @@ private List getAMRFromAuthModules(OAuth2Request request, OAuth2Provider String authModules; if (request.getToken(AuthorizationCode.class) != null) { authModules = request.getToken(AuthorizationCode.class).getAuthModules(); + } else if (request.getToken(DeviceCode.class) != null) { + authModules = request.getToken(DeviceCode.class).getAuthModules(); } else if (request.getToken(RefreshToken.class) != null) { authModules = request.getToken(RefreshToken.class).getAuthModules(); } else { @@ -432,6 +434,8 @@ private List getAMRFromAuthModules(OAuth2Request request, OAuth2Provider private String getAuthenticationContextClassReference(OAuth2Request request) { if (request.getToken(AuthorizationCode.class) != null) { return request.getToken(AuthorizationCode.class).getAuthenticationContextClassReference(); + } else if(request.getToken(DeviceCode.class) != null){ + return request.getToken(DeviceCode.class).getAcrValues(); } else if (request.getToken(RefreshToken.class) != null) { return request.getToken(RefreshToken.class).getAuthenticationContextClassReference(); } else { @@ -897,7 +901,7 @@ public DeviceCode createDeviceCode(Set scope, ResourceOwner resourceOwne Integer maxAge, String claims, OAuth2Request request, String codeChallenge, String codeChallengeMethod) throws ServerException, NotFoundException { - logger.message("DefaultOAuthTokenStoreImpl::Creating Authorization code"); + logger.message("DefaultOAuthTokenStoreImpl::Creating Device code"); final OAuth2ProviderSettings providerSettings = providerSettingsFactory.get(request); final String deviceCode = UUID.randomUUID().toString(); diff --git a/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatelessTokenStore.java b/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatelessTokenStore.java index 7b76e397ed..15ce50b2fc 100644 --- a/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatelessTokenStore.java +++ b/openam-oauth2/src/main/java/org/forgerock/openam/oauth2/StatelessTokenStore.java @@ -17,7 +17,6 @@ package org.forgerock.openam.oauth2; -import static com.sun.identity.shared.DateUtils.stringToDate; import static org.forgerock.json.JsonValue.json; import static org.forgerock.openam.oauth2.OAuth2Constants.Bearer.BEARER; import static org.forgerock.openam.oauth2.OAuth2Constants.CoreTokenParams.*; @@ -92,6 +91,8 @@ import org.forgerock.openam.cts.api.tokens.Token; import org.forgerock.openam.cts.exceptions.CoreTokenException; import org.forgerock.openam.oauth2.OAuth2Constants.ProofOfPossession; +import org.forgerock.openam.oauth2.OAuth2Constants.TokenEndpoint; +import org.forgerock.openam.rest.jakarta.servlet.ServletUtils; import org.forgerock.openam.tokens.CoreTokenField; import org.forgerock.openam.utils.RealmNormaliser; import org.forgerock.openam.utils.StringUtils; @@ -100,6 +101,7 @@ import org.forgerock.util.encode.Base64; import org.forgerock.util.query.QueryFilter; import org.joda.time.Duration; +import org.restlet.Request; /** * Stateless implementation of the OAuth2 Token Store. @@ -201,8 +203,12 @@ public AccessToken createAccessToken(String grantType, String accessTokenType, S //realmAccess.put("roles", new HashSet<>(Arrays.asList( new String[] {"admin", "user"} ))); AuthorizationCode authCode = request.getToken(AuthorizationCode.class); + DeviceCode deviceCode = request.getToken(DeviceCode.class); + RefreshToken currentRefreshToken = request.getToken(RefreshToken.class); + String sessionId = null; + if (authCode != null) { - String sessionId = authCode.getSessionId(); + sessionId = authCode.getSessionId(); if (StringUtils.isNotBlank(sessionId)) { try { final SSOTokenManager ssoTokenManager = SSOTokenManager.getInstance(); @@ -238,7 +244,7 @@ public AccessToken createAccessToken(String grantType, String accessTokenType, S .claim(AUDIT_TRACKING_ID, UUID.randomUUID().toString()) .claim(AUTH_GRANT_ID, refreshToken != null ? refreshToken.getAuthGrantId() : UUID.randomUUID().toString()) .claim(AUTH_TIME, authTime); - + // Propagate authentication context (acr) and authentication modules (amr) into the // stateless JWT access token, mirroring the behaviour of createRefreshToken. The values // are sourced from the AuthorizationCode (authorization_code grant) or from the previous @@ -246,19 +252,29 @@ public AccessToken createAccessToken(String grantType, String accessTokenType, S // the access token payload without an extra /oauth2/tokeninfo round-trip. String authModules = null; String acr = null; - if (authCode != null) { - authModules = authCode.getAuthModules(); - acr = authCode.getAuthenticationContextClassReference(); - } - RefreshToken currentRefreshToken = request.getToken(RefreshToken.class); - if (currentRefreshToken != null) { + + + if (TokenEndpoint.REFRESH_TOKEN.equals(grantType) && currentRefreshToken != null) { authModules = currentRefreshToken.getAuthModules(); acr = currentRefreshToken.getAuthenticationContextClassReference(); + + } else if (TokenEndpoint.DEVICE_CODE.equals(grantType) && deviceCode != null) { + authModules = deviceCode.getAuthModules(); + acr = deviceCode.getAcrValues(); + + } else if (authCode != null) { + authModules = authCode.getAuthModules(); + acr = authCode.getAuthenticationContextClassReference(); } + + + if (authModules != null) { - claimsSetBuilder.claim(AUTH_MODULES, authModules); + + claimsSetBuilder.claim("amr", getAMRFromAuthModules(authModules, providerSettings)); } if (acr != null) { + claimsSetBuilder.claim(ACR, acr); } @@ -275,8 +291,9 @@ public AccessToken createAccessToken(String grantType, String accessTokenType, S accessTokenContext.put(ACR, acr); } if (authModules != null) { - accessTokenContext.put("amr", authModules); + accessTokenContext.put("amr", getAMRFromAuthModules(authModules, providerSettings)); } + Map modifiedClaims = accessTokenModifier.getModifiedClaims(request, realm, resourceOwnerId, clientId, scope, accessTokenContext); for (Map.Entry entry : modifiedClaims.entrySet()) { @@ -534,24 +551,34 @@ public RefreshToken createRefreshToken(String grantType, String clientId, String for(org.forgerock.oauth2.core.Token token : request.getTokens()) { if(token instanceof AuthorizationCode) { claimsSetBuilder.claim(NONCE, ((AuthorizationCode)token).getNonce()); + } else if(token instanceof DeviceCode) { + claimsSetBuilder.claim(NONCE, ((DeviceCode)token).getNonce()); } } + + String authModules = null; String acr = null; AuthorizationCode authorizationCode = request.getToken(AuthorizationCode.class); - if (authorizationCode != null) { - authModules = authorizationCode.getAuthModules(); - acr = authorizationCode.getAuthenticationContextClassReference(); - } - + DeviceCode deviceCode = request.getToken(DeviceCode.class); RefreshToken currentRefreshToken = request.getToken(RefreshToken.class); - if (currentRefreshToken != null) { + + if (TokenEndpoint.REFRESH_TOKEN.equals(grantType) && currentRefreshToken != null) { authModules = currentRefreshToken.getAuthModules(); acr = currentRefreshToken.getAuthenticationContextClassReference(); + + } else if (TokenEndpoint.DEVICE_CODE.equals(grantType) && deviceCode != null) { + authModules = deviceCode.getAuthModules(); + acr = deviceCode.getAcrValues(); + + } else if (authorizationCode != null) { + authModules = authorizationCode.getAuthModules(); + acr = authorizationCode.getAuthenticationContextClassReference(); } + if (authModules != null) { - claimsSetBuilder.claim(AUTH_MODULES, authModules); + claimsSetBuilder.claim("amr", getAMRFromAuthModules(authModules, providerSettings)); } if (acr != null) { claimsSetBuilder.claim(ACR, acr); @@ -574,7 +601,7 @@ public RefreshToken createRefreshToken(String grantType, String clientId, String refreshTokenContext.put(ACR, acr); } if (authModules != null) { - refreshTokenContext.put("amr", authModules); + refreshTokenContext.put("amr", getAMRFromAuthModules(authModules, providerSettings)); } Map modifiedRefreshClaims = accessTokenModifier.getModifiedClaims(request, realm, resourceOwnerId, clientId, scope, refreshTokenContext); @@ -864,6 +891,39 @@ protected void validateTokenRealm(String tokenRealm, OAuth2Request request) thro throw new NotFoundException(e.getMessage()); } } + + private String getAuthModulesFromSSOToken(OAuth2Request request) { + String authModules = null; + try { + final SSOTokenManager ssoTokenManager = SSOTokenManager.getInstance(); + SSOToken token = ssoTokenManager.createSSOToken(ServletUtils.getRequest(request.getRequest())); + if (token != null) { + authModules = token.getProperty(ISAuthConstants.AUTH_TYPE); + } + } catch (SSOException e) { + logger.warning("Could not get list of auth modules from authentication", e); + } + return authModules; + } + + private List getAMRFromAuthModules(String authModules, OAuth2ProviderSettings providerSettings) throws ServerException { + List amr = null; + + if (authModules != null) { + Map amrMappings = providerSettings.getAMRAuthModuleMappings(); + if (!amrMappings.isEmpty()) { + amr = new ArrayList(); + List modulesUsed = Arrays.asList(authModules.split("\\|")); + for (Map.Entry amrToModuleMapping : amrMappings.entrySet()) { + if (modulesUsed.contains(amrToModuleMapping.getValue())) { + amr.add(amrToModuleMapping.getKey()); + } + } + } + } + + return amr; + } private JsonValue convertToken(StatelessToken token) { Map map = new HashMap<>(); @@ -878,4 +938,5 @@ private JsonValue convertToken(StatelessToken token) { map.put(SCOPE, token.getScope()); return json(map); } -} + +} \ No newline at end of file diff --git a/openam-oauth2/src/test/java/org/forgerock/openam/oauth2/StatelessTokenStoreTest.java b/openam-oauth2/src/test/java/org/forgerock/openam/oauth2/StatelessTokenStoreTest.java index 54b5a97c38..824fd87bd0 100644 --- a/openam-oauth2/src/test/java/org/forgerock/openam/oauth2/StatelessTokenStoreTest.java +++ b/openam-oauth2/src/test/java/org/forgerock/openam/oauth2/StatelessTokenStoreTest.java @@ -17,6 +17,7 @@ package org.forgerock.openam.oauth2; import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.forgerock.json.JsonValue.*; import static org.mockito.ArgumentMatchers.any; @@ -145,6 +146,11 @@ public void whenAuthorizationCodePresentAcrAndAmrGetAddedToAccessToken() throws // Given givenBaseProviderSettings(); given(utils.getConfirmationKey(request)).willReturn(null); + + Map amrMappings = new HashMap<>(); + amrMappings.put("amr1", "DataStore"); + + given(settings.getAMRAuthModuleMappings()).willReturn(amrMappings); AuthorizationCode authorizationCode = mock(AuthorizationCode.class); given(authorizationCode.getAuthModules()).willReturn("DataStore"); @@ -158,7 +164,7 @@ public void whenAuthorizationCodePresentAcrAndAmrGetAddedToAccessToken() throws // Then assertThat(token.getTokenInfo().get("acr")).isEqualTo("urn:mace:incommon:iap:silver"); - assertThat(token.getTokenInfo().get("authModules")).isEqualTo("DataStore"); + assertThat(token.getTokenInfo().get("amr")).isEqualTo(singletonList("amr1")); } @Test @@ -167,6 +173,11 @@ public void whenRefreshTokenPresentAcrAndAmrGetAddedToAccessToken() throws Excep givenBaseProviderSettings(); given(utils.getConfirmationKey(request)).willReturn(null); + Map amrMappings = new HashMap<>(); + amrMappings.put("amr2", "LDAP"); + + given(settings.getAMRAuthModuleMappings()).willReturn(amrMappings); + RefreshToken currentRefreshToken = mock(RefreshToken.class); given(currentRefreshToken.getAuthModules()).willReturn("LDAP"); given(currentRefreshToken.getAuthenticationContextClassReference()).willReturn("urn:mace:incommon:iap:bronze"); @@ -178,7 +189,7 @@ public void whenRefreshTokenPresentAcrAndAmrGetAddedToAccessToken() throws Excep // Then assertThat(token.getTokenInfo().get("acr")).isEqualTo("urn:mace:incommon:iap:bronze"); - assertThat(token.getTokenInfo().get("authModules")).isEqualTo("LDAP"); + assertThat(token.getTokenInfo().get("amr")).isEqualTo(singletonList("amr2")); } @Test @@ -186,6 +197,12 @@ public void whenRefreshTokenPresentItOverridesAuthorizationCodeAcrAndAmr() throw // Given givenBaseProviderSettings(); given(utils.getConfirmationKey(request)).willReturn(null); + + Map amrMappings = new HashMap<>(); + amrMappings.put("amr1", "DataStore"); + amrMappings.put("amr2", "LDAP"); + + given(settings.getAMRAuthModuleMappings()).willReturn(amrMappings); AuthorizationCode authorizationCode = mock(AuthorizationCode.class); given(authorizationCode.getAuthModules()).willReturn("DataStore"); @@ -204,7 +221,7 @@ public void whenRefreshTokenPresentItOverridesAuthorizationCodeAcrAndAmr() throw // Then assertThat(token.getTokenInfo().get("acr")).isEqualTo("acr-from-refresh"); - assertThat(token.getTokenInfo().get("authModules")).isEqualTo("LDAP"); + assertThat(token.getTokenInfo().get("amr")).isEqualTo(singletonList("amr2")); } @Test @@ -219,7 +236,6 @@ public void whenNoAcrOrAmrAvailableTheyAreNotAddedToAccessToken() throws Excepti // Then assertThat(token.getTokenInfo()).doesNotContainKey("acr"); - assertThat(token.getTokenInfo()).doesNotContainKey("authModules"); } @Test