diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java index 146966cb056..956c7d896a7 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java @@ -10,19 +10,16 @@ import io.jans.as.common.service.AttributeService; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.token.JsonWebResponse; +import io.jans.as.server.model.audit.OAuth2AuditLog; import io.jans.as.server.model.ldap.TokenEntity; import io.jans.model.custom.script.conf.CustomScriptConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.Response; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; import java.util.function.Function; /** @@ -60,6 +57,7 @@ public class ExecutionContext { private Set scopes; private String claimsAsString; private List userSessions; + private OAuth2AuditLog auditLog; @NotNull private final Map attributes = new HashMap<>(); @@ -90,6 +88,14 @@ public HttpServletResponse getHttpResponse() { return httpResponse; } + public OAuth2AuditLog getAuditLog() { + return auditLog; + } + + public void setAuditLog(OAuth2AuditLog auditLog) { + this.auditLog = auditLog; + } + public Client getClient() { return client; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java index 861b6a095e6..a3d187110c4 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java @@ -181,79 +181,17 @@ public Response requestAccessToken(String grantType, String code, executionContext.setCertAsPem(request.getHeader(X_CLIENTCERT)); executionContext.setDpop(dpopStr); executionContext.setClient(client); - executionContext.setDpop(dpopStr); executionContext.setAppConfiguration(appConfiguration); executionContext.setAttributeService(attributeService); + executionContext.setAuditLog(auditLog); if (gt == GrantType.AUTHORIZATION_CODE) { - log.debug("Attempting to find authorizationCodeGrant by clientId: '{}', code: '{}'", client.getClientId(), code); - final AuthorizationCodeGrant authorizationCodeGrant = authorizationGrantList.getAuthorizationCodeGrant(code); - log.trace("AuthorizationCodeGrant : '{}'", authorizationCodeGrant); - - if (authorizationCodeGrant == null) { - log.debug("AuthorizationCodeGrant is empty by clientId: '{}', code: '{}'", client.getClientId(), code); - // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code - grantService.removeAllByAuthorizationCode(code); - return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Unable to find grant object for given code."), auditLog); - } - - if (!client.getClientId().equals(authorizationCodeGrant.getClientId())) { - log.debug("AuthorizationCodeGrant is found but belongs to another client. Grant's clientId: '{}', code: '{}'", authorizationCodeGrant.getClientId(), code); - // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code - grantService.removeAllByAuthorizationCode(code); - return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Client mismatch."), auditLog); - } - - validatePKCE(authorizationCodeGrant, codeVerifier, auditLog); - - authorizationCodeGrant.setIsCachedWithNoPersistence(false); - authorizationCodeGrant.save(); - - RefreshToken reToken = createRefreshToken(request, client, scope, authorizationCodeGrant, dpopStr); - - scope = authorizationCodeGrant.checkScopesPolicy(scope); - - executionContext.setGrant(authorizationCodeGrant); - AccessToken accToken = authorizationCodeGrant.createAccessToken(executionContext); // create token after scopes are checked - - IdToken idToken = null; - if (authorizationCodeGrant.getScopes().contains(OPENID)) { - String nonce = authorizationCodeGrant.getNonce(); - boolean includeIdTokenClaims = Boolean.TRUE.equals( - appConfiguration.getLegacyIdTokenClaims()); - final String idTokenTokenBindingCnf = client.getIdTokenTokenBindingCnf(); - Function authorizationCodePreProcessing = jsonWebResponse -> { - if (StringUtils.isNotBlank(idTokenTokenBindingCnf) && StringUtils.isNotBlank(authorizationCodeGrant.getTokenBindingHash())) { - TokenBindingMessage.setCnfClaim(jsonWebResponse, authorizationCodeGrant.getTokenBindingHash(), idTokenTokenBindingCnf); - } - return null; - }; - - ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(request, authorizationCodeGrant, client, appConfiguration, attributeService); - - executionContext.setIncludeIdTokenClaims(includeIdTokenClaims); - executionContext.setPreProcessing(JwrService.wrapWithSidFunction(authorizationCodePreProcessing, sessionIdObj != null ? sessionIdObj.getOutsideSid() : null)); - executionContext.setPostProcessor(externalUpdateTokenService.buildModifyIdTokenProcessor(context)); - - idToken = authorizationCodeGrant.createIdToken( - nonce, authorizationCodeGrant.getAuthorizationCode(), accToken, null, null, executionContext); - } - - auditLog.updateOAuth2AuditLog(authorizationCodeGrant, true); - - grantService.removeAuthorizationCode(authorizationCodeGrant.getAuthorizationCode().getCode()); - - final String entity = getJSonResponse(accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken); - return response(Response.ok().entity(entity), auditLog); + return processAuthorizationCode(code, scope, codeVerifier, sessionIdObj, executionContext); } if (gt == GrantType.REFRESH_TOKEN) { AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByRefreshToken(client.getClientId(), refreshToken); - - if (authorizationGrant == null) { - log.trace("Grant object is not found by refresh token."); - return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Unable to find grant object by refresh token or otherwise token type or client does not match."), auditLog); - } + tokenRestWebServiceValidator.validateGrant(authorizationGrant, client, refreshToken, auditLog); final RefreshToken refreshTokenObject = authorizationGrant.getRefreshToken(refreshToken); if (refreshTokenObject == null || !refreshTokenObject.isValid()) { @@ -510,6 +448,58 @@ public Response requestAccessToken(String grantType, String code, return response(builder, auditLog); } + private Response processAuthorizationCode(String code, String scope, String codeVerifier, SessionId sessionIdObj, ExecutionContext executionContext) { + Client client = executionContext.getClient(); + + log.debug("Attempting to find authorizationCodeGrant by clientId: '{}', code: '{}'", client.getClientId(), code); + final AuthorizationCodeGrant authorizationCodeGrant = authorizationGrantList.getAuthorizationCodeGrant(code); + log.trace("AuthorizationCodeGrant : '{}'", authorizationCodeGrant); + + // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code + tokenRestWebServiceValidator.validateGrant(authorizationCodeGrant, client, code, executionContext.getAuditLog(), grant -> grantService.removeAllByAuthorizationCode(code)); + validatePKCE(authorizationCodeGrant, codeVerifier, executionContext.getAuditLog()); + + authorizationCodeGrant.setIsCachedWithNoPersistence(false); + authorizationCodeGrant.save(); + + RefreshToken reToken = createRefreshToken(executionContext.getHttpRequest(), client, scope, authorizationCodeGrant, executionContext.getDpop()); + + scope = authorizationCodeGrant.checkScopesPolicy(scope); + + executionContext.setGrant(authorizationCodeGrant); + AccessToken accToken = authorizationCodeGrant.createAccessToken(executionContext); // create token after scopes are checked + + IdToken idToken = null; + if (authorizationCodeGrant.getScopes().contains(OPENID)) { + String nonce = authorizationCodeGrant.getNonce(); + boolean includeIdTokenClaims = Boolean.TRUE.equals( + appConfiguration.getLegacyIdTokenClaims()); + final String idTokenTokenBindingCnf = client.getIdTokenTokenBindingCnf(); + Function authorizationCodePreProcessing = jsonWebResponse -> { + if (StringUtils.isNotBlank(idTokenTokenBindingCnf) && StringUtils.isNotBlank(authorizationCodeGrant.getTokenBindingHash())) { + TokenBindingMessage.setCnfClaim(jsonWebResponse, authorizationCodeGrant.getTokenBindingHash(), idTokenTokenBindingCnf); + } + return null; + }; + + ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(executionContext.getHttpRequest(), authorizationCodeGrant, client, appConfiguration, attributeService); + + executionContext.setIncludeIdTokenClaims(includeIdTokenClaims); + executionContext.setPreProcessing(JwrService.wrapWithSidFunction(authorizationCodePreProcessing, sessionIdObj != null ? sessionIdObj.getOutsideSid() : null)); + executionContext.setPostProcessor(externalUpdateTokenService.buildModifyIdTokenProcessor(context)); + + idToken = authorizationCodeGrant.createIdToken( + nonce, authorizationCodeGrant.getAuthorizationCode(), accToken, null, null, executionContext); + } + + executionContext.getAuditLog().updateOAuth2AuditLog(authorizationCodeGrant, true); + + grantService.removeAuthorizationCode(authorizationCodeGrant.getAuthorizationCode().getCode()); + + final String entity = getJSonResponse(accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken); + return response(Response.ok().entity(entity), executionContext.getAuditLog()); + } + @Nullable private Client getClient() { SessionClient sessionClient = identity.getSessionClient(); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidator.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidator.java index 4d147344d1f..d1d18abefca 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidator.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidator.java @@ -7,6 +7,7 @@ import io.jans.as.model.token.TokenErrorResponseType; import io.jans.as.server.audit.ApplicationAuditLogger; import io.jans.as.server.model.audit.OAuth2AuditLog; +import io.jans.as.server.model.common.AuthorizationGrant; import io.jans.as.server.model.common.DeviceAuthorizationCacheControl; import io.jans.as.server.util.ServerUtil; import jakarta.ejb.Stateless; @@ -21,6 +22,7 @@ import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; import static io.jans.as.model.config.Constants.REASON_CLIENT_NOT_AUTHORIZED; @@ -130,4 +132,27 @@ public void validateDeviceAuthorization(Client client, String deviceCode, Device throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_GRANT, REASON_CLIENT_NOT_AUTHORIZED), oAuth2AuditLog)); } } + + public void validateGrant(AuthorizationGrant grant, Client client, Object identifier, OAuth2AuditLog auditLog) { + validateGrant(grant, client, identifier, auditLog, null); + } + + + public void validateGrant(AuthorizationGrant grant, Client client, Object identifier, OAuth2AuditLog auditLog, Consumer onFailure) { + if (grant == null) { + log.debug("AuthorizationGrant not found by clientId: '{}', identifier: '{}'", client.getClientId(), identifier); + if (onFailure != null) { + onFailure.accept(grant); + } + throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_GRANT, "Unable to find grant object for given code."), auditLog)); + } + + if (!client.getClientId().equals(grant.getClientId())) { + log.debug("AuthorizationCodeGrant is found but belongs to another client. Grant's clientId: '{}', identifier: '{}'", grant.getClientId(), identifier); + if (onFailure != null) { + onFailure.accept(grant); + } + throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_GRANT, "Client mismatch."), auditLog)); + } + } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java b/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java index e685027be3d..7833f502e07 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; import io.jans.as.common.service.common.ApplicationFactory; @@ -140,7 +141,7 @@ public static CacheControl cacheControlWithNoStoreTransformAndPrivate() { } public static ObjectMapper createJsonMapper() { - final AnnotationIntrospector jaxb = new JaxbAnnotationIntrospector(); + final AnnotationIntrospector jaxb = new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()); final AnnotationIntrospector jackson = new JacksonAnnotationIntrospector(); final AnnotationIntrospector pair = AnnotationIntrospector.pair(jackson, jaxb); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidatorTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidatorTest.java index e691d557a44..ba7e6217546 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidatorTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceValidatorTest.java @@ -1,11 +1,14 @@ package io.jans.as.server.token.ws.rs; +import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.model.common.GrantType; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.error.ErrorResponseFactory; import io.jans.as.server.audit.ApplicationAuditLogger; import io.jans.as.server.model.audit.OAuth2AuditLog; +import io.jans.as.server.model.common.AuthorizationGrant; +import io.jans.as.server.model.common.AuthorizationGrantType; import io.jans.as.server.model.common.DeviceAuthorizationCacheControl; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -16,6 +19,8 @@ import org.testng.annotations.Listeners; import org.testng.annotations.Test; +import java.util.Date; + import static io.jans.as.server.util.TestUtil.assertBadRequest; import static org.junit.Assert.fail; import static org.testng.AssertJUnit.assertEquals; @@ -41,12 +46,12 @@ public class TokenRestWebServiceValidatorTest { private ErrorResponseFactory errorResponseFactory; @InjectMocks - private TokenRestWebServiceValidator tokenRestWebServiceValidator; + private TokenRestWebServiceValidator validator; @Test public void validateParams_whenGrantTypeIsBlank_shouldRaiseError() { try { - tokenRestWebServiceValidator.validateParams("", "some_code", "https://my.redirect", "refresh_token", AUDIT_LOG); + validator.validateParams("", "some_code", "https://my.redirect", "refresh_token", AUDIT_LOG); } catch (WebApplicationException e) { assertBadRequest(e.getResponse()); return; @@ -57,7 +62,7 @@ public void validateParams_whenGrantTypeIsBlank_shouldRaiseError() { @Test public void validateParams_whenGrantTypeIsAuthorizationCodeAndCodeIsBlank_shouldRaiseError() { try { - tokenRestWebServiceValidator.validateParams(GrantType.AUTHORIZATION_CODE.getValue(), "", "https://my.redirect", "refresh_token", AUDIT_LOG); + validator.validateParams(GrantType.AUTHORIZATION_CODE.getValue(), "", "https://my.redirect", "refresh_token", AUDIT_LOG); } catch (WebApplicationException e) { assertBadRequest(e.getResponse()); return; @@ -69,7 +74,7 @@ public void validateParams_whenGrantTypeIsAuthorizationCodeAndCodeIsBlank_should @Test public void validateParams_whenGrantTypeIsAuthorizationCodeAndRedirectUriIsBlank_shouldRaiseError() { try { - tokenRestWebServiceValidator.validateParams(GrantType.AUTHORIZATION_CODE.getValue(), "some_code", "", "refresh_token", AUDIT_LOG); + validator.validateParams(GrantType.AUTHORIZATION_CODE.getValue(), "some_code", "", "refresh_token", AUDIT_LOG); } catch (WebApplicationException e) { assertBadRequest(e.getResponse()); return; @@ -80,7 +85,7 @@ public void validateParams_whenGrantTypeIsAuthorizationCodeAndRedirectUriIsBlank @Test public void validateParams_whenGrantTypeIsRefreshTokenAndRefreshTokenIsBlank_shouldRaiseError() { try { - tokenRestWebServiceValidator.validateParams(GrantType.REFRESH_TOKEN.getValue(), "some_code", "https://my.redirect", "", AUDIT_LOG); + validator.validateParams(GrantType.REFRESH_TOKEN.getValue(), "some_code", "https://my.redirect", "", AUDIT_LOG); } catch (WebApplicationException e) { assertBadRequest(e.getResponse()); return; @@ -91,7 +96,7 @@ public void validateParams_whenGrantTypeIsRefreshTokenAndRefreshTokenIsBlank_sho @Test public void validateParams_whenGrantTypeIsAuthorizationCodeAndCodeIsNotBlank_shouldNotRaiseError() { try { - tokenRestWebServiceValidator.validateParams(GrantType.AUTHORIZATION_CODE.getValue(), "some_code", "https://my.redirect", "", AUDIT_LOG); + validator.validateParams(GrantType.AUTHORIZATION_CODE.getValue(), "some_code", "https://my.redirect", "", AUDIT_LOG); } catch (WebApplicationException e) { fail("Error occurs. We should not get it."); } @@ -100,7 +105,7 @@ public void validateParams_whenGrantTypeIsAuthorizationCodeAndCodeIsNotBlank_sho @Test public void validateParams_whenGrantTypeIsRefreshTokenAndRefreshTokenIsNotBlank_shouldNotRaiseError() { try { - tokenRestWebServiceValidator.validateParams(GrantType.REFRESH_TOKEN.getValue(), "", "https://my.redirect", "refresh_token", AUDIT_LOG); + validator.validateParams(GrantType.REFRESH_TOKEN.getValue(), "", "https://my.redirect", "refresh_token", AUDIT_LOG); } catch (WebApplicationException e) { fail("Error occurs. We should not get it."); } @@ -109,7 +114,7 @@ public void validateParams_whenGrantTypeIsRefreshTokenAndRefreshTokenIsNotBlank_ @Test public void validateGrantType_whenClientDotNotHaveGrantType_shouldRaiseError() { try { - tokenRestWebServiceValidator.validateGrantType(GrantType.AUTHORIZATION_CODE, new Client(), AUDIT_LOG); + validator.validateGrantType(GrantType.AUTHORIZATION_CODE, new Client(), AUDIT_LOG); } catch (WebApplicationException e) { assertBadRequest(e.getResponse()); return; @@ -120,7 +125,7 @@ public void validateGrantType_whenClientDotNotHaveGrantType_shouldRaiseError() { @Test public void validateClient_whenClientIsNull_shouldRaiseError() { try { - tokenRestWebServiceValidator.validateClient(null, AUDIT_LOG); + validator.validateClient(null, AUDIT_LOG); } catch (WebApplicationException e) { assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), e.getResponse().getStatus()); return; @@ -133,7 +138,7 @@ public void validateClient_whenClientIsDisabled_shouldRaiseError() { try { Client client = new Client(); client.setDisabled(true); - tokenRestWebServiceValidator.validateClient(client, AUDIT_LOG); + validator.validateClient(client, AUDIT_LOG); } catch (WebApplicationException e) { assertEquals(Response.Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); return; @@ -147,7 +152,7 @@ public void validateDeviceAuthorizationCacheControl_whenDeviceAuthzIsNull_should Client client = new Client(); client.setClientId("testId"); - tokenRestWebServiceValidator.validateDeviceAuthorization(client, "code", null, AUDIT_LOG); + validator.validateDeviceAuthorization(client, "code", null, AUDIT_LOG); } catch (WebApplicationException e) { assertBadRequest(e.getResponse()); return; @@ -164,11 +169,63 @@ public void validateDeviceAuthorizationCacheControl_whenDeviceAuthzDoesNotBelong DeviceAuthorizationCacheControl deviceAuthorization = new DeviceAuthorizationCacheControl(); deviceAuthorization.setClient(client); - tokenRestWebServiceValidator.validateDeviceAuthorization(new Client(), "code", deviceAuthorization, AUDIT_LOG); + validator.validateDeviceAuthorization(new Client(), "code", deviceAuthorization, AUDIT_LOG); } catch (WebApplicationException e) { assertBadRequest(e.getResponse()); return; } fail("No error when client is null."); } + + @Test + public void validateGrant_whenGrantIsNull_shouldRaiseError() { + try { + Client client = new Client(); + client.setClientId("testId"); + + validator.validateGrant(null, client, "code", AUDIT_LOG); + } catch (WebApplicationException e) { + assertBadRequest(e.getResponse()); + return; + } + fail("No error when grant is null."); + } + + @Test + public void validateGrant_whenGrantDoesNotBelongToGivenClient_shouldRaiseError() { + try { + Client client = new Client(); + client.setClientId("testId"); + + AuthorizationGrant grant = new AuthorizationGrant() { + @Override + public GrantType getGrantType() { + return GrantType.AUTHORIZATION_CODE; + } + }; + grant.init(new User(), AuthorizationGrantType.AUTHORIZATION_CODE, new Client(), new Date()); + + validator.validateGrant(grant, client, "code", AUDIT_LOG); + } catch (WebApplicationException e) { + assertBadRequest(e.getResponse()); + return; + } + fail("No error when grant and client is not matched."); + } + + @Test + public void validateGrant_whenGrantMatchesToClient_shouldNotRaiseError() { + Client client = new Client(); + client.setClientId("testId"); + + AuthorizationGrant grant = new AuthorizationGrant() { + @Override + public GrantType getGrantType() { + return GrantType.AUTHORIZATION_CODE; + } + }; + grant.init(new User(), AuthorizationGrantType.AUTHORIZATION_CODE, client, new Date()); + + validator.validateGrant(grant, client, "code", AUDIT_LOG); + } }