From 19dfcd4ba9a609ffc9e4e397b37980611b4455e6 Mon Sep 17 00:00:00 2001
From: Joe Grandja <10884212+jgrandja@users.noreply.github.com>
Date: Tue, 6 Aug 2024 17:31:04 -0400
Subject: [PATCH] Add support for OpenID Connect 1.0 prompt=none parameter
Closes gh-501
---
...tionCodeRequestAuthenticationProvider.java | 51 +++++++++++++
...ionCodeRequestAuthenticationConverter.java | 11 ++-
...odeRequestAuthenticationProviderTests.java | 73 +++++++++++++++++++
...Auth2AuthorizationEndpointFilterTests.java | 17 ++++-
4 files changed, 150 insertions(+), 2 deletions(-)
diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
index b49b975d..1f3cef97 100644
--- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
+++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
@@ -16,7 +16,10 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -70,6 +73,9 @@ import org.springframework.util.StringUtils;
* @see Section 4.1.1
* Authorization Request
+ * @see Section 3.1.2.1
+ * Authentication Request
*/
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
@@ -158,6 +164,22 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
authorizationCodeRequestAuthentication, registeredClient, null);
}
+ // prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
+ Set promptValues = Collections.emptySet();
+ if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
+ String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
+ if (StringUtils.hasText(prompt)) {
+ promptValues = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
+ if (promptValues.contains(OidcPrompts.NONE)) {
+ if (promptValues.contains(OidcPrompts.LOGIN) || promptValues.contains(OidcPrompts.CONSENT)
+ || promptValues.contains(OidcPrompts.SELECT_ACCOUNT)) {
+ throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authorizationCodeRequestAuthentication,
+ registeredClient);
+ }
+ }
+ }
+ }
+
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated authorization code request parameters");
}
@@ -168,6 +190,11 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
if (!isPrincipalAuthenticated(principal)) {
+ if (promptValues.contains(OidcPrompts.NONE)) {
+ // Return an error instead of displaying the login page (via the
+ // configured AuthenticationEntryPoint)
+ throwError("login_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
+ }
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not authenticate authorization code request since principal not authenticated");
}
@@ -192,6 +219,11 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
}
if (this.authorizationConsentRequired.test(authenticationContextBuilder.build())) {
+ if (promptValues.contains(OidcPrompts.NONE)) {
+ // Return an error instead of displaying the consent page
+ throwError("consent_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
+ }
+
String state = DEFAULT_STATE_GENERATOR.generateKey();
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.attribute(OAuth2ParameterNames.STATE, state)
@@ -425,4 +457,23 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
return null;
}
+ /*
+ * The values defined for the "prompt" parameter for the OpenID Connect 1.0
+ * Authentication Request.
+ */
+ private static final class OidcPrompts {
+
+ private static final String NONE = "none";
+
+ private static final String LOGIN = "login";
+
+ private static final String CONSENT = "consent";
+
+ private static final String SELECT_ACCOUNT = "select_account";
+
+ private OidcPrompts() {
+ }
+
+ }
+
}
diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java
index d54442b0..c574d615 100644
--- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java
+++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 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.
@@ -39,6 +39,7 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2Author
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
@@ -131,6 +132,14 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI);
}
+ // prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
+ if (!CollectionUtils.isEmpty(scopes) && scopes.contains(OidcScopes.OPENID)) {
+ String prompt = parameters.getFirst("prompt");
+ if (StringUtils.hasText(prompt) && parameters.get("prompt").size() != 1) {
+ throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt");
+ }
+ }
+
Map additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.RESPONSE_TYPE) && !key.equals(OAuth2ParameterNames.CLIENT_ID)
diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
index 403fe504..332aa1c6 100644
--- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
+++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
@@ -366,6 +366,59 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
authentication.getRedirectUri()));
}
+ @Test
+ public void authenticateWhenAuthenticationRequestWithPromptNoneLoginThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+ assertWhenAuthenticationRequestWithPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+ "none login");
+ }
+
+ @Test
+ public void authenticateWhenAuthenticationRequestWithPromptNoneConsentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+ assertWhenAuthenticationRequestWithPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+ "none consent");
+ }
+
+ @Test
+ public void authenticateWhenAuthenticationRequestWithPromptNoneSelectAccountThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+ assertWhenAuthenticationRequestWithPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+ "none select_account");
+ }
+
+ private void assertWhenAuthenticationRequestWithPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+ String prompt) {
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
+ given(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+ .willReturn(registeredClient);
+ String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+ Map additionalParameters = new HashMap<>();
+ additionalParameters.put("prompt", prompt);
+ OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+ AUTHORIZATION_URI, registeredClient.getClientId(), this.principal, redirectUri, STATE,
+ registeredClient.getScopes(), additionalParameters);
+ assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+ .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+ .satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+ OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authentication.getRedirectUri()));
+ }
+
+ @Test
+ public void authenticateWhenPrincipalNotAuthenticatedAndPromptNoneThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
+ given(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+ .willReturn(registeredClient);
+ this.principal.setAuthenticated(false);
+ String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+ Map additionalParameters = new HashMap<>();
+ additionalParameters.put("prompt", "none");
+ OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+ AUTHORIZATION_URI, registeredClient.getClientId(), this.principal, redirectUri, STATE,
+ registeredClient.getScopes(), additionalParameters);
+ assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+ .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+ .satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+ "login_required", "prompt", authentication.getRedirectUri()));
+ }
+
@Test
public void authenticateWhenPrincipalNotAuthenticatedThenReturnAuthorizationCodeRequest() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -385,6 +438,26 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
assertThat(authenticationResult.isAuthenticated()).isFalse();
}
+ @Test
+ public void authenticateWhenRequireAuthorizationConsentAndPromptNoneThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+ .scope(OidcScopes.OPENID)
+ .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
+ .build();
+ given(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+ .willReturn(registeredClient);
+ String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+ Map additionalParameters = new HashMap<>();
+ additionalParameters.put("prompt", "none");
+ OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+ AUTHORIZATION_URI, registeredClient.getClientId(), this.principal, redirectUri, STATE,
+ registeredClient.getScopes(), additionalParameters);
+ assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+ .isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+ .satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+ "consent_required", "prompt", authentication.getRedirectUri()));
+ }
+
@Test
public void authenticateWhenRequireAuthorizationConsentThenReturnAuthorizationConsent() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java
index ba89f50b..4a749470 100644
--- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java
+++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 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.
@@ -288,6 +288,21 @@ public class OAuth2AuthorizationEndpointFilterTests {
});
}
+ @Test
+ public void doFilterWhenAuthenticationRequestMultiplePromptThenInvalidRequestError() throws Exception {
+ // Setup OpenID Connect request
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scopes((scopes) -> {
+ scopes.clear();
+ scopes.add(OidcScopes.OPENID);
+ }).build();
+ doFilterWhenAuthorizationRequestInvalidParameterThenError(registeredClient, "prompt",
+ OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
+ request.addParameter("prompt", "none");
+ request.addParameter("prompt", "login");
+ updateQueryString(request);
+ });
+ }
+
@Test
public void doFilterWhenAuthorizationRequestAuthenticationExceptionThenErrorResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().redirectUris((redirectUris) -> {