|
@@ -20,8 +20,11 @@ import java.time.Instant;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
import java.util.Base64;
|
|
|
import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
import java.util.HashSet;
|
|
|
+import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
+import java.util.function.Function;
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
|
@@ -34,6 +37,8 @@ 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.OAuth2TokenType;
|
|
|
+import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
|
|
|
+import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationValidator;
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
|
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
|
@@ -68,11 +73,14 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
|
|
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
|
|
|
private static final Pattern LOOPBACK_ADDRESS_PATTERN =
|
|
|
Pattern.compile("^127(?:\\.[0-9]+){0,2}\\.[0-9]+$|^\\[(?:0*:)*?:?0*1]$");
|
|
|
+ private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
|
|
|
+ createDefaultAuthenticationValidatorResolver();
|
|
|
private final RegisteredClientRepository registeredClientRepository;
|
|
|
private final OAuth2AuthorizationService authorizationService;
|
|
|
private final OAuth2AuthorizationConsentService authorizationConsentService;
|
|
|
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
|
|
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
|
|
+ private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
|
|
|
|
|
|
/**
|
|
|
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
|
|
@@ -106,6 +114,26 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
|
|
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter.
|
|
|
+ *
|
|
|
+ * <p>
|
|
|
+ * The following OAuth 2.0 Authorization Request parameters are supported:
|
|
|
+ * <ol>
|
|
|
+ * <li>{@link OAuth2ParameterNames#REDIRECT_URI}</li>
|
|
|
+ * <li>{@link OAuth2ParameterNames#SCOPE}</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * <p>
|
|
|
+ * <b>NOTE:</b> The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
|
|
|
+ *
|
|
|
+ * @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter
|
|
|
+ */
|
|
|
+ public void setAuthenticationValidatorResolver(Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver) {
|
|
|
+ Assert.notNull(authenticationValidatorResolver, "authenticationValidatorResolver cannot be null");
|
|
|
+ this.authenticationValidatorResolver = authenticationValidatorResolver;
|
|
|
+ }
|
|
|
+
|
|
|
private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
|
|
|
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
|
|
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
|
@@ -117,29 +145,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
|
|
authorizationCodeRequestAuthentication, null);
|
|
|
}
|
|
|
|
|
|
- if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
|
|
- if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
|
|
- authorizationCodeRequestAuthentication, registeredClient);
|
|
|
- }
|
|
|
- } else if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
|
|
|
- registeredClient.getRedirectUris().size() != 1) {
|
|
|
- // redirect_uri is REQUIRED for OpenID Connect
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
|
|
- authorizationCodeRequestAuthentication, registeredClient);
|
|
|
- }
|
|
|
+ Map<Object, Object> context = new HashMap<>();
|
|
|
+ context.put(RegisteredClient.class, registeredClient);
|
|
|
+ OAuth2AuthenticationContext authenticationContext = new OAuth2AuthenticationContext(
|
|
|
+ authorizationCodeRequestAuthentication, context);
|
|
|
+
|
|
|
+ OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI);
|
|
|
+ redirectUriValidator.validate(authenticationContext);
|
|
|
|
|
|
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
|
|
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
|
|
|
authorizationCodeRequestAuthentication, registeredClient);
|
|
|
}
|
|
|
|
|
|
- Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
|
|
- Set<String> allowedScopes = registeredClient.getScopes();
|
|
|
- if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
|
|
- throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
|
|
- authorizationCodeRequestAuthentication, registeredClient);
|
|
|
- }
|
|
|
+ OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator(OAuth2ParameterNames.SCOPE);
|
|
|
+ scopeValidator.validate(authenticationContext);
|
|
|
|
|
|
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
|
|
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
|
|
@@ -170,7 +190,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
|
|
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
|
|
|
.clientId(registeredClient.getClientId())
|
|
|
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
|
|
|
- .scopes(requestedScopes)
|
|
|
+ .scopes(authorizationCodeRequestAuthentication.getScopes())
|
|
|
.state(authorizationCodeRequestAuthentication.getState())
|
|
|
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
|
|
|
.build();
|
|
@@ -227,6 +247,13 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
|
|
.build();
|
|
|
}
|
|
|
|
|
|
+ private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) {
|
|
|
+ OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName);
|
|
|
+ return authenticationValidator != null ?
|
|
|
+ authenticationValidator :
|
|
|
+ DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
|
|
|
+ }
|
|
|
+
|
|
|
private OAuth2AuthorizationCode createAuthorizationCode() {
|
|
|
Instant issuedAt = Instant.now();
|
|
|
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
|
|
@@ -329,6 +356,13 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
|
|
.build();
|
|
|
}
|
|
|
|
|
|
+ private static Function<String, OAuth2AuthenticationValidator> createDefaultAuthenticationValidatorResolver() {
|
|
|
+ Map<String, OAuth2AuthenticationValidator> authenticationValidators = new HashMap<>();
|
|
|
+ authenticationValidators.put(OAuth2ParameterNames.REDIRECT_URI, new DefaultRedirectUriOAuth2AuthenticationValidator());
|
|
|
+ authenticationValidators.put(OAuth2ParameterNames.SCOPE, new DefaultScopeOAuth2AuthenticationValidator());
|
|
|
+ return authenticationValidators::get;
|
|
|
+ }
|
|
|
+
|
|
|
private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
|
|
|
OAuth2AuthorizationRequest authorizationRequest) {
|
|
|
return OAuth2Authorization.withRegisteredClient(registeredClient)
|
|
@@ -474,4 +508,45 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
|
|
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
|
|
|
}
|
|
|
|
|
|
+ private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void validate(OAuth2AuthenticationContext authenticationContext) {
|
|
|
+ OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
|
|
+ authenticationContext.getAuthentication();
|
|
|
+ RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
|
|
|
+
|
|
|
+ if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
|
|
+ if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
|
|
|
+ throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
|
|
+ authorizationCodeRequestAuthentication, registeredClient);
|
|
|
+ }
|
|
|
+ } else if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
|
|
|
+ registeredClient.getRedirectUris().size() != 1) {
|
|
|
+ // redirect_uri is REQUIRED for OpenID Connect
|
|
|
+ throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
|
|
+ authorizationCodeRequestAuthentication, registeredClient);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class DefaultScopeOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void validate(OAuth2AuthenticationContext authenticationContext) {
|
|
|
+ OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
|
|
+ authenticationContext.getAuthentication();
|
|
|
+ RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
|
|
|
+
|
|
|
+ Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
|
|
+ Set<String> allowedScopes = registeredClient.getScopes();
|
|
|
+ if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
|
|
+ throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
|
|
+ authorizationCodeRequestAuthentication, registeredClient);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
}
|