Skip to content

Commit af30e4c

Browse files
authored
feat(jans-auth-server): check offline_access implementation has all conditions defined in spec #1945 (#3004)
1 parent d6a5e99 commit af30e4c

File tree

4 files changed

+179
-50
lines changed

4 files changed

+179
-50
lines changed

jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java

+12
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ public class ClientAttributes implements Serializable {
104104
@JsonProperty("idTokenLifetime")
105105
private Integer idTokenLifetime;
106106

107+
@JsonProperty("allowOfflineAccessWithoutConsent")
108+
private Boolean allowOfflineAccessWithoutConsent;
109+
110+
public Boolean getAllowOfflineAccessWithoutConsent() {
111+
return allowOfflineAccessWithoutConsent;
112+
}
113+
114+
public void setAllowOfflineAccessWithoutConsent(Boolean allowOfflineAccessWithoutConsent) {
115+
this.allowOfflineAccessWithoutConsent = allowOfflineAccessWithoutConsent;
116+
}
117+
107118
public Integer getIdTokenLifetime() {
108119
return idTokenLifetime;
109120
}
@@ -366,6 +377,7 @@ public String toString() {
366377
", authorizationEncryptedResponseEnc=" + authorizationEncryptedResponseEnc +
367378
", publicSubjectIdentifierAttribute=" + publicSubjectIdentifierAttribute +
368379
", redirectUrisRegex=" + redirectUrisRegex +
380+
", allowOfflineAccessWithoutConsent=" + allowOfflineAccessWithoutConsent +
369381
", defaultPromptLogin=" + defaultPromptLogin +
370382
'}';
371383
}

jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java

+20-50
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,13 @@
99
import com.google.common.collect.Maps;
1010
import io.jans.as.common.model.common.User;
1111
import io.jans.as.common.model.registration.Client;
12+
import io.jans.as.common.model.session.SessionId;
13+
import io.jans.as.common.model.session.SessionIdState;
1214
import io.jans.as.common.util.RedirectUri;
1315
import io.jans.as.model.authorize.AuthorizeErrorResponseType;
1416
import io.jans.as.model.authorize.AuthorizeRequestParam;
1517
import io.jans.as.model.authorize.AuthorizeResponseParam;
16-
import io.jans.as.model.common.BackchannelTokenDeliveryMode;
17-
import io.jans.as.model.common.GrantType;
18-
import io.jans.as.model.common.Prompt;
19-
import io.jans.as.model.common.ResponseMode;
20-
import io.jans.as.model.common.ResponseType;
21-
import io.jans.as.model.common.ScopeConstants;
22-
import io.jans.as.model.common.SubjectType;
18+
import io.jans.as.model.common.*;
2319
import io.jans.as.model.configuration.AppConfiguration;
2420
import io.jans.as.model.crypto.binding.TokenBindingMessage;
2521
import io.jans.as.model.crypto.binding.TokenBindingParseException;
@@ -32,22 +28,7 @@
3228
import io.jans.as.server.ciba.CIBAPushTokenDeliveryService;
3329
import io.jans.as.server.model.authorize.AuthorizeParamsValidator;
3430
import io.jans.as.server.model.authorize.ScopeChecker;
35-
import io.jans.as.server.model.common.AccessToken;
36-
import io.jans.as.server.model.common.AuthorizationCode;
37-
import io.jans.as.server.model.common.AuthorizationGrant;
38-
import io.jans.as.server.model.common.AuthorizationGrantList;
39-
import io.jans.as.server.model.common.CIBAGrant;
40-
import io.jans.as.server.model.common.CibaRequestCacheControl;
41-
import io.jans.as.server.model.common.CibaRequestStatus;
42-
import io.jans.as.server.model.common.DefaultScope;
43-
import io.jans.as.server.model.common.DeviceAuthorizationCacheControl;
44-
import io.jans.as.server.model.common.DeviceAuthorizationStatus;
45-
import io.jans.as.server.model.common.DeviceCodeGrant;
46-
import io.jans.as.server.model.common.ExecutionContext;
47-
import io.jans.as.server.model.common.IdToken;
48-
import io.jans.as.server.model.common.RefreshToken;
49-
import io.jans.as.common.model.session.SessionId;
50-
import io.jans.as.common.model.session.SessionIdState;
31+
import io.jans.as.server.model.common.*;
5132
import io.jans.as.server.model.config.ConfigurationFactory;
5233
import io.jans.as.server.model.config.Constants;
5334
import io.jans.as.server.model.exception.AcrChangedException;
@@ -56,15 +37,7 @@
5637
import io.jans.as.server.model.ldap.ClientAuthorization;
5738
import io.jans.as.server.model.token.JwrService;
5839
import io.jans.as.server.security.Identity;
59-
import io.jans.as.server.service.AttributeService;
60-
import io.jans.as.server.service.AuthenticationFilterService;
61-
import io.jans.as.server.service.ClientAuthorizationsService;
62-
import io.jans.as.server.service.ClientService;
63-
import io.jans.as.server.service.CookieService;
64-
import io.jans.as.server.service.DeviceAuthorizationService;
65-
import io.jans.as.server.service.RequestParameterService;
66-
import io.jans.as.server.service.SessionIdService;
67-
import io.jans.as.server.service.UserService;
40+
import io.jans.as.server.service.*;
6841
import io.jans.as.server.service.ciba.CibaRequestService;
6942
import io.jans.as.server.service.external.ExternalPostAuthnService;
7043
import io.jans.as.server.service.external.ExternalUpdateTokenService;
@@ -93,17 +66,12 @@
9366
import org.slf4j.Logger;
9467

9568
import java.net.URI;
96-
import java.util.Arrays;
97-
import java.util.Date;
98-
import java.util.HashMap;
99-
import java.util.List;
100-
import java.util.Map;
69+
import java.util.*;
10170
import java.util.Map.Entry;
102-
import java.util.Set;
10371
import java.util.function.Function;
10472

10573
import static io.jans.as.model.util.StringUtils.implode;
106-
import static org.apache.commons.lang3.BooleanUtils.isTrue;
74+
import static org.apache.commons.lang3.BooleanUtils.*;
10775

10876
/**
10977
* Implementation for request authorization through REST web services.
@@ -365,7 +333,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx
365333

366334
authzRequestService.setDefaultAcrsIfNeeded(authzRequest, client);
367335

368-
checkScopes(responseTypes, prompts, client, scopes);
336+
checkOfflineAccessScopes(responseTypes, prompts, client, scopes);
369337
checkResponseType(authzRequest, responseTypes, client);
370338

371339
AuthorizationGrant authorizationGrant = null;
@@ -688,17 +656,19 @@ private void validateMaxAge(AuthzRequest authzRequest, List<Prompt> prompts, Ses
688656
}
689657
}
690658

691-
private void checkScopes(List<ResponseType> responseTypes, List<Prompt> prompts, Client client, Set<String> scopes) {
692-
if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !client.getTrustedClient()) {
693-
if (!responseTypes.contains(ResponseType.CODE)) {
694-
log.trace("Removed (ignored) offline_scope. Can't find `code` in response_type which is required.");
695-
scopes.remove(ScopeConstants.OFFLINE_ACCESS);
696-
}
659+
public void checkOfflineAccessScopes(List<ResponseType> responseTypes, List<Prompt> prompts, Client client, Set<String> scopes) {
660+
if (!scopes.contains(ScopeConstants.OFFLINE_ACCESS) || client.getTrustedClient()) {
661+
return;
662+
}
697663

698-
if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !prompts.contains(Prompt.CONSENT)) {
699-
log.error("Removed offline_access. Can't find prompt=consent. Consent is required for offline_access.");
700-
scopes.remove(ScopeConstants.OFFLINE_ACCESS);
701-
}
664+
if (!responseTypes.contains(ResponseType.CODE)) {
665+
log.trace("Removed (ignored) offline_scope. Can't find `code` in response_type which is required.");
666+
scopes.remove(ScopeConstants.OFFLINE_ACCESS);
667+
}
668+
669+
if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !prompts.contains(Prompt.CONSENT) && !toBoolean(client.getAttributes().getAllowOfflineAccessWithoutConsent())) {
670+
log.error("Removed offline_access. Can't find prompt=consent. Consent is required for offline_access.");
671+
scopes.remove(ScopeConstants.OFFLINE_ACCESS);
702672
}
703673
}
704674

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package io.jans.as.server.authorize.ws.rs;
2+
3+
import com.google.common.collect.Lists;
4+
import com.google.common.collect.Sets;
5+
import io.jans.as.common.model.registration.Client;
6+
import io.jans.as.model.common.ResponseType;
7+
import io.jans.as.model.common.ScopeConstants;
8+
import io.jans.as.model.configuration.AppConfiguration;
9+
import io.jans.as.model.error.ErrorResponseFactory;
10+
import io.jans.as.server.audit.ApplicationAuditLogger;
11+
import io.jans.as.server.ciba.CIBAPingCallbackService;
12+
import io.jans.as.server.ciba.CIBAPushTokenDeliveryService;
13+
import io.jans.as.server.model.authorize.ScopeChecker;
14+
import io.jans.as.server.model.common.AuthorizationGrantList;
15+
import io.jans.as.server.model.config.ConfigurationFactory;
16+
import io.jans.as.server.security.Identity;
17+
import io.jans.as.server.service.*;
18+
import io.jans.as.server.service.ciba.CibaRequestService;
19+
import io.jans.as.server.service.external.ExternalPostAuthnService;
20+
import io.jans.as.server.service.external.ExternalUpdateTokenService;
21+
import org.mockito.InjectMocks;
22+
import org.mockito.Mock;
23+
import org.mockito.testng.MockitoTestNGListener;
24+
import org.slf4j.Logger;
25+
import org.testng.annotations.Listeners;
26+
import org.testng.annotations.Test;
27+
28+
import java.util.Set;
29+
30+
import static org.testng.Assert.assertEquals;
31+
import static org.testng.Assert.assertTrue;
32+
33+
/**
34+
* @author Yuriy Z
35+
*/
36+
@Listeners(MockitoTestNGListener.class)
37+
public class AuthorizeRestWebServiceImplTest {
38+
39+
@InjectMocks
40+
private AuthorizeRestWebServiceImpl authorizeRestWebService;
41+
42+
@Mock
43+
private Logger log;
44+
45+
@Mock
46+
private ApplicationAuditLogger applicationAuditLogger;
47+
48+
@Mock
49+
private ErrorResponseFactory errorResponseFactory;
50+
51+
@Mock
52+
private AuthorizationGrantList authorizationGrantList;
53+
54+
@Mock
55+
private ClientService clientService;
56+
57+
@Mock
58+
private UserService userService;
59+
60+
@Mock
61+
private Identity identity;
62+
63+
@Mock
64+
private AuthenticationFilterService authenticationFilterService;
65+
66+
@Mock
67+
private SessionIdService sessionIdService;
68+
69+
@Mock
70+
private CookieService cookieService;
71+
72+
@Mock
73+
private ScopeChecker scopeChecker;
74+
75+
@Mock
76+
private ClientAuthorizationsService clientAuthorizationsService;
77+
78+
@Mock
79+
private RequestParameterService requestParameterService;
80+
81+
@Mock
82+
private AppConfiguration appConfiguration;
83+
84+
@Mock
85+
private ConfigurationFactory configurationFactory;
86+
87+
@Mock
88+
private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator;
89+
90+
@Mock
91+
private CIBAPushTokenDeliveryService cibaPushTokenDeliveryService;
92+
93+
@Mock
94+
private CIBAPingCallbackService cibaPingCallbackService;
95+
96+
@Mock
97+
private ExternalPostAuthnService externalPostAuthnService;
98+
99+
@Mock
100+
private CibaRequestService cibaRequestService;
101+
102+
@Mock
103+
private DeviceAuthorizationService deviceAuthorizationService;
104+
105+
@Mock
106+
private AttributeService attributeService;
107+
108+
@Mock
109+
private ExternalUpdateTokenService externalUpdateTokenService;
110+
111+
@Mock
112+
private AuthzRequestService authzRequestService;
113+
114+
@Test
115+
public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndConsentNot_shouldRemoveOfflineAccess() {
116+
final Set<String> scopes = Sets.newHashSet(ScopeConstants.OFFLINE_ACCESS);
117+
authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.CODE), Lists.newArrayList(), new Client(), scopes);
118+
assertTrue(scopes.isEmpty());
119+
}
120+
121+
@Test
122+
public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndConsentNotButAllowedByClient_shouldNotRemoveOfflineAccess() {
123+
final Set<String> scopes = Sets.newHashSet(ScopeConstants.OFFLINE_ACCESS);
124+
final Client client = new Client();
125+
client.getAttributes().setAllowOfflineAccessWithoutConsent(true);
126+
127+
authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.CODE), Lists.newArrayList(), client, scopes);
128+
assertEquals(scopes.iterator().next(), ScopeConstants.OFFLINE_ACCESS);
129+
}
130+
131+
@Test
132+
public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndResponseTypeCodeAbsent_shouldRemoveOfflineAccess() {
133+
final Set<String> scopes = Sets.newHashSet(ScopeConstants.OFFLINE_ACCESS);
134+
135+
authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.TOKEN), Lists.newArrayList(), new Client(), scopes);
136+
assertTrue(scopes.isEmpty());
137+
}
138+
139+
@Test
140+
public void checkOfflineAccessScopes_whenOfflineAccessIsPresentAndResponseTypeCodeAbsent_shouldRemoveOfflineAccessOnly() {
141+
final Set<String> scopes = Sets.newHashSet("openid", ScopeConstants.OFFLINE_ACCESS);
142+
143+
authorizeRestWebService.checkOfflineAccessScopes(Lists.newArrayList(ResponseType.TOKEN), Lists.newArrayList(), new Client(), scopes);
144+
assertEquals(scopes.iterator().next(), "openid");
145+
}
146+
}

jans-auth-server/server/src/test/resources/testng.xml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<class name="io.jans.as.server.token.ws.rs.TokenRestWebServiceValidatorTest" />
2424
<class name="io.jans.as.server.ws.rs.stat.MonthsTest" />
2525
<class name="io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceValidatorTest" />
26+
<class name="io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceImplTest" />
2627
<class name="io.jans.as.server.session.ws.rs.EndSessionRestWebServiceImplTest" />
2728
</classes>
2829
</test>

0 commit comments

Comments
 (0)