57 changed files with 4492 additions and 124 deletions
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
@ -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()); |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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" |
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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()); |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
}; |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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 |
||||
@ -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; |
||||
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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" |
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
|
||||
} |
||||
@ -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"; |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
server: |
||||
port: 9000 |
||||
|
||||
logging: |
||||
level: |
||||
org.springframework.security: trace |
||||
@ -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; |
||||
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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…
Reference in new issue