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 7 commits 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
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,6 @@ spring:

[NOTE]
====
Public Clients are supported using https://tools.ietf.org/html/rfc7636[Proof Key for Code Exchange] (PKCE).
PKCE will automatically be used when `client-authentication-method` is set to "none" (`ClientAuthenticationMethod.NONE`).
https://tools.ietf.org/html/rfc7636[Proof Key for Code Exchange] (PKCE) will be enabled by default for public as well as confidential clients. Refer https://docs.spring.io/spring-security/reference/servlet/oauth2/client/client-authentication.html#oauth2-client-authentication-public[this
section] to disable PKCE.
====
3 changes: 2 additions & 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,8 @@ 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`: This will by default be `true`, as PKCE will be enabled by default.
If set to `false`,then PKCE will be disabled for public as well as confidential clients.

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,10 @@ private ClientRegistration create() {
clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName
: this.registrationId;
clientRegistration.clientSettings = this.clientSettings;
if (this.clientSettings.requireProofKey) {
clientRegistration.clientSettings.requireProofKey = clientRegistration.authorizationGrantType
.equals(AuthorizationGrantType.AUTHORIZATION_CODE);
}
return clientRegistration;
}

Expand Down Expand Up @@ -713,12 +717,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 @@ -756,9 +754,7 @@ public static final class ClientSettings implements Serializable {

private boolean requireProofKey;

private ClientSettings() {

}
private ClientSettings() {}

public boolean isRequireProofKey() {
return this.requireProofKey;
Expand Down Expand Up @@ -794,6 +790,7 @@ public static final class Builder {
private boolean requireProofKey;

private Builder() {
this.requireProofKey = true;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
Expand Down Expand Up @@ -184,8 +183,7 @@ private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientR
// value.
applyNonce(builder);
}
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())
|| clientRegistration.getClientSettings().isRequireProofKey()) {
if (clientRegistration.getClientSettings().isRequireProofKey()) {
DEFAULT_PKCE_APPLIER.accept(builder);
}
return builder;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
Expand Down Expand Up @@ -34,7 +34,6 @@
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
Expand Down Expand Up @@ -196,8 +195,7 @@ private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientR
// value.
applyNonce(builder);
}
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())
|| clientRegistration.getClientSettings().isRequireProofKey()) {
if (clientRegistration.getClientSettings().isRequireProofKey()) {
DEFAULT_PKCE_APPLIER.accept(builder);
}
return 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
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 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.
Expand Down Expand Up @@ -64,6 +64,22 @@ public static ClientRegistration.Builder clientRegistration2() {
// @formatter:on
}

public static ClientRegistration.Builder publicClientRegistrationWithNoPkce() {
return ClientRegistration.withRegistrationId("no-pkce")
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientSettings(ClientRegistration.ClientSettings.builder().requireProofKey(false).build())
.scope("read:user")
.authorizationUri("https://example.com/login/oauth/authorize")
.tokenUri("https://example.com/login/oauth/access_token")
.userInfoUri("https://api.example.com/user")
.userNameAttributeName("id")
.clientName("Client Name")
.clientId("client-id")
.clientSecret(null);
}

public static ClientRegistration.Builder clientCredentials() {
// @formatter:off
return clientRegistration()
Expand Down
Loading