From 33a2a2d23f76e4f8af5003a59ea60fa129b9f00a Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Tue, 15 Nov 2022 16:34:22 +0200 Subject: [PATCH] feat(jans-auth-server): check offline_access implementation has all conditions defined in spec #1945 --- .../persistence/model/ClientAttributes.java | 12 ++ .../ws/rs/AuthorizeRestWebServiceImpl.java | 70 +++------ .../rs/AuthorizeRestWebServiceImplTest.java | 146 ++++++++++++++++++ .../server/src/test/resources/testng.xml | 1 + 4 files changed, 179 insertions(+), 50 deletions(-) create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImplTest.java diff --git a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java index 155eae7bc92..cfa458061d1 100644 --- a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java +++ b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java @@ -104,6 +104,17 @@ public class ClientAttributes implements Serializable { @JsonProperty("idTokenLifetime") private Integer idTokenLifetime; + @JsonProperty("allowOfflineAccessWithoutConsent") + private Boolean allowOfflineAccessWithoutConsent; + + public Boolean getAllowOfflineAccessWithoutConsent() { + return allowOfflineAccessWithoutConsent; + } + + public void setAllowOfflineAccessWithoutConsent(Boolean allowOfflineAccessWithoutConsent) { + this.allowOfflineAccessWithoutConsent = allowOfflineAccessWithoutConsent; + } + public Integer getIdTokenLifetime() { return idTokenLifetime; } @@ -366,6 +377,7 @@ public String toString() { ", authorizationEncryptedResponseEnc=" + authorizationEncryptedResponseEnc + ", publicSubjectIdentifierAttribute=" + publicSubjectIdentifierAttribute + ", redirectUrisRegex=" + redirectUrisRegex + + ", allowOfflineAccessWithoutConsent=" + allowOfflineAccessWithoutConsent + ", defaultPromptLogin=" + defaultPromptLogin + '}'; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java index 2731fb17192..ef4c662e2b9 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java @@ -9,17 +9,13 @@ import com.google.common.collect.Maps; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; +import io.jans.as.common.model.session.SessionId; +import io.jans.as.common.model.session.SessionIdState; import io.jans.as.common.util.RedirectUri; import io.jans.as.model.authorize.AuthorizeErrorResponseType; import io.jans.as.model.authorize.AuthorizeRequestParam; import io.jans.as.model.authorize.AuthorizeResponseParam; -import io.jans.as.model.common.BackchannelTokenDeliveryMode; -import io.jans.as.model.common.GrantType; -import io.jans.as.model.common.Prompt; -import io.jans.as.model.common.ResponseMode; -import io.jans.as.model.common.ResponseType; -import io.jans.as.model.common.ScopeConstants; -import io.jans.as.model.common.SubjectType; +import io.jans.as.model.common.*; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.crypto.binding.TokenBindingMessage; import io.jans.as.model.crypto.binding.TokenBindingParseException; @@ -32,22 +28,7 @@ import io.jans.as.server.ciba.CIBAPushTokenDeliveryService; import io.jans.as.server.model.authorize.AuthorizeParamsValidator; import io.jans.as.server.model.authorize.ScopeChecker; -import io.jans.as.server.model.common.AccessToken; -import io.jans.as.server.model.common.AuthorizationCode; -import io.jans.as.server.model.common.AuthorizationGrant; -import io.jans.as.server.model.common.AuthorizationGrantList; -import io.jans.as.server.model.common.CIBAGrant; -import io.jans.as.server.model.common.CibaRequestCacheControl; -import io.jans.as.server.model.common.CibaRequestStatus; -import io.jans.as.server.model.common.DefaultScope; -import io.jans.as.server.model.common.DeviceAuthorizationCacheControl; -import io.jans.as.server.model.common.DeviceAuthorizationStatus; -import io.jans.as.server.model.common.DeviceCodeGrant; -import io.jans.as.server.model.common.ExecutionContext; -import io.jans.as.server.model.common.IdToken; -import io.jans.as.server.model.common.RefreshToken; -import io.jans.as.common.model.session.SessionId; -import io.jans.as.common.model.session.SessionIdState; +import io.jans.as.server.model.common.*; import io.jans.as.server.model.config.ConfigurationFactory; import io.jans.as.server.model.config.Constants; import io.jans.as.server.model.exception.AcrChangedException; @@ -56,15 +37,7 @@ import io.jans.as.server.model.ldap.ClientAuthorization; import io.jans.as.server.model.token.JwrService; import io.jans.as.server.security.Identity; -import io.jans.as.server.service.AttributeService; -import io.jans.as.server.service.AuthenticationFilterService; -import io.jans.as.server.service.ClientAuthorizationsService; -import io.jans.as.server.service.ClientService; -import io.jans.as.server.service.CookieService; -import io.jans.as.server.service.DeviceAuthorizationService; -import io.jans.as.server.service.RequestParameterService; -import io.jans.as.server.service.SessionIdService; -import io.jans.as.server.service.UserService; +import io.jans.as.server.service.*; import io.jans.as.server.service.ciba.CibaRequestService; import io.jans.as.server.service.external.ExternalPostAuthnService; import io.jans.as.server.service.external.ExternalUpdateTokenService; @@ -93,17 +66,12 @@ import org.slf4j.Logger; import java.net.URI; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import java.util.function.Function; import static io.jans.as.model.util.StringUtils.implode; -import static org.apache.commons.lang3.BooleanUtils.isTrue; +import static org.apache.commons.lang3.BooleanUtils.*; /** * Implementation for request authorization through REST web services. @@ -365,7 +333,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx authzRequestService.setDefaultAcrsIfNeeded(authzRequest, client); - checkScopes(responseTypes, prompts, client, scopes); + checkOfflineAccessScopes(responseTypes, prompts, client, scopes); checkResponseType(authzRequest, responseTypes, client); AuthorizationGrant authorizationGrant = null; @@ -688,17 +656,19 @@ private void validateMaxAge(AuthzRequest authzRequest, List prompts, Ses } } - private void checkScopes(List responseTypes, List prompts, Client client, Set scopes) { - if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !client.getTrustedClient()) { - if (!responseTypes.contains(ResponseType.CODE)) { - log.trace("Removed (ignored) offline_scope. Can't find `code` in response_type which is required."); - scopes.remove(ScopeConstants.OFFLINE_ACCESS); - } + public void checkOfflineAccessScopes(List responseTypes, List prompts, Client client, Set scopes) { + if (!scopes.contains(ScopeConstants.OFFLINE_ACCESS) || client.getTrustedClient()) { + return; + } - if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !prompts.contains(Prompt.CONSENT)) { - log.error("Removed offline_access. Can't find prompt=consent. Consent is required for offline_access."); - scopes.remove(ScopeConstants.OFFLINE_ACCESS); - } + if (!responseTypes.contains(ResponseType.CODE)) { + log.trace("Removed (ignored) offline_scope. Can't find `code` in response_type which is required."); + scopes.remove(ScopeConstants.OFFLINE_ACCESS); + } + + if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !prompts.contains(Prompt.CONSENT) && !toBoolean(client.getAttributes().getAllowOfflineAccessWithoutConsent())) { + log.error("Removed offline_access. Can't find prompt=consent. Consent is required for offline_access."); + scopes.remove(ScopeConstants.OFFLINE_ACCESS); } } diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImplTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImplTest.java new file mode 100644 index 00000000000..725b968bfbf --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImplTest.java @@ -0,0 +1,146 @@ +package io.jans.as.server.authorize.ws.rs; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.common.ResponseType; +import io.jans.as.model.common.ScopeConstants; +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.ciba.CIBAPingCallbackService; +import io.jans.as.server.ciba.CIBAPushTokenDeliveryService; +import io.jans.as.server.model.authorize.ScopeChecker; +import io.jans.as.server.model.common.AuthorizationGrantList; +import io.jans.as.server.model.config.ConfigurationFactory; +import io.jans.as.server.security.Identity; +import io.jans.as.server.service.*; +import io.jans.as.server.service.ciba.CibaRequestService; +import io.jans.as.server.service.external.ExternalPostAuthnService; +import io.jans.as.server.service.external.ExternalUpdateTokenService; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.slf4j.Logger; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class AuthorizeRestWebServiceImplTest { + + @InjectMocks + private AuthorizeRestWebServiceImpl authorizeRestWebService; + + @Mock + private Logger log; + + @Mock + private ApplicationAuditLogger applicationAuditLogger; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Mock + private AuthorizationGrantList authorizationGrantList; + + @Mock + private ClientService clientService; + + @Mock + private UserService userService; + + @Mock + private Identity identity; + + @Mock + private AuthenticationFilterService authenticationFilterService; + + @Mock + private SessionIdService sessionIdService; + + @Mock + private CookieService cookieService; + + @Mock + private ScopeChecker scopeChecker; + + @Mock + private ClientAuthorizationsService clientAuthorizationsService; + + @Mock + private RequestParameterService requestParameterService; + + @Mock + private AppConfiguration appConfiguration; + + @Mock + private ConfigurationFactory configurationFactory; + + @Mock + private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator; + + @Mock + private CIBAPushTokenDeliveryService cibaPushTokenDeliveryService; + + @Mock + private CIBAPingCallbackService cibaPingCallbackService; + + @Mock + private ExternalPostAuthnService externalPostAuthnService; + + @Mock + private CibaRequestService cibaRequestService; + + @Mock + private DeviceAuthorizationService deviceAuthorizationService; + + @Mock + private AttributeService attributeService; + + @Mock + private ExternalUpdateTokenService externalUpdateTokenService; + + @Mock + private AuthzRequestService authzRequestService; + + @Test + public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndConsentNot_shouldRemoveOfflineAccess() { + final Set scopes = Sets.newHashSet(ScopeConstants.OFFLINE_ACCESS); + authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.CODE), Lists.newArrayList(), new Client(), scopes); + assertTrue(scopes.isEmpty()); + } + + @Test + public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndConsentNotButAllowedByClient_shouldNotRemoveOfflineAccess() { + final Set scopes = Sets.newHashSet(ScopeConstants.OFFLINE_ACCESS); + final Client client = new Client(); + client.getAttributes().setAllowOfflineAccessWithoutConsent(true); + + authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.CODE), Lists.newArrayList(), client, scopes); + assertEquals(scopes.iterator().next(), ScopeConstants.OFFLINE_ACCESS); + } + + @Test + public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndResponseTypeCodeAbsent_shouldRemoveOfflineAccess() { + final Set scopes = Sets.newHashSet(ScopeConstants.OFFLINE_ACCESS); + + authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.TOKEN), Lists.newArrayList(), new Client(), scopes); + assertTrue(scopes.isEmpty()); + } + + @Test + public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndResponseTypeCodeAbsent_shouldRemoveOfflineAccessOnly() { + final Set scopes = Sets.newHashSet("openid", ScopeConstants.OFFLINE_ACCESS); + + authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.TOKEN), Lists.newArrayList(), new Client(), scopes); + assertEquals(scopes.iterator().next(), "openid"); + } +} diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index 976129a0509..328bf6abdf3 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -23,6 +23,7 @@ +