Skip to content

Commit c1f7893

Browse files
author
Steve Riesenberg
committed
Add consent AuthenticationProvider
Issue spring-projectsgh-44
1 parent 100b37b commit c1f7893

15 files changed

+978
-114
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationService.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ private static boolean hasToken(OAuth2Authorization authorization, String token,
159159
return matchesAccessToken(authorization, token);
160160
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
161161
return matchesRefreshToken(authorization, token);
162+
} else if (OAuth2DeviceCode.TOKEN_TYPE.equals(tokenType)) {
163+
return matchesDeviceCode(authorization, token);
164+
} else if (OAuth2UserCode.TOKEN_TYPE.equals(tokenType)) {
165+
return matchesUserCode(authorization, token);
162166
}
163167
return false;
164168
}
@@ -185,6 +189,18 @@ private static boolean matchesRefreshToken(OAuth2Authorization authorization, St
185189
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
186190
}
187191

192+
private static boolean matchesDeviceCode(OAuth2Authorization authorization, String token) {
193+
OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode =
194+
authorization.getToken(OAuth2DeviceCode.class);
195+
return deviceCode != null && deviceCode.getToken().getTokenValue().equals(token);
196+
}
197+
198+
private static boolean matchesUserCode(OAuth2Authorization authorization, String token) {
199+
OAuth2Authorization.Token<OAuth2UserCode> userCode =
200+
authorization.getToken(OAuth2UserCode.class);
201+
return userCode != null && userCode.getToken().getTokenValue().equals(token);
202+
}
203+
188204
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
189205
private final int maxSize;
190206

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2020-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import java.util.Collections;
19+
import java.util.HashSet;
20+
import java.util.Map;
21+
import java.util.Set;
22+
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.security.core.Authentication;
25+
26+
/**
27+
* An {@link Authentication} implementation for the Authorization Consent used
28+
* in the OAuth 2.0 Device Authorization Grant.
29+
*
30+
* @author Steve Riesenberg
31+
* @since 1.1
32+
*/
33+
public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2AuthorizationConsentAuthenticationToken {
34+
35+
private final String userCode;
36+
37+
private final Set<String> requestedScopes;
38+
39+
/**
40+
* Constructs an {@code OAuth2DeviceAuthorizationConsentAuthenticationToken} using the provided parameters.
41+
*
42+
* @param authorizationUri the authorization URI
43+
* @param clientId the client identifier
44+
* @param principal the {@code Principal} (Resource Owner)
45+
* @param userCode the user code associated with the device authorization request
46+
* @param state the state
47+
* @param requestedScopes the requested scope(s)
48+
* @param authorizedScopes the authorized scope(s)
49+
* @param additionalParameters the additional parameters
50+
*/
51+
public OAuth2DeviceAuthorizationConsentAuthenticationToken(String authorizationUri, String clientId,
52+
Authentication principal, String userCode, String state, @Nullable Set<String> requestedScopes,
53+
@Nullable Set<String> authorizedScopes, @Nullable Map<String, Object> additionalParameters) {
54+
super(authorizationUri, clientId, principal, state, authorizedScopes, additionalParameters);
55+
this.userCode = userCode;
56+
this.requestedScopes = Collections.unmodifiableSet(
57+
requestedScopes != null ?
58+
new HashSet<>(requestedScopes) :
59+
Collections.emptySet());
60+
}
61+
62+
public String getUserCode() {
63+
return this.userCode;
64+
}
65+
66+
public Set<String> getRequestedScopes() {
67+
return this.requestedScopes;
68+
}
69+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationProvider.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implem
6464

6565
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
6666

67-
private static final String DEFAULT_VERIFICATION_URI = "/activate";
67+
private static final String DEFAULT_VERIFICATION_URI = "/oauth2/device_verification"; // TODO
6868

6969
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
7070
new Base64StringKeyGenerator(Base64.getUrlEncoder());
@@ -118,7 +118,6 @@ public Authentication authenticate(Authentication authentication) throws Authent
118118
this.logger.trace("Validated device authorization request parameters");
119119
}
120120

121-
// TODO: Do we need to capture authorizationUri?
122121
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
123122
.authorizationUri(deviceAuthorizationRequestAuthentication.getAuthorizationUri())
124123
.clientId(registeredClient.getClientId())
@@ -187,7 +186,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
187186
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(issuerUri)
188187
.path(this.verificationUri);
189188
String verificationUri = uriComponentsBuilder.build().toUriString();
190-
String verificationUriComplete = uriComponentsBuilder.queryParam("code", userCode.getTokenValue()).build()
189+
String verificationUriComplete = uriComponentsBuilder.queryParam("user_code", userCode.getTokenValue()).build()
191190
.toUriString();
192191

193192
if (this.logger.isTraceEnabled()) {

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationToken.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.lang.NonNull;
2323
import org.springframework.security.authentication.AbstractAuthenticationToken;
2424
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
2526
import org.springframework.util.Assert;
2627
import org.springframework.util.CollectionUtils;
2728

@@ -34,6 +35,7 @@
3435
* @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
3536
*/
3637
public final class OAuth2DeviceAuthorizationRequestAuthenticationToken extends AbstractAuthenticationToken {
38+
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
3739
private String clientId;
3840
private Authentication principal;
3941
private String authorizationUri;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.server.authorization.authentication;
18+
19+
import java.util.Set;
20+
21+
import org.apache.commons.logging.Log;
22+
import org.apache.commons.logging.LogFactory;
23+
24+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
25+
import org.springframework.security.authentication.AuthenticationProvider;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.AuthenticationException;
28+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
29+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
30+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
31+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
32+
import org.springframework.security.oauth2.core.oidc.OidcScopes;
33+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
34+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
35+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
36+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
37+
import org.springframework.security.oauth2.server.authorization.OAuth2UserCode;
38+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
39+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
40+
import org.springframework.util.Assert;
41+
42+
/**
43+
* @author Steve Riesenberg
44+
* @since 1.1
45+
*/
46+
public final class OAuth2DeviceVerificationAuthenticationProvider implements AuthenticationProvider {
47+
48+
private final Log logger = LogFactory.getLog(getClass());
49+
private final RegisteredClientRepository registeredClientRepository;
50+
private final OAuth2AuthorizationService authorizationService;
51+
private final OAuth2AuthorizationConsentService authorizationConsentService;
52+
53+
public OAuth2DeviceVerificationAuthenticationProvider(
54+
RegisteredClientRepository registeredClientRepository,
55+
OAuth2AuthorizationService authorizationService,
56+
OAuth2AuthorizationConsentService authorizationConsentService) {
57+
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
58+
Assert.notNull(authorizationService, "authorizationService cannot be null");
59+
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
60+
this.registeredClientRepository = registeredClientRepository;
61+
this.authorizationService = authorizationService;
62+
this.authorizationConsentService = authorizationConsentService;
63+
}
64+
65+
@Override
66+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
67+
OAuth2DeviceVerificationAuthenticationToken deviceVerificationAuthentication =
68+
(OAuth2DeviceVerificationAuthenticationToken) authentication;
69+
70+
String userCode = deviceVerificationAuthentication.getUserCode();
71+
72+
OAuth2Authorization authorization = this.authorizationService.findByToken(
73+
userCode, OAuth2UserCode.TOKEN_TYPE);
74+
if (authorization == null) {
75+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
76+
}
77+
78+
if (this.logger.isTraceEnabled()) {
79+
this.logger.trace("Retrieved authorization with user code");
80+
}
81+
82+
RegisteredClient registeredClient = this.registeredClientRepository.findById(
83+
authorization.getRegisteredClientId());
84+
Assert.notNull(registeredClient, "registeredClient cannot be null");
85+
86+
if (this.logger.isTraceEnabled()) {
87+
this.logger.trace("Retrieved registered client");
88+
}
89+
90+
Authentication principal = (Authentication) deviceVerificationAuthentication.getPrincipal();
91+
if (!isPrincipalAuthenticated(principal)) {
92+
if (this.logger.isTraceEnabled()) {
93+
this.logger.trace("Did not authenticate device authorization request since principal not authenticated");
94+
}
95+
// Return the authorization request as-is where isAuthenticated() is false
96+
return deviceVerificationAuthentication;
97+
}
98+
99+
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
100+
Assert.notNull(authorizationRequest, "authorizationRequest cannot be null");
101+
102+
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
103+
registeredClient.getId(), principal.getName());
104+
105+
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
106+
currentAuthorizationConsent.getScopes() : null;
107+
108+
if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
109+
String state = authorization.getAttribute(OAuth2ParameterNames.STATE);
110+
111+
return new OAuth2DeviceAuthorizationConsentAuthenticationToken(
112+
authorizationRequest.getAuthorizationUri(), registeredClient.getClientId(), principal, userCode,
113+
state, authorizationRequest.getScopes(), currentAuthorizedScopes, null);
114+
}
115+
116+
return null;
117+
}
118+
119+
private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
120+
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
121+
122+
if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {
123+
return false;
124+
}
125+
// 'openid' scope does not require consent
126+
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
127+
authorizationRequest.getScopes().size() == 1) {
128+
return false;
129+
}
130+
131+
if (authorizationConsent != null &&
132+
authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
133+
return false;
134+
}
135+
136+
return true;
137+
}
138+
139+
private static boolean isPrincipalAuthenticated(Authentication principal) {
140+
return principal != null &&
141+
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
142+
principal.isAuthenticated();
143+
}
144+
145+
@Override
146+
public boolean supports(Class<?> authentication) {
147+
return OAuth2DeviceVerificationAuthenticationToken.class.isAssignableFrom(authentication);
148+
}
149+
150+
}

0 commit comments

Comments
 (0)