Browse Source

Add support for OAuth 2.0 Device Authorization Grant

Closes gh-44
pull/1134/head
Steve Riesenberg 3 years ago committed by Steve Riesenberg
parent
commit
291ba8c92d
  1. 18
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationService.java
  2. 14
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java
  3. 6
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java
  4. 267
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java
  5. 103
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationToken.java
  6. 274
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationProvider.java
  7. 154
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationRequestAuthenticationToken.java
  8. 259
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java
  9. 59
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationToken.java
  10. 203
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java
  11. 117
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationToken.java
  12. 26
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java
  13. 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java
  14. 229
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java
  15. 283
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java
  16. 9
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java
  17. 40
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java
  18. 16
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java
  19. 26
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettings.java
  20. 155
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DefaultConsentPage.java
  21. 108
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java
  22. 241
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java
  23. 266
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceVerificationEndpointFilter.java
  24. 6
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java
  25. 115
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceAuthorizationConsentAuthenticationConverter.java
  26. 82
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceAuthorizationRequestAuthenticationConverter.java
  27. 81
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceCodeAuthenticationConverter.java
  28. 90
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceVerificationAuthenticationConverter.java
  29. 11
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java
  30. 15
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java
  31. 2
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettingsTests.java
  32. 7
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/TokenSettingsTests.java
  33. 27
      samples/device-client/samples-device-client.gradle
  34. 32
      samples/device-client/src/main/java/sample/DeviceClientApplication.java
  35. 56
      samples/device-client/src/main/java/sample/config/SecurityConfig.java
  36. 71
      samples/device-client/src/main/java/sample/config/WebClientConfig.java
  37. 192
      samples/device-client/src/main/java/sample/web/DeviceController.java
  38. 52
      samples/device-client/src/main/java/sample/web/DeviceControllerAdvice.java
  39. 122
      samples/device-client/src/main/java/sample/web/authentication/DeviceCodeOAuth2AuthorizedClientProvider.java
  40. 85
      samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceAccessTokenResponseClient.java
  41. 41
      samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceGrantRequest.java
  42. 29
      samples/device-client/src/main/resources/application.yml
  43. 13
      samples/device-client/src/main/resources/static/assets/css/style.css
  44. 87
      samples/device-client/src/main/resources/templates/authorize.html
  45. 35
      samples/device-client/src/main/resources/templates/authorized.html
  46. 26
      samples/device-client/src/main/resources/templates/index.html
  47. 37
      samples/device-grant-authorizationserver/samples-device-grant-authorizationserver.gradle
  48. 32
      samples/device-grant-authorizationserver/src/main/java/sample/DeviceGrantAuthorizationServerApplication.java
  49. 170
      samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java
  50. 47
      samples/device-grant-authorizationserver/src/main/java/sample/web/DeviceController.java
  51. 48
      samples/device-grant-authorizationserver/src/main/java/sample/web/DeviceErrorController.java
  52. 6
      samples/device-grant-authorizationserver/src/main/resources/application.yml
  53. 13
      samples/device-grant-authorizationserver/src/main/resources/static/assets/css/style.css
  54. 25
      samples/device-grant-authorizationserver/src/main/resources/templates/access-denied.html
  55. 33
      samples/device-grant-authorizationserver/src/main/resources/templates/activate.html
  56. 25
      samples/device-grant-authorizationserver/src/main/resources/templates/activated.html
  57. 25
      samples/device-grant-authorizationserver/src/main/resources/templates/error.html

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

@ -24,7 +24,9 @@ import java.util.concurrent.ConcurrentHashMap; @@ -24,7 +24,9 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2UserCode;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
@ -164,6 +166,10 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza @@ -164,6 +166,10 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
return matchesIdToken(authorization, token);
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
return matchesRefreshToken(authorization, token);
} else if (OAuth2ParameterNames.DEVICE_CODE.equals(tokenType.getValue())) {
return matchesDeviceCode(authorization, token);
} else if (OAuth2ParameterNames.USER_CODE.equals(tokenType.getValue())) {
return matchesUserCode(authorization, token);
}
return false;
}
@ -196,6 +202,18 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza @@ -196,6 +202,18 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
return idToken != null && idToken.getToken().getTokenValue().equals(token);
}
private static boolean matchesDeviceCode(OAuth2Authorization authorization, String token) {
OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode =
authorization.getToken(OAuth2DeviceCode.class);
return deviceCode != null && deviceCode.getToken().getTokenValue().equals(token);
}
private static boolean matchesUserCode(OAuth2Authorization authorization, String token) {
OAuth2Authorization.Token<OAuth2UserCode> userCode =
authorization.getToken(OAuth2UserCode.class);
return userCode != null && userCode.getToken().getTokenValue().equals(token);
}
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;

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

@ -1,5 +1,5 @@ @@ -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.
@ -253,6 +253,18 @@ public class OAuth2Authorization implements Serializable { @@ -253,6 +253,18 @@ 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.
*/

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

@ -1,5 +1,5 @@ @@ -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.
@ -113,6 +113,10 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA @@ -113,6 +113,10 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA
super(authentication);
}
private Builder(OAuth2DeviceAuthorizationConsentAuthenticationToken authentication) {
super(authentication);
}
/**
* Sets the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
*

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

@ -0,0 +1,267 @@ @@ -0,0 +1,267 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
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;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
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.util.Assert;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Consent
* used in the Device Authorization Grant.
*
* @author Steve Riesenberg
* @since 1.1
* @see OAuth2DeviceAuthorizationConsentAuthenticationToken
* @see OAuth2AuthorizationConsent
* @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
* @see OAuth2DeviceVerificationAuthenticationProvider
* @see OAuth2DeviceCodeAuthenticationProvider
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2AuthorizationConsentService
*/
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 OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private final Log logger = LogFactory.getLog(getClass());
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationConsentService authorizationConsentService;
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
/**
* Constructs an {@code OAuth2DeviceAuthorizationConsentAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param authorizationConsentService the authorization consent service
*/
public OAuth2DeviceAuthorizationConsentAuthenticationProvider(
RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService,
OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.authorizationConsentService = authorizationConsentService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2DeviceAuthorizationConsentAuthenticationToken deviceAuthorizationConsentAuthentication =
(OAuth2DeviceAuthorizationConsentAuthenticationToken) authentication;
OAuth2Authorization authorization = this.authorizationService.findByToken(
deviceAuthorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);
if (authorization == null) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with device authorization consent state");
}
Authentication principal = (Authentication) deviceAuthorizationConsentAuthentication.getPrincipal();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
deviceAuthorizationConsentAuthentication.getClientId());
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
}
if (this.logger.isTraceEnabled()) {
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<>();
if (!requestedScopes.containsAll(authorizedScopes)) {
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated device authorization consent request parameters");
}
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
authorization.getRegisteredClientId(), principal.getName());
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : Collections.emptySet();
if (!currentAuthorizedScopes.isEmpty()) {
for (String requestedScope : requestedScopes) {
if (currentAuthorizedScopes.contains(requestedScope)) {
authorizedScopes.add(requestedScope);
}
}
}
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
if (currentAuthorizationConsent != null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved existing authorization consent");
}
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
} else {
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
authorization.getRegisteredClientId(), principal.getName());
}
authorizedScopes.forEach(authorizationConsentBuilder::scope);
if (this.authorizationConsentCustomizer != null) {
// @formatter:off
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
OAuth2AuthorizationConsentAuthenticationContext.with(deviceAuthorizationConsentAuthentication)
.authorizationConsent(authorizationConsentBuilder)
.registeredClient(registeredClient)
.authorization(authorization)
.authorizationRequest(authorizationRequest)
.build();
// @formatter:on
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Customized authorization consent");
}
}
Set<GrantedAuthority> authorities = new HashSet<>();
authorizationConsentBuilder.authorities(authorities::addAll);
OAuth2Authorization.Token<OAuth2DeviceCode> deviceCodeToken = authorization.getToken(OAuth2DeviceCode.class);
OAuth2Authorization.Token<OAuth2UserCode> userCodeToken = authorization.getToken(OAuth2UserCode.class);
if (authorities.isEmpty()) {
// Authorization consent denied (or revoked)
if (currentAuthorizationConsent != null) {
this.authorizationConsentService.remove(currentAuthorizationConsent);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Revoked authorization consent");
}
}
authorization = OAuth2Authorization.from(authorization)
.token(deviceCodeToken.getToken(), metadata ->
metadata.put(OAuth2Authorization.Token.ACCESS_DENIED_METADATA_NAME, true))
.token(userCodeToken.getToken(), metadata ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
.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);
}
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
this.authorizationConsentService.save(authorizationConsent);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved authorization consent");
}
}
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
.principalName(principal.getName())
.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)
.attributes(attrs -> attrs.remove(OAuth2ParameterNames.STATE))
.build();
this.authorizationService.save(updatedAuthorization);
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");
}
return new OAuth2DeviceVerificationAuthenticationToken(registeredClient.getClientId(), principal,
deviceAuthorizationConsentAuthentication.getUserCode());
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2DeviceAuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext}
* containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information.
*
* <p>
* The following context attributes are available:
* <ul>
* <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
* 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 OAuth2Authorization} associated with the state token presented in the
* authorization consent request.</li>
* <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
* </ul>
*
* @param authorizationConsentCustomizer the {@code Consumer} providing access to the
* {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder}
*/
public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
this.authorizationConsentCustomizer = authorizationConsentCustomizer;
}
private static void throwError(String errorCode, String parameterName) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
throw new OAuth2AuthorizationException(error);
}
}

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

@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;
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
* in the OAuth 2.0 Device Authorization Grant.
*
* @author Steve Riesenberg
* @since 1.1
*/
public class OAuth2DeviceAuthorizationConsentAuthenticationToken extends OAuth2AuthorizationConsentAuthenticationToken {
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
private final String userCode;
private final Set<String> requestedScopes;
/**
* Constructs an {@code OAuth2DeviceAuthorizationConsentAuthenticationToken} using the provided parameters.
*
* @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 state the state
* @param authorizedScopes the authorized scope(s)
* @param additionalParameters the additional parameters
*/
public OAuth2DeviceAuthorizationConsentAuthenticationToken(String authorizationUri, String clientId,
Authentication principal, String userCode, String state, @Nullable Set<String> authorizedScopes,
@Nullable Map<String, Object> additionalParameters) {
super(authorizationUri, clientId, principal, state, authorizedScopes, additionalParameters);
Assert.hasText(userCode, "userCode cannot be empty");
this.userCode = userCode;
this.requestedScopes = null;
setAuthenticated(false);
}
/**
* Constructs an {@code OAuth2DeviceAuthorizationConsentAuthenticationToken} using the provided parameters.
*
* @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 state the state
* @param requestedScopes the requested scope(s)
* @param authorizedScopes the authorized scope(s)
*/
public OAuth2DeviceAuthorizationConsentAuthenticationToken(String authorizationUri, String clientId,
Authentication principal, String userCode, String state, @Nullable Set<String> requestedScopes,
@Nullable Set<String> authorizedScopes) {
super(authorizationUri, clientId, principal, state, authorizedScopes, null);
Assert.hasText(userCode, "userCode cannot be empty");
this.userCode = userCode;
this.requestedScopes = Collections.unmodifiableSet(
requestedScopes != null ?
new HashSet<>(requestedScopes) :
Collections.emptySet());
setAuthenticated(true);
}
/**
* Returns the user code.
*
* @return the user code
*/
public String getUserCode() {
return this.userCode;
}
/**
* Returns the requested scopes.
*
* @return the requested scopes
*/
public Set<String> getRequestedScopes() {
return this.requestedScopes;
}
}

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

@ -0,0 +1,274 @@ @@ -0,0 +1,274 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
import java.time.Instant;
import java.util.Base64;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
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;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
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 Device Authorization Request
* used in the OAuth 2.0 Device Authorization Grant.
*
* @author Steve Riesenberg
* @since 1.1
* @see OAuth2DeviceAuthorizationRequestAuthenticationToken
* @see OAuth2DeviceVerificationAuthenticationProvider
* @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
* @see OAuth2DeviceCodeAuthenticationProvider
* @see OAuth2AuthorizationService
* @see OAuth2TokenGenerator
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0 Device Authorization Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.1">Section 3.1 Device Authorization Request</a>
*/
public final class OAuth2DeviceAuthorizationRequestAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final OAuth2TokenType DEVICE_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.DEVICE_CODE);
private static final OAuth2TokenType USER_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.USER_CODE);
private final Log logger = LogFactory.getLog(getClass());
private final OAuth2AuthorizationService authorizationService;
private OAuth2TokenGenerator<OAuth2DeviceCode> deviceCodeGenerator = new OAuth2DeviceCodeGenerator();
private OAuth2TokenGenerator<OAuth2UserCode> userCodeGenerator = new OAuth2UserCodeGenerator();
/**
* Constructs an {@code OAuth2DeviceAuthorizationRequestAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
*/
public OAuth2DeviceAuthorizationRequestAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthentication =
(OAuth2DeviceAuthorizationRequestAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal =
getAuthenticatedClientElseThrowInvalidClient(deviceAuthorizationRequestAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (this.logger.isTraceEnabled()) {
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);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated device authorization request parameters");
}
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(clientPrincipal)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.authorizationGrant(deviceAuthorizationRequestAuthentication);
// @formatter:on
// Generate a high-entropy string to use as the device code
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(DEVICE_CODE_TOKEN_TYPE).build();
OAuth2DeviceCode deviceCode = this.deviceCodeGenerator.generate(tokenContext);
if (deviceCode == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the device code.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (this.logger.isTraceEnabled()) {
logger.trace("Generated device code");
}
// Generate a low-entropy string to use as the user code
tokenContext = tokenContextBuilder.tokenType(USER_CODE_TOKEN_TYPE).build();
OAuth2UserCode userCode = this.userCodeGenerator.generate(tokenContext);
if (userCode == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the user code.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (this.logger.isTraceEnabled()) {
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())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.token(deviceCode)
.token(userCode)
.attribute(Principal.class.getName(), clientPrincipal)
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
.build();
// @formatter:on
this.authorizationService.save(authorization);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved authorization");
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated device authorization request");
}
return new OAuth2DeviceAuthorizationRequestAuthenticationToken(clientPrincipal, requestedScopes, deviceCode, userCode);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2DeviceAuthorizationRequestAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2DeviceCode}.
*
* @param deviceCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2DeviceCode}
*/
public void setDeviceCodeGenerator(OAuth2TokenGenerator<OAuth2DeviceCode> deviceCodeGenerator) {
Assert.notNull(deviceCodeGenerator, "deviceCodeGenerator cannot be null");
this.deviceCodeGenerator = deviceCodeGenerator;
}
/**
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2UserCode}.
*
* @param userCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2UserCode}
*/
public void setUserCodeGenerator(OAuth2TokenGenerator<OAuth2UserCode> userCodeGenerator) {
Assert.notNull(userCodeGenerator, "userCodeGenerator cannot be null");
this.userCodeGenerator = userCodeGenerator;
}
private static void throwError(String errorCode, String parameterName) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
private static final class OAuth2DeviceCodeGenerator implements OAuth2TokenGenerator<OAuth2DeviceCode> {
private final StringKeyGenerator deviceCodeGenerator =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
@Nullable
@Override
public OAuth2DeviceCode generate(OAuth2TokenContext context) {
if (context.getTokenType() == null ||
!OAuth2ParameterNames.DEVICE_CODE.equals(context.getTokenType().getValue())) {
return null;
}
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getDeviceCodeTimeToLive());
return new OAuth2DeviceCode(this.deviceCodeGenerator.generateKey(), issuedAt, expiresAt);
}
}
private static final class UserCodeStringKeyGenerator implements StringKeyGenerator {
// @formatter:off
private static final char[] VALID_CHARS = {
'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M',
'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'
};
// @formatter:on
private final BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(8);
@Override
public String generateKey() {
byte[] bytes = this.keyGenerator.generateKey();
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
int offset = Math.abs(b % 20);
sb.append(VALID_CHARS[offset]);
}
sb.insert(4, '-');
return sb.toString();
}
}
private static final class OAuth2UserCodeGenerator implements OAuth2TokenGenerator<OAuth2UserCode> {
private final StringKeyGenerator userCodeGenerator = new UserCodeStringKeyGenerator();
@Nullable
@Override
public OAuth2UserCode generate(OAuth2TokenContext context) {
if (context.getTokenType() == null ||
!OAuth2ParameterNames.USER_CODE.equals(context.getTokenType().getValue())) {
return null;
}
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getDeviceCodeTimeToLive());
return new OAuth2UserCode(this.userCodeGenerator.generateKey(), issuedAt, expiresAt);
}
}
}

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

@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.HashSet;
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.core.OAuth2DeviceCode;
import org.springframework.security.oauth2.core.OAuth2UserCode;
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
import org.springframework.util.Assert;
/**
* An {@link Authentication} implementation for the OAuth 2.0 Device Authorization Request
* used in the Device Authorization Grant.
*
* @author Steve Riesenberg
* @since 1.1
* @see AbstractAuthenticationToken
* @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
*/
public class OAuth2DeviceAuthorizationRequestAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
private final Authentication clientPrincipal;
private final String authorizationUri;
private final Set<String> scopes;
private final OAuth2DeviceCode deviceCode;
private final OAuth2UserCode userCode;
private final Map<String, Object> additionalParameters;
/**
* Constructs an {@code OAuth2DeviceAuthorizationRequestAuthenticationToken} using the provided parameters.
*
* @param clientPrincipal the authenticated client principal
* @param authorizationUri the authorization {@code URI}
* @param scopes the requested scope(s)
* @param additionalParameters the additional parameters
*/
public OAuth2DeviceAuthorizationRequestAuthenticationToken(Authentication clientPrincipal, String authorizationUri,
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
this.clientPrincipal = clientPrincipal;
this.authorizationUri = authorizationUri;
this.scopes = Collections.unmodifiableSet(
scopes != null ?
new HashSet<>(scopes) :
Collections.emptySet());
this.additionalParameters = additionalParameters;
this.deviceCode = null;
this.userCode = null;
}
/**
* Constructs an {@code OAuth2DeviceAuthorizationRequestAuthenticationToken} using the provided parameters.
*
* @param clientPrincipal the authenticated client principal
* @param scopes the requested scope(s)
* @param deviceCode the {@link OAuth2DeviceCode}
* @param userCode the {@link OAuth2UserCode}
*/
public OAuth2DeviceAuthorizationRequestAuthenticationToken(Authentication clientPrincipal, @Nullable Set<String> scopes,
OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) {
super(Collections.emptyList());
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
Assert.notNull(deviceCode, "deviceCode cannot be null");
Assert.notNull(userCode, "userCode cannot be null");
this.clientPrincipal = clientPrincipal;
this.scopes = Collections.unmodifiableSet(
scopes != null ?
new HashSet<>(scopes) :
Collections.emptySet());
this.deviceCode = deviceCode;
this.userCode = userCode;
this.authorizationUri = null;
this.additionalParameters = null;
setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.clientPrincipal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the authorization {@code URI}.
*
* @return the authorization {@code URI}.
*/
public String getAuthorizationUri() {
return authorizationUri;
}
/**
* Returns the requested scope(s).
*
* @return the requested scope(s).
*/
public Set<String> getScopes() {
return this.scopes;
}
/**
* Returns the device code.
*
* @return the device code
*/
public OAuth2DeviceCode getDeviceCode() {
return this.deviceCode;
}
/**
* Returns the user code.
*
* @return the user code
*/
public OAuth2UserCode getUserCode() {
return this.userCode;
}
/**
* Returns the additional parameters.
*
* @return the additional parameters
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}

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

@ -0,0 +1,259 @@ @@ -0,0 +1,259 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AuthenticationProvider;
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;
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.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Device Authorization Grant.
*
* @author Steve Riesenberg
* @since 1.1
* @see OAuth2DeviceCodeAuthenticationToken
* @see OAuth2AccessTokenAuthenticationToken
* @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
* @see OAuth2DeviceVerificationAuthenticationProvider
* @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
* @see OAuth2AuthorizationService
* @see OAuth2TokenGenerator
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0 Device Authorization Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.4">Section 3.4 Device Access Token Request</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.5">Section 3.5 Device Access Token Response</a>
*/
public final class OAuth2DeviceCodeAuthenticationProvider implements AuthenticationProvider {
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final String DEVICE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc8628#section-3.5";
private static final OAuth2TokenType DEVICE_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.DEVICE_CODE);
private final Log logger = LogFactory.getLog(getClass());
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
/**
* Constructs an {@code OAuth2DeviceCodeAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
*/
public OAuth2DeviceCodeAuthenticationProvider(
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2DeviceCodeAuthenticationToken deviceCodeAuthentication =
(OAuth2DeviceCodeAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
.getAuthenticatedClientElseThrowInvalidClient(deviceCodeAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved registered client");
}
OAuth2Authorization authorization = this.authorizationService.findByToken(
deviceCodeAuthentication.getDeviceCode(), DEVICE_CODE_TOKEN_TYPE);
if (authorization == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with device code");
}
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationRequest.class.getName());
OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);
if (!registeredClient.getClientId().equals(authorizationRequest.getClientId())) {
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()));
}
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
// 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("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
// client SHOULD repeat the access token request to the token
// endpoint (a process known as polling). Before each new request,
// the client MUST wait at least the number of seconds specified by
// the "interval" parameter of the device authorization response (see
// 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))) {
OAuth2Error error = new OAuth2Error("authorization_pending", null, DEVICE_ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (!deviceCode.isActive()) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated token request parameters");
}
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(authorization.getAttribute(Principal.class.getName()))
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorization(authorization)
.authorizedScopes(authorization.getAuthorizedScopes())
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.authorizationGrant(deviceCodeAuthentication);
// @formatter:on
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
// Invalidate the device code as it can only be used (successfully) once
.token(deviceCode.getToken(), metadata ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
// @formatter:on
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", DEFAULT_ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Generated access token");
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- 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)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", DEFAULT_ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Generated refresh token");
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved authorization");
}
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2DeviceCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}

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

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
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.
*
* @author Steve Riesenberg
* @since 1.1
* @see OAuth2AuthorizationGrantAuthenticationToken
* @see OAuth2DeviceCodeAuthenticationProvider
*/
public class OAuth2DeviceCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private final String deviceCode;
/**
* Constructs an {@code OAuth2DeviceCodeAuthenticationToken} using the provided parameters.
*
* @param deviceCode the device code
* @param clientPrincipal the authenticated client principal
* @param additionalParameters the additional parameters
*/
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;
}
/**
* Returns the device code.
*
* @return the device code
*/
public String getDeviceCode() {
return this.deviceCode;
}
}

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

@ -0,0 +1,203 @@ @@ -0,0 +1,203 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
import java.util.Base64;
import java.util.Set;
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;
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;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
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.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.
*
* @author Steve Riesenberg
* @since 1.1
* @see OAuth2DeviceVerificationAuthenticationToken
* @see OAuth2AuthorizationConsent
* @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
* @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
* @see OAuth2DeviceCodeAuthenticationProvider
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2AuthorizationConsentService
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0 Device Authorization Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.3">Section 3.3 User Interaction</a>
*/
public final class OAuth2DeviceVerificationAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType USER_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.USER_CODE);
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final Log logger = LogFactory.getLog(getClass());
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationConsentService authorizationConsentService;
/**
* Constructs an {@code OAuth2DeviceVerificationAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param authorizationConsentService the authorization consent service
*/
public OAuth2DeviceVerificationAuthenticationProvider(
RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService,
OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.authorizationConsentService = authorizationConsentService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2DeviceVerificationAuthenticationToken deviceVerificationAuthentication =
(OAuth2DeviceVerificationAuthenticationToken) authentication;
OAuth2Authorization authorization = this.authorizationService.findByToken(
deviceVerificationAuthentication.getUserCode(), USER_CODE_TOKEN_TYPE);
if (authorization == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (this.logger.isTraceEnabled()) {
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");
}
// Return the authorization request as-is where isAuthenticated() is false
return deviceVerificationAuthentication;
}
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
registeredClient.getId(), principal.getName());
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : null;
if (requiresAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
String state = DEFAULT_STATE_GENERATOR.generateKey();
authorization = OAuth2Authorization.from(authorization)
.attribute(OAuth2ParameterNames.STATE, state)
.build();
if (this.logger.isTraceEnabled()) {
logger.trace("Generated authorization consent state");
}
this.authorizationService.save(authorization);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved authorization");
}
return new OAuth2DeviceAuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
registeredClient.getClientId(), principal, deviceVerificationAuthentication.getUserCode(), state,
authorizationRequest.getScopes(), currentAuthorizedScopes);
}
OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode = authorization.getToken(OAuth2DeviceCode.class);
OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
.principalName(principal.getName())
.authorizedScopes(currentAuthorizedScopes)
.token(deviceCode.getToken(), metadata -> metadata
.put(OAuth2Authorization.Token.ACCESS_GRANTED_METADATA_NAME, true))
.token(userCode.getToken(), metadata -> metadata
.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
.attribute(Principal.class.getName(), principal)
.attributes(attrs -> attrs.remove(OAuth2ParameterNames.STATE))
.build();
this.authorizationService.save(updatedAuthorization);
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");
}
return new OAuth2DeviceVerificationAuthenticationToken(registeredClient.getClientId(), principal,
deviceVerificationAuthentication.getUserCode());
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2DeviceVerificationAuthenticationToken.class.isAssignableFrom(authentication);
}
private static boolean requiresAuthorizationConsent(RegisteredClient registeredClient,
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {
return false;
}
if (authorizationConsent != null &&
authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
return false;
}
return true;
}
private static boolean isPrincipalAuthenticated(Authentication principal) {
return principal != null &&
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
principal.isAuthenticated();
}
}

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

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.Map;
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 Verification {@code URI}
* (submission of the user code) used in the OAuth 2.0 Device Authorization Grant.
*
* @author Steve Riesenberg
* @since 1.1
* @see AbstractAuthenticationToken
* @see OAuth2DeviceVerificationAuthenticationProvider
*/
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;
/**
* 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 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;
this.principal = principal;
this.userCode = userCode;
this.additionalParameters = additionalParameters;
}
/**
* 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
*/
public OAuth2DeviceVerificationAuthenticationToken(String clientId, Authentication principal, String userCode) {
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;
this.principal = principal;
this.userCode = userCode;
this.additionalParameters = null;
setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the client identifier.
*
* @return the client identifier
*/
public String getClientId() {
return this.clientId;
}
/**
* Returns the user code.
*
* @return the user code
*/
public String getUserCode() {
return this.userCode;
}
/**
* Returns the additional parameters.
*
* @return the additional parameters, or an empty {@code Map} if not available
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}

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

@ -210,6 +210,30 @@ public final class OAuth2AuthorizationServerConfigurer @@ -210,6 +210,30 @@ public final class OAuth2AuthorizationServerConfigurer
return this;
}
/**
* Configures the OAuth 2.0 Device Authorization Endpoint.
*
* @param deviceAuthorizationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2DeviceAuthorizationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
* @since 1.1
*/
public OAuth2AuthorizationServerConfigurer deviceAuthorizationEndpoint(Customizer<OAuth2DeviceAuthorizationEndpointConfigurer> deviceAuthorizationEndpointCustomizer) {
deviceAuthorizationEndpointCustomizer.customize(getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Device Verification Endpoint.
*
* @param deviceVerificationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2DeviceVerificationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
* @since 1.1
*/
public OAuth2AuthorizationServerConfigurer deviceVerificationEndpoint(Customizer<OAuth2DeviceVerificationEndpointConfigurer> deviceVerificationEndpointCustomizer) {
deviceVerificationEndpointCustomizer.customize(getConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class));
return this;
}
/**
* Configures OpenID Connect 1.0 support (disabled by default).
*
@ -326,6 +350,8 @@ public final class OAuth2AuthorizationServerConfigurer @@ -326,6 +350,8 @@ public final class OAuth2AuthorizationServerConfigurer
configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenRevocationEndpointConfigurer.class, new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
configurers.put(OAuth2DeviceAuthorizationEndpointConfigurer.class, new OAuth2DeviceAuthorizationEndpointConfigurer(this::postProcess));
configurers.put(OAuth2DeviceVerificationEndpointConfigurer.class, new OAuth2DeviceVerificationEndpointConfigurer(this::postProcess));
return configurers;
}

5
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java

@ -1,5 +1,5 @@ @@ -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.
@ -168,6 +168,9 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co @@ -168,6 +168,9 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
HttpMethod.POST.name()),
new AntPathRequestMatcher(
authorizationServerSettings.getTokenRevocationEndpoint(),
HttpMethod.POST.name()),
new AntPathRequestMatcher(
authorizationServerSettings.getDeviceAuthorizationEndpoint(),
HttpMethod.POST.name()));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);

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

@ -0,0 +1,229 @@ @@ -0,0 +1,229 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
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.OAuth2Error;
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;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceAuthorizationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationRequestAuthenticationConverter;
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.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Device Authorization Endpoint.
*
* @author Steve Riesenberg
* @since 1.1
* @see OAuth2AuthorizationServerConfigurer#deviceAuthorizationEndpoint
* @see OAuth2DeviceAuthorizationEndpointFilter
*/
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<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
private AuthenticationSuccessHandler deviceAuthorizationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private String verificationUri;
/**
* Restrict for internal use only.
*/
OAuth2DeviceAuthorizationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* 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 deviceAuthorizationRequestConverter the {@link AuthenticationConverter} used when attempting to extract a Device Authorization Request from {@link HttpServletRequest}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverter(AuthenticationConverter deviceAuthorizationRequestConverter) {
Assert.notNull(deviceAuthorizationRequestConverter, "deviceAuthorizationRequestConverter cannot be null");
this.authenticationConverters.add(deviceAuthorizationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default
* and (optionally) added {@link #deviceAuthorizationRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
*
* @param deviceAuthorizationRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationRequestConverters(
Consumer<List<AuthenticationConverter>> deviceAuthorizationRequestConvertersConsumer) {
Assert.notNull(deviceAuthorizationRequestConvertersConsumer, "deviceAuthorizationRequestConvertersConsumer cannot be null");
this.authenticationConvertersConsumer = deviceAuthorizationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default
* and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s
* allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}.
*
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* and returning the Device Authorization Response.
*
* @param deviceAuthorizationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationResponseHandler(AuthenticationSuccessHandler deviceAuthorizationResponseHandler) {
this.deviceAuthorizationResponseHandler = deviceAuthorizationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Sets the end-user verification {@code URI} on the authorization server.
*
* @param verificationUri the end-user verification {@code URI} on the authorization server
* @return the {@link OAuth2DeviceAuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceAuthorizationEndpointConfigurer verificationUri(String verificationUri) {
this.verificationUri = verificationUri;
return this;
}
@Override
public void init(HttpSecurity builder) {
AuthorizationServerSettings authorizationServerSettings =
OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
this.requestMatcher = new AntPathRequestMatcher(
authorizationServerSettings.getDeviceAuthorizationEndpoint(), HttpMethod.POST.name());
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(authenticationProvider ->
builder.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
public void configure(HttpSecurity builder) {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
OAuth2DeviceAuthorizationEndpointFilter deviceAuthorizationEndpointFilter =
new OAuth2DeviceAuthorizationEndpointFilter(
authenticationManager, authorizationServerSettings.getDeviceAuthorizationEndpoint());
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.authenticationConverters.isEmpty()) {
authenticationConverters.addAll(0, this.authenticationConverters);
}
this.authenticationConvertersConsumer.accept(authenticationConverters);
deviceAuthorizationEndpointFilter.setAuthenticationConverter(
new DelegatingAuthenticationConverter(authenticationConverters));
if (this.deviceAuthorizationResponseHandler != null) {
deviceAuthorizationEndpointFilter.setAuthenticationSuccessHandler(this.deviceAuthorizationResponseHandler);
}
if (this.errorResponseHandler != null) {
deviceAuthorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
if (this.verificationUri != null) {
deviceAuthorizationEndpointFilter.setVerificationUri(this.verificationUri);
}
builder.addFilterAfter(postProcess(deviceAuthorizationEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2DeviceAuthorizationRequestAuthenticationConverter());
return authenticationConverters;
}
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
OAuth2DeviceAuthorizationRequestAuthenticationProvider deviceAuthorizationRequestAuthenticationProvider =
new OAuth2DeviceAuthorizationRequestAuthenticationProvider(authorizationService);
authenticationProviders.add(deviceAuthorizationRequestAuthenticationProvider);
return authenticationProviders;
}
}

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

@ -0,0 +1,283 @@ @@ -0,0 +1,283 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
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.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;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter;
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.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
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 Verification Endpoint.
*
* @author Steve Riesenberg
* @since 1.1
* @see OAuth2AuthorizationServerConfigurer#deviceVerificationEndpoint
* @see OAuth2DeviceVerificationEndpointFilter
*/
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<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
private AuthenticationSuccessHandler deviceVerificationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private String consentPage;
/**
* Restrict for internal use only.
*/
OAuth2DeviceVerificationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract a Device Verification Request (or 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}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverter(AuthenticationConverter deviceVerificationRequestConverter) {
Assert.notNull(deviceVerificationRequestConverter, "deviceVerificationRequestConverter cannot be null");
this.authenticationConverters.add(deviceVerificationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default
* and (optionally) added {@link #deviceVerificationRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
*
* @param deviceVerificationRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationRequestConverters(
Consumer<List<AuthenticationConverter>> deviceVerificationRequestConvertersConsumer) {
Assert.notNull(deviceVerificationRequestConvertersConsumer, "deviceVerificationRequestConvertersConsumer cannot be null");
this.authenticationConvertersConsumer = deviceVerificationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceVerificationAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2DeviceVerificationAuthenticationToken}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default
* and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s
* allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}.
*
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationConsentAuthenticationToken}
* and returning the response.
*
* @param deviceVerificationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer deviceVerificationResponseHandler(AuthenticationSuccessHandler deviceVerificationResponseHandler) {
this.deviceVerificationResponseHandler = deviceVerificationResponseHandler;
return this;
}
/**
* 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 OAuth2AuthenticationException}
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Specify the URI to redirect Resource Owners to if consent is required during
* the {@code device_code} flow. A default consent page will be generated when
* this attribute is not specified.
*
* If a URI is specified, applications are required to process the specified URI to generate
* a consent page. The query string will contain the following parameters:
*
* <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 state} - a CSRF protection token</li>
* <li>@code code} - the user code</li>
* </ul>
*
* In general, the consent page should create a form that submits
* a request with the following requirements:
*
* <ul>
* <li>It must be an HTTP POST</li>
* <li>It must be submitted to {@link AuthorizationServerSettings#getDeviceVerificationEndpoint()}</li>
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
* <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>
* </ul>
*
* @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
* @return the {@link OAuth2DeviceVerificationEndpointConfigurer} for further configuration
*/
public OAuth2DeviceVerificationEndpointConfigurer consentPage(String consentPage) {
this.consentPage = consentPage;
return this;
}
@Override
public void init(HttpSecurity builder) {
AuthorizationServerSettings authorizationServerSettings =
OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(
authorizationServerSettings.getDeviceVerificationEndpoint(), HttpMethod.GET.name()),
new AntPathRequestMatcher(
authorizationServerSettings.getDeviceVerificationEndpoint(), HttpMethod.POST.name()));
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(authenticationProvider ->
builder.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
public void configure(HttpSecurity builder) {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings =
OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter =
new OAuth2DeviceVerificationEndpointFilter(
authenticationManager, authorizationServerSettings.getDeviceVerificationEndpoint());
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.authenticationConverters.isEmpty()) {
authenticationConverters.addAll(0, this.authenticationConverters);
}
this.authenticationConvertersConsumer.accept(authenticationConverters);
deviceVerificationEndpointFilter.setAuthenticationConverter(
new DelegatingAuthenticationConverter(authenticationConverters));
if (this.deviceVerificationResponseHandler != null) {
deviceVerificationEndpointFilter.setAuthenticationSuccessHandler(this.deviceVerificationResponseHandler);
}
if (this.errorResponseHandler != null) {
deviceVerificationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
if (StringUtils.hasText(this.consentPage)) {
deviceVerificationEndpointFilter.setConsentPage(this.consentPage);
}
builder.addFilterAfter(postProcess(deviceVerificationEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2DeviceVerificationAuthenticationConverter());
authenticationConverters.add(new OAuth2DeviceAuthorizationConsentAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity builder) {
RegisteredClientRepository registeredClientRepository =
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder);
OAuth2AuthorizationService authorizationService =
OAuth2ConfigurerUtils.getAuthorizationService(builder);
OAuth2AuthorizationConsentService authorizationConsentService =
OAuth2ConfigurerUtils.getAuthorizationConsentService(builder);
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
// @formatter:off
OAuth2DeviceVerificationAuthenticationProvider deviceVerificationAuthenticationProvider =
new OAuth2DeviceVerificationAuthenticationProvider(
registeredClientRepository, authorizationService, authorizationConsentService);
// @formatter:on
authenticationProviders.add(deviceVerificationAuthenticationProvider);
// @formatter:off
OAuth2DeviceAuthorizationConsentAuthenticationProvider deviceAuthorizationConsentAuthenticationProvider =
new OAuth2DeviceAuthorizationConsentAuthenticationProvider(
registeredClientRepository, authorizationService, authorizationConsentService);
// @formatter:on
authenticationProviders.add(deviceAuthorizationConsentAuthenticationProvider);
return authenticationProviders;
}
}

9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java

@ -1,5 +1,5 @@ @@ -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.
@ -36,6 +36,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O @@ -36,6 +36,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.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
@ -43,6 +44,7 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenE @@ -43,6 +44,7 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenE
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
@ -208,6 +210,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure @@ -208,6 +210,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
authenticationConverters.add(new OAuth2AuthorizationCodeAuthenticationConverter());
authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter());
authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter());
authenticationConverters.add(new OAuth2DeviceCodeAuthenticationConverter());
return authenticationConverters;
}
@ -232,6 +235,10 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure @@ -232,6 +235,10 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
new OAuth2ClientCredentialsAuthenticationProvider(authorizationService, tokenGenerator);
authenticationProviders.add(clientCredentialsAuthenticationProvider);
OAuth2DeviceCodeAuthenticationProvider deviceCodeAuthenticationProvider =
new OAuth2DeviceCodeAuthenticationProvider(authorizationService, tokenGenerator);
authenticationProviders.add(deviceCodeAuthenticationProvider);
return authenticationProviders;
}

40
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java

@ -52,6 +52,24 @@ public final class AuthorizationServerSettings extends AbstractSettings { @@ -52,6 +52,24 @@ public final class AuthorizationServerSettings extends AbstractSettings {
return getSetting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT);
}
/**
* Returns the OAuth 2.0 Device Authorization endpoint. The default is {@code /oauth2/device_authorization}.
*
* @return the Authorization endpoint
*/
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}.
*
* @return the Authorization endpoint
*/
public String getDeviceVerificationEndpoint() {
return getSetting(ConfigurationSettingNames.AuthorizationServer.DEVICE_VERIFICATION_ENDPOINT);
}
/**
* Returns the OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
*
@ -124,6 +142,8 @@ public final class AuthorizationServerSettings extends AbstractSettings { @@ -124,6 +142,8 @@ public final class AuthorizationServerSettings extends AbstractSettings {
public static Builder builder() {
return new Builder()
.authorizationEndpoint("/oauth2/authorize")
.deviceAuthorizationEndpoint("/oauth2/device_authorization")
.deviceVerificationEndpoint("/oauth2/device_verification")
.tokenEndpoint("/oauth2/token")
.jwkSetEndpoint("/oauth2/jwks")
.tokenRevocationEndpoint("/oauth2/revoke")
@ -173,6 +193,26 @@ public final class AuthorizationServerSettings extends AbstractSettings { @@ -173,6 +193,26 @@ public final class AuthorizationServerSettings extends AbstractSettings {
return setting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
}
/**
* Sets the OAuth 2.0 Device Authorization endpoint.
*
* @param deviceAuthorizationEndpoint the Device Authorization endpoint
* @return the {@link Builder} for further configuration
*/
public Builder deviceAuthorizationEndpoint(String deviceAuthorizationEndpoint) {
return setting(ConfigurationSettingNames.AuthorizationServer.DEVICE_AUTHORIZATION_ENDPOINT, deviceAuthorizationEndpoint);
}
/**
* Sets the OAuth 2.0 Device Verification endpoint.
*
* @param deviceVerificationEndpoint the Device Verification endpoint
* @return the {@link Builder} for further configuration
*/
public Builder deviceVerificationEndpoint(String deviceVerificationEndpoint) {
return setting(ConfigurationSettingNames.AuthorizationServer.DEVICE_VERIFICATION_ENDPOINT, deviceVerificationEndpoint);
}
/**
* Sets the OAuth 2.0 Token endpoint.
*

16
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java

@ -86,6 +86,16 @@ public final class ConfigurationSettingNames { @@ -86,6 +86,16 @@ public final class ConfigurationSettingNames {
*/
public static final String AUTHORIZATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("authorization-endpoint");
/**
* Set the OAuth 2.0 Device Authorization endpoint.
*/
public static final String DEVICE_AUTHORIZATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("device-authorization-endpoint");
/**
* Set the OAuth 2.0 Device Verification endpoint.
*/
public static final String DEVICE_VERIFICATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("device-verification-endpoint");
/**
* Set the OAuth 2.0 Token endpoint.
*/
@ -150,6 +160,12 @@ public final class ConfigurationSettingNames { @@ -150,6 +160,12 @@ public final class ConfigurationSettingNames {
*/
public static final String ACCESS_TOKEN_FORMAT = TOKEN_SETTINGS_NAMESPACE.concat("access-token-format");
/**
* Set the time-to-live for a device code.
* @since 1.1
*/
public static final String DEVICE_CODE_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("device-code-time-to-live");
/**
* Set to {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued.

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

@ -1,5 +1,5 @@ @@ -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.
@ -66,6 +66,16 @@ public final class TokenSettings extends AbstractSettings { @@ -66,6 +66,16 @@ public final class TokenSettings extends AbstractSettings {
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT);
}
/**
* Returns the time-to-live for a device code. The default is 30 minutes.
*
* @return the time-to-live for an authorization code
* @since 1.1
*/
public Duration getDeviceCodeTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.DEVICE_CODE_TIME_TO_LIVE);
}
/**
* Returns {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued. The default is {@code true}.
@ -103,6 +113,7 @@ public final class TokenSettings extends AbstractSettings { @@ -103,6 +113,7 @@ public final class TokenSettings extends AbstractSettings {
.authorizationCodeTimeToLive(Duration.ofMinutes(5))
.accessTokenTimeToLive(Duration.ofMinutes(5))
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.deviceCodeTimeToLive(Duration.ofMinutes(30))
.reuseRefreshTokens(true)
.refreshTokenTimeToLive(Duration.ofMinutes(60))
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);
@ -166,6 +177,19 @@ public final class TokenSettings extends AbstractSettings { @@ -166,6 +177,19 @@ public final class TokenSettings extends AbstractSettings {
return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT, accessTokenFormat);
}
/**
* Set the time-to-live for a device code. Must be greater than {@code Duration.ZERO}.
*
* @param deviceCodeTimeToLive the time-to-live for a device code
* @return the {@link Builder} for further configuration
* @since 1.1
*/
public Builder deviceCodeTimeToLive(Duration deviceCodeTimeToLive) {
Assert.notNull(deviceCodeTimeToLive, "deviceCodeTimeToLive cannot be null");
Assert.isTrue(deviceCodeTimeToLive.getSeconds() > 0, "deviceCodeTimeToLive must be greater than Duration.ZERO");
return setting(ConfigurationSettingNames.Token.DEVICE_CODE_TIME_TO_LIVE, deviceCodeTimeToLive);
}
/**
* Set to {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued.

155
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DefaultConsentPage.java

@ -0,0 +1,155 @@ @@ -0,0 +1,155 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
/**
* For internal use only.
*/
class DefaultConsentPage {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private DefaultConsentPage() {
}
static void displayConsent(HttpServletRequest request, HttpServletResponse response, String clientId,
Authentication principal, Set<String> requestedScopes, Set<String> authorizedScopes, String state,
Map<String, String> additionalParameters) throws IOException {
String consentPage = generateConsentPage(request, clientId, principal, requestedScopes, authorizedScopes, state, additionalParameters);
response.setContentType(TEXT_HTML_UTF8.toString());
response.setContentLength(consentPage.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(consentPage);
}
private static String generateConsentPage(HttpServletRequest request,
String clientId, Authentication principal, Set<String> requestedScopes, Set<String> authorizedScopes, String state,
Map<String, String> additionalParameters) {
Set<String> scopesToAuthorize = new HashSet<>();
Set<String> scopesPreviouslyAuthorized = new HashSet<>();
for (String scope : requestedScopes) {
if (authorizedScopes.contains(scope)) {
scopesPreviouslyAuthorized.add(scope);
} else if (!scope.equals(OidcScopes.OPENID)) { // openid scope does not require consent
scopesToAuthorize.add(scope);
}
}
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.3.1
// The server SHOULD display
// the "user_code" to the user and ask them to verify that it matches
// the "user_code" being displayed on the device to confirm they are
// authorizing the correct device.
String userCode = additionalParameters.get(OAuth2ParameterNames.USER_CODE);
StringBuilder builder = new StringBuilder();
builder.append("<!DOCTYPE html>");
builder.append("<html lang=\"en\">");
builder.append("<head>");
builder.append(" <meta charset=\"utf-8\">");
builder.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">");
builder.append(" <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\" integrity=\"sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z\" crossorigin=\"anonymous\">");
builder.append(" <title>Consent required</title>");
builder.append(" <script>");
builder.append(" function cancelConsent() {");
builder.append(" document.consent_form.reset();");
builder.append(" document.consent_form.submit();");
builder.append(" }");
builder.append(" </script>");
builder.append("</head>");
builder.append("<body>");
builder.append("<div class=\"container\">");
builder.append(" <div class=\"py-5\">");
builder.append(" <h1 class=\"text-center\">Consent required</h1>");
builder.append(" </div>");
builder.append(" <div class=\"row\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <p><span class=\"font-weight-bold text-primary\">" + clientId + "</span> wants to access your account <span class=\"font-weight-bold\">" + principal.getName() + "</span></p>");
builder.append(" </div>");
builder.append(" </div>");
if (userCode != null) {
builder.append(" <div class=\"row\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <p class=\"alert alert-warning\">You have provided the code <span class=\"font-weight-bold\">" + userCode + "</span>. Verify that this code matches what is shown on your device.</p>");
builder.append(" </div>");
builder.append(" </div>");
}
builder.append(" <div class=\"row pb-3\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <p>The following permissions are requested by the above app.<br/>Please review these and consent if you approve.</p>");
builder.append(" </div>");
builder.append(" </div>");
builder.append(" <div class=\"row\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <form name=\"consent_form\" method=\"post\" action=\"" + request.getRequestURI() + "\">");
builder.append(" <input type=\"hidden\" name=\"client_id\" value=\"" + clientId + "\">");
builder.append(" <input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
if (userCode != null) {
builder.append(" <input type=\"hidden\" name=\"user_code\" value=\"" + userCode + "\">");
}
for (String scope : scopesToAuthorize) {
builder.append(" <div class=\"form-group form-check py-1\">");
builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"" + scope + "\" id=\"" + scope + "\">");
builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
builder.append(" </div>");
}
if (!scopesPreviouslyAuthorized.isEmpty()) {
builder.append(" <p>You have already granted the following permissions to the above app:</p>");
for (String scope : scopesPreviouslyAuthorized) {
builder.append(" <div class=\"form-group form-check py-1\">");
builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" id=\"" + scope + "\" checked disabled>");
builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
builder.append(" </div>");
}
}
builder.append(" <div class=\"form-group pt-3\">");
builder.append(" <button class=\"btn btn-primary btn-lg\" type=\"submit\" id=\"submit-consent\">Submit Consent</button>");
builder.append(" </div>");
builder.append(" <div class=\"form-group\">");
builder.append(" <button class=\"btn btn-link regular\" type=\"button\" onclick=\"cancelConsent();\" id=\"cancel-consent\">Cancel</button>");
builder.append(" </div>");
builder.append(" </form>");
builder.append(" </div>");
builder.append(" </div>");
builder.append(" <div class=\"row pt-4\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <p><small>Your consent to provide access is required.<br/>If you do not approve, click Cancel, in which case no information will be shared with the app.</small></p>");
builder.append(" </div>");
builder.append(" </div>");
builder.append("</div>");
builder.append("</body>");
builder.append("</html>");
return builder.toString();
}
}

108
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java

@ -18,7 +18,7 @@ package org.springframework.security.oauth2.server.authorization.web; @@ -18,7 +18,7 @@ package org.springframework.security.oauth2.server.authorization.web;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Collections;
import java.util.Set;
import jakarta.servlet.FilterChain;
@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletResponse; @@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
@ -288,7 +287,7 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte @@ -288,7 +287,7 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
if (this.logger.isTraceEnabled()) {
this.logger.trace("Displaying generated consent screen");
}
DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes, state);
DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes, state, Collections.emptyMap());
}
}
@ -367,107 +366,4 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte @@ -367,107 +366,4 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
this.redirectStrategy.sendRedirect(request, response, redirectUri);
}
/**
* For internal use only.
*/
private static class DefaultConsentPage {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static void displayConsent(HttpServletRequest request, HttpServletResponse response,
String clientId, Authentication principal, Set<String> requestedScopes, Set<String> authorizedScopes, String state)
throws IOException {
String consentPage = generateConsentPage(request, clientId, principal, requestedScopes, authorizedScopes, state);
response.setContentType(TEXT_HTML_UTF8.toString());
response.setContentLength(consentPage.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(consentPage);
}
private static String generateConsentPage(HttpServletRequest request,
String clientId, Authentication principal, Set<String> requestedScopes, Set<String> authorizedScopes, String state) {
Set<String> scopesToAuthorize = new HashSet<>();
Set<String> scopesPreviouslyAuthorized = new HashSet<>();
for (String scope : requestedScopes) {
if (authorizedScopes.contains(scope)) {
scopesPreviouslyAuthorized.add(scope);
} else if (!scope.equals(OidcScopes.OPENID)) { // openid scope does not require consent
scopesToAuthorize.add(scope);
}
}
StringBuilder builder = new StringBuilder();
builder.append("<!DOCTYPE html>");
builder.append("<html lang=\"en\">");
builder.append("<head>");
builder.append(" <meta charset=\"utf-8\">");
builder.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">");
builder.append(" <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\" integrity=\"sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z\" crossorigin=\"anonymous\">");
builder.append(" <title>Consent required</title>");
builder.append(" <script>");
builder.append(" function cancelConsent() {");
builder.append(" document.consent_form.reset();");
builder.append(" document.consent_form.submit();");
builder.append(" }");
builder.append(" </script>");
builder.append("</head>");
builder.append("<body>");
builder.append("<div class=\"container\">");
builder.append(" <div class=\"py-5\">");
builder.append(" <h1 class=\"text-center\">Consent required</h1>");
builder.append(" </div>");
builder.append(" <div class=\"row\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <p><span class=\"font-weight-bold text-primary\">" + clientId + "</span> wants to access your account <span class=\"font-weight-bold\">" + principal.getName() + "</span></p>");
builder.append(" </div>");
builder.append(" </div>");
builder.append(" <div class=\"row pb-3\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <p>The following permissions are requested by the above app.<br/>Please review these and consent if you approve.</p>");
builder.append(" </div>");
builder.append(" </div>");
builder.append(" <div class=\"row\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <form name=\"consent_form\" method=\"post\" action=\"" + request.getRequestURI() + "\">");
builder.append(" <input type=\"hidden\" name=\"client_id\" value=\"" + clientId + "\">");
builder.append(" <input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
for (String scope : scopesToAuthorize) {
builder.append(" <div class=\"form-group form-check py-1\">");
builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"" + scope + "\" id=\"" + scope + "\">");
builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
builder.append(" </div>");
}
if (!scopesPreviouslyAuthorized.isEmpty()) {
builder.append(" <p>You have already granted the following permissions to the above app:</p>");
for (String scope : scopesPreviouslyAuthorized) {
builder.append(" <div class=\"form-group form-check py-1\">");
builder.append(" <input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" id=\"" + scope + "\" checked disabled>");
builder.append(" <label class=\"form-check-label\" for=\"" + scope + "\">" + scope + "</label>");
builder.append(" </div>");
}
}
builder.append(" <div class=\"form-group pt-3\">");
builder.append(" <button class=\"btn btn-primary btn-lg\" type=\"submit\" id=\"submit-consent\">Submit Consent</button>");
builder.append(" </div>");
builder.append(" <div class=\"form-group\">");
builder.append(" <button class=\"btn btn-link regular\" type=\"button\" onclick=\"cancelConsent();\" id=\"cancel-consent\">Cancel</button>");
builder.append(" </div>");
builder.append(" </form>");
builder.append(" </div>");
builder.append(" </div>");
builder.append(" <div class=\"row pt-4\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <p><small>Your consent to provide access is required.<br/>If you do not approve, click Cancel, in which case no information will be shared with the app.</small></p>");
builder.append(" </div>");
builder.append(" </div>");
builder.append("</div>");
builder.append("</body>");
builder.append("</html>");
return builder.toString();
}
}
}

241
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java

@ -0,0 +1,241 @@ @@ -0,0 +1,241 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.log.LogMessage;
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.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2UserCode;
import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse;
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;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationRequestAuthenticationConverter;
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.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
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 OAuth 2.0 Device Authorization Request.
*
* @author Steve Riesenberg
* @since 1.1
* @see AuthenticationManager
* @see OAuth2DeviceAuthorizationRequestAuthenticationConverter
* @see OAuth2DeviceAuthorizationRequestAuthenticationProvider
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0 Device Authorization Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.1">Section 3.1 Device Authorization Request</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.2">Section 3.2 Device Authorization Response</a>
*/
public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerRequestFilter {
private static final String DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI = "/oauth2/device_authorize";
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 AuthenticationSuccessHandler authenticationSuccessHandler = this::sendDeviceAuthorizationResponse;
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
private String verificationUri = DEFAULT_DEVICE_VERIFICATION_URI;
/**
* Constructs an {@code OAuth2DeviceAuthorizationEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
*/
public OAuth2DeviceAuthorizationEndpointFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI);
}
/**
* Constructs an {@code OAuth2DeviceAuthorizationEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
* @param deviceAuthorizationEndpointUri the endpoint {@code URI} for device authorization requests
*/
public OAuth2DeviceAuthorizationEndpointFilter(AuthenticationManager authenticationManager, String deviceAuthorizationEndpointUri) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(deviceAuthorizationEndpointUri, "deviceAuthorizationEndpointUri cannot be empty");
this.authenticationManager = authenticationManager;
this.deviceAuthorizationEndpointMatcher = new AntPathRequestMatcher(deviceAuthorizationEndpointUri,
HttpMethod.POST.name());
this.authenticationConverter = new OAuth2DeviceAuthorizationRequestAuthenticationConverter();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.deviceAuthorizationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthenticationToken =
(OAuth2DeviceAuthorizationRequestAuthenticationToken) this.authenticationConverter.convert(request);
deviceAuthorizationRequestAuthenticationToken.setDetails(
this.authenticationDetailsSource.buildDetails(request));
OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthenticationTokenResult =
(OAuth2DeviceAuthorizationRequestAuthenticationToken) this.authenticationManager.authenticate(
deviceAuthorizationRequestAuthenticationToken);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response,
deviceAuthorizationRequestAuthenticationTokenResult);
} catch (OAuth2AuthenticationException ex) {
SecurityContextHolder.clearContext();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Device authorization request failed: %s", ex.getError()), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
/**
* 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}.
*
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}
*/
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* and returning the Device Authorization Response.
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
*/
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceAuthorizationRequestAuthenticationToken}
* and returning the {@link OAuth2Error Error Response}.
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
*/
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
/**
* Sets the end-user verification {@code URI} on the authorization server.
*
* @param verificationUri the end-user verification {@code URI} on the authorization server
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.2">Section 3.2 Device Authorization Response</a>
*/
public void setVerificationUri(String verificationUri) {
Assert.hasText(verificationUri, "verificationUri cannot be empty");
this.verificationUri = verificationUri;
}
private void sendDeviceAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2DeviceAuthorizationRequestAuthenticationToken deviceAuthorizationRequestAuthenticationToken =
(OAuth2DeviceAuthorizationRequestAuthenticationToken) authentication;
OAuth2DeviceCode deviceCode = deviceAuthorizationRequestAuthenticationToken.getDeviceCode();
OAuth2UserCode userCode = deviceAuthorizationRequestAuthenticationToken.getUserCode();
// Generate the fully-qualified verification URI
String issuerUri = AuthorizationServerContextHolder.getContext().getIssuer();
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(issuerUri)
.path(this.verificationUri);
String verificationUri = uriComponentsBuilder.build().toUriString();
// @formatter:off
String verificationUriComplete = uriComponentsBuilder
.queryParam(OAuth2ParameterNames.USER_CODE, userCode.getTokenValue())
.build().toUriString();
// @formatter:on
// @formatter:off
OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse =
OAuth2DeviceAuthorizationResponse.with(deviceCode, userCode)
.verificationUri(verificationUri)
.verificationUriComplete(verificationUriComplete)
.build();
// @formatter:on
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.deviceAuthorizationHttpResponseConverter.write(deviceAuthorizationResponse, null, httpResponse);
}
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
this.errorHttpResponseConverter.write(error, null, httpResponse);
}
}

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

@ -0,0 +1,266 @@ @@ -0,0 +1,266 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
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;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
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.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
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.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.RedirectUrlBuilder;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
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.
*
* @author Steve Riesenberg
* @since 1.1
* @see AuthenticationManager
* @see OAuth2DeviceVerificationAuthenticationConverter
* @see OAuth2DeviceVerificationAuthenticationProvider
* @see OAuth2DeviceAuthorizationConsentAuthenticationConverter
* @see OAuth2DeviceAuthorizationConsentAuthenticationProvider
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628">OAuth 2.0 Device Authorization Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.3">Section 3.3 User Interaction</a>
*/
public final class OAuth2DeviceVerificationEndpointFilter extends OncePerRequestFilter {
private final AuthenticationManager authenticationManager;
private final RequestMatcher deviceVerificationEndpointMatcher;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
new WebAuthenticationDetailsSource();
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler =
new SimpleUrlAuthenticationSuccessHandler("/?success");
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
private String consentPage;
public OAuth2DeviceVerificationEndpointFilter(AuthenticationManager authenticationManager, String deviceVerificationEndpointUri) {
this.authenticationManager = authenticationManager;
this.deviceVerificationEndpointMatcher = createDefaultRequestMatcher(deviceVerificationEndpointUri);
this.authenticationConverter = new DelegatingAuthenticationConverter(
Arrays.asList(
new OAuth2DeviceVerificationAuthenticationConverter(),
new OAuth2DeviceAuthorizationConsentAuthenticationConverter()));
}
private RequestMatcher createDefaultRequestMatcher(String deviceVerificationEndpointUri) {
RequestMatcher verificationRequestGetMatcher = new AntPathRequestMatcher(
deviceVerificationEndpointUri, HttpMethod.GET.name());
RequestMatcher verificationRequestPostMatcher = new AntPathRequestMatcher(
deviceVerificationEndpointUri, HttpMethod.POST.name());
RequestMatcher userCodeParameterMatcher = request ->
request.getParameter(OAuth2ParameterNames.USER_CODE) != null;
return new AndRequestMatcher(
new OrRequestMatcher(verificationRequestGetMatcher, verificationRequestPostMatcher),
userCodeParameterMatcher);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.deviceVerificationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
Authentication authentication = this.authenticationConverter.convert(request);
if (authentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authentication)
.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
if (!authenticationResult.isAuthenticated()) {
// If the Principal (Resource Owner) is not authenticated then
// pass through the chain with the expectation that the authentication process
// will commence via AuthenticationEntryPoint
filterChain.doFilter(request, response);
return;
}
if (authenticationResult instanceof OAuth2DeviceAuthorizationConsentAuthenticationToken) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Device authorization consent is required");
}
sendAuthorizationConsent(request, response, authenticationResult);
return;
}
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
} catch (OAuth2AuthenticationException ex) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Device verification request failed: %s", ex.getError()), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
/**
* Sets the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}.
*
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}
*/
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or 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}
*/
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
* and returning the response.
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
*/
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
* and returning the {@link OAuth2Error Error Response}.
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2DeviceVerificationAuthenticationToken}
*/
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
/**
* Specify the URI to redirect Resource Owners to if consent is required. A default consent
* page will be generated when this attribute is not specified.
*
* @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
*/
public void setConsentPage(String consentPage) {
this.consentPage = consentPage;
}
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2DeviceAuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
(OAuth2DeviceAuthorizationConsentAuthenticationToken) authentication;
String clientId = authorizationConsentAuthentication.getClientId();
Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
Set<String> requestedScopes = authorizationConsentAuthentication.getRequestedScopes();
Set<String> authorizedScopes = authorizationConsentAuthentication.getScopes();
String state = authorizationConsentAuthentication.getState();
String userCode = authorizationConsentAuthentication.getUserCode();
if (hasConsentUri()) {
String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
.queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
.queryParam(OAuth2ParameterNames.STATE, state)
.queryParam(OAuth2ParameterNames.USER_CODE, userCode)
.toUriString();
this.redirectStrategy.sendRedirect(request, response, redirectUri);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Displaying generated consent screen");
}
Map<String, String> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.USER_CODE, userCode);
DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes, state, additionalParameters);
}
}
private boolean hasConsentUri() {
return StringUtils.hasText(this.consentPage);
}
private String resolveConsentUri(HttpServletRequest request) {
if (UrlUtils.isAbsoluteUrl(this.consentPage)) {
return this.consentPage;
}
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(request.getScheme());
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(request.getServerPort());
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(this.consentPage);
return urlBuilder.getUrl();
}
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError();
response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString());
}
}

6
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java

@ -1,5 +1,5 @@ @@ -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.
@ -53,6 +53,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O @@ -53,6 +53,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@ -136,7 +137,8 @@ public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter { @@ -136,7 +137,8 @@ public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
Arrays.asList(
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter()));
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2DeviceCodeAuthenticationConverter()));
}
@Override

115
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceAuthorizationConsentAuthenticationConverter.java

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract an 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.
*
* @author Steve Riesenberg
* @since 1.1
* @see AuthenticationConverter
* @see OAuth2DeviceAuthorizationConsentAuthenticationToken
* @see OAuth2DeviceVerificationEndpointFilter
*/
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 Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
@Override
public Authentication convert(HttpServletRequest request) {
if (!"POST".equals(request.getMethod())) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
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) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, DEFAULT_ERROR_URI);
}
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null) {
principal = ANONYMOUS_AUTHENTICATION;
}
// state (REQUIRED)
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
if (!StringUtils.hasText(state) || parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.STATE,
DEFAULT_ERROR_URI);
}
// scope (OPTIONAL)
Set<String> scopes = null;
if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.STATE) &&
!key.equals(OAuth2ParameterNames.SCOPE) &&
!key.equals(OAuth2ParameterNames.USER_CODE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2DeviceAuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal,
OAuth2EndpointUtils.normalizeUserCode(userCode), state, scopes, additionalParameters);
}
}

82
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceAuthorizationRequestAuthenticationConverter.java

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract a Device Authorization Request from {@link HttpServletRequest} for the
* OAuth 2.0 Device Authorization Grant and then converts it to an
* {@link OAuth2DeviceAuthorizationRequestAuthenticationToken} used for authenticating
* the request.
*
* @author Steve Riesenberg
* @since 1.1
*/
public final class OAuth2DeviceAuthorizationRequestAuthenticationConverter implements AuthenticationConverter {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc8628#section-3.1";
@Override
public Authentication convert(HttpServletRequest request) {
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
String authorizationUri = request.getRequestURL().toString();
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2DeviceAuthorizationRequestAuthenticationToken(clientPrincipal, authorizationUri,
requestedScopes, additionalParameters);
}
}

81
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceCodeAuthenticationConverter.java

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceAuthorizationEndpointFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract an 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.
*
* @author Steve Riesenberg
* @since 1.1
* @see AuthenticationConverter
* @see OAuth2DeviceCodeAuthenticationToken
* @see OAuth2DeviceAuthorizationEndpointFilter
*/
public final class OAuth2DeviceCodeAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.DEVICE_CODE.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// device_code (REQUIRED)
String deviceCode = parameters.getFirst(OAuth2ParameterNames.DEVICE_CODE);
if (!StringUtils.hasText(deviceCode) || parameters.get(OAuth2ParameterNames.DEVICE_CODE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.DEVICE_CODE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.DEVICE_CODE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2DeviceCodeAuthenticationToken(deviceCode, clientPrincipal, additionalParameters);
}
}

90
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2DeviceVerificationAuthenticationConverter.java

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract a user code from {@link HttpServletRequest} for the
* OAuth 2.0 Device Authorization Grant and then converts it to an
* {@link OAuth2DeviceVerificationAuthenticationToken} used for authenticating
* the request.
*
* @author Steve Riesenberg
* @since 1.1
* @see AuthenticationConverter
* @see OAuth2DeviceVerificationAuthenticationToken
* @see OAuth2DeviceVerificationEndpointFilter
*/
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 Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
@Override
public Authentication convert(HttpServletRequest request) {
if (!("GET".equals(request.getMethod()) || "POST".equals(request.getMethod()))) {
return null;
}
if (request.getParameter(OAuth2ParameterNames.STATE) != null
|| request.getParameter(OAuth2ParameterNames.USER_CODE) == null) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 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);
}
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null) {
principal = ANONYMOUS_AUTHENTICATION;
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.USER_CODE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2DeviceVerificationAuthenticationToken(principal,
OAuth2EndpointUtils.normalizeUserCode(userCode), additionalParameters);
}
}

11
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java

@ -1,5 +1,5 @@ @@ -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.
@ -26,6 +26,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -26,6 +26,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -81,4 +82,12 @@ final class OAuth2EndpointUtils { @@ -81,4 +82,12 @@ final class OAuth2EndpointUtils {
throw new OAuth2AuthenticationException(error);
}
static String normalizeUserCode(String userCode) {
Assert.hasText(userCode, "userCode cannot be empty");
StringBuilder sb = new StringBuilder(userCode.toUpperCase().replaceAll("[^A-Z\\d]+", ""));
Assert.isTrue(sb.length() == 8, "userCode must be exactly 8 alpha/numeric characters");
sb.insert(4, '-');
return sb.toString();
}
}

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

@ -24,13 +24,12 @@ import java.util.Base64; @@ -24,13 +24,12 @@ import java.util.Base64;
import java.util.List;
import java.util.function.Consumer;
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 jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
@ -72,6 +71,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O @@ -72,6 +71,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
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.authentication.PublicClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
@ -90,6 +90,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati @@ -90,6 +90,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati
import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
@ -291,7 +292,8 @@ public class OAuth2ClientCredentialsGrantTests { @@ -291,7 +292,8 @@ public class OAuth2ClientCredentialsGrantTests {
converter == authenticationConverter ||
converter instanceof OAuth2AuthorizationCodeAuthenticationConverter ||
converter instanceof OAuth2RefreshTokenAuthenticationConverter ||
converter instanceof OAuth2ClientCredentialsAuthenticationConverter);
converter instanceof OAuth2ClientCredentialsAuthenticationConverter ||
converter instanceof OAuth2DeviceCodeAuthenticationConverter);
verify(authenticationProvider).authenticate(eq(clientCredentialsAuthentication));
@ -303,7 +305,8 @@ public class OAuth2ClientCredentialsGrantTests { @@ -303,7 +305,8 @@ public class OAuth2ClientCredentialsGrantTests {
provider == authenticationProvider ||
provider instanceof OAuth2AuthorizationCodeAuthenticationProvider ||
provider instanceof OAuth2RefreshTokenAuthenticationProvider ||
provider instanceof OAuth2ClientCredentialsAuthenticationProvider);
provider instanceof OAuth2ClientCredentialsAuthenticationProvider ||
provider instanceof OAuth2DeviceCodeAuthenticationProvider);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication));
}

2
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettingsTests.java

@ -86,7 +86,7 @@ public class AuthorizationServerSettingsTests { @@ -86,7 +86,7 @@ public class AuthorizationServerSettingsTests {
.settings(settings -> settings.put("name2", "value2"))
.build();
assertThat(authorizationServerSettings.getSettings()).hasSize(10);
assertThat(authorizationServerSettings.getSettings()).hasSize(12);
assertThat(authorizationServerSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(authorizationServerSettings.<String>getSetting("name2")).isEqualTo("value2");
}

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

@ -1,5 +1,5 @@ @@ -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.
@ -34,8 +34,9 @@ public class TokenSettingsTests { @@ -34,8 +34,9 @@ public class TokenSettingsTests {
@Test
public void buildWhenDefaultThenDefaultsAreSet() {
TokenSettings tokenSettings = TokenSettings.builder().build();
assertThat(tokenSettings.getSettings()).hasSize(6);
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.isReuseRefreshTokens()).isTrue();
@ -163,7 +164,7 @@ public class TokenSettingsTests { @@ -163,7 +164,7 @@ public class TokenSettingsTests {
.setting("name1", "value1")
.settings(settings -> settings.put("name2", "value2"))
.build();
assertThat(tokenSettings.getSettings()).hasSize(8);
assertThat(tokenSettings.getSettings()).hasSize(9);
assertThat(tokenSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(tokenSettings.<String>getSetting("name2")).isEqualTo("value2");
}

27
samples/device-client/samples-device-client.gradle

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
plugins {
id "org.springframework.boot" version "3.0.0"
id "io.spring.dependency-management" version "1.0.11.RELEASE"
id "java"
}
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "17"
repositories {
mavenCentral()
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
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"
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.springframework.security:spring-security-test"
}

32
samples/device-client/src/main/java/sample/DeviceClientApplication.java

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@SpringBootApplication
public class DeviceClientApplication {
public static void main(String[] args) {
SpringApplication.run(DeviceClientApplication.class, args);
}
}

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

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/webjars/**", "/assets/**");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/authorize").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
)
.oauth2Client(Customizer.withDefaults());
// @formatter:on
return http.build();
}
}

71
samples/device-client/src/main/java/sample/config/WebClientConfig.java

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
import sample.web.authentication.DeviceCodeOAuth2AuthorizedClientProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
// @formatter:off
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
// @formatter:on
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.provider(new DeviceCodeOAuth2AuthorizedClientProvider())
.authorizationCode()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Set a contextAttributesMapper to obtain device_code from the request
authorizedClientManager.setContextAttributesMapper(DeviceCodeOAuth2AuthorizedClientProvider
.deviceCodeContextAttributesMapper());
return authorizedClientManager;
}
}

192
samples/device-client/src/main/java/sample/web/DeviceController.java

@ -0,0 +1,192 @@ @@ -0,0 +1,192 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@Controller
public class DeviceController {
private static final ParameterizedTypeReference<Map<String, Object>> TYPE_REFERENCE =
new ParameterizedTypeReference<>() {};
private final ClientRegistrationRepository clientRegistrationRepository;
private final WebClient webClient;
private final String messagesBaseUri;
private final SecurityContextRepository securityContextRepository =
new HttpSessionSecurityContextRepository();
private final SecurityContextHolderStrategy securityContextHolderStrategy =
SecurityContextHolder.getContextHolderStrategy();
public DeviceController(ClientRegistrationRepository clientRegistrationRepository, WebClient webClient,
@Value("${messages.base-uri}") String messagesBaseUri) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.webClient = webClient;
this.messagesBaseUri = messagesBaseUri;
}
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/authorize")
public String authorize(Model model, HttpServletRequest request, HttpServletResponse response) {
// @formatter:off
ClientRegistration clientRegistration =
this.clientRegistrationRepository.findByRegistrationId(
"messaging-client-device-grant");
// @formatter:on
MultiValueMap<String, String> requestParameters = new LinkedMultiValueMap<>();
requestParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
requestParameters.add(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(
clientRegistration.getScopes(), " "));
// @formatter:off
Map<String, Object> responseParameters =
this.webClient.post()
.uri(clientRegistration.getProviderDetails().getAuthorizationUri())
.headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(),
clientRegistration.getClientSecret()))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(requestParameters))
.retrieve()
.bodyToMono(TYPE_REFERENCE)
.block();
// @formatter:on
Objects.requireNonNull(responseParameters, "Device Authorization Response cannot be null");
Instant issuedAt = Instant.now();
Integer expiresIn = (Integer) responseParameters.get(OAuth2ParameterNames.EXPIRES_IN);
Instant expiresAt = issuedAt.plusSeconds(expiresIn);
String deviceCodeValue = (String) responseParameters.get(OAuth2ParameterNames.DEVICE_CODE);
OAuth2DeviceCode deviceCode = new OAuth2DeviceCode(deviceCodeValue, issuedAt, expiresAt);
saveSecurityContext(deviceCode, request, response);
model.addAttribute("deviceCode", deviceCode.getTokenValue());
model.addAttribute("expiresAt", deviceCode.getExpiresAt());
model.addAttribute("userCode", responseParameters.get(OAuth2ParameterNames.USER_CODE));
model.addAttribute("verificationUri", responseParameters.get(OAuth2ParameterNames.VERIFICATION_URI));
// Note: You could use a QR-code to display this URL
model.addAttribute("verificationUriComplete", responseParameters.get(
OAuth2ParameterNames.VERIFICATION_URI_COMPLETE));
return "authorize";
}
/**
* @see DeviceControllerAdvice
*/
@PostMapping("/authorize")
public ResponseEntity<Void> poll(@RequestParam(OAuth2ParameterNames.DEVICE_CODE) String deviceCode,
@RegisteredOAuth2AuthorizedClient("messaging-client-device-grant")
OAuth2AuthorizedClient authorizedClient) {
// The client will repeatedly poll until authorization is granted.
//
// The OAuth2AuthorizedClientManager uses the device_code parameter
// to make a token request, which returns authorization_pending until
// the user has granted authorization.
//
// If the user has denied authorization, access_denied is returned and
// polling should stop.
//
// If the device code expires, expired_token is returned and polling
// should stop.
//
// This endpoint simply returns 200 OK when client is authorized.
return ResponseEntity.status(HttpStatus.OK).build();
}
@GetMapping("/authorized")
public String authorized(Model model,
@RegisteredOAuth2AuthorizedClient("messaging-client-device-grant")
OAuth2AuthorizedClient authorizedClient) {
String[] messages = this.webClient.get()
.uri(this.messagesBaseUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String[].class)
.block();
model.addAttribute("messages", messages);
return "authorized";
}
private void saveSecurityContext(OAuth2DeviceCode deviceCode, HttpServletRequest request,
HttpServletResponse response) {
// @formatter:off
UsernamePasswordAuthenticationToken deviceAuthentication =
UsernamePasswordAuthenticationToken.authenticated(
deviceCode, null, AuthorityUtils.createAuthorityList("ROLE_DEVICE"));
// @formatter:on
SecurityContext securityContext = this.securityContextHolderStrategy.createEmptyContext();
securityContext.setAuthentication(deviceAuthentication);
this.securityContextHolderStrategy.setContext(securityContext);
this.securityContextRepository.saveContext(securityContext, request, response);
}
}

52
samples/device-client/src/main/java/sample/web/DeviceControllerAdvice.java

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@ControllerAdvice
public class DeviceControllerAdvice {
private static final Set<String> DEVICE_GRANT_ERRORS = new HashSet<>(Arrays.asList(
"authorization_pending",
"slow_down",
"access_denied",
"expired_token"
));
@ExceptionHandler(OAuth2AuthorizationException.class)
public ResponseEntity<OAuth2Error> handleError(OAuth2AuthorizationException ex) {
String errorCode = ex.getError().getErrorCode();
if (DEVICE_GRANT_ERRORS.contains(errorCode)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getError());
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getError());
}
}

122
samples/device-client/src/main/java/sample/web/authentication/DeviceCodeOAuth2AuthorizedClientProvider.java

@ -0,0 +1,122 @@ @@ -0,0 +1,122 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web.authentication;
import java.time.Clock;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.client.ClientAuthorizationException;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.Assert;
/**
* @author Steve Riesenberg
* @since 1.1
*/
public final class DeviceCodeOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
private OAuth2AccessTokenResponseClient<OAuth2DeviceGrantRequest> accessTokenResponseClient =
new OAuth2DeviceAccessTokenResponseClient();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
public DeviceCodeOAuth2AuthorizedClientProvider() {
}
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2DeviceGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
}
public void setClockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
}
public void setClock(Clock clock) {
this.clock = clock;
}
@Override
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
ClientRegistration clientRegistration = context.getClientRegistration();
if (!AuthorizationGrantType.DEVICE_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
return null;
}
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
// If client is already authorized but access token is NOT expired than no
// need for re-authorization
return null;
}
if (authorizedClient != null && authorizedClient.getRefreshToken() != null) {
// If client is already authorized but access token is expired and a
// refresh token is available, delegate to refresh_token.
return null;
}
// *****************************************************************
// Get device_code set via DefaultOAuth2AuthorizedClientManager#setContextAttributesMapper()
// *****************************************************************
String deviceCode = context.getAttribute(OAuth2ParameterNames.DEVICE_CODE);
// Attempt to authorize the client, which will repeatedly fail until the user grants authorization
OAuth2DeviceGrantRequest deviceGrantRequest = new OAuth2DeviceGrantRequest(clientRegistration, deviceCode);
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, deviceGrantRequest);
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
}
private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration,
OAuth2DeviceGrantRequest deviceGrantRequest) {
try {
return this.accessTokenResponseClient.getTokenResponse(deviceGrantRequest);
} catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
}
}
private boolean hasTokenExpired(OAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}
public static Function<OAuth2AuthorizeRequest, Map<String, Object>> deviceCodeContextAttributesMapper() {
return (authorizeRequest) -> {
HttpServletRequest request = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
Assert.notNull(request, "request cannot be null");
// Obtain device code from request
String deviceCode = request.getParameter(OAuth2ParameterNames.DEVICE_CODE);
return (deviceCode != null) ? Collections.singletonMap(OAuth2ParameterNames.DEVICE_CODE, deviceCode) :
Collections.emptyMap();
};
}
}

85
samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceAccessTokenResponseClient.java

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web.authentication;
import java.util.Arrays;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* @author Steve Riesenberg
* @since 1.1
*/
public final class OAuth2DeviceAccessTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2DeviceGrantRequest> {
private RestOperations restOperations;
public OAuth2DeviceAccessTokenResponseClient() {
RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}
public void setRestOperations(RestOperations restOperations) {
this.restOperations = restOperations;
}
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2DeviceGrantRequest deviceGrantRequest) {
ClientRegistration clientRegistration = deviceGrantRequest.getClientRegistration();
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
MultiValueMap<String, Object> requestParameters = new LinkedMultiValueMap<>();
requestParameters.add(OAuth2ParameterNames.GRANT_TYPE, deviceGrantRequest.getGrantType().getValue());
requestParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
requestParameters.add(OAuth2ParameterNames.DEVICE_CODE, deviceGrantRequest.getDeviceCode());
// @formatter:off
RequestEntity<MultiValueMap<String, Object>> requestEntity =
RequestEntity.post(deviceGrantRequest.getClientRegistration().getProviderDetails().getTokenUri())
.headers(headers)
.body(requestParameters);
// @formatter:on
try {
return this.restOperations.exchange(requestEntity, OAuth2AccessTokenResponse.class).getBody();
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_token_response",
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(), null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}
}

41
samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceGrantRequest.java

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web.authentication;
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* @author Steve Riesenberg
* @since 1.1
*/
public final class OAuth2DeviceGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
private final String deviceCode;
public OAuth2DeviceGrantRequest(ClientRegistration clientRegistration, String deviceCode) {
super(AuthorizationGrantType.DEVICE_CODE, clientRegistration);
Assert.hasText(deviceCode, "deviceCode cannot be empty");
this.deviceCode = deviceCode;
}
public String getDeviceCode() {
return deviceCode;
}
}

29
samples/device-client/src/main/resources/application.yml

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
server:
port: 8080
logging:
level:
root: INFO
org.springframework.security: trace
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
messaging-client-device-grant:
provider: spring
client-id: messaging-client
client-secret: secret
authorization-grant-type: urn:ietf:params:oauth:grant-type:device_code
scope: message.read,message.write
client-name: messaging-client-device-grant
provider:
spring:
issuer-uri: http://localhost:9000
authorization-uri: ${spring.security.oauth2.client.provider.spring.issuer-uri}/oauth2/device_authorization
messages:
base-uri: http://127.0.0.1:8090/messages

13
samples/device-client/src/main/resources/static/assets/css/style.css

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
html, body, .container, .jumbotron {
height: 100%;
}
.jumbotron {
margin-bottom: 0;
}
.gap {
margin-top: 70px;
}
.code {
font-size: 2em;
letter-spacing: 2rem;
}

87
samples/device-client/src/main/resources/templates/authorize.html

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Device Grant Example</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/style.css" th:href="@{/assets/css/style.css}" />
</head>
<body>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-8">
<h2>Device Activation</h2>
<p>Please visit <a th:href="${verificationUri}" th:text="${verificationUri?.replaceFirst('https?://', '')}"></a> on another device to continue.</p>
<p class="gap">Activation Code</p>
<div class="well">
<span class="code" th:text="${userCode}"></span>
<form id="authorize-form" th:action="@{/authorize}" method="post">
<input type="hidden" id="device_code" name="device_code" th:value="${deviceCode}" />
</form>
</div>
</div>
<div class="col-md-4">
<img src="https://cdn.pixabay.com/photo/2017/07/03/15/20/technology-2468063_1280.png" class="img-responsive" alt="Devices">
</div>
</div>
</div>
</div>
<script src="/webjars/jquery/jquery.min.js" th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script type="text/javascript">
function authorize() {
let deviceCode = $('#device_code').val();
let csrfToken = $('[name=_csrf]').val();
if (deviceCode) {
$.ajax({
url: '/authorize',
method: 'POST',
data: {
device_code: deviceCode,
_csrf: csrfToken
},
timeout: 0
}).fail((err) => {
let response = err.responseJSON;
if (response.errorCode === 'authorization_pending') {
console.log('authorization pending, continuing to poll...');
} else if (response.errorCode === 'slow_down') {
console.log('slowing down...');
slowDown();
} else if (response.errorCode === 'token_expired') {
console.log('token expired, stopping...');
clear();
location.href = '/';
} else if (response.errorCode === 'access_denied') {
console.log('access denied, stopping...');
clear();
location.href = '/';
}
}).done(() => window.location.href = '/authorized');
}
}
function schedule() {
authorize.handler = window.setInterval(authorize, authorize.interval * 1000);
}
function clear() {
if (authorize.handler !== null) {
window.clearInterval(authorize.handler);
}
}
function slowDown() {
authorize.interval += 5;
clear();
schedule();
}
authorize.interval = 5;
authorize.handler = null;
window.addEventListener('load', schedule);
</script>
</body>
</html>

35
samples/device-client/src/main/resources/templates/authorized.html

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Device Grant Example</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/style.css" th:href="@{/assets/css/style.css}" />
</head>
<body>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-8">
<h2>Success!</h2>
<p>This device has been activated.</p>
</div>
<div class="col-md-4">
<img src="https://cdn.pixabay.com/photo/2017/07/03/15/20/technology-2468063_1280.png" class="img-responsive" alt="Devices">
</div>
<div class="col-md-12" th:if="${messages}">
<h4>Messages:</h4>
<table class="table table-condensed">
<tbody>
<tr class="row" th:each="message : ${messages}">
<td th:text="${message}">message</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>

26
samples/device-client/src/main/resources/templates/index.html

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Device Grant Example</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/style.css" th:href="@{/assets/css/style.css}" />
</head>
<body>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-8">
<h2>Activation Required</h2>
<p>You must activate this device. Please log in to continue.</p>
<a th:href="@{/authorize}" class="btn btn-primary" role="button">Log In</a>
</div>
<div class="col-md-4">
<img src="https://cdn.pixabay.com/photo/2017/07/03/15/20/technology-2468063_1280.png" class="img-responsive" alt="Devices">
</div>
</div>
</div>
</div>
</body>
</html>

37
samples/device-grant-authorizationserver/samples-device-grant-authorizationserver.gradle

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
plugins {
id "org.springframework.boot" version "3.0.0"
id "io.spring.dependency-management" version "1.0.11.RELEASE"
id "java"
}
group = project.rootProject.group
version = project.rootProject.version
sourceCompatibility = "17"
repositories {
maven {
url = "https://repo.spring.io/snapshot"
}
mavenCentral()
}
// Temporarily use SNAPSHOT version
// TODO: Use 6.1.0-M2 version after release
ext["spring-security.version"] = "6.1.0-SNAPSHOT"
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
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"
runtimeOnly "com.h2database:h2"
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.springframework.security:spring-security-test"
}

32
samples/device-grant-authorizationserver/src/main/java/sample/DeviceGrantAuthorizationServerApplication.java

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@SpringBootApplication
public class DeviceGrantAuthorizationServerApplication {
public static void main(String[] args) {
SpringApplication.run(DeviceGrantAuthorizationServerApplication.class, args);
}
}

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

@ -0,0 +1,170 @@ @@ -0,0 +1,170 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.config;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.deviceAuthorizationEndpoint((deviceAuthorizationEndpoint) -> deviceAuthorizationEndpoint
.verificationUri("/activate")
)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
// @formatter:off
http
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// @formatter:on
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
// @formatter:on
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
// @formatter:off
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
// @formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}

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

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
/*
* Copyright 2002-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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author Steve Riesenberg
*/
@Controller
public class DeviceController {
@GetMapping("/activate")
public String activate(@RequestParam(value = "user_code", required = false) String userCode) {
if (userCode != null) {
return "redirect:/oauth2/device_verification?user_code=" + userCode;
}
return "activate";
}
@GetMapping("/activated")
public String activated() {
return "activated";
}
@GetMapping(value = "/", params = "success")
public String success() {
return "activated";
}
}

48
samples/device-grant-authorizationserver/src/main/java/sample/web/DeviceErrorController.java

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import java.util.Map;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* @author Steve Riesenberg
* @since 1.1
*/
@Controller
public class DeviceErrorController implements ErrorController {
@RequestMapping("/error")
public ModelAndView handleError(HttpServletRequest request) {
String message = getErrorMessage(request);
if (message.startsWith("[access_denied]")) {
return new ModelAndView("access-denied");
}
return new ModelAndView("error", Map.of("message", message));
}
private String getErrorMessage(HttpServletRequest request) {
return (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
}
}

6
samples/device-grant-authorizationserver/src/main/resources/application.yml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
server:
port: 9000
logging:
level:
org.springframework.security: trace

13
samples/device-grant-authorizationserver/src/main/resources/static/assets/css/style.css

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
html, body, .container, .jumbotron {
height: 100%;
}
.jumbotron {
margin-bottom: 0;
}
.gap {
margin-top: 70px;
}
.code {
font-size: 2em;
letter-spacing: 2rem;
}

25
samples/device-grant-authorizationserver/src/main/resources/templates/access-denied.html

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Device Grant Example</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/style.css" th:href="@{/assets/css/style.css}" />
</head>
<body>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-8">
<h2>Access Denied</h2>
<p>You have denied access. Please return to your device to continue.</p>
</div>
<div class="col-md-4">
<img src="https://cdn.pixabay.com/photo/2017/07/03/15/20/technology-2468063_1280.png" class="img-responsive" alt="Devices">
</div>
</div>
</div>
</div>
</body>
</html>

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

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Device Grant Example</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/style.css" th:href="@{/assets/css/style.css}" />
</head>
<body>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-8">
<form th:action="@{/oauth2/device_verification}" method="get">
<h2>Device Activation</h2>
<p>Enter the activation code to authorize the device.</p>
<p class="gap">Activation Code</p>
<div class="form-group">
<label class="sr-only" for="user_code">Activation Code</label>
<input type="text" class="form-control" id="user_code" name="user_code" placeholder="Activation Code" autofocus>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<div class="col-md-4">
<img src="https://cdn.pixabay.com/photo/2017/07/03/15/20/technology-2468063_1280.png" class="img-responsive" alt="Devices">
</div>
</div>
</div>
</div>
</body>
</html>

25
samples/device-grant-authorizationserver/src/main/resources/templates/activated.html

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Device Grant Example</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/style.css" th:href="@{/assets/css/style.css}" />
</head>
<body>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-8">
<h2>Success!</h2>
<p>You have successfully activated your device. Please return to your device to continue.</p>
</div>
<div class="col-md-4">
<img src="https://cdn.pixabay.com/photo/2017/07/03/15/20/technology-2468063_1280.png" class="img-responsive" alt="Devices">
</div>
</div>
</div>
</div>
</body>
</html>

25
samples/device-grant-authorizationserver/src/main/resources/templates/error.html

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Device Grant Example</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/style.css" th:href="@{/assets/css/style.css}" />
</head>
<body>
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-8">
<h2>Error</h2>
<p th:text="${message}"></p>
</div>
<div class="col-md-4">
<img src="https://cdn.pixabay.com/photo/2017/07/03/15/20/technology-2468063_1280.png" class="img-responsive" alt="Devices">
</div>
</div>
</div>
</div>
</body>
</html>
Loading…
Cancel
Save