Skip to content

Commit 1354ca4

Browse files
committed
Polish gh-1106 Device Authorization Grant
1 parent 47ff4ad commit 1354ca4

File tree

38 files changed

+371
-308
lines changed

38 files changed

+371
-308
lines changed

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
* @since 0.1.1
3939
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
4040
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
41+
* @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4. Device Authorization Grant Metadata</a>
4142
*/
4243
public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
4344
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
@@ -96,6 +97,17 @@ public B authorizationEndpoint(String authorizationEndpoint) {
9697
return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
9798
}
9899

100+
/**
101+
* Use this {@code device_authorization_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
102+
*
103+
* @param deviceAuthorizationEndpoint the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
104+
* @return the {@link AbstractBuilder} for further configuration
105+
* @since 1.1
106+
*/
107+
public B deviceAuthorizationEndpoint(String deviceAuthorizationEndpoint) {
108+
return claim(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT, deviceAuthorizationEndpoint);
109+
}
110+
99111
/**
100112
* Use this {@code token_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
101113
*
@@ -346,6 +358,9 @@ protected void validate() {
346358
validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL");
347359
Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
348360
validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
361+
if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT) != null) {
362+
validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT), "deviceAuthorizationEndpoint must be a valid URL");
363+
}
349364
Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
350365
validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
351366
if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ private static boolean hasToken(OAuth2Authorization authorization, String token,
155155
matchesAuthorizationCode(authorization, token) ||
156156
matchesAccessToken(authorization, token) ||
157157
matchesIdToken(authorization, token) ||
158-
matchesRefreshToken(authorization, token);
158+
matchesRefreshToken(authorization, token) ||
159+
matchesDeviceCode(authorization, token) ||
160+
matchesUserCode(authorization, token);
159161
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
160162
return matchesState(authorization, token);
161163
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -253,18 +253,6 @@ public static class Token<T extends OAuth2Token> implements Serializable {
253253
*/
254254
public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("invalidated");
255255

256-
/**
257-
* The name of the metadata that indicates if access has been denied by the resource owner.
258-
* Used with the OAuth 2.0 Device Authorization Grant.
259-
*/
260-
public static final String ACCESS_DENIED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("access_denied");
261-
262-
/**
263-
* The name of the metadata that indicates if access has been denied by the resource owner.
264-
* Used with the OAuth 2.0 Device Authorization Grant.
265-
*/
266-
public static final String ACCESS_GRANTED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("access_granted");
267-
268256
/**
269257
* The name of the metadata used for the claims of the token.
270258
*/

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
* @see OAuth2AuthorizationServerMetadataClaimNames
3131
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
3232
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
33+
* @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4. Device Authorization Grant Metadata</a>
3334
*/
3435
public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor {
3536

@@ -51,6 +52,16 @@ default URL getAuthorizationEndpoint() {
5152
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT);
5253
}
5354

55+
/**
56+
* Returns the {@code URL} of the OAuth 2.0 Device Authorization Endpoint {@code (device_authorization_endpoint)}.
57+
*
58+
* @return the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
59+
* @since 1.1
60+
*/
61+
default URL getDeviceAuthorizationEndpoint() {
62+
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT);
63+
}
64+
5465
/**
5566
* Returns the {@code URL} of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}.
5667
*

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
* @since 0.1.1
2424
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
2525
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
26+
* @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4. Device Authorization Grant Metadata</a>
2627
*/
2728
public class OAuth2AuthorizationServerMetadataClaimNames {
2829

@@ -36,6 +37,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
3637
*/
3738
public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
3839

40+
/**
41+
* {@code device_authorization_endpoint} - the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
42+
* @since 1.1
43+
*/
44+
public static final String DEVICE_AUTHORIZATION_ENDPOINT = "device_authorization_endpoint";
45+
3946
/**
4047
* {@code token_endpoint} - the {@code URL} of the OAuth 2.0 Token Endpoint
4148
*/

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -113,10 +113,6 @@ private Builder(OAuth2AuthorizationConsentAuthenticationToken authentication) {
113113
super(authentication);
114114
}
115115

116-
private Builder(OAuth2DeviceAuthorizationConsentAuthenticationToken authentication) {
117-
super(authentication);
118-
}
119-
120116
/**
121117
* Sets the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
122118
*

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ public OAuth2AuthorizationConsentAuthenticationProvider(RegisteredClientReposito
9191

9292
@Override
9393
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
94+
if (authentication instanceof OAuth2DeviceAuthorizationConsentAuthenticationToken) {
95+
// This is NOT an OAuth 2.0 Authorization Consent for the Authorization Code Grant,
96+
// return null and let OAuth2DeviceAuthorizationConsentAuthenticationProvider handle it instead
97+
return null;
98+
}
99+
94100
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
95101
(OAuth2AuthorizationConsentAuthenticationToken) authentication;
96102

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

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.authentication;
1717

18-
import java.security.Principal;
1918
import java.util.Collections;
2019
import java.util.HashSet;
2120
import java.util.Set;
@@ -24,6 +23,7 @@
2423
import org.apache.commons.logging.Log;
2524
import org.apache.commons.logging.LogFactory;
2625

26+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
2727
import org.springframework.security.authentication.AuthenticationProvider;
2828
import org.springframework.security.core.Authentication;
2929
import org.springframework.security.core.AuthenticationException;
@@ -33,7 +33,6 @@
3333
import org.springframework.security.oauth2.core.OAuth2Error;
3434
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
3535
import org.springframework.security.oauth2.core.OAuth2UserCode;
36-
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
3736
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
3837
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
3938
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
@@ -45,8 +44,8 @@
4544
import org.springframework.util.Assert;
4645

4746
/**
48-
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Consent
49-
* used in the Device Authorization Grant.
47+
* An {@link AuthenticationProvider} implementation for the Device Authorization Consent
48+
* used in the OAuth 2.0 Device Authorization Grant.
5049
*
5150
* @author Steve Riesenberg
5251
* @since 1.1
@@ -61,7 +60,7 @@
6160
*/
6261
public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
6362

64-
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
63+
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
6564
static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
6665

6766
private final Log logger = LogFactory.getLog(getClass());
@@ -104,7 +103,11 @@ public Authentication authenticate(Authentication authentication) throws Authent
104103
this.logger.trace("Retrieved authorization with device authorization consent state");
105104
}
106105

106+
// The authorization must be associated to the current principal
107107
Authentication principal = (Authentication) deviceAuthorizationConsentAuthentication.getPrincipal();
108+
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
109+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
110+
}
108111

109112
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
110113
deviceAuthorizationConsentAuthentication.getClientId());
@@ -116,12 +119,8 @@ public Authentication authenticate(Authentication authentication) throws Authent
116119
this.logger.trace("Retrieved registered client");
117120
}
118121

119-
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
120-
OAuth2AuthorizationRequest.class.getName());
121-
Set<String> requestedScopes = authorizationRequest.getScopes();
122-
Set<String> authorizedScopes = deviceAuthorizationConsentAuthentication.getScopes() != null ?
123-
new HashSet<>(deviceAuthorizationConsentAuthentication.getScopes()) :
124-
new HashSet<>();
122+
Set<String> requestedScopes = authorization.getAttribute(OAuth2ParameterNames.SCOPE);
123+
Set<String> authorizedScopes = new HashSet<>(deviceAuthorizationConsentAuthentication.getScopes());
125124
if (!requestedScopes.containsAll(authorizedScopes)) {
126125
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE);
127126
}
@@ -162,7 +161,6 @@ public Authentication authenticate(Authentication authentication) throws Authent
162161
.authorizationConsent(authorizationConsentBuilder)
163162
.registeredClient(registeredClient)
164163
.authorization(authorization)
165-
.authorizationRequest(authorizationRequest)
166164
.build();
167165
// @formatter:on
168166
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
@@ -187,15 +185,16 @@ public Authentication authenticate(Authentication authentication) throws Authent
187185
}
188186
authorization = OAuth2Authorization.from(authorization)
189187
.token(deviceCodeToken.getToken(), metadata ->
190-
metadata.put(OAuth2Authorization.Token.ACCESS_DENIED_METADATA_NAME, true))
188+
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
191189
.token(userCodeToken.getToken(), metadata ->
192190
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
191+
.attributes(attrs -> attrs.remove(OAuth2ParameterNames.STATE))
193192
.build();
194193
this.authorizationService.save(authorization);
195194
if (this.logger.isTraceEnabled()) {
196195
this.logger.trace("Invalidated device code and user code because authorization consent was denied");
197196
}
198-
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.ACCESS_DENIED);
197+
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID);
199198
}
200199

201200
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
@@ -206,26 +205,23 @@ public Authentication authenticate(Authentication authentication) throws Authent
206205
}
207206
}
208207

209-
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
210-
.principalName(principal.getName())
208+
authorization = OAuth2Authorization.from(authorization)
211209
.authorizedScopes(authorizedScopes)
212-
.token(deviceCodeToken.getToken(), metadata -> metadata
213-
.put(OAuth2Authorization.Token.ACCESS_GRANTED_METADATA_NAME, true))
214-
.token(userCodeToken.getToken(), metadata -> metadata
215-
.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
216-
.attribute(Principal.class.getName(), principal)
210+
.token(userCodeToken.getToken(), metadata ->
211+
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
217212
.attributes(attrs -> attrs.remove(OAuth2ParameterNames.STATE))
213+
.attributes(attrs -> attrs.remove(OAuth2ParameterNames.SCOPE))
218214
.build();
219-
this.authorizationService.save(updatedAuthorization);
215+
this.authorizationService.save(authorization);
220216

221217
if (this.logger.isTraceEnabled()) {
222218
this.logger.trace("Saved authorization with authorized scopes");
223219
// This log is kept separate for consistency with other providers
224-
this.logger.trace("Authenticated authorization consent request");
220+
this.logger.trace("Authenticated device authorization consent request");
225221
}
226222

227-
return new OAuth2DeviceVerificationAuthenticationToken(registeredClient.getClientId(), principal,
228-
deviceAuthorizationConsentAuthentication.getUserCode());
223+
return new OAuth2DeviceVerificationAuthenticationToken(principal,
224+
deviceAuthorizationConsentAuthentication.getUserCode(), registeredClient.getClientId());
229225
}
230226

231227
@Override
@@ -244,10 +240,9 @@ public boolean supports(Class<?> authentication) {
244240
* prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
245241
* <li>The {@link Authentication} of type
246242
* {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}.</li>
247-
* <li>The {@link RegisteredClient} associated with the authorization request.</li>
243+
* <li>The {@link RegisteredClient} associated with the device authorization request.</li>
248244
* <li>The {@link OAuth2Authorization} associated with the state token presented in the
249-
* authorization consent request.</li>
250-
* <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
245+
* device authorization consent request.</li>
251246
* </ul>
252247
*
253248
* @param authorizationConsentCustomizer the {@code Consumer} providing access to the
@@ -258,8 +253,14 @@ public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsen
258253
this.authorizationConsentCustomizer = authorizationConsentCustomizer;
259254
}
260255

256+
private static boolean isPrincipalAuthenticated(Authentication principal) {
257+
return principal != null &&
258+
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
259+
principal.isAuthenticated();
260+
}
261+
261262
private static void throwError(String errorCode, String parameterName) {
262-
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
263+
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
263264
throw new OAuth2AuthenticationException(error);
264265
}
265266

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@
2121
import java.util.Set;
2222

2323
import org.springframework.lang.Nullable;
24+
import org.springframework.security.authentication.AbstractAuthenticationToken;
2425
import org.springframework.security.core.Authentication;
2526
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
2627
import org.springframework.util.Assert;
2728

2829
/**
29-
* An {@link Authentication} implementation for the Authorization Consent used
30+
* An {@link Authentication} implementation for the Device Authorization Consent used
3031
* in the OAuth 2.0 Device Authorization Grant.
3132
*
3233
* @author Steve Riesenberg
3334
* @since 1.1
35+
* @see AbstractAuthenticationToken
36+
* @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
3437
*/
3538
public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2AuthorizationConsentAuthenticationToken {
3639
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
@@ -43,7 +46,7 @@ public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2A
4346
* @param authorizationUri the authorization URI
4447
* @param clientId the client identifier
4548
* @param principal the {@code Principal} (Resource Owner)
46-
* @param userCode the user code associated with the device authorization request
49+
* @param userCode the user code associated with the device authorization response
4750
* @param state the state
4851
* @param authorizedScopes the authorized scope(s)
4952
* @param additionalParameters the additional parameters
@@ -64,7 +67,7 @@ public OAuth2DeviceAuthorizationConsentAuthenticationToken(String authorizationU
6467
* @param authorizationUri the authorization URI
6568
* @param clientId the client identifier
6669
* @param principal the {@code Principal} (Resource Owner)
67-
* @param userCode the user code associated with the device authorization request
70+
* @param userCode the user code associated with the device authorization response
6871
* @param state the state
6972
* @param requestedScopes the requested scope(s)
7073
* @param authorizedScopes the authorized scope(s)

0 commit comments

Comments
 (0)