Decompose OAuth2AuthorizationCodeRequestAuthenticationProvider
Closes gh-896
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.time.Instant;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2TokenGenerator} that generates an {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2TokenGenerator
|
||||
* @see OAuth2AuthorizationCode
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
*/
|
||||
final class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
|
||||
private final StringKeyGenerator authorizationCodeGenerator =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
|
||||
if (context.getTokenType() == null ||
|
||||
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
|
||||
return null;
|
||||
}
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive());
|
||||
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,19 +16,14 @@
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
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.core.GrantedAuthority;
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
@@ -54,7 +49,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
@@ -63,6 +58,7 @@ import org.springframework.util.StringUtils;
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
* @see OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
@@ -71,7 +67,6 @@ import org.springframework.util.StringUtils;
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
@@ -80,7 +75,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
|
||||
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationValidator();
|
||||
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
|
||||
@@ -104,73 +98,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
return authorizationCodeRequestAuthentication.isConsent() ?
|
||||
authenticateAuthorizationConsent(authentication) :
|
||||
authenticateAuthorizationRequest(authentication);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeGenerator = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
|
||||
* and is responsible for validating specific OAuth 2.0 Authorization Request parameters
|
||||
* associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
* The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
|
||||
*
|
||||
* @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationValidator(Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 OAuth2AuthorizationCodeRequestAuthenticationToken}.</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 Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationCodeRequestAuthentication.getClientId());
|
||||
if (registeredClient == null) {
|
||||
@@ -234,12 +161,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : null;
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.scopes(currentAuthorizedScopes)
|
||||
.state(state)
|
||||
.consentRequired(true)
|
||||
.build();
|
||||
return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
|
||||
registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
|
||||
}
|
||||
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
@@ -262,136 +185,42 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(authorizationRequest.getScopes())
|
||||
.state(authorizationRequest.getState())
|
||||
.authorizationCode(authorizationCode)
|
||||
.build();
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
|
||||
registeredClient.getClientId(), principal, authorizationCode, redirectUri,
|
||||
authorizationRequest.getState(), authorizationRequest.getScopes());
|
||||
}
|
||||
|
||||
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationCodeRequestAuthentication.getState(), STATE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationCodeRequestAuthentication, null, null);
|
||||
}
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeGenerator = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
// The 'in-flight' authorization must be associated to the current principal
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationCodeRequestAuthentication, null, null);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationCodeRequestAuthentication.getClientId());
|
||||
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
Set<String> requestedScopes = authorizationRequest.getScopes();
|
||||
Set<String> authorizedScopes = new HashSet<>(authorizationCodeRequestAuthentication.getScopes());
|
||||
if (!requestedScopes.containsAll(authorizedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : Collections.emptySet();
|
||||
|
||||
if (!currentAuthorizedScopes.isEmpty()) {
|
||||
for (String requestedScope : requestedScopes) {
|
||||
if (currentAuthorizedScopes.contains(requestedScope)) {
|
||||
authorizedScopes.add(requestedScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
|
||||
// 'openid' scope is auto-approved as it does not require consent
|
||||
authorizedScopes.add(OidcScopes.OPENID);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
|
||||
if (currentAuthorizationConsent != null) {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
|
||||
} else {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
}
|
||||
authorizedScopes.forEach(authorizationConsentBuilder::scope);
|
||||
|
||||
if (this.authorizationConsentCustomizer != null) {
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
|
||||
OAuth2AuthorizationConsentAuthenticationContext.with(authorizationCodeRequestAuthentication)
|
||||
.authorizationConsent(authorizationConsentBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.authorization(authorization)
|
||||
.authorizationRequest(authorizationRequest)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
|
||||
}
|
||||
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
authorizationConsentBuilder.authorities(authorities::addAll);
|
||||
|
||||
if (authorities.isEmpty()) {
|
||||
// Authorization consent denied (or revoked)
|
||||
if (currentAuthorizationConsent != null) {
|
||||
this.authorizationConsentService.remove(currentAuthorizationConsent);
|
||||
}
|
||||
this.authorizationService.remove(authorization);
|
||||
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
|
||||
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
|
||||
this.authorizationConsentService.save(authorizationConsent);
|
||||
}
|
||||
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes);
|
||||
OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
|
||||
if (authorizationCode == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the authorization code.", ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
|
||||
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.token(authorizationCode)
|
||||
.attributes(attrs -> {
|
||||
attrs.remove(OAuth2ParameterNames.STATE);
|
||||
})
|
||||
.build();
|
||||
this.authorizationService.save(updatedAuthorization);
|
||||
|
||||
String redirectUri = authorizationRequest.getRedirectUri();
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(authorizedScopes)
|
||||
.state(authorizationRequest.getState())
|
||||
.authorizationCode(authorizationCode)
|
||||
.build();
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
|
||||
* and is responsible for validating specific OAuth 2.0 Authorization Request parameters
|
||||
* associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
* The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
|
||||
*
|
||||
* @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationValidator(Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
|
||||
@@ -454,14 +283,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
throwError(errorCode, parameterName, authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
throwError(errorCode, parameterName, ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName, String errorUri,
|
||||
@@ -475,30 +297,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
|
||||
boolean redirectOnError = true;
|
||||
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
|
||||
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
|
||||
parameterName.equals(OAuth2ParameterNames.STATE))) {
|
||||
redirectOnError = false;
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
|
||||
|
||||
if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
|
||||
String state = authorizationCodeRequestAuthentication.isConsent() && authorizationRequest != null ?
|
||||
authorizationRequest.getState() : authorizationCodeRequestAuthentication.getState();
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(redirectUri)
|
||||
.state(state)
|
||||
.build();
|
||||
} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(null) // Prevent redirects
|
||||
.build();
|
||||
}
|
||||
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(),
|
||||
(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
|
||||
authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
|
||||
authorizationCodeRequestAuthentication.getAdditionalParameters());
|
||||
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
|
||||
}
|
||||
@@ -513,32 +324,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
return null;
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
|
||||
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
|
||||
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.scopes(authorizationCodeRequestAuthentication.getScopes())
|
||||
.state(authorizationCodeRequestAuthentication.getState())
|
||||
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
|
||||
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
|
||||
}
|
||||
|
||||
private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
|
||||
private final StringKeyGenerator authorizationCodeGenerator =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
|
||||
if (context.getTokenType() == null ||
|
||||
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
|
||||
return null;
|
||||
}
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive());
|
||||
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,45 +15,104 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent)
|
||||
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
|
||||
public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
|
||||
private String authorizationUri;
|
||||
private String clientId;
|
||||
private Authentication principal;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String state;
|
||||
private Map<String, Object> additionalParameters;
|
||||
private boolean consentRequired;
|
||||
private boolean consent;
|
||||
private OAuth2AuthorizationCode authorizationCode;
|
||||
private final String authorizationUri;
|
||||
private final String clientId;
|
||||
private final Authentication principal;
|
||||
private final String redirectUri;
|
||||
private final String state;
|
||||
private final Set<String> scopes;
|
||||
private final Map<String, Object> additionalParameters;
|
||||
private final OAuth2AuthorizationCode authorizationCode;
|
||||
|
||||
private OAuth2AuthorizationCodeRequestAuthenticationToken() {
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @param redirectUri the redirect uri
|
||||
* @param state the state
|
||||
* @param scopes the requested scope(s)
|
||||
* @param additionalParameters the additional parameters
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId, Authentication principal,
|
||||
@Nullable String redirectUri, @Nullable String state, @Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
this.redirectUri = redirectUri;
|
||||
this.state = state;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ?
|
||||
new HashSet<>(scopes) :
|
||||
Collections.emptySet());
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ?
|
||||
new HashMap<>(additionalParameters) :
|
||||
Collections.emptyMap());
|
||||
this.authorizationCode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @param authorizationCode the {@link OAuth2AuthorizationCode}
|
||||
* @param redirectUri the redirect uri
|
||||
* @param state the state
|
||||
* @param scopes the authorized scope(s)
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId, Authentication principal,
|
||||
OAuth2AuthorizationCode authorizationCode, @Nullable String redirectUri, @Nullable String state, @Nullable Set<String> scopes) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
Assert.notNull(authorizationCode, "authorizationCode cannot be null");
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
this.authorizationCode = authorizationCode;
|
||||
this.redirectUri = redirectUri;
|
||||
this.state = state;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ?
|
||||
new HashSet<>(scopes) :
|
||||
Collections.emptySet());
|
||||
this.additionalParameters = Collections.emptyMap();
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,15 +153,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested (or authorized) scope(s).
|
||||
*
|
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state.
|
||||
*
|
||||
@@ -113,34 +163,24 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested (or authorized) scope(s).
|
||||
*
|
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return the additional parameters
|
||||
* @return the additional parameters, or an empty {@code Map} if not available
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if authorization consent is required, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if authorization consent is required, {@code false} otherwise
|
||||
*/
|
||||
public boolean isConsentRequired() {
|
||||
return this.consentRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this {@code Authentication} represents an authorization consent request,
|
||||
* {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
|
||||
*/
|
||||
public boolean isConsent() {
|
||||
return this.consent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
@@ -151,170 +191,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
|
||||
return this.authorizationCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the given client identifier
|
||||
* and {@code Principal} (Resource Owner).
|
||||
*
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(@NonNull String clientId, @NonNull Authentication principal) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
return new Builder(clientId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*/
|
||||
public static final class Builder implements Serializable {
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
|
||||
private String authorizationUri;
|
||||
private String clientId;
|
||||
private Authentication principal;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String state;
|
||||
private Map<String, Object> additionalParameters;
|
||||
private boolean consentRequired;
|
||||
private boolean consent;
|
||||
private OAuth2AuthorizationCode authorizationCode;
|
||||
|
||||
private Builder(String clientId, Authentication principal) {
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authorization URI.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder authorizationUri(String authorizationUri) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the redirect uri.
|
||||
*
|
||||
* @param redirectUri the redirect uri
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder redirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the requested (or authorized) scope(s).
|
||||
*
|
||||
* @param scopes the requested (or authorized) scope(s)
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder scopes(Set<String> scopes) {
|
||||
if (scopes != null) {
|
||||
this.scopes = new HashSet<>(scopes);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state.
|
||||
*
|
||||
* @param state the state
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder state(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the additional parameters.
|
||||
*
|
||||
* @param additionalParameters the additional parameters
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder additionalParameters(Map<String, Object> additionalParameters) {
|
||||
if (additionalParameters != null) {
|
||||
this.additionalParameters = new HashMap<>(additionalParameters);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if authorization consent is required, {@code false} otherwise.
|
||||
*
|
||||
* @param consentRequired {@code true} if authorization consent is required, {@code false} otherwise
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder consentRequired(boolean consentRequired) {
|
||||
this.consentRequired = consentRequired;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise.
|
||||
*
|
||||
* @param consent {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder consent(boolean consent) {
|
||||
this.consent = consent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCode the {@link OAuth2AuthorizationCode}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder authorizationCode(OAuth2AuthorizationCode authorizationCode) {
|
||||
this.authorizationCode = authorizationCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken build() {
|
||||
Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
|
||||
if (this.consent) {
|
||||
Assert.hasText(this.state, "state cannot be empty");
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken();
|
||||
|
||||
authentication.authorizationUri = this.authorizationUri;
|
||||
authentication.clientId = this.clientId;
|
||||
authentication.principal = this.principal;
|
||||
authentication.redirectUri = this.redirectUri;
|
||||
authentication.scopes = Collections.unmodifiableSet(
|
||||
!CollectionUtils.isEmpty(this.scopes) ?
|
||||
this.scopes :
|
||||
Collections.emptySet());
|
||||
authentication.state = this.state;
|
||||
authentication.additionalParameters = Collections.unmodifiableMap(
|
||||
!CollectionUtils.isEmpty(this.additionalParameters) ?
|
||||
this.additionalParameters :
|
||||
Collections.emptyMap());
|
||||
authentication.consentRequired = this.consentRequired;
|
||||
authentication.consent = this.consent;
|
||||
authentication.authorizationCode = this.authorizationCode;
|
||||
if (this.authorizationCode != null || this.consentRequired) {
|
||||
authentication.setAuthenticated(true);
|
||||
}
|
||||
|
||||
return authentication;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -189,38 +189,23 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator impleme
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
|
||||
boolean redirectOnError = true;
|
||||
String redirectUri = StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri()) ?
|
||||
authorizationCodeRequestAuthentication.getRedirectUri() :
|
||||
registeredClient.getRedirectUris().iterator().next();
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
|
||||
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) {
|
||||
redirectOnError = false;
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
|
||||
|
||||
if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
String redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(redirectUri)
|
||||
.build();
|
||||
} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(null) // Prevent redirects
|
||||
.build();
|
||||
}
|
||||
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(),
|
||||
(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
|
||||
authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
|
||||
authorizationCodeRequestAuthentication.getAdditionalParameters());
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
|
||||
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
|
||||
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
|
||||
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.scopes(authorizationCodeRequestAuthentication.getScopes())
|
||||
.state(authorizationCodeRequestAuthentication.getState())
|
||||
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
|
||||
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.springframework.util.Assert;
|
||||
* @since 0.2.1
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OAuth2AuthorizationConsent
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer)
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer)
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsentAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
private final Map<Object, Object> context;
|
||||
@@ -95,12 +95,12 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
* Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationConsentAuthenticationToken}.
|
||||
*
|
||||
* @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* @param authentication the {@link OAuth2AuthorizationConsentAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
public static Builder with(OAuth2AuthorizationConsentAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OAuth2AuthorizationConsentAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
private Builder(OAuth2AuthorizationConsentAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
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.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
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.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 org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Consent
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2AuthorizationConsentAuthenticationToken
|
||||
* @see OAuth2AuthorizationConsent
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String 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 RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2AuthorizationConsentService authorizationConsentService;
|
||||
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
|
||||
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationConsentAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
* @param authorizationConsentService the authorization consent service
|
||||
*/
|
||||
public OAuth2AuthorizationConsentAuthenticationProvider(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 {
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
|
||||
(OAuth2AuthorizationConsentAuthenticationToken) authentication;
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationConsentAuthentication, null, null);
|
||||
}
|
||||
|
||||
// The 'in-flight' authorization must be associated to the current principal
|
||||
Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationConsentAuthentication, null, null);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationConsentAuthentication.getClientId());
|
||||
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationConsentAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
Set<String> requestedScopes = authorizationRequest.getScopes();
|
||||
Set<String> authorizedScopes = new HashSet<>(authorizationConsentAuthentication.getScopes());
|
||||
if (!requestedScopes.containsAll(authorizedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationConsentAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : Collections.emptySet();
|
||||
|
||||
if (!currentAuthorizedScopes.isEmpty()) {
|
||||
for (String requestedScope : requestedScopes) {
|
||||
if (currentAuthorizedScopes.contains(requestedScope)) {
|
||||
authorizedScopes.add(requestedScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
|
||||
// 'openid' scope is auto-approved as it does not require consent
|
||||
authorizedScopes.add(OidcScopes.OPENID);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
|
||||
if (currentAuthorizationConsent != null) {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
|
||||
} else {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
}
|
||||
authorizedScopes.forEach(authorizationConsentBuilder::scope);
|
||||
|
||||
if (this.authorizationConsentCustomizer != null) {
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
|
||||
OAuth2AuthorizationConsentAuthenticationContext.with(authorizationConsentAuthentication)
|
||||
.authorizationConsent(authorizationConsentBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.authorization(authorization)
|
||||
.authorizationRequest(authorizationRequest)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
|
||||
}
|
||||
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
authorizationConsentBuilder.authorities(authorities::addAll);
|
||||
|
||||
if (authorities.isEmpty()) {
|
||||
// Authorization consent denied (or revoked)
|
||||
if (currentAuthorizationConsent != null) {
|
||||
this.authorizationConsentService.remove(currentAuthorizationConsent);
|
||||
}
|
||||
this.authorizationService.remove(authorization);
|
||||
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationConsentAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
|
||||
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
|
||||
this.authorizationConsentService.save(authorizationConsent);
|
||||
}
|
||||
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
authorizationConsentAuthentication, registeredClient, authorization, authorizedScopes);
|
||||
OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
|
||||
if (authorizationCode == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the authorization code.", ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
|
||||
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.token(authorizationCode)
|
||||
.attributes(attrs -> {
|
||||
attrs.remove(OAuth2ParameterNames.STATE);
|
||||
})
|
||||
.build();
|
||||
this.authorizationService.save(updatedAuthorization);
|
||||
|
||||
String redirectUri = authorizationRequest.getRedirectUri();
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationRequest.getAuthorizationUri(), registeredClient.getClientId(), principal, authorizationCode,
|
||||
redirectUri, authorizationRequest.getState(), authorizedScopes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
|
||||
*/
|
||||
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeGenerator = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 OAuth2AuthorizationConsentAuthenticationToken}.</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 OAuth2TokenContext createAuthorizationCodeTokenContext(
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
|
||||
|
||||
// @formatter:off
|
||||
return DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal((Authentication) authorizationConsentAuthentication.getPrincipal())
|
||||
.authorization(authorization)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrant(authorizationConsentAuthentication)
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static boolean isPrincipalAuthenticated(Authentication principal) {
|
||||
return principal != null &&
|
||||
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
|
||||
principal.isAuthenticated();
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
|
||||
throwError(error, parameterName, authorizationConsentAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
private static void throwError(OAuth2Error error, String parameterName,
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
|
||||
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
|
||||
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
|
||||
parameterName.equals(OAuth2ParameterNames.STATE))) {
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
String state = authorizationRequest != null ?
|
||||
authorizationRequest.getState() :
|
||||
authorizationConsentAuthentication.getState();
|
||||
Set<String> requestedScopes = authorizationRequest != null ?
|
||||
authorizationRequest.getScopes() :
|
||||
authorizationConsentAuthentication.getScopes();
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationConsentAuthentication.getAuthorizationUri(), authorizationConsentAuthentication.getClientId(),
|
||||
(Authentication) authorizationConsentAuthentication.getPrincipal(), redirectUri,
|
||||
state, requestedScopes, null);
|
||||
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
|
||||
}
|
||||
|
||||
private static String resolveRedirectUri(OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {
|
||||
if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
|
||||
return authorizationRequest.getRedirectUri();
|
||||
}
|
||||
if (registeredClient != null) {
|
||||
return registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.HashMap;
|
||||
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.server.authorization.util.SpringAuthorizationServerVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Consent
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
*/
|
||||
public class OAuth2AuthorizationConsentAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
|
||||
private final String authorizationUri;
|
||||
private final String clientId;
|
||||
private final Authentication principal;
|
||||
private final String state;
|
||||
private final Set<String> scopes;
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationConsentAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @param state the state
|
||||
* @param scopes the requested (or authorized) scope(s)
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2AuthorizationConsentAuthenticationToken(String authorizationUri, String clientId, Authentication principal,
|
||||
String state, @Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
Assert.hasText(state, "state cannot be empty");
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
this.state = state;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ?
|
||||
new HashSet<>(scopes) :
|
||||
Collections.emptySet());
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ?
|
||||
new HashMap<>(additionalParameters) :
|
||||
Collections.emptyMap());
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization URI.
|
||||
*
|
||||
* @return the authorization URI
|
||||
*/
|
||||
public String getAuthorizationUri() {
|
||||
return this.authorizationUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client identifier.
|
||||
*
|
||||
* @return the client identifier
|
||||
*/
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state.
|
||||
*
|
||||
* @return the state
|
||||
*/
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested (or authorized) scope(s).
|
||||
*
|
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return the additional parameters, or an empty {@code Map} if not available
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,10 +31,13 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
@@ -72,7 +75,8 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken}
|
||||
* used for authenticating the request.
|
||||
*
|
||||
* @param authorizationRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
@@ -170,7 +174,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
*
|
||||
* <ul>
|
||||
* <li>It must be an HTTP POST</li>
|
||||
* <li>It must be submitted to {@link AuthorizationServerSettings#getAuthorizationEndpoint()} ()}</li>
|
||||
* <li>It must be submitted to {@link AuthorizationServerSettings#getAuthorizationEndpoint()}</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}
|
||||
@@ -242,6 +246,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
|
||||
authenticationConverters.add(new OAuth2AuthorizationConsentAuthenticationConverter());
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
@@ -256,6 +261,13 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
|
||||
authenticationProviders.add(authorizationCodeRequestAuthenticationProvider);
|
||||
|
||||
OAuth2AuthorizationConsentAuthenticationProvider authorizationConsentAuthenticationProvider =
|
||||
new OAuth2AuthorizationConsentAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
|
||||
authenticationProviders.add(authorizationConsentAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -28,6 +29,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -40,7 +42,11 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
@@ -61,7 +67,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
* which handles the processing of the OAuth 2.0 Authorization Request (and Consent).
|
||||
* which handles the processing of the OAuth 2.0 Authorization Request and Consent.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Paurav Munshi
|
||||
@@ -71,6 +77,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* @since 0.0.1
|
||||
* @see AuthenticationManager
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||
@@ -110,7 +117,10 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.authorizationEndpointMatcher = createDefaultRequestMatcher(authorizationEndpointUri);
|
||||
this.authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
|
||||
this.authenticationConverter = new DelegatingAuthenticationConverter(
|
||||
Arrays.asList(
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationConverter(),
|
||||
new OAuth2AuthorizationConsentAuthenticationConverter()));
|
||||
}
|
||||
|
||||
private static RequestMatcher createDefaultRequestMatcher(String authorizationEndpointUri) {
|
||||
@@ -145,14 +155,14 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
}
|
||||
|
||||
try {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationConverter.convert(request);
|
||||
authorizationCodeRequestAuthentication.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
Authentication authentication = this.authenticationConverter.convert(request);
|
||||
if (authentication instanceof AbstractAuthenticationToken) {
|
||||
((AbstractAuthenticationToken) authentication)
|
||||
.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
}
|
||||
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication);
|
||||
|
||||
if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) {
|
||||
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
|
||||
@@ -160,13 +170,15 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
return;
|
||||
}
|
||||
|
||||
if (authorizationCodeRequestAuthenticationResult.isConsentRequired()) {
|
||||
sendAuthorizationConsent(request, response, authorizationCodeRequestAuthentication, authorizationCodeRequestAuthenticationResult);
|
||||
if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {
|
||||
sendAuthorizationConsent(request, response,
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,
|
||||
(OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(
|
||||
request, response, authorizationCodeRequestAuthenticationResult);
|
||||
request, response, authenticationResult);
|
||||
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
@@ -186,7 +198,8 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken}
|
||||
* used for authenticating the request.
|
||||
*
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
*/
|
||||
@@ -229,13 +242,13 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
|
||||
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult) throws IOException {
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException {
|
||||
|
||||
String clientId = authorizationCodeRequestAuthenticationResult.getClientId();
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthenticationResult.getPrincipal();
|
||||
String clientId = authorizationConsentAuthentication.getClientId();
|
||||
Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
|
||||
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
||||
Set<String> authorizedScopes = authorizationCodeRequestAuthenticationResult.getScopes();
|
||||
String state = authorizationCodeRequestAuthenticationResult.getState();
|
||||
Set<String> authorizedScopes = authorizationConsentAuthentication.getScopes();
|
||||
String state = authorizationConsentAuthentication.getState();
|
||||
|
||||
if (hasConsentUri()) {
|
||||
String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,7 +43,7 @@ import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Attempts to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* Attempts to extract an Authorization Request from {@link HttpServletRequest}
|
||||
* for the OAuth 2.0 Authorization Code Grant and then converts it to
|
||||
* an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
@@ -62,20 +62,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
boolean authorizationRequest = false;
|
||||
if ("GET".equals(request.getMethod()) || OIDC_REQUEST_MATCHER.matches(request)) {
|
||||
authorizationRequest = true;
|
||||
|
||||
// response_type (REQUIRED)
|
||||
String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
if (!StringUtils.hasText(responseType) ||
|
||||
parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
} else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
}
|
||||
// response_type (REQUIRED)
|
||||
String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
if (!StringUtils.hasText(responseType) ||
|
||||
parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
} else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
}
|
||||
|
||||
String authorizationUri = request.getRequestURL().toString();
|
||||
@@ -101,37 +100,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
|
||||
// scope (OPTIONAL)
|
||||
Set<String> scopes = null;
|
||||
if (authorizationRequest) {
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
if (StringUtils.hasText(scope) &&
|
||||
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
|
||||
}
|
||||
if (StringUtils.hasText(scope)) {
|
||||
scopes = new HashSet<>(
|
||||
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
}
|
||||
} else {
|
||||
// Consent request
|
||||
if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
|
||||
scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
|
||||
}
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
if (StringUtils.hasText(scope) &&
|
||||
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
|
||||
}
|
||||
if (StringUtils.hasText(scope)) {
|
||||
scopes = new HashSet<>(
|
||||
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
}
|
||||
|
||||
// state
|
||||
// RECOMMENDED for Authorization Request
|
||||
// state (RECOMMENDED)
|
||||
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
|
||||
if (authorizationRequest) {
|
||||
if (StringUtils.hasText(state) &&
|
||||
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
} else {
|
||||
// REQUIRED for Authorization Consent Request
|
||||
if (!StringUtils.hasText(state) ||
|
||||
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
if (StringUtils.hasText(state) &&
|
||||
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
|
||||
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
||||
@@ -159,14 +142,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
}
|
||||
});
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal)
|
||||
.authorizationUri(authorizationUri)
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(scopes)
|
||||
.state(state)
|
||||
.additionalParameters(additionalParameters)
|
||||
.consent(!authorizationRequest)
|
||||
.build();
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
|
||||
redirectUri, state, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
private static RequestMatcher createOidcRequestMatcher() {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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 javax.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.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
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 Authorization Code Grant and then converts it to
|
||||
* an {@link OAuth2AuthorizationConsentAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see AuthenticationConverter
|
||||
* @see OAuth2AuthorizationConsentAuthenticationToken
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsentAuthenticationConverter implements AuthenticationConverter {
|
||||
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
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()) ||
|
||||
request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
String authorizationUri = request.getRequestURL().toString();
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId) ||
|
||||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
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) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal,
|
||||
state, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user