Эх сурвалжийг харах

Polish gh-1106 Device Authorization Grant

Joe Grandja 2 жил өмнө
parent
commit
1354ca4549
38 өөрчлөгдсөн 371 нэмэгдсэн , 308 устгасан
  1. 16 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java
  2. 3 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationService.java
  3. 1 13
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java
  4. 12 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java
  5. 8 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java
  6. 1 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java
  7. 6 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java
  8. 30 29
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java
  9. 6 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationToken.java
  10. 16 18
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationProvider.java
  11. 11 6
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationToken.java
  12. 45 41
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java
  13. 4 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationToken.java
  14. 39 39
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java
  15. 25 21
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationToken.java
  16. 4 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java
  17. 15 12
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java
  18. 25 23
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java
  19. 2 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java
  20. 2 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  21. 7 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java
  22. 3 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java
  23. 3 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java
  24. 29 33
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java
  25. 11 11
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceVerificationEndpointFilter.java
  26. 2 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java
  27. 22 19
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceAuthorizationConsentAuthenticationConverter.java
  28. 5 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceCodeAuthenticationConverter.java
  29. 3 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceVerificationAuthenticationConverter.java
  30. 4 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java
  31. 1 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java
  32. 1 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettingsTests.java
  33. 1 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java
  34. 1 1
      samples/device-client/src/main/java/sample/config/SecurityConfig.java
  35. 0 2
      samples/device-grant-authorizationserver/samples-device-grant-authorizationserver.gradle
  36. 4 4
      samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java
  37. 1 1
      samples/device-grant-authorizationserver/src/main/java/sample/web/DeviceController.java
  38. 2 2
      samples/device-grant-authorizationserver/src/main/resources/templates/activate.html

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@ import org.springframework.util.Assert;
  * @since 0.1.1
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
  * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
+ * @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4. Device Authorization Grant Metadata</a>
  */
 public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
 	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
@@ -96,6 +97,17 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
 			return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
 		}
 
+		/**
+		 * Use this {@code device_authorization_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param deviceAuthorizationEndpoint the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 1.1
+		 */
+		public B deviceAuthorizationEndpoint(String deviceAuthorizationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT, deviceAuthorizationEndpoint);
+		}
+
 		/**
 		 * Use this {@code token_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
 		 *
@@ -346,6 +358,9 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
 			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL");
 			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
 			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT), "deviceAuthorizationEndpoint must be a valid URL");
+			}
 			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
 			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
 			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {

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

@@ -155,7 +155,9 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
 					matchesAuthorizationCode(authorization, token) ||
 					matchesAccessToken(authorization, token) ||
 					matchesIdToken(authorization, token) ||
-					matchesRefreshToken(authorization, token);
+					matchesRefreshToken(authorization, token) ||
+					matchesDeviceCode(authorization, token) ||
+					matchesUserCode(authorization, token);
 		} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
 			return matchesState(authorization, token);
 		} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -253,18 +253,6 @@ public class OAuth2Authorization implements Serializable {
 		 */
 		public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("invalidated");
 
-		/**
-		 * The name of the metadata that indicates if access has been denied by the resource owner.
-		 * Used with the OAuth 2.0 Device Authorization Grant.
-		 */
-		public static final String ACCESS_DENIED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("access_denied");
-
-		/**
-		 * The name of the metadata that indicates if access has been denied by the resource owner.
-		 * Used with the OAuth 2.0 Device Authorization Grant.
-		 */
-		public static final String ACCESS_GRANTED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("access_granted");
-
 		/**
 		 * The name of the metadata used for the claims of the token.
 		 */

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.ClaimAccessor;
  * @see OAuth2AuthorizationServerMetadataClaimNames
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
  * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
+ * @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4. Device Authorization Grant Metadata</a>
  */
 public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor {
 
@@ -51,6 +52,16 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
 		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT);
 	}
 
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Device Authorization Endpoint {@code (device_authorization_endpoint)}.
+	 *
+	 * @return the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
+	 * @since 1.1
+	 */
+	default URL getDeviceAuthorizationEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT);
+	}
+
 	/**
 	 * Returns the {@code URL} of the OAuth 2.0 Token Endpoint {@code (token_endpoint)}.
 	 *

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ package org.springframework.security.oauth2.server.authorization;
  * @since 0.1.1
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
  * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
+ * @see <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8628.html#section-4">4. Device Authorization Grant Metadata</a>
  */
 public class OAuth2AuthorizationServerMetadataClaimNames {
 
@@ -36,6 +37,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
 	 */
 	public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
 
+	/**
+	 * {@code device_authorization_endpoint} - the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
+	 * @since 1.1
+	 */
+	public static final String DEVICE_AUTHORIZATION_ENDPOINT = "device_authorization_endpoint";
+
 	/**
 	 * {@code token_endpoint} - the {@code URL} of the OAuth 2.0 Token Endpoint
 	 */

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -113,10 +113,6 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA
 			super(authentication);
 		}
 
-		private Builder(OAuth2DeviceAuthorizationConsentAuthenticationToken authentication) {
-			super(authentication);
-		}
-
 		/**
 		 * Sets the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
 		 *

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

@@ -91,6 +91,12 @@ public final class OAuth2AuthorizationConsentAuthenticationProvider implements A
 
 	@Override
 	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		if (authentication instanceof OAuth2DeviceAuthorizationConsentAuthenticationToken) {
+			// This is NOT an OAuth 2.0 Authorization Consent for the Authorization Code Grant,
+			// return null and let OAuth2DeviceAuthorizationConsentAuthenticationProvider handle it instead
+			return null;
+		}
+
 		OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
 				(OAuth2AuthorizationConsentAuthenticationToken) authentication;
 

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

@@ -15,7 +15,6 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import java.security.Principal;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -24,6 +23,7 @@ import java.util.function.Consumer;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
@@ -33,7 +33,6 @@ import org.springframework.security.oauth2.core.OAuth2DeviceCode;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2UserCode;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
@@ -45,8 +44,8 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.util.Assert;
 
 /**
- * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Consent
- * used in the Device Authorization Grant.
+ * An {@link AuthenticationProvider} implementation for the Device Authorization Consent
+ * used in the OAuth 2.0 Device Authorization Grant.
  *
  * @author Steve Riesenberg
  * @since 1.1
@@ -61,7 +60,7 @@ import org.springframework.util.Assert;
  */
 public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
 
-	private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
 	static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
 
 	private final Log logger = LogFactory.getLog(getClass());
@@ -104,7 +103,11 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implem
 			this.logger.trace("Retrieved authorization with device authorization consent state");
 		}
 
+		// The authorization must be associated to the current principal
 		Authentication principal = (Authentication) deviceAuthorizationConsentAuthentication.getPrincipal();
+		if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
+		}
 
 		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
 				deviceAuthorizationConsentAuthentication.getClientId());
@@ -116,12 +119,8 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implem
 			this.logger.trace("Retrieved registered client");
 		}
 
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationRequest.class.getName());
-		Set<String> requestedScopes = authorizationRequest.getScopes();
-		Set<String> authorizedScopes = deviceAuthorizationConsentAuthentication.getScopes() != null ?
-				new HashSet<>(deviceAuthorizationConsentAuthentication.getScopes()) :
-				new HashSet<>();
+		Set<String> requestedScopes = authorization.getAttribute(OAuth2ParameterNames.SCOPE);
+		Set<String> authorizedScopes = new HashSet<>(deviceAuthorizationConsentAuthentication.getScopes());
 		if (!requestedScopes.containsAll(authorizedScopes)) {
 			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE);
 		}
@@ -162,7 +161,6 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implem
 							.authorizationConsent(authorizationConsentBuilder)
 							.registeredClient(registeredClient)
 							.authorization(authorization)
-							.authorizationRequest(authorizationRequest)
 							.build();
 			// @formatter:on
 			this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
@@ -187,15 +185,16 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implem
 			}
 			authorization = OAuth2Authorization.from(authorization)
 					.token(deviceCodeToken.getToken(), metadata ->
-							metadata.put(OAuth2Authorization.Token.ACCESS_DENIED_METADATA_NAME, true))
+							metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
 					.token(userCodeToken.getToken(), metadata ->
 							metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
+					.attributes(attrs -> attrs.remove(OAuth2ParameterNames.STATE))
 					.build();
 			this.authorizationService.save(authorization);
 			if (this.logger.isTraceEnabled()) {
 				this.logger.trace("Invalidated device code and user code because authorization consent was denied");
 			}
-			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.ACCESS_DENIED);
+			throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID);
 		}
 
 		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
@@ -206,26 +205,23 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implem
 			}
 		}
 
-		OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
-				.principalName(principal.getName())
+		authorization = OAuth2Authorization.from(authorization)
 				.authorizedScopes(authorizedScopes)
-				.token(deviceCodeToken.getToken(), metadata -> metadata
-						.put(OAuth2Authorization.Token.ACCESS_GRANTED_METADATA_NAME, true))
-				.token(userCodeToken.getToken(), metadata -> metadata
-						.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
-				.attribute(Principal.class.getName(), principal)
+				.token(userCodeToken.getToken(), metadata ->
+						metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
 				.attributes(attrs -> attrs.remove(OAuth2ParameterNames.STATE))
+				.attributes(attrs -> attrs.remove(OAuth2ParameterNames.SCOPE))
 				.build();
-		this.authorizationService.save(updatedAuthorization);
+		this.authorizationService.save(authorization);
 
 		if (this.logger.isTraceEnabled()) {
 			this.logger.trace("Saved authorization with authorized scopes");
 			// This log is kept separate for consistency with other providers
-			this.logger.trace("Authenticated authorization consent request");
+			this.logger.trace("Authenticated device authorization consent request");
 		}
 
-		return new OAuth2DeviceVerificationAuthenticationToken(registeredClient.getClientId(), principal,
-				deviceAuthorizationConsentAuthentication.getUserCode());
+		return new OAuth2DeviceVerificationAuthenticationToken(principal,
+				deviceAuthorizationConsentAuthentication.getUserCode(), registeredClient.getClientId());
 	}
 
 	@Override
@@ -244,10 +240,9 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implem
 	 * prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
 	 * <li>The {@link Authentication} of type
 	 * {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}.</li>
-	 * <li>The {@link RegisteredClient} associated with the authorization request.</li>
+	 * <li>The {@link RegisteredClient} associated with the device authorization request.</li>
 	 * <li>The {@link OAuth2Authorization} associated with the state token presented in the
-	 * authorization consent request.</li>
-	 * <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
+	 * device authorization consent request.</li>
 	 * </ul>
 	 *
 	 * @param authorizationConsentCustomizer the {@code Consumer} providing access to the
@@ -258,8 +253,14 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationProvider implem
 		this.authorizationConsentCustomizer = authorizationConsentCustomizer;
 	}
 
+	private static boolean isPrincipalAuthenticated(Authentication principal) {
+		return principal != null &&
+				!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
+				principal.isAuthenticated();
+	}
+
 	private static void throwError(String errorCode, String parameterName) {
-		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
 		throw new OAuth2AuthenticationException(error);
 	}
 

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

@@ -21,16 +21,19 @@ import java.util.Map;
 import java.util.Set;
 
 import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
 import org.springframework.util.Assert;
 
 /**
- * An {@link Authentication} implementation for the Authorization Consent used
+ * An {@link Authentication} implementation for the Device Authorization Consent used
  * in the OAuth 2.0 Device Authorization Grant.
  *
  * @author Steve Riesenberg
  * @since 1.1
+ * @see AbstractAuthenticationToken
+ * @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
  */
 public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2AuthorizationConsentAuthenticationToken {
 	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
@@ -43,7 +46,7 @@ public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2A
 	 * @param authorizationUri the authorization URI
 	 * @param clientId the client identifier
 	 * @param principal the {@code Principal} (Resource Owner)
-	 * @param userCode the user code associated with the device authorization request
+	 * @param userCode the user code associated with the device authorization response
 	 * @param state the state
 	 * @param authorizedScopes the authorized scope(s)
 	 * @param additionalParameters the additional parameters
@@ -64,7 +67,7 @@ public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2A
 	 * @param authorizationUri the authorization URI
 	 * @param clientId the client identifier
 	 * @param principal the {@code Principal} (Resource Owner)
-	 * @param userCode the user code associated with the device authorization request
+	 * @param userCode the user code associated with the device authorization response
 	 * @param state the state
 	 * @param requestedScopes the requested scope(s)
 	 * @param authorizedScopes the authorized scope(s)

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

@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.authentication;
 import java.security.Principal;
 import java.time.Instant;
 import java.util.Base64;
+import java.util.HashSet;
 import java.util.Set;
 
 import org.apache.commons.logging.Log;
@@ -37,7 +38,6 @@ import org.springframework.security.oauth2.core.OAuth2DeviceCode;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2UserCode;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -48,6 +48,7 @@ import org.springframework.security.oauth2.server.authorization.token.DefaultOAu
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
 
 import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
 
@@ -100,11 +101,19 @@ public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implem
 			this.logger.trace("Retrieved registered client");
 		}
 
-		// Validate client grant types has device_code grant type
 		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.DEVICE_CODE)) {
 			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID);
 		}
 
+		Set<String> requestedScopes = deviceAuthorizationRequestAuthentication.getScopes();
+		if (!CollectionUtils.isEmpty(requestedScopes)) {
+			for (String requestedScope : requestedScopes) {
+				if (!registeredClient.getScopes().contains(requestedScope)) {
+					throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE);
+				}
+			}
+		}
+
 		if (this.logger.isTraceEnabled()) {
 			this.logger.trace("Validated device authorization request parameters");
 		}
@@ -128,7 +137,7 @@ public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implem
 		}
 
 		if (this.logger.isTraceEnabled()) {
-			logger.trace("Generated device code");
+			this.logger.trace("Generated device code");
 		}
 
 		// Generate a low-entropy string to use as the user code
@@ -141,21 +150,9 @@ public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implem
 		}
 
 		if (this.logger.isTraceEnabled()) {
-			logger.trace("Generated user code");
+			this.logger.trace("Generated user code");
 		}
 
-		String authorizationUri = deviceAuthorizationRequestAuthentication.getAuthorizationUri();
-
-		Set<String> requestedScopes = deviceAuthorizationRequestAuthentication.getScopes();
-
-		// @formatter:off
-		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
-				.authorizationUri(authorizationUri)
-				.clientId(registeredClient.getClientId())
-				.scopes(requestedScopes)
-				.build();
-		// @formatter:on
-
 		// @formatter:off
 		OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
 				.principalName(clientPrincipal.getName())
@@ -163,7 +160,7 @@ public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implem
 				.token(deviceCode)
 				.token(userCode)
 				.attribute(Principal.class.getName(), clientPrincipal)
-				.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
+				.attribute(OAuth2ParameterNames.SCOPE, new HashSet<>(requestedScopes))
 				.build();
 		// @formatter:on
 		this.authorizationService.save(authorization);
@@ -176,7 +173,8 @@ public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implem
 			this.logger.trace("Authenticated device authorization request");
 		}
 
-		return new OAuth2DeviceAuthorizationRequestAuthenticationToken(clientPrincipal, requestedScopes, deviceCode, userCode);
+		return new OAuth2DeviceAuthorizationRequestAuthenticationToken(
+				clientPrincipal, requestedScopes, deviceCode, userCode);
 	}
 
 	@Override

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

@@ -16,6 +16,7 @@
 package org.springframework.security.oauth2.server.authorization.authentication;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -29,12 +30,13 @@ import org.springframework.security.oauth2.server.authorization.util.SpringAutho
 import org.springframework.util.Assert;
 
 /**
- * An {@link Authentication} implementation for the OAuth 2.0 Device Authorization Request
- * used in the Device Authorization Grant.
+ * An {@link Authentication} implementation for the Device Authorization Request
+ * used in the OAuth 2.0 Device Authorization Grant.
  *
  * @author Steve Riesenberg
  * @since 1.1
  * @see AbstractAuthenticationToken
+ * @see OAuth2ClientAuthenticationToken
  * @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
  */
 public class OAuth2DeviceAuthorizationRequestAuthenticationToken extends AbstractAuthenticationToken {
@@ -65,7 +67,10 @@ public class OAuth2DeviceAuthorizationRequestAuthenticationToken extends Abstrac
 				scopes != null ?
 						new HashSet<>(scopes) :
 						Collections.emptySet());
-		this.additionalParameters = additionalParameters;
+		this.additionalParameters = Collections.unmodifiableMap(
+				additionalParameters != null ?
+						new HashMap<>(additionalParameters) :
+						Collections.emptyMap());
 		this.deviceCode = null;
 		this.userCode = null;
 	}
@@ -109,16 +114,16 @@ public class OAuth2DeviceAuthorizationRequestAuthenticationToken extends Abstrac
 	/**
 	 * Returns the authorization {@code URI}.
 	 *
-	 * @return the authorization {@code URI}.
+	 * @return the authorization {@code URI}
 	 */
 	public String getAuthorizationUri() {
-		return authorizationUri;
+		return this.authorizationUri;
 	}
 
 	/**
 	 * Returns the requested scope(s).
 	 *
-	 * @return the requested scope(s).
+	 * @return the requested scope(s)
 	 */
 	public Set<String> getScopes() {
 		return this.scopes;

+ 45 - 41
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java

@@ -26,7 +26,6 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClaimAccessor;
-import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2DeviceCode;
@@ -34,7 +33,7 @@ import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2RefreshToken;
 import org.springframework.security.oauth2.core.OAuth2Token;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.OAuth2UserCode;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -46,8 +45,11 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.util.Assert;
 
+import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
+
 /**
- * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Device Authorization Grant.
+ * An {@link AuthenticationProvider} implementation for the Device Access Token Request
+ * used in the OAuth 2.0 Device Authorization Grant.
  *
  * @author Steve Riesenberg
  * @since 1.1
@@ -94,8 +96,8 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
 		OAuth2DeviceCodeAuthenticationToken deviceCodeAuthentication =
 				(OAuth2DeviceCodeAuthenticationToken) authentication;
 
-		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
-				.getAuthenticatedClientElseThrowInvalidClient(deviceCodeAuthentication);
+		OAuth2ClientAuthenticationToken clientPrincipal =
+				getAuthenticatedClientElseThrowInvalidClient(deviceCodeAuthentication);
 		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
 
 		if (this.logger.isTraceEnabled()) {
@@ -112,19 +114,17 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
 			this.logger.trace("Retrieved authorization with device code");
 		}
 
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationRequest.class.getName());
-
+		OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
 		OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);
 
-		if (!registeredClient.getClientId().equals(authorizationRequest.getClientId())) {
+		if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
 			if (!deviceCode.isInvalidated()) {
 				// Invalidate the device code given that a different client is attempting to use it
 				authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, deviceCode.getToken());
 				this.authorizationService.save(authorization);
 				if (this.logger.isWarnEnabled()) {
 					this.logger.warn(LogMessage.format(
-							"Invalidated device code used by registered client '%s'", registeredClient.getId()));
+							"Invalidated device code used by registered client '%s'", authorization.getRegisteredClientId()));
 				}
 			}
 			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
@@ -133,29 +133,6 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
 		// In https://www.rfc-editor.org/rfc/rfc8628.html#section-3.5,
 		// the following error codes are defined:
 
-		//   access_denied
-		//      The authorization request was denied.
-		if (Boolean.TRUE.equals(deviceCode.getMetadata(OAuth2Authorization.Token.ACCESS_DENIED_METADATA_NAME))) {
-			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED, null, DEVICE_ERROR_URI);
-			throw new OAuth2AuthenticationException(error);
-		}
-
-		//   expired_token
-		//      The "device_code" has expired, and the device authorization
-		//      session has concluded.  The client MAY commence a new device
-		//      authorization request but SHOULD wait for user interaction before
-		//      restarting to avoid unnecessary polling.
-		if (deviceCode.isExpired()) {
-			OAuth2Error error = new OAuth2Error(EXPIRED_TOKEN, null, DEVICE_ERROR_URI);
-			throw new OAuth2AuthenticationException(error);
-		}
-
-		//   slow_down
-		//      A variant of "authorization_pending", the authorization request is
-		//      still pending and polling should continue, but the interval MUST
-		//      be increased by 5 seconds for this and all subsequent requests.
-		// Note: This error is not handled in the framework.
-
 		//   authorization_pending
 		//      The authorization request is still pending as the end user hasn't
 		//      yet completed the user-interaction steps (Section 3.3).  The
@@ -166,17 +143,43 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
 		//      Section 3.2), or 5 seconds if none was provided, and respect any
 		//      increase in the polling interval required by the "slow_down"
 		//      error.
-		if (!Boolean.TRUE.equals(deviceCode.getMetadata(OAuth2Authorization.Token.ACCESS_GRANTED_METADATA_NAME))) {
+		if (!userCode.isInvalidated()) {
 			OAuth2Error error = new OAuth2Error(AUTHORIZATION_PENDING, null, DEVICE_ERROR_URI);
 			throw new OAuth2AuthenticationException(error);
 		}
 
-		if (!deviceCode.isActive()) {
-			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		//   slow_down
+		//      A variant of "authorization_pending", the authorization request is
+		//      still pending and polling should continue, but the interval MUST
+		//      be increased by 5 seconds for this and all subsequent requests.
+		//	NOTE: This error is not handled in the framework.
+
+		//   access_denied
+		//      The authorization request was denied.
+		if (deviceCode.isInvalidated()) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED, null, DEVICE_ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		//   expired_token
+		//      The "device_code" has expired, and the device authorization
+		//      session has concluded.  The client MAY commence a new device
+		//      authorization request but SHOULD wait for user interaction before
+		//      restarting to avoid unnecessary polling.
+		if (deviceCode.isExpired()) {
+			// Invalidate the device code
+			authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, deviceCode.getToken());
+			this.authorizationService.save(authorization);
+			if (this.logger.isWarnEnabled()) {
+				this.logger.warn(LogMessage.format(
+						"Invalidated device code used by registered client '%s'", authorization.getRegisteredClientId()));
+			}
+			OAuth2Error error = new OAuth2Error(EXPIRED_TOKEN, null, DEVICE_ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
 		}
 
 		if (this.logger.isTraceEnabled()) {
-			this.logger.trace("Validated token request parameters");
+			this.logger.trace("Validated device token request parameters");
 		}
 
 		// @formatter:off
@@ -222,10 +225,7 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
 
 		// ----- Refresh token -----
 		OAuth2RefreshToken refreshToken = null;
-		if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
-				// Do not issue refresh token to public client
-				!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
-
+		if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
 			tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
 			OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
 			if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
@@ -250,6 +250,10 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
 			this.logger.trace("Saved authorization");
 		}
 
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated device token request");
+		}
+
 		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken);
 	}
 

+ 4 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationToken.java

@@ -23,7 +23,8 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.util.Assert;
 
 /**
- * An {@link Authentication} implementation used for the OAuth 2.0 Device Authorization Grant.
+ * An {@link Authentication} implementation for the Device Access Token Request
+ * used in the OAuth 2.0 Device Authorization Grant.
  *
  * @author Steve Riesenberg
  * @since 1.1
@@ -41,7 +42,8 @@ public class OAuth2DeviceCodeAuthenticationToken extends OAuth2AuthorizationGran
 	 * @param clientPrincipal the authenticated client principal
 	 * @param additionalParameters the additional parameters
 	 */
-	public OAuth2DeviceCodeAuthenticationToken(String deviceCode, Authentication clientPrincipal, @Nullable Map<String, Object> additionalParameters) {
+	public OAuth2DeviceCodeAuthenticationToken(String deviceCode, Authentication clientPrincipal,
+			@Nullable Map<String, Object> additionalParameters) {
 		super(AuthorizationGrantType.DEVICE_CODE, clientPrincipal, additionalParameters);
 		Assert.hasText(deviceCode, "deviceCode cannot be empty");
 		this.deviceCode = deviceCode;

+ 39 - 39
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java

@@ -29,10 +29,8 @@ import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
 import org.springframework.security.crypto.keygen.StringKeyGenerator;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
-import org.springframework.security.oauth2.core.OAuth2DeviceCode;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2UserCode;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
@@ -41,16 +39,17 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.util.Assert;
 
 /**
- * An {@link AuthenticationProvider} implementation for the Verification {@code URI}
- * (submission of the user code)} used in the OAuth 2.0 Device Authorization Grant.
+ * An {@link AuthenticationProvider} implementation for the Device Verification Request
+ * (submission of the user code) used in the OAuth 2.0 Device Authorization Grant.
  *
  * @author Steve Riesenberg
  * @since 1.1
  * @see OAuth2DeviceVerificationAuthenticationToken
- * @see OAuth2AuthorizationConsent
  * @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
  * @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
  * @see OAuth2DeviceCodeAuthenticationProvider
@@ -105,38 +104,37 @@ public final class OAuth2DeviceVerificationAuthenticationProvider implements Aut
 			this.logger.trace("Retrieved authorization with user code");
 		}
 
-		RegisteredClient registeredClient = this.registeredClientRepository.findById(
-				authorization.getRegisteredClientId());
-
-		if (this.logger.isTraceEnabled()) {
-			this.logger.trace("Retrieved registered client");
-		}
-
 		Authentication principal = (Authentication) deviceVerificationAuthentication.getPrincipal();
 		if (!isPrincipalAuthenticated(principal)) {
 			if (this.logger.isTraceEnabled()) {
-				this.logger.trace("Did not authenticate device authorization request since principal not authenticated");
+				this.logger.trace("Did not authenticate device verification request since principal not authenticated");
 			}
-			// Return the authorization request as-is where isAuthenticated() is false
+			// Return the device verification request as-is where isAuthenticated() is false
 			return deviceVerificationAuthentication;
 		}
 
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		RegisteredClient registeredClient = this.registeredClientRepository.findById(
+				authorization.getRegisteredClientId());
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		Set<String> requestedScopes = authorization.getAttribute(OAuth2ParameterNames.SCOPE);
 
 		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
 				registeredClient.getId(), principal.getName());
 
-		Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
-				currentAuthorizationConsent.getScopes() : null;
-
-		if (requiresAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
+		if (requiresAuthorizationConsent(requestedScopes, currentAuthorizationConsent)) {
 			String state = DEFAULT_STATE_GENERATOR.generateKey();
 			authorization = OAuth2Authorization.from(authorization)
+					.principalName(principal.getName())
+					.attribute(Principal.class.getName(), principal)
 					.attribute(OAuth2ParameterNames.STATE, state)
 					.build();
 
 			if (this.logger.isTraceEnabled()) {
-				logger.trace("Generated authorization consent state");
+				this.logger.trace("Generated device authorization consent state");
 			}
 
 			this.authorizationService.save(authorization);
@@ -145,33 +143,39 @@ public final class OAuth2DeviceVerificationAuthenticationProvider implements Aut
 				this.logger.trace("Saved authorization");
 			}
 
-			return new OAuth2DeviceAuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+			Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
+					currentAuthorizationConsent.getScopes() : null;
+
+			AuthorizationServerSettings authorizationServerSettings =
+					AuthorizationServerContextHolder.getContext().getAuthorizationServerSettings();
+			String deviceVerificationUri = authorizationServerSettings.getDeviceVerificationEndpoint();
+
+			return new OAuth2DeviceAuthorizationConsentAuthenticationToken(deviceVerificationUri,
 					registeredClient.getClientId(), principal, deviceVerificationAuthentication.getUserCode(), state,
-					authorizationRequest.getScopes(), currentAuthorizedScopes);
+					requestedScopes, currentAuthorizedScopes);
 		}
 
-		OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);
 		OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
-		OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
+		// @formatter:off
+		authorization = OAuth2Authorization.from(authorization)
 				.principalName(principal.getName())
-				.authorizedScopes(authorizationRequest.getScopes())
-				.token(deviceCode.getToken(), metadata -> metadata
-						.put(OAuth2Authorization.Token.ACCESS_GRANTED_METADATA_NAME, true))
+				.authorizedScopes(requestedScopes)
 				.token(userCode.getToken(), metadata -> metadata
 						.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
 				.attribute(Principal.class.getName(), principal)
-				.attributes(attrs -> attrs.remove(OAuth2ParameterNames.STATE))
+				.attributes(attributes -> attributes.remove(OAuth2ParameterNames.SCOPE))
 				.build();
-		this.authorizationService.save(updatedAuthorization);
+		// @formatter:on
+		this.authorizationService.save(authorization);
 
 		if (this.logger.isTraceEnabled()) {
 			this.logger.trace("Saved authorization with authorized scopes");
 			// This log is kept separate for consistency with other providers
-			this.logger.trace("Authenticated authorization consent request");
+			this.logger.trace("Authenticated device verification request");
 		}
 
-		return new OAuth2DeviceVerificationAuthenticationToken(registeredClient.getClientId(), principal,
-				deviceVerificationAuthentication.getUserCode());
+		return new OAuth2DeviceVerificationAuthenticationToken(principal,
+				deviceVerificationAuthentication.getUserCode(), registeredClient.getClientId());
 	}
 
 	@Override
@@ -179,15 +183,11 @@ public final class OAuth2DeviceVerificationAuthenticationProvider implements Aut
 		return OAuth2DeviceVerificationAuthenticationToken.class.isAssignableFrom(authentication);
 	}
 
-	private static boolean requiresAuthorizationConsent(RegisteredClient registeredClient,
-			OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
-
-		if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {
-			return false;
-		}
+	private static boolean requiresAuthorizationConsent(
+			Set<String> requestedScopes, OAuth2AuthorizationConsent authorizationConsent) {
 
 		if (authorizationConsent != null &&
-				authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
+				authorizationConsent.getScopes().containsAll(requestedScopes)) {
 			return false;
 		}
 

+ 25 - 21
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationToken.java

@@ -16,6 +16,7 @@
 package org.springframework.security.oauth2.server.authorization.authentication;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 
 import org.springframework.lang.Nullable;
@@ -25,7 +26,7 @@ import org.springframework.security.oauth2.server.authorization.util.SpringAutho
 import org.springframework.util.Assert;
 
 /**
- * An {@link Authentication} implementation for the Verification {@code URI}
+ * An {@link Authentication} implementation for the Device Verification Request
  * (submission of the user code) used in the OAuth 2.0 Device Authorization Grant.
  *
  * @author Steve Riesenberg
@@ -35,44 +36,47 @@ import org.springframework.util.Assert;
  */
 public class OAuth2DeviceVerificationAuthenticationToken extends AbstractAuthenticationToken {
 	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
-	private final String clientId;
 	private final Authentication principal;
 	private final String userCode;
 	private final Map<String, Object> additionalParameters;
+	private final String clientId;
 
 	/**
 	 * Constructs an {@code OAuth2DeviceVerificationAuthenticationToken} using the provided parameters.
 	 *
 	 * @param principal the {@code Principal} (Resource Owner)
-	 * @param userCode the user code associated with the device authorization request
+	 * @param userCode the user code associated with the device authorization response
 	 * @param additionalParameters the additional parameters
 	 */
 	public OAuth2DeviceVerificationAuthenticationToken(Authentication principal, String userCode,
 			@Nullable Map<String, Object> additionalParameters) {
 		super(Collections.emptyList());
 		Assert.notNull(principal, "principal cannot be null");
-		Assert.notNull(userCode, "userCode cannot be null");
-		this.clientId = null;
+		Assert.hasText(userCode, "userCode cannot be empty");
 		this.principal = principal;
 		this.userCode = userCode;
-		this.additionalParameters = additionalParameters;
+		this.additionalParameters = Collections.unmodifiableMap(
+				additionalParameters != null ?
+						new HashMap<>(additionalParameters) :
+						Collections.emptyMap());
+		this.clientId = null;
 	}
 
 	/**
 	 * Constructs an {@code OAuth2DeviceVerificationAuthenticationToken} using the provided parameters.
 	 *
-	 * @param clientId the client identifier
 	 * @param principal the {@code Principal} (Resource Owner)
-	 * @param userCode the user code associated with the device authorization request
+	 * @param userCode the user code associated with the device authorization response
+	 * @param clientId the client identifier
 	 */
-	public OAuth2DeviceVerificationAuthenticationToken(String clientId, Authentication principal, String userCode) {
+	public OAuth2DeviceVerificationAuthenticationToken(Authentication principal, String userCode, String clientId) {
 		super(Collections.emptyList());
-		Assert.hasText(clientId, "clientId cannot be empty");
 		Assert.notNull(principal, "principal cannot be null");
-		Assert.notNull(userCode, "userCode cannot be null");
-		this.clientId = clientId;
+		Assert.hasText(userCode, "userCode cannot be empty");
+		Assert.hasText(clientId, "clientId cannot be empty");
 		this.principal = principal;
 		this.userCode = userCode;
+		this.clientId = clientId;
 		this.additionalParameters = null;
 		setAuthenticated(true);
 	}
@@ -87,15 +91,6 @@ public class OAuth2DeviceVerificationAuthenticationToken extends AbstractAuthent
 		return "";
 	}
 
-	/**
-	 * Returns the client identifier.
-	 *
-	 * @return the client identifier
-	 */
-	public String getClientId() {
-		return this.clientId;
-	}
-
 	/**
 	 * Returns the user code.
 	 *
@@ -114,4 +109,13 @@ public class OAuth2DeviceVerificationAuthenticationToken extends AbstractAuthent
 		return this.additionalParameters;
 	}
 
+	/**
+	 * Returns the client identifier.
+	 *
+	 * @return the client identifier
+	 */
+	public String getClientId() {
+		return this.clientId;
+	}
+
 }

+ 4 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java

@@ -67,6 +67,8 @@ import org.springframework.util.Assert;
  * @see OAuth2TokenEndpointConfigurer
  * @see OAuth2TokenIntrospectionEndpointConfigurer
  * @see OAuth2TokenRevocationEndpointConfigurer
+ * @see OAuth2DeviceAuthorizationEndpointConfigurer
+ * @see OAuth2DeviceVerificationEndpointConfigurer
  * @see OidcConfigurer
  * @see RegisteredClientRepository
  * @see OAuth2AuthorizationService
@@ -316,7 +318,8 @@ public final class OAuth2AuthorizationServerConfigurer
 					new OrRequestMatcher(
 							getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
 							getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class),
-							getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class))
+							getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class),
+							getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class))
 			);
 		}
 	}

+ 15 - 12
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java

@@ -26,7 +26,9 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
@@ -41,6 +43,7 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
 
 /**
  * Configurer for the OAuth 2.0 Device Authorization Endpoint.
@@ -53,8 +56,8 @@ import org.springframework.util.Assert;
 public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
 
 	private RequestMatcher requestMatcher;
-	private final List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
-	private Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer = (authenticationConverters) -> {};
+	private final List<AuthenticationConverter> deviceAuthorizationRequestConverters = new ArrayList<>();
+	private Consumer<List<AuthenticationConverter>> deviceAuthorizationRequestConvertersConsumer = (deviceAuthorizationRequestConverters) -> {};
 	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
 	private AuthenticationSuccessHandler deviceAuthorizationResponseHandler;
@@ -77,7 +80,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 	 */
 	public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverter(AuthenticationConverter deviceAuthorizationRequestConverter) {
 		Assert.notNull(deviceAuthorizationRequestConverter, "deviceAuthorizationRequestConverter cannot be null");
-		this.authenticationConverters.add(deviceAuthorizationRequestConverter);
+		this.deviceAuthorizationRequestConverters.add(deviceAuthorizationRequestConverter);
 		return this;
 	}
 
@@ -92,7 +95,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 	public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverters(
 			Consumer<List<AuthenticationConverter>> deviceAuthorizationRequestConvertersConsumer) {
 		Assert.notNull(deviceAuthorizationRequestConvertersConsumer, "deviceAuthorizationRequestConvertersConsumer cannot be null");
-		this.authenticationConvertersConsumer = deviceAuthorizationRequestConvertersConsumer;
+		this.deviceAuthorizationRequestConvertersConsumer = deviceAuthorizationRequestConvertersConsumer;
 		return this;
 	}
 
@@ -125,7 +128,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 
 	/**
 	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
-	 * and returning the Device Authorization Response.
+	 * and returning the {@link OAuth2DeviceAuthorizationResponse Device Authorization Response}.
 	 *
 	 * @param deviceAuthorizationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
 	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
@@ -136,10 +139,10 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 	}
 
 	/**
-	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
 	 * and returning the {@link OAuth2Error Error Response}.
 	 *
-	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
 	 * @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
 	 */
 	public OAuth2DeviceAuthorizationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
@@ -184,10 +187,10 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 						authenticationManager, authorizationServerSettings.getDeviceAuthorizationEndpoint());
 
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
-		if (!this.authenticationConverters.isEmpty()) {
-			authenticationConverters.addAll(0, this.authenticationConverters);
+		if (!this.deviceAuthorizationRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.deviceAuthorizationRequestConverters);
 		}
-		this.authenticationConvertersConsumer.accept(authenticationConverters);
+		this.deviceAuthorizationRequestConvertersConsumer.accept(authenticationConverters);
 		deviceAuthorizationEndpointFilter.setAuthenticationConverter(
 				new DelegatingAuthenticationConverter(authenticationConverters));
 		if (this.deviceAuthorizationResponseHandler != null) {
@@ -196,7 +199,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 		if (this.errorResponseHandler != null) {
 			deviceAuthorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
 		}
-		if (this.verificationUri != null) {
+		if (StringUtils.hasText(this.verificationUri)) {
 			deviceAuthorizationEndpointFilter.setVerificationUri(this.verificationUri);
 		}
 		builder.addFilterAfter(postProcess(deviceAuthorizationEndpointFilter), AuthorizationFilter.class);
@@ -214,7 +217,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 		return authenticationConverters;
 	}
 
-	private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
+	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
 		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 
 		OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);

+ 25 - 23
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java

@@ -30,7 +30,6 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationProvider;
@@ -41,10 +40,10 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2Device
 import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationConsentAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceVerificationAuthenticationConverter;
-import org.springframework.security.web.access.intercept.AuthorizationFilter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -62,8 +61,8 @@ import org.springframework.util.StringUtils;
 public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOAuth2Configurer {
 
 	private RequestMatcher requestMatcher;
-	private final List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
-	private Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer = (authenticationConverters) -> {};
+	private final List<AuthenticationConverter> deviceVerificationRequestConverters = new ArrayList<>();
+	private Consumer<List<AuthenticationConverter>> deviceVerificationRequestConvertersConsumer = (deviceVerificationRequestConverters) -> {};
 	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
 	private AuthenticationSuccessHandler deviceVerificationResponseHandler;
@@ -78,15 +77,15 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 	}
 
 	/**
-	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Device Verification Request (or Consent) from {@link HttpServletRequest}
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Device Verification Request (or Device Authorization Consent) from {@link HttpServletRequest}
 	 * to an instance of {@link OAuth2DeviceVerificationAuthenticationToken} or {@link OAuth2DeviceAuthorizationConsentAuthenticationToken} used for authenticating the request.
 	 *
-	 * @param deviceVerificationRequestConverter the {@link AuthenticationConverter} used when attempting to extract a Device Authorization Request from {@link HttpServletRequest}
+	 * @param deviceVerificationRequestConverter the {@link AuthenticationConverter} used when attempting to extract a Device Verification Request (or Device Authorization Consent) from {@link HttpServletRequest}
 	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
 	 */
 	public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverter(AuthenticationConverter deviceVerificationRequestConverter) {
 		Assert.notNull(deviceVerificationRequestConverter, "deviceVerificationRequestConverter cannot be null");
-		this.authenticationConverters.add(deviceVerificationRequestConverter);
+		this.deviceVerificationRequestConverters.add(deviceVerificationRequestConverter);
 		return this;
 	}
 
@@ -101,14 +100,14 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 	public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverters(
 			Consumer<List<AuthenticationConverter>> deviceVerificationRequestConvertersConsumer) {
 		Assert.notNull(deviceVerificationRequestConvertersConsumer, "deviceVerificationRequestConvertersConsumer cannot be null");
-		this.authenticationConvertersConsumer = deviceVerificationRequestConvertersConsumer;
+		this.deviceVerificationRequestConvertersConsumer = deviceVerificationRequestConvertersConsumer;
 		return this;
 	}
 
 	/**
-	 * Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceVerificationAuthenticationToken}.
+	 * Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceVerificationAuthenticationToken} or {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}.
 	 *
-	 * @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceVerificationAuthenticationToken}
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceVerificationAuthenticationToken} or {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}
 	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
 	 */
 	public OAuth2DeviceVerificationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
@@ -133,10 +132,10 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 	}
 
 	/**
-	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}
+	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
 	 * and returning the response.
 	 *
-	 * @param deviceVerificationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 * @param deviceVerificationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
 	 * @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
 	 */
 	public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationResponseHandler(AuthenticationSuccessHandler deviceVerificationResponseHandler) {
@@ -166,9 +165,9 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 	 *
 	 * <ul>
 	 * <li>{@code client_id} - the client identifier</li>
-	 * <li>{@code scope} - a space-delimited list of scopes present in the authorization request</li>
+	 * <li>{@code scope} - a space-delimited list of scopes present in the device authorization request</li>
 	 * <li>{@code state} - a CSRF protection token</li>
-	 * <li>@code code} - the user code</li>
+	 * <li>{@code user_code} - the user code</li>
 	 * </ul>
 	 *
 	 * In general, the consent page should create a form that submits
@@ -181,7 +180,7 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 	 * <li>It must include the received {@code state} as an HTTP parameter</li>
 	 * <li>It must include the list of {@code scope}s the {@code Resource Owner}
 	 * consented to as an HTTP parameter</li>
-	 * <li>It must include the user {@code code} as an HTTP parameter</li>
+	 * <li>It must include the received {@code user_code} as an HTTP parameter</li>
 	 * </ul>
 	 *
 	 * @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
@@ -198,9 +197,11 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 				OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
 		this.requestMatcher = new OrRequestMatcher(
 				new AntPathRequestMatcher(
-						authorizationServerSettings.getDeviceVerificationEndpoint(), HttpMethod.GET.name()),
+						authorizationServerSettings.getDeviceVerificationEndpoint(),
+						HttpMethod.GET.name()),
 				new AntPathRequestMatcher(
-						authorizationServerSettings.getDeviceVerificationEndpoint(), HttpMethod.POST.name()));
+						authorizationServerSettings.getDeviceVerificationEndpoint(),
+						HttpMethod.POST.name()));
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
 		if (!this.authenticationProviders.isEmpty()) {
@@ -214,18 +215,18 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 	@Override
 	public void configure(HttpSecurity builder) {
 		AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
-
 		AuthorizationServerSettings authorizationServerSettings =
 				OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
 
 		OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter =
 				new OAuth2DeviceVerificationEndpointFilter(
-						authenticationManager, authorizationServerSettings.getDeviceVerificationEndpoint());
+						authenticationManager,
+						authorizationServerSettings.getDeviceVerificationEndpoint());
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
-		if (!this.authenticationConverters.isEmpty()) {
-			authenticationConverters.addAll(0, this.authenticationConverters);
+		if (!this.deviceVerificationRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.deviceVerificationRequestConverters);
 		}
-		this.authenticationConvertersConsumer.accept(authenticationConverters);
+		this.deviceVerificationRequestConvertersConsumer.accept(authenticationConverters);
 		deviceVerificationEndpointFilter.setAuthenticationConverter(
 				new DelegatingAuthenticationConverter(authenticationConverters));
 		if (this.deviceVerificationResponseHandler != null) {
@@ -237,7 +238,7 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 		if (StringUtils.hasText(this.consentPage)) {
 			deviceVerificationEndpointFilter.setConsentPage(this.consentPage);
 		}
-		builder.addFilterAfter(postProcess(deviceVerificationEndpointFilter), AuthorizationFilter.class);
+		builder.addFilterBefore(postProcess(deviceVerificationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 	}
 
 	@Override
@@ -247,6 +248,7 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 
 	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
 		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
 		authenticationConverters.add(new OAuth2DeviceVerificationAuthenticationConverter());
 		authenticationConverters.add(new OAuth2DeviceAuthorizationConsentAuthenticationConverter());
 

+ 2 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -136,6 +136,7 @@ public class OAuth2AuthorizationServerMetadataHttpMessageConverter
 			Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
 			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter);
 			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT, urlConverter);
 			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
 			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
 			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, urlConverter);

+ 2 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java

@@ -94,6 +94,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 		OidcProviderConfiguration.Builder providerConfiguration = OidcProviderConfiguration.builder()
 				.issuer(issuer)
 				.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
+				.deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint()))
 				.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
 				.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint()))
@@ -103,6 +104,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
 				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
+				.grantType(AuthorizationGrantType.DEVICE_CODE.getValue())
 				.tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint()))
 				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))

+ 7 - 3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java

@@ -55,16 +55,18 @@ public final class AuthorizationServerSettings extends AbstractSettings {
 	/**
 	 * Returns the OAuth 2.0 Device Authorization endpoint. The default is {@code /oauth2/device_authorization}.
 	 *
-	 * @return the Authorization endpoint
+	 * @return the Device Authorization endpoint
+	 * @since 1.1
 	 */
 	public String getDeviceAuthorizationEndpoint() {
 		return getSetting(ConfigurationSettingNames.AuthorizationServer.DEVICE_AUTHORIZATION_ENDPOINT);
 	}
 
 	/**
-	 * Returns the OAuth 2.0 Device VERIFICATION endpoint. The default is {@code /oauth2/device_verification}.
+	 * Returns the OAuth 2.0 Device Verification endpoint. The default is {@code /oauth2/device_verification}.
 	 *
-	 * @return the Authorization endpoint
+	 * @return the Device Verification endpoint
+	 * @since 1.1
 	 */
 	public String getDeviceVerificationEndpoint() {
 		return getSetting(ConfigurationSettingNames.AuthorizationServer.DEVICE_VERIFICATION_ENDPOINT);
@@ -198,6 +200,7 @@ public final class AuthorizationServerSettings extends AbstractSettings {
 		 *
 		 * @param deviceAuthorizationEndpoint the Device Authorization endpoint
 		 * @return the {@link Builder} for further configuration
+		 * @since 1.1
 		 */
 		public Builder deviceAuthorizationEndpoint(String deviceAuthorizationEndpoint) {
 			return setting(ConfigurationSettingNames.AuthorizationServer.DEVICE_AUTHORIZATION_ENDPOINT, deviceAuthorizationEndpoint);
@@ -208,6 +211,7 @@ public final class AuthorizationServerSettings extends AbstractSettings {
 		 *
 		 * @param deviceVerificationEndpoint the Device Verification endpoint
 		 * @return the {@link Builder} for further configuration
+		 * @since 1.1
 		 */
 		public Builder deviceVerificationEndpoint(String deviceVerificationEndpoint) {
 			return setting(ConfigurationSettingNames.AuthorizationServer.DEVICE_VERIFICATION_ENDPOINT, deviceVerificationEndpoint);

+ 3 - 3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java

@@ -67,9 +67,9 @@ public final class TokenSettings extends AbstractSettings {
 	}
 
 	/**
-	 * Returns the time-to-live for a device code. The default is 30 minutes.
+	 * Returns the time-to-live for a device code. The default is 5 minutes.
 	 *
-	 * @return the time-to-live for an authorization code
+	 * @return the time-to-live for a device code
 	 * @since 1.1
 	 */
 	public Duration getDeviceCodeTimeToLive() {
@@ -113,7 +113,7 @@ public final class TokenSettings extends AbstractSettings {
 				.authorizationCodeTimeToLive(Duration.ofMinutes(5))
 				.accessTokenTimeToLive(Duration.ofMinutes(5))
 				.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
-				.deviceCodeTimeToLive(Duration.ofMinutes(30))
+				.deviceCodeTimeToLive(Duration.ofMinutes(5))
 				.reuseRefreshTokens(true)
 				.refreshTokenTimeToLive(Duration.ofMinutes(60))
 				.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);

+ 3 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -92,6 +92,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 		OAuth2AuthorizationServerMetadata.Builder authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
 				.issuer(issuer)
 				.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
+				.deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint()))
 				.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
 				.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint()))
@@ -99,6 +100,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
 				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
+				.grantType(AuthorizationGrantType.DEVICE_CODE.getValue())
 				.tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint()))
 				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))

+ 29 - 33
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java

@@ -27,6 +27,7 @@ import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationDetailsSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
@@ -40,7 +41,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizati
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.http.converter.OAuth2DeviceAuthorizationResponseHttpMessageConverter;
 import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
@@ -56,7 +56,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 
 /**
- * A {@code Filter} for the OAuth 2.0 Device Authorization Grant,
+ * A {@code Filter} for the OAuth 2.0 Device Authorization endpoint,
  * which handles the processing of the OAuth 2.0 Device Authorization Request.
  *
  * @author Steve Riesenberg
@@ -72,20 +72,18 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 
 	private static final String DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI = "/oauth2/device_authorization";
 
-	private static final String DEFAULT_DEVICE_VERIFICATION_URI = "/oauth2/device_verification";
-
 	private final AuthenticationManager authenticationManager;
 	private final RequestMatcher deviceAuthorizationEndpointMatcher;
 	private final HttpMessageConverter<OAuth2DeviceAuthorizationResponse> deviceAuthorizationHttpResponseConverter =
 			new OAuth2DeviceAuthorizationResponseHttpMessageConverter();
 	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
 			new OAuth2ErrorHttpMessageConverter();
-	private AuthenticationConverter authenticationConverter;
 	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
 			new WebAuthenticationDetailsSource();
+	private AuthenticationConverter authenticationConverter;
 	private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendDeviceAuthorizationResponse;
 	private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
-	private String verificationUri = DEFAULT_DEVICE_VERIFICATION_URI;
+	private String verificationUri = OAuth2DeviceVerificationEndpointFilter.DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI;
 
 	/**
 	 * Constructs an {@code OAuth2DeviceAuthorizationEndpointFilter} using the provided parameters.
@@ -121,17 +119,17 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 		}
 
 		try {
-			OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthenticationToken =
-					(OAuth2DeviceAuthorizationRequestAuthenticationToken) this.authenticationConverter.convert(request);
-			deviceAuthorizationRequestAuthenticationToken.setDetails(
-					this.authenticationDetailsSource.buildDetails(request));
+			Authentication deviceAuthorizationRequestAuthentication = this.authenticationConverter.convert(request);
+			if (deviceAuthorizationRequestAuthentication instanceof AbstractAuthenticationToken) {
+				((AbstractAuthenticationToken) deviceAuthorizationRequestAuthentication)
+						.setDetails(this.authenticationDetailsSource.buildDetails(request));
+			}
 
-			OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthenticationTokenResult =
-					(OAuth2DeviceAuthorizationRequestAuthenticationToken) this.authenticationManager.authenticate(
-							deviceAuthorizationRequestAuthenticationToken);
+			Authentication deviceAuthorizationRequestAuthenticationResult =
+					this.authenticationManager.authenticate(deviceAuthorizationRequestAuthentication);
 
 			this.authenticationSuccessHandler.onAuthenticationSuccess(request, response,
-					deviceAuthorizationRequestAuthenticationTokenResult);
+					deviceAuthorizationRequestAuthenticationResult);
 		} catch (OAuth2AuthenticationException ex) {
 			SecurityContextHolder.clearContext();
 			if (this.logger.isTraceEnabled()) {
@@ -141,17 +139,6 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 		}
 	}
 
-	/**
-	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Device Authorization Request from {@link HttpServletRequest}
-	 * to an instance of {@link OAuth2DeviceAuthorizationRequestAuthenticationToken} used for authenticating the request.
-	 *
-	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a DeviceAuthorization Request from {@link HttpServletRequest}
-	 */
-	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
-		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
-		this.authenticationConverter = authenticationConverter;
-	}
-
 	/**
 	 * Sets the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}.
 	 *
@@ -162,9 +149,20 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 		this.authenticationDetailsSource = authenticationDetailsSource;
 	}
 
+	/**
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Device Authorization Request from {@link HttpServletRequest}
+	 * to an instance of {@link OAuth2DeviceAuthorizationRequestAuthenticationToken} used for authenticating the request.
+	 *
+	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a Device Authorization Request from {@link HttpServletRequest}
+	 */
+	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationConverter = authenticationConverter;
+	}
+
 	/**
 	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
-	 * and returning the Device Authorization Response.
+	 * and returning the {@link OAuth2DeviceAuthorizationResponse Device Authorization Response}.
 	 *
 	 * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
 	 */
@@ -174,10 +172,10 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 	}
 
 	/**
-	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
 	 * and returning the {@link OAuth2Error Error Response}.
 	 *
-	 * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
+	 * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
 	 */
 	public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
 		Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
@@ -198,11 +196,11 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 	private void sendDeviceAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
 			Authentication authentication) throws IOException {
 
-		OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthenticationToken =
+		OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthentication =
 				(OAuth2DeviceAuthorizationRequestAuthenticationToken) authentication;
 
-		OAuth2DeviceCode deviceCode = deviceAuthorizationRequestAuthenticationToken.getDeviceCode();
-		OAuth2UserCode userCode = deviceAuthorizationRequestAuthenticationToken.getUserCode();
+		OAuth2DeviceCode deviceCode = deviceAuthorizationRequestAuthentication.getDeviceCode();
+		OAuth2UserCode userCode = deviceAuthorizationRequestAuthentication.getUserCode();
 
 		// Generate the fully-qualified verification URI
 		String issuerUri = AuthorizationServerContextHolder.getContext().getIssuer();
@@ -237,5 +235,3 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 	}
 
 }
-
-

+ 11 - 11
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceVerificationEndpointFilter.java

@@ -63,9 +63,9 @@ import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 
 /**
- * A {@code Filter} for the OAuth 2.0 Device Authorization Grant, which handles
- * the processing of the Verification {@code URI} (submission of the user code)
- * and OAuth 2.0 Authorization Consent.
+ * A {@code Filter} for the OAuth 2.0 Device Authorization Grant,
+ * which handles the processing of the Device Verification Request (submission of the user code)
+ * and the Device Authorization Consent.
  *
  * @author Steve Riesenberg
  * @since 1.1
@@ -79,7 +79,7 @@ import org.springframework.web.util.UriComponentsBuilder;
  */
 public final class OAuth2DeviceVerificationEndpointFilter extends OncePerRequestFilter {
 
-	private static final String DEFAULT_DEVICE_VERIFICATION_URI = "/oauth2/device_verification";
+	static final String DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI = "/oauth2/device_verification";
 
 	private final AuthenticationManager authenticationManager;
 	private final RequestMatcher deviceVerificationEndpointMatcher;
@@ -93,16 +93,16 @@ public final class OAuth2DeviceVerificationEndpointFilter extends OncePerRequest
 	private String consentPage;
 
 	/**
-	 * Construct an {@code OAuth2DeviceVerificationEndpointFilter} using the provided parameters.
+	 * Constructs an {@code OAuth2DeviceVerificationEndpointFilter} using the provided parameters.
 	 *
 	 * @param authenticationManager the authentication manager
 	 */
 	public OAuth2DeviceVerificationEndpointFilter(AuthenticationManager authenticationManager) {
-		this(authenticationManager, DEFAULT_DEVICE_VERIFICATION_URI);
+		this(authenticationManager, DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI);
 	}
 
 	/**
-	 * Construct an {@code OAuth2DeviceVerificationEndpointFilter} using the provided parameters.
+	 * Constructs an {@code OAuth2DeviceVerificationEndpointFilter} using the provided parameters.
 	 *
 	 * @param authenticationManager the authentication manager
 	 * @param deviceVerificationEndpointUri the endpoint {@code URI} for device verification requests
@@ -184,11 +184,11 @@ public final class OAuth2DeviceVerificationEndpointFilter extends OncePerRequest
 	}
 
 	/**
-	 * Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Device Verification Request (or Device Authorization Consent) from {@link HttpServletRequest}
 	 * to an instance of {@link OAuth2DeviceVerificationAuthenticationToken} or {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}
 	 * used for authenticating the request.
 	 *
-	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
+	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a Device Verification Request (or Device Authorization Consent) from {@link HttpServletRequest}
 	 */
 	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
 		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
@@ -207,10 +207,10 @@ public final class OAuth2DeviceVerificationEndpointFilter extends OncePerRequest
 	}
 
 	/**
-	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
 	 * and returning the {@link OAuth2Error Error Response}.
 	 *
-	 * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
+	 * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
 	 */
 	public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
 		Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");

+ 2 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java

@@ -49,6 +49,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
@@ -92,6 +93,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
  * @see OAuth2AuthorizationCodeAuthenticationProvider
  * @see OAuth2RefreshTokenAuthenticationProvider
  * @see OAuth2ClientCredentialsAuthenticationProvider
+ * @see OAuth2DeviceCodeAuthenticationProvider
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.2">Section 3.2 Token Endpoint</a>
  */
 public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {

+ 22 - 19
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceAuthorizationConsentAuthenticationConverter.java

@@ -35,7 +35,7 @@ import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 
 /**
- * Attempts to extract an Authorization Consent from {@link HttpServletRequest}
+ * Attempts to extract a Device Authorization Consent from {@link HttpServletRequest}
  * for the OAuth 2.0 Device Authorization Grant and then converts it to an
  * {@link OAuth2DeviceAuthorizationConsentAuthenticationToken} used for
  * authenticating the request.
@@ -48,14 +48,14 @@ import org.springframework.util.StringUtils;
  */
 public final class OAuth2DeviceAuthorizationConsentAuthenticationConverter implements AuthenticationConverter {
 
-	private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
-	private static final String DEVICE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc8628#section-3.3";
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
 	private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
 			"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
 
 	@Override
 	public Authentication convert(HttpServletRequest request) {
-		if (!"POST".equals(request.getMethod())) {
+		if (!"POST".equals(request.getMethod()) ||
+				request.getParameter(OAuth2ParameterNames.STATE) == null) {
 			return null;
 		}
 
@@ -63,22 +63,14 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationConverter imple
 
 		String authorizationUri = request.getRequestURL().toString();
 
-		// user_code (REQUIRED)
-		String userCode = parameters.getFirst(OAuth2ParameterNames.USER_CODE);
-		if (!StringUtils.hasText(userCode) || parameters.get(OAuth2ParameterNames.USER_CODE).size() != 1) {
-			OAuth2EndpointUtils.throwError(
-					OAuth2ErrorCodes.INVALID_REQUEST,
-					OAuth2ParameterNames.USER_CODE,
-					DEVICE_ERROR_URI);
-		}
-
 		// client_id (REQUIRED)
 		String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
-		if (!StringUtils.hasText(clientId) || parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
+		if (!StringUtils.hasText(clientId) ||
+				parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
 			OAuth2EndpointUtils.throwError(
 					OAuth2ErrorCodes.INVALID_REQUEST,
 					OAuth2ParameterNames.CLIENT_ID,
-					DEFAULT_ERROR_URI);
+					ERROR_URI);
 		}
 
 		Authentication principal = SecurityContextHolder.getContext().getAuthentication();
@@ -86,13 +78,24 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationConverter imple
 			principal = ANONYMOUS_AUTHENTICATION;
 		}
 
+		// user_code (REQUIRED)
+		String userCode = parameters.getFirst(OAuth2ParameterNames.USER_CODE);
+		if (!StringUtils.hasText(userCode) ||
+				parameters.get(OAuth2ParameterNames.USER_CODE).size() != 1) {
+			OAuth2EndpointUtils.throwError(
+					OAuth2ErrorCodes.INVALID_REQUEST,
+					OAuth2ParameterNames.USER_CODE,
+					ERROR_URI);
+		}
+
 		// state (REQUIRED)
 		String state = parameters.getFirst(OAuth2ParameterNames.STATE);
-		if (!StringUtils.hasText(state) || parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
+		if (!StringUtils.hasText(state) ||
+				parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
 			OAuth2EndpointUtils.throwError(
 					OAuth2ErrorCodes.INVALID_REQUEST,
 					OAuth2ParameterNames.STATE,
-					DEFAULT_ERROR_URI);
+					ERROR_URI);
 		}
 
 		// scope (OPTIONAL)
@@ -104,9 +107,9 @@ public final class OAuth2DeviceAuthorizationConsentAuthenticationConverter imple
 		Map<String, Object> additionalParameters = new HashMap<>();
 		parameters.forEach((key, value) -> {
 			if (!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
+					!key.equals(OAuth2ParameterNames.USER_CODE) &&
 					!key.equals(OAuth2ParameterNames.STATE) &&
-					!key.equals(OAuth2ParameterNames.SCOPE) &&
-					!key.equals(OAuth2ParameterNames.USER_CODE)) {
+					!key.equals(OAuth2ParameterNames.SCOPE)) {
 				additionalParameters.put(key, value.get(0));
 			}
 		});

+ 5 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceCodeAuthenticationConverter.java

@@ -20,6 +20,7 @@ import java.util.Map;
 
 import jakarta.servlet.http.HttpServletRequest;
 
+import org.springframework.lang.Nullable;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -32,7 +33,7 @@ import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 
 /**
- * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the
+ * Attempts to extract a Device Access Token Request from {@link HttpServletRequest} for the
  * OAuth 2.0 Device Authorization Grant and then converts it to an
  * {@link OAuth2DeviceCodeAuthenticationToken} used for authenticating the
  * authorization grant.
@@ -45,6 +46,7 @@ import org.springframework.util.StringUtils;
  */
 public final class OAuth2DeviceCodeAuthenticationConverter implements AuthenticationConverter {
 
+	@Nullable
 	@Override
 	public Authentication convert(HttpServletRequest request) {
 		// grant_type (REQUIRED)
@@ -59,7 +61,8 @@ public final class OAuth2DeviceCodeAuthenticationConverter implements Authentica
 
 		// device_code (REQUIRED)
 		String deviceCode = parameters.getFirst(OAuth2ParameterNames.DEVICE_CODE);
-		if (!StringUtils.hasText(deviceCode) || parameters.get(OAuth2ParameterNames.DEVICE_CODE).size() != 1) {
+		if (!StringUtils.hasText(deviceCode) ||
+				parameters.get(OAuth2ParameterNames.DEVICE_CODE).size() != 1) {
 			OAuth2EndpointUtils.throwError(
 					OAuth2ErrorCodes.INVALID_REQUEST,
 					OAuth2ParameterNames.DEVICE_CODE,

+ 3 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceVerificationAuthenticationConverter.java

@@ -46,7 +46,7 @@ import org.springframework.util.StringUtils;
  */
 public final class OAuth2DeviceVerificationAuthenticationConverter implements AuthenticationConverter {
 
-	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc8628#section-3.3";
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
 	private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
 			"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
 
@@ -64,7 +64,8 @@ public final class OAuth2DeviceVerificationAuthenticationConverter implements Au
 
 		// user_code (REQUIRED)
 		String userCode = parameters.getFirst(OAuth2ParameterNames.USER_CODE);
-		if (!StringUtils.hasText(userCode) || parameters.get(OAuth2ParameterNames.USER_CODE).size() != 1) {
+		if (!StringUtils.hasText(userCode) ||
+				parameters.get(OAuth2ParameterNames.USER_CODE).size() != 1) {
 			OAuth2EndpointUtils.throwError(
 					OAuth2ErrorCodes.INVALID_REQUEST,
 					OAuth2ParameterNames.USER_CODE,

+ 4 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java

@@ -24,12 +24,13 @@ import java.util.Base64;
 import java.util.List;
 import java.util.function.Consumer;
 
-import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.jose.jwk.source.JWKSource;
-import com.nimbusds.jose.proc.SecurityContext;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;

+ 1 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java

@@ -126,7 +126,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
 		assertThat(providerConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/v1/jwks\"");
 		assertThat(providerConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]");
 		assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
-		assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]");
+		assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:device_code\"]");
 		assertThat(providerConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\"");
 		assertThat(providerConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
 		assertThat(providerConfigurationResponse).contains("\"introspection_endpoint\":\"https://example.com/issuer1/oauth2/v1/introspect\"");

+ 1 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettingsTests.java

@@ -36,9 +36,9 @@ public class TokenSettingsTests {
 		TokenSettings tokenSettings = TokenSettings.builder().build();
 		assertThat(tokenSettings.getSettings()).hasSize(7);
 		assertThat(tokenSettings.getAuthorizationCodeTimeToLive()).isEqualTo(Duration.ofMinutes(5));
-		assertThat(tokenSettings.getDeviceCodeTimeToLive()).isEqualTo(Duration.ofMinutes(30));
 		assertThat(tokenSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
 		assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.SELF_CONTAINED);
+		assertThat(tokenSettings.getDeviceCodeTimeToLive()).isEqualTo(Duration.ofMinutes(5));
 		assertThat(tokenSettings.isReuseRefreshTokens()).isTrue();
 		assertThat(tokenSettings.getRefreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
 		assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);

+ 1 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

@@ -122,7 +122,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 		assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
 		assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/v1/jwks\"");
 		assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]");
-		assertThat(authorizationServerMetadataResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:device_code\"]");
 		assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\"");
 		assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
 		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/issuer1/oauth2/v1/introspect\"");

+ 1 - 1
samples/device-client/src/main/java/sample/config/SecurityConfig.java

@@ -28,7 +28,7 @@ import org.springframework.security.web.authentication.LoginUrlAuthenticationEnt
  * @author Steve Riesenberg
  * @since 1.1
  */
-@Configuration
+@Configuration(proxyBeanMethods = false)
 @EnableWebSecurity
 public class SecurityConfig {
 

+ 0 - 2
samples/device-grant-authorizationserver/samples-device-grant-authorizationserver.gradle

@@ -18,9 +18,7 @@ dependencies {
 	implementation "org.springframework.boot:spring-boot-starter-security"
 	implementation "org.springframework.boot:spring-boot-starter-jdbc"
 	implementation project(":spring-security-oauth2-authorization-server")
-	implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
 	implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
-	implementation "org.springframework:spring-webflux"
 	implementation "org.webjars:webjars-locator-core"
 	implementation "org.webjars:bootstrap:3.4.1"
 	implementation "org.webjars:jquery:3.4.1"

+ 4 - 4
samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java

@@ -56,7 +56,7 @@ import org.springframework.security.web.authentication.LoginUrlAuthenticationEnt
  * @author Steve Riesenberg
  * @since 1.1
  */
-@Configuration
+@Configuration(proxyBeanMethods = false)
 @EnableWebSecurity
 public class SecurityConfig {
 
@@ -100,7 +100,7 @@ public class SecurityConfig {
 	public UserDetailsService userDetailsService() {
 		// @formatter:off
 		UserDetails userDetails = User.withDefaultPasswordEncoder()
-				.username("user")
+				.username("user1")
 				.password("password")
 				.roles("USER")
 				.build();
@@ -144,7 +144,7 @@ public class SecurityConfig {
 		return new ImmutableJWKSet<>(jwkSet);
 	}
 
-	private static KeyPair generateRsaKey() { 
+	private static KeyPair generateRsaKey() {
 		KeyPair keyPair;
 		try {
 			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
@@ -167,4 +167,4 @@ public class SecurityConfig {
 		return AuthorizationServerSettings.builder().build();
 	}
 
-}
+}

+ 1 - 1
samples/device-grant-authorizationserver/src/main/java/sample/web/DeviceController.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2020-2023 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.

+ 2 - 2
samples/device-grant-authorizationserver/src/main/resources/templates/activate.html

@@ -12,7 +12,7 @@
             <div class="container">
                 <div class="row">
                     <div class="col-md-8">
-                        <form th:action="@{/oauth2/device_verification}" method="get">
+                        <form th:action="@{/oauth2/device_verification}" method="post">
                             <h2>Device Activation</h2>
                             <p>Enter the activation code to authorize the device.</p>
                             <p class="gap">Activation Code</p>
@@ -30,4 +30,4 @@
             </div>
         </div>
     </body>
-</html>
+</html>