Skip to content

PKCE configuration - enabled by default #17507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 6.5.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public void configureWhenAuthorizationCodeRequestThenRedirectForAuthorization()
.andExpect(status().is3xxRedirection()).andReturn();
assertThat(mvcResult.getResponse().getRedirectedUrl())
.matches("https://provider.com/oauth2/authorize\\?" + "response_type=code&client_id=client-1&"
+ "scope=user&state=.{15,}&" + "redirect_uri=http://localhost/client-1");
+ "scope=user&state=.{15,}&" + "redirect_uri=http://localhost/client-1&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
// @formatter:on
}

Expand All @@ -166,9 +166,9 @@ public void configureWhenOauth2ClientInLambdaThenRedirectForAuthorization() thro
MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1"))
.andExpect(status().is3xxRedirection())
.andReturn();
assertThat(mvcResult.getResponse().getRedirectedUrl())
.matches("https://provider.com/oauth2/authorize\\?" + "response_type=code&client_id=client-1&"
+ "scope=user&state=.{15,}&" + "redirect_uri=http://localhost/client-1");
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?"
+ "response_type=code&client_id=client-1&" + "scope=user&state=.{15,}&"
+ "redirect_uri=http://localhost/client-1&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand Down Expand Up @@ -215,9 +215,9 @@ public void configureWhenRequestCacheProvidedAndClientAuthorizationRequiredExcep
MvcResult mvcResult = this.mockMvc.perform(get("/resource1").with(user("user1")))
.andExpect(status().is3xxRedirection())
.andReturn();
assertThat(mvcResult.getResponse().getRedirectedUrl())
.matches("https://provider.com/oauth2/authorize\\?" + "response_type=code&client_id=client-1&"
+ "scope=user&state=.{15,}&" + "redirect_uri=http://localhost/client-1");
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?"
+ "response_type=code&client_id=client-1&" + "scope=user&state=.{15,}&"
+ "redirect_uri=http://localhost/client-1&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ public void requestWhenAuthorizeThenRedirect() throws Exception {
.andExpect(status().is3xxRedirection())
.andReturn();
// @formatter:on
assertThat(result.getResponse().getRedirectedUrl()).matches(
"https://accounts.google.com/o/oauth2/v2/auth\\?" + "response_type=code&client_id=google-client-id&"
+ "scope=scope1%20scope2&state=.{15,}&redirect_uri=http://localhost/callback/google");
assertThat(result.getResponse().getRedirectedUrl()).matches("https://accounts.google.com/o/oauth2/v2/auth\\?"
+ "response_type=code&client_id=google-client-id&"
+ "scope=scope1%20scope2&state=.{15,}&redirect_uri=http://localhost/callback/google&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -134,9 +134,9 @@ public void requestWhenCustomClientRegistrationRepositoryThenCalled() throws Exc
.andExpect(status().is3xxRedirection())
.andReturn();
// @formatter:on
assertThat(result.getResponse().getRedirectedUrl()).matches(
"https://accounts.google.com/o/oauth2/v2/auth\\?" + "response_type=code&client_id=google-client-id&"
+ "scope=scope1%20scope2&state=.{15,}&redirect_uri=http://localhost/callback/google");
assertThat(result.getResponse().getRedirectedUrl()).matches("https://accounts.google.com/o/oauth2/v2/auth\\?"
+ "response_type=code&client_id=google-client-id&"
+ "scope=scope1%20scope2&state=.{15,}&redirect_uri=http://localhost/callback/google&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
verify(this.clientRegistrationRepository).findByRegistrationId(any());
}

Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ This information is available only if the Spring Boot property `spring.security.
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
The supported values are *header*, *form*, and *query*.
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled.

You can initially configure a `ClientRegistration` by using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,15 @@ private ClientRegistration create() {
clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName
: this.registrationId;
clientRegistration.clientSettings = this.clientSettings;
if (clientRegistration.clientSettings.requireProofKey) {
clientRegistration.clientSettings.requireProofKey = AuthorizationGrantType.AUTHORIZATION_CODE
.equals(this.authorizationGrantType)
|| ClientAuthenticationMethod.NONE.equals(this.clientAuthenticationMethod);
}
else {
clientRegistration.clientSettings.requireProofKey = ClientAuthenticationMethod.NONE
.equals(this.clientAuthenticationMethod);
}
return clientRegistration;
}

Expand Down Expand Up @@ -713,12 +722,6 @@ private void validateAuthorizationGrantTypes() {
"AuthorizationGrantType: %s does not match the pre-defined constant %s and won't match a valid OAuth2AuthorizedClientProvider",
this.authorizationGrantType, authorizationGrantType));
}
if (!AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)
&& this.clientSettings.isRequireProofKey()) {
throw new IllegalStateException(
"clientSettings.isRequireProofKey=true is only valid with authorizationGrantType=AUTHORIZATION_CODE. Got authorizationGrantType="
+ this.authorizationGrantType);
}
}
}

Expand Down Expand Up @@ -754,7 +757,7 @@ public static final class ClientSettings implements Serializable {
@Serial
private static final long serialVersionUID = 7495627155437124692L;

private boolean requireProofKey;
private boolean requireProofKey = true;

private ClientSettings() {

Expand Down Expand Up @@ -791,7 +794,7 @@ public static Builder builder() {

public static final class Builder {

private boolean requireProofKey;
private boolean requireProofKey = true;

private Builder() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

/**
* Tests for {@link ClientRegistration}.
Expand Down Expand Up @@ -780,7 +779,7 @@ void buildWhenDefaultClientSettingsThenDefaulted() {
// should not be null
assertThat(clientRegistration.getClientSettings()).isNotNull();
// proof key should be false for passivity
assertThat(clientRegistration.getClientSettings().isRequireProofKey()).isFalse();
assertThat(clientRegistration.getClientSettings().isRequireProofKey()).isTrue();
}

// gh-16382
Expand Down Expand Up @@ -816,10 +815,8 @@ void buildWhenInvalidGrantTypeForPkceThenException(AuthorizationGrantType invali
.authorizationUri(AUTHORIZATION_URI)
.tokenUri(TOKEN_URI);

assertThatIllegalStateException().describedAs(
"clientSettings.isRequireProofKey=true is only valid with authorizationGrantType=AUTHORIZATION_CODE. Got authorizationGrantType={}",
invalidGrantType)
.isThrownBy(builder::build);
ClientRegistration clientRegistration = builder.build();
assertThat(clientRegistration.getClientSettings().isRequireProofKey()).isFalse();
}

static List<AuthorizationGrantType> invalidPkceGrantTypes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {

private ClientRegistration pkceClientRegistration;

private ClientRegistration nonProofKeyPublicClientRegistration;

private ClientRegistration fineRedirectUriTemplateRegistration;

private ClientRegistration publicClientRegistration;
Expand All @@ -76,7 +78,11 @@ public void setUp() {
this.registration2 = TestClientRegistrations.clientRegistration2().build();

this.pkceClientRegistration = pkceClientRegistration().build();

this.nonProofKeyPublicClientRegistration = TestClientRegistrations.clientRegistration()
.registrationId("invalid-public-client-registration-id")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.clientSettings(ClientRegistration.ClientSettings.builder().requireProofKey(false).build())
.build();
this.fineRedirectUriTemplateRegistration = fineRedirectUriTemplateClientRegistration().build();
// @formatter:off
this.publicClientRegistration = TestClientRegistrations.clientRegistration()
Expand All @@ -92,7 +98,7 @@ public void setUp() {
// @formatter:on
this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1,
this.registration2, this.pkceClientRegistration, this.fineRedirectUriTemplateRegistration,
this.publicClientRegistration, this.oidcRegistration);
this.publicClientRegistration, this.oidcRegistration, this.nonProofKeyPublicClientRegistration);
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository,
this.authorizationRequestBaseUri);
}
Expand Down Expand Up @@ -173,12 +179,14 @@ public void resolveWhenAuthorizationRequestWithValidClientThenResolves() {
assertThat(authorizationRequest.getState()).isNotNull();
assertThat(authorizationRequest.getAdditionalParameters())
.doesNotContainKey(OAuth2ParameterNames.REGISTRATION_ID);
assertThat(authorizationRequest.getAttributes())
.containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
assertThat(authorizationRequest.getAttributes()).containsExactly(
entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()),
entry(PkceParameterNames.CODE_VERIFIER,
authorizationRequest.getAttributes().get(PkceParameterNames.CODE_VERIFIER)));
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=read:user&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id");
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -190,8 +198,10 @@ public void resolveWhenClientAuthorizationRequiredExceptionAvailableThenResolves
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request,
clientRegistration.getRegistrationId());
assertThat(authorizationRequest).isNotNull();
assertThat(authorizationRequest.getAttributes())
.containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
assertThat(authorizationRequest.getAttributes()).containsExactly(
entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()),
entry(PkceParameterNames.CODE_VERIFIER,
authorizationRequest.getAttributes().get(PkceParameterNames.CODE_VERIFIER)));
}

@Test
Expand Down Expand Up @@ -299,7 +309,8 @@ public void resolveWhenAuthorizationRequestIncludesPort80ThenExpandedRedirectUri
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=read:user&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id");
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id"
+ "&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -315,7 +326,8 @@ public void resolveWhenAuthorizationRequestIncludesPort443ThenExpandedRedirectUr
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=read:user&state=.{15,}&"
+ "redirect_uri=https://example.com/login/oauth2/code/registration-id");
+ "redirect_uri=https://example.com/login/oauth2/code/registration-id"
+ "&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -329,7 +341,7 @@ public void resolveWhenClientAuthorizationRequiredExceptionAvailableThenRedirect
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=read:user&state=.{15,}&"
+ "redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
+ "redirect_uri=http://localhost/authorize/oauth2/code/registration-id&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -342,7 +354,8 @@ public void resolveWhenAuthorizationRequestOAuth2LoginThenRedirectUriIsLogin() {
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id-2&"
+ "scope=read:user&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id-2"
+ "&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -356,7 +369,8 @@ public void resolveWhenAuthorizationRequestHasActionParameterAuthorizeThenRedire
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=read:user&state=.{15,}&"
+ "redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
+ "redirect_uri=http://localhost/authorize/oauth2/code/registration-id&"
+ "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -370,7 +384,7 @@ public void resolveWhenAuthorizationRequestHasActionParameterLoginThenRedirectUr
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id-2&"
+ "scope=read:user&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
+ "redirect_uri=http://localhost/login/oauth2/code/registration-id-2&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand Down Expand Up @@ -427,33 +441,32 @@ public void resolveWhenAuthorizationRequestApplyPkceToConfidentialClientsThenApp
assertPkceApplied(authorizationRequest, clientRegistration);
}

// gh-6548
@Test
public void resolveWhenAuthorizationRequestApplyPkceToSpecificConfidentialClientThenApplied() {
this.resolver.setAuthorizationRequestCustomizer((builder) -> {
builder.attributes((attrs) -> {
String registrationId = (String) attrs.get(OAuth2ParameterNames.REGISTRATION_ID);
if (this.registration1.getRegistrationId().equals(registrationId)) {
OAuth2AuthorizationRequestCustomizers.withPkce().accept(builder);
}
});
});

public void resolveWhenAuthorizationRequestApplyPkceToConfidentialClientThenApplied() {
// pkce enabled by default for private clients
ClientRegistration clientRegistration = this.registration1;
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertPkceApplied(authorizationRequest, clientRegistration);
}

clientRegistration = this.registration2;
requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
authorizationRequest = this.resolver.resolve(request);
assertPkceNotApplied(authorizationRequest, clientRegistration);
@Test
public void resolveWhenAuthorizationRequestApplyPkceToPublicClientWithRequireProofKeyFalseThenApplied() {
ClientRegistration clientRegistration = this.nonProofKeyPublicClientRegistration; // change
// to
// non
// proof
// key
// public
// client
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertPkceApplied(authorizationRequest, clientRegistration);
}

// gh-6548
private void assertPkceApplied(OAuth2AuthorizationRequest authorizationRequest,
ClientRegistration clientRegistration) {
assertThat(authorizationRequest.getAdditionalParameters()).containsKey(PkceParameterNames.CODE_CHALLENGE);
Expand Down Expand Up @@ -510,7 +523,7 @@ public void resolveWhenAuthenticationRequestWithValidOidcClientThenResolves() {
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=openid&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/oidc-registration-id&"
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

// gh-7696
Expand All @@ -530,7 +543,8 @@ public void resolveWhenAuthorizationRequestCustomizerRemovesNonceThenQueryExclud
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=openid&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/oidc-registration-id");
+ "redirect_uri=http://localhost/login/oauth2/code/oidc-registration-id&"
+ "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256");
}

@Test
Expand All @@ -548,7 +562,8 @@ public void resolveWhenAuthorizationRequestCustomizerAddsParameterThenQueryInclu
.matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&"
+ "scope=openid&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/oidc-registration-id&"
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "param1=value1");
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"
+ "&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256&param1=value1");
}

@Test
Expand All @@ -565,7 +580,8 @@ public void resolveWhenAuthorizationRequestCustomizerOverridesParameterThenQuery
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches(
"https://example.com/login/oauth/authorize\\?" + "response_type=code&" + "scope=openid&state=.{15,}&"
+ "redirect_uri=http://localhost/login/oauth2/code/oidc-registration-id&"
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "appid=client-id");
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"
+ "&code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}&code_challenge_method=S256&appid=client-id");
}

@Test
Expand Down
Loading