mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-23 12:32:13 +00:00
Add ClientRegistration.clientSettings.requireProofKey
Setting ClientRegistration.clientSettings.requireProofKey=true will enable PKCE for clients using authorization_code grant type. Closes gh-16386
This commit is contained in:
commit
4fc99aa9e1
@ -110,6 +110,10 @@ nohttp {
|
|||||||
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
|
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('checkstyleNohttp') {
|
||||||
|
maxHeapSize = '1g'
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('cloneRepository', IncludeRepoTask) {
|
tasks.register('cloneRepository', IncludeRepoTask) {
|
||||||
repository = project.getProperties().get("repositoryName")
|
repository = project.getProperties().get("repositoryName")
|
||||||
ref = project.getProperties().get("ref")
|
ref = project.getProperties().get("ref")
|
||||||
|
@ -79,6 +79,10 @@ If the client is running in an untrusted environment (eg. native application or
|
|||||||
. `client-secret` is omitted (or empty)
|
. `client-secret` is omitted (or empty)
|
||||||
. `client-authentication-method` is set to "none" (`ClientAuthenticationMethod.NONE`)
|
. `client-authentication-method` is set to "none" (`ClientAuthenticationMethod.NONE`)
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
. When `ClientRegistration.clientSettings.requireProofKey` is `true` (in this case `ClientRegistration.authorizationGrantType` must be `authorization_code`)
|
||||||
|
|
||||||
[TIP]
|
[TIP]
|
||||||
====
|
====
|
||||||
If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultServerOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`.
|
If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultServerOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`.
|
||||||
|
@ -39,6 +39,10 @@ public final class ClientRegistration {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class ClientSettings {
|
||||||
|
private boolean requireProofKey; // <17>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`.
|
<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`.
|
||||||
@ -64,6 +68,7 @@ The name may be used in certain scenarios, such as when displaying the name of t
|
|||||||
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
|
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
|
||||||
The supported values are *header*, *form* and *query*.
|
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.
|
<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.
|
||||||
|
|
||||||
A `ClientRegistration` can be initially configured 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].
|
A `ClientRegistration` can be initially configured 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].
|
||||||
|
|
||||||
|
@ -77,9 +77,14 @@ spring:
|
|||||||
Public Clients are supported by using https://tools.ietf.org/html/rfc7636[Proof Key for Code Exchange] (PKCE).
|
Public Clients are supported by using https://tools.ietf.org/html/rfc7636[Proof Key for Code Exchange] (PKCE).
|
||||||
If the client is running in an untrusted environment (such as a native application or web browser-based application) and is therefore incapable of maintaining the confidentiality of its credentials, PKCE is automatically used when the following conditions are true:
|
If the client is running in an untrusted environment (such as a native application or web browser-based application) and is therefore incapable of maintaining the confidentiality of its credentials, PKCE is automatically used when the following conditions are true:
|
||||||
|
|
||||||
. `client-secret` is omitted (or empty)
|
. `client-secret` is omitted (or empty) and
|
||||||
. `client-authentication-method` is set to `none` (`ClientAuthenticationMethod.NONE`)
|
. `client-authentication-method` is set to `none` (`ClientAuthenticationMethod.NONE`)
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
. When `ClientRegistration.clientSettings.requireProofKey` is `true` (in this case `ClientRegistration.authorizationGrantType` must be `authorization_code`)
|
||||||
|
|
||||||
|
|
||||||
[TIP]
|
[TIP]
|
||||||
====
|
====
|
||||||
If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`.
|
If the OAuth 2.0 Provider supports PKCE for https://tools.ietf.org/html/rfc6749#section-2.1[Confidential Clients], you may (optionally) configure it using `DefaultOAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce())`.
|
||||||
|
@ -40,6 +40,10 @@ public final class ClientRegistration {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class ClientSettings {
|
||||||
|
private boolean requireProofKey; // <17>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`.
|
<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`.
|
||||||
@ -65,6 +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.
|
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
|
||||||
The supported values are *header*, *form*, and *query*.
|
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.
|
<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.
|
||||||
|
|
||||||
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].
|
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].
|
||||||
|
|
||||||
|
@ -10,3 +10,7 @@ Below are the highlights of the release, or you can view https://github.com/spri
|
|||||||
|
|
||||||
The `security.security.reached.filter.section` key name was corrected to `spring.security.reached.filter.section`.
|
The `security.security.reached.filter.section` key name was corrected to `spring.security.reached.filter.section`.
|
||||||
Note that this may affect reports that operate on this key name.
|
Note that this may affect reports that operate on this key name.
|
||||||
|
|
||||||
|
== OAuth
|
||||||
|
|
||||||
|
* https://github.com/spring-projects/spring-security/pull/16386[gh-16386] - Enable PKCE for confidential clients using `ClientRegistration.clientSettings.requireProofKey=true` for xref:servlet/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[servlet] and xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[reactive] applications
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -26,6 +26,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
@ -71,6 +72,8 @@ public final class ClientRegistration implements Serializable {
|
|||||||
|
|
||||||
private String clientName;
|
private String clientName;
|
||||||
|
|
||||||
|
private ClientSettings clientSettings;
|
||||||
|
|
||||||
private ClientRegistration() {
|
private ClientRegistration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,6 +165,14 @@ public final class ClientRegistration implements Serializable {
|
|||||||
return this.clientName;
|
return this.clientName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link ClientSettings client configuration settings}.
|
||||||
|
* @return the {@link ClientSettings}
|
||||||
|
*/
|
||||||
|
public ClientSettings getClientSettings() {
|
||||||
|
return this.clientSettings;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
@ -175,6 +186,7 @@ public final class ClientRegistration implements Serializable {
|
|||||||
+ '\'' + ", scopes=" + this.scopes
|
+ '\'' + ", scopes=" + this.scopes
|
||||||
+ ", providerDetails=" + this.providerDetails
|
+ ", providerDetails=" + this.providerDetails
|
||||||
+ ", clientName='" + this.clientName + '\''
|
+ ", clientName='" + this.clientName + '\''
|
||||||
|
+ ", clientSettings='" + this.clientSettings + '\''
|
||||||
+ '}';
|
+ '}';
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
@ -367,6 +379,8 @@ public final class ClientRegistration implements Serializable {
|
|||||||
|
|
||||||
private String clientName;
|
private String clientName;
|
||||||
|
|
||||||
|
private ClientSettings clientSettings = ClientSettings.builder().build();
|
||||||
|
|
||||||
private Builder(String registrationId) {
|
private Builder(String registrationId) {
|
||||||
this.registrationId = registrationId;
|
this.registrationId = registrationId;
|
||||||
}
|
}
|
||||||
@ -391,6 +405,7 @@ public final class ClientRegistration implements Serializable {
|
|||||||
this.configurationMetadata = new HashMap<>(configurationMetadata);
|
this.configurationMetadata = new HashMap<>(configurationMetadata);
|
||||||
}
|
}
|
||||||
this.clientName = clientRegistration.clientName;
|
this.clientName = clientRegistration.clientName;
|
||||||
|
this.clientSettings = clientRegistration.clientSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -594,6 +609,17 @@ public final class ClientRegistration implements Serializable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link ClientSettings client configuration settings}.
|
||||||
|
* @param clientSettings the client configuration settings
|
||||||
|
* @return the {@link Builder}
|
||||||
|
*/
|
||||||
|
public Builder clientSettings(ClientSettings clientSettings) {
|
||||||
|
Assert.notNull(clientSettings, "clientSettings cannot be null");
|
||||||
|
this.clientSettings = clientSettings;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a new {@link ClientRegistration}.
|
* Builds a new {@link ClientRegistration}.
|
||||||
* @return a {@link ClientRegistration}
|
* @return a {@link ClientRegistration}
|
||||||
@ -627,12 +653,13 @@ public final class ClientRegistration implements Serializable {
|
|||||||
clientRegistration.providerDetails = createProviderDetails(clientRegistration);
|
clientRegistration.providerDetails = createProviderDetails(clientRegistration);
|
||||||
clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName
|
clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName
|
||||||
: this.registrationId;
|
: this.registrationId;
|
||||||
|
clientRegistration.clientSettings = this.clientSettings;
|
||||||
return clientRegistration;
|
return clientRegistration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientAuthenticationMethod deduceClientAuthenticationMethod(ClientRegistration clientRegistration) {
|
private ClientAuthenticationMethod deduceClientAuthenticationMethod(ClientRegistration clientRegistration) {
|
||||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)
|
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)
|
||||||
&& !StringUtils.hasText(this.clientSecret)) {
|
&& (!StringUtils.hasText(this.clientSecret))) {
|
||||||
return ClientAuthenticationMethod.NONE;
|
return ClientAuthenticationMethod.NONE;
|
||||||
}
|
}
|
||||||
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
|
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
|
||||||
@ -685,6 +712,12 @@ public final class ClientRegistration implements Serializable {
|
|||||||
"AuthorizationGrantType: %s does not match the pre-defined constant %s and won't match a valid OAuth2AuthorizedClientProvider",
|
"AuthorizationGrantType: %s does not match the pre-defined constant %s and won't match a valid OAuth2AuthorizedClientProvider",
|
||||||
this.authorizationGrantType, authorizationGrantType));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -709,4 +742,76 @@ public final class ClientRegistration implements Serializable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A facility for client configuration settings.
|
||||||
|
*
|
||||||
|
* @author DingHao
|
||||||
|
* @since 6.5
|
||||||
|
*/
|
||||||
|
public static final class ClientSettings {
|
||||||
|
|
||||||
|
private boolean requireProofKey;
|
||||||
|
|
||||||
|
private ClientSettings() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRequireProofKey() {
|
||||||
|
return this.requireProofKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof ClientSettings that)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.requireProofKey == that.requireProofKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(this.requireProofKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ClientSettings{" + "requireProofKey=" + this.requireProofKey + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
private boolean requireProofKey;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to {@code true} if the client is required to provide a proof key
|
||||||
|
* challenge and verifier when performing the Authorization Code Grant flow.
|
||||||
|
* @param requireProofKey {@code true} if the client is required to provide a
|
||||||
|
* proof key challenge and verifier, {@code false} otherwise
|
||||||
|
* @return the {@link Builder} for further configuration
|
||||||
|
*/
|
||||||
|
public Builder requireProofKey(boolean requireProofKey) {
|
||||||
|
this.requireProofKey = requireProofKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientSettings build() {
|
||||||
|
ClientSettings clientSettings = new ClientSettings();
|
||||||
|
clientSettings.requireProofKey = this.requireProofKey;
|
||||||
|
return clientSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -183,7 +183,8 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
|
|||||||
// value.
|
// value.
|
||||||
applyNonce(builder);
|
applyNonce(builder);
|
||||||
}
|
}
|
||||||
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
|
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())
|
||||||
|
|| clientRegistration.getClientSettings().isRequireProofKey()) {
|
||||||
DEFAULT_PKCE_APPLIER.accept(builder);
|
DEFAULT_PKCE_APPLIER.accept(builder);
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
|
@ -196,7 +196,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolver implements ServerOA
|
|||||||
// value.
|
// value.
|
||||||
applyNonce(builder);
|
applyNonce(builder);
|
||||||
}
|
}
|
||||||
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
|
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())
|
||||||
|
|| clientRegistration.getClientSettings().isRequireProofKey()) {
|
||||||
DEFAULT_PKCE_APPLIER.accept(builder);
|
DEFAULT_PKCE_APPLIER.accept(builder);
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
|
@ -214,6 +214,71 @@ public class OAuth2AuthorizedClientMixinTests {
|
|||||||
assertThat(authorizedClient.getRefreshToken()).isNull();
|
assertThat(authorizedClient.getRefreshToken()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializeWhenClientSettingsPropertyDoesNotExistThenDefaulted() throws JsonProcessingException {
|
||||||
|
// ClientRegistration.clientSettings was added later, so old values will be
|
||||||
|
// serialized without that property
|
||||||
|
// this test checks for passivity
|
||||||
|
ClientRegistration clientRegistration = this.clientRegistrationBuilder.build();
|
||||||
|
ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails();
|
||||||
|
ClientRegistration.ProviderDetails.UserInfoEndpoint userInfoEndpoint = providerDetails.getUserInfoEndpoint();
|
||||||
|
String scopes = "";
|
||||||
|
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
|
||||||
|
scopes = StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), ",", "\"", "\"");
|
||||||
|
}
|
||||||
|
String configurationMetadata = "\"@class\": \"java.util.Collections$UnmodifiableMap\"";
|
||||||
|
if (!CollectionUtils.isEmpty(providerDetails.getConfigurationMetadata())) {
|
||||||
|
configurationMetadata += "," + providerDetails.getConfigurationMetadata()
|
||||||
|
.keySet()
|
||||||
|
.stream()
|
||||||
|
.map((key) -> "\"" + key + "\": \"" + providerDetails.getConfigurationMetadata().get(key) + "\"")
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
// @formatter:off
|
||||||
|
String json = "{\n" +
|
||||||
|
" \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration\",\n" +
|
||||||
|
" \"registrationId\": \"" + clientRegistration.getRegistrationId() + "\",\n" +
|
||||||
|
" \"clientId\": \"" + clientRegistration.getClientId() + "\",\n" +
|
||||||
|
" \"clientSecret\": \"" + clientRegistration.getClientSecret() + "\",\n" +
|
||||||
|
" \"clientAuthenticationMethod\": {\n" +
|
||||||
|
" \"value\": \"" + clientRegistration.getClientAuthenticationMethod().getValue() + "\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"authorizationGrantType\": {\n" +
|
||||||
|
" \"value\": \"" + clientRegistration.getAuthorizationGrantType().getValue() + "\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"redirectUri\": \"" + clientRegistration.getRedirectUri() + "\",\n" +
|
||||||
|
" \"scopes\": [\n" +
|
||||||
|
" \"java.util.Collections$UnmodifiableSet\",\n" +
|
||||||
|
" [" + scopes + "]\n" +
|
||||||
|
" ],\n" +
|
||||||
|
" \"providerDetails\": {\n" +
|
||||||
|
" \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails\",\n" +
|
||||||
|
" \"authorizationUri\": \"" + providerDetails.getAuthorizationUri() + "\",\n" +
|
||||||
|
" \"tokenUri\": \"" + providerDetails.getTokenUri() + "\",\n" +
|
||||||
|
" \"userInfoEndpoint\": {\n" +
|
||||||
|
" \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails$UserInfoEndpoint\",\n" +
|
||||||
|
" \"uri\": " + ((userInfoEndpoint.getUri() != null) ? "\"" + userInfoEndpoint.getUri() + "\"" : null) + ",\n" +
|
||||||
|
" \"authenticationMethod\": {\n" +
|
||||||
|
" \"value\": \"" + userInfoEndpoint.getAuthenticationMethod().getValue() + "\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"userNameAttributeName\": " + ((userInfoEndpoint.getUserNameAttributeName() != null) ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"jwkSetUri\": " + ((providerDetails.getJwkSetUri() != null) ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" +
|
||||||
|
" \"issuerUri\": " + ((providerDetails.getIssuerUri() != null) ? "\"" + providerDetails.getIssuerUri() + "\"" : null) + ",\n" +
|
||||||
|
" \"configurationMetadata\": {\n" +
|
||||||
|
" " + configurationMetadata + "\n" +
|
||||||
|
" }\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"clientName\": \"" + clientRegistration.getClientName() + "\"\n" +
|
||||||
|
"}";
|
||||||
|
// @formatter:on
|
||||||
|
// validate the test input
|
||||||
|
assertThat(json).doesNotContain("clientSettings");
|
||||||
|
ClientRegistration registration = this.mapper.readValue(json, ClientRegistration.class);
|
||||||
|
// the default value of requireProofKey is false
|
||||||
|
assertThat(registration.getClientSettings().isRequireProofKey()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
private static String asJson(OAuth2AuthorizedClient authorizedClient) {
|
private static String asJson(OAuth2AuthorizedClient authorizedClient) {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
return "{\n" +
|
return "{\n" +
|
||||||
@ -276,7 +341,10 @@ public class OAuth2AuthorizedClientMixinTests {
|
|||||||
" " + configurationMetadata + "\n" +
|
" " + configurationMetadata + "\n" +
|
||||||
" }\n" +
|
" }\n" +
|
||||||
" },\n" +
|
" },\n" +
|
||||||
" \"clientName\": \"" + clientRegistration.getClientName() + "\"\n" +
|
" \"clientName\": \"" + clientRegistration.getClientName() + "\",\n" +
|
||||||
|
" \"clientSettings\": {\n" +
|
||||||
|
" \"requireProofKey\": " + clientRegistration.getClientSettings().isRequireProofKey() + "\n" +
|
||||||
|
" }\n" +
|
||||||
"}";
|
"}";
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,20 @@
|
|||||||
|
|
||||||
package org.springframework.security.oauth2.client.registration;
|
package org.springframework.security.oauth2.client.registration;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
import org.springframework.security.oauth2.core.AuthenticationMethod;
|
import org.springframework.security.oauth2.core.AuthenticationMethod;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
@ -31,6 +37,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link ClientRegistration}.
|
* Tests for {@link ClientRegistration}.
|
||||||
@ -753,4 +760,86 @@ public class ClientRegistrationTests {
|
|||||||
assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(clientAuthenticationMethod);
|
assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(clientAuthenticationMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clientSettingsWhenNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> ClientRegistration.withRegistrationId(REGISTRATION_ID).clientSettings(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-16382
|
||||||
|
@Test
|
||||||
|
void buildWhenDefaultClientSettingsThenDefaulted() {
|
||||||
|
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||||
|
.clientId(CLIENT_ID)
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.redirectUri(REDIRECT_URI)
|
||||||
|
.authorizationUri(AUTHORIZATION_URI)
|
||||||
|
.tokenUri(TOKEN_URI)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// should not be null
|
||||||
|
assertThat(clientRegistration.getClientSettings()).isNotNull();
|
||||||
|
// proof key should be false for passivity
|
||||||
|
assertThat(clientRegistration.getClientSettings().isRequireProofKey()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-16382
|
||||||
|
@Test
|
||||||
|
void buildWhenNewAuthorizationCodeAndPkceThenBuilds() {
|
||||||
|
ClientRegistration.ClientSettings pkceEnabled = ClientRegistration.ClientSettings.builder()
|
||||||
|
.requireProofKey(true)
|
||||||
|
.build();
|
||||||
|
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||||
|
.clientId(CLIENT_ID)
|
||||||
|
.clientSettings(pkceEnabled)
|
||||||
|
.authorizationGrantType(new AuthorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()))
|
||||||
|
.redirectUri(REDIRECT_URI)
|
||||||
|
.authorizationUri(AUTHORIZATION_URI)
|
||||||
|
.tokenUri(TOKEN_URI)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// proof key should be false for passivity
|
||||||
|
assertThat(clientRegistration.getClientSettings().isRequireProofKey()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("invalidPkceGrantTypes")
|
||||||
|
void buildWhenInvalidGrantTypeForPkceThenException(AuthorizationGrantType invalidGrantType) {
|
||||||
|
ClientRegistration.ClientSettings pkceEnabled = ClientRegistration.ClientSettings.builder()
|
||||||
|
.requireProofKey(true)
|
||||||
|
.build();
|
||||||
|
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||||
|
.clientId(CLIENT_ID)
|
||||||
|
.clientSettings(pkceEnabled)
|
||||||
|
.authorizationGrantType(invalidGrantType)
|
||||||
|
.redirectUri(REDIRECT_URI)
|
||||||
|
.authorizationUri(AUTHORIZATION_URI)
|
||||||
|
.tokenUri(TOKEN_URI);
|
||||||
|
|
||||||
|
assertThatIllegalStateException().describedAs(
|
||||||
|
"clientSettings.isRequireProofKey=true is only valid with authorizationGrantType=AUTHORIZATION_CODE. Got authorizationGrantType={}",
|
||||||
|
invalidGrantType)
|
||||||
|
.isThrownBy(builder::build);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AuthorizationGrantType> invalidPkceGrantTypes() {
|
||||||
|
return Arrays.stream(AuthorizationGrantType.class.getFields())
|
||||||
|
.filter((field) -> Modifier.isFinal(field.getModifiers())
|
||||||
|
&& field.getType() == AuthorizationGrantType.class)
|
||||||
|
.map((field) -> getStaticValue(field, AuthorizationGrantType.class))
|
||||||
|
.filter((grantType) -> grantType != AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
// ensure works with .equals
|
||||||
|
.map((grantType) -> new AuthorizationGrantType(grantType.getValue()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T getStaticValue(Field field, Class<T> clazz) {
|
||||||
|
try {
|
||||||
|
return (T) field.get(null);
|
||||||
|
}
|
||||||
|
catch (IllegalAccessException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -56,6 +56,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
|||||||
|
|
||||||
private ClientRegistration registration2;
|
private ClientRegistration registration2;
|
||||||
|
|
||||||
|
private ClientRegistration pkceClientRegistration;
|
||||||
|
|
||||||
private ClientRegistration fineRedirectUriTemplateRegistration;
|
private ClientRegistration fineRedirectUriTemplateRegistration;
|
||||||
|
|
||||||
private ClientRegistration publicClientRegistration;
|
private ClientRegistration publicClientRegistration;
|
||||||
@ -72,6 +74,9 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
|||||||
public void setUp() {
|
public void setUp() {
|
||||||
this.registration1 = TestClientRegistrations.clientRegistration().build();
|
this.registration1 = TestClientRegistrations.clientRegistration().build();
|
||||||
this.registration2 = TestClientRegistrations.clientRegistration2().build();
|
this.registration2 = TestClientRegistrations.clientRegistration2().build();
|
||||||
|
|
||||||
|
this.pkceClientRegistration = pkceClientRegistration().build();
|
||||||
|
|
||||||
this.fineRedirectUriTemplateRegistration = fineRedirectUriTemplateClientRegistration().build();
|
this.fineRedirectUriTemplateRegistration = fineRedirectUriTemplateClientRegistration().build();
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
this.publicClientRegistration = TestClientRegistrations.clientRegistration()
|
this.publicClientRegistration = TestClientRegistrations.clientRegistration()
|
||||||
@ -86,8 +91,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
|||||||
.build();
|
.build();
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1,
|
this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1,
|
||||||
this.registration2, this.fineRedirectUriTemplateRegistration, this.publicClientRegistration,
|
this.registration2, this.pkceClientRegistration, this.fineRedirectUriTemplateRegistration,
|
||||||
this.oidcRegistration);
|
this.publicClientRegistration, this.oidcRegistration);
|
||||||
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository,
|
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository,
|
||||||
this.authorizationRequestBaseUri);
|
this.authorizationRequestBaseUri);
|
||||||
}
|
}
|
||||||
@ -563,6 +568,32 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
|||||||
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "appid=client-id");
|
+ "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "appid=client-id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolveWhenAuthorizationRequestProvideCodeChallengeMethod() {
|
||||||
|
ClientRegistration clientRegistration = this.pkceClientRegistration;
|
||||||
|
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||||
|
request.setServletPath(requestUri);
|
||||||
|
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
|
||||||
|
assertThat(authorizationRequest.getAdditionalParameters().containsKey(PkceParameterNames.CODE_CHALLENGE_METHOD))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClientRegistration.Builder pkceClientRegistration() {
|
||||||
|
return ClientRegistration.withRegistrationId("pkce")
|
||||||
|
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
|
||||||
|
.clientSettings(ClientRegistration.ClientSettings.builder().requireProofKey(true).build())
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.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-3")
|
||||||
|
.clientSecret("client-secret");
|
||||||
|
}
|
||||||
|
|
||||||
private static ClientRegistration.Builder fineRedirectUriTemplateClientRegistration() {
|
private static ClientRegistration.Builder fineRedirectUriTemplateClientRegistration() {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
return ClientRegistration.withRegistrationId("fine-redirect-uri-template-client-registration")
|
return ClientRegistration.withRegistrationId("fine-redirect-uri-template-client-registration")
|
||||||
|
@ -169,6 +169,22 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests {
|
|||||||
assertPkceNotApplied(request, registration2);
|
assertPkceNotApplied(request, registration2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveWhenRequireProofKeyTrueThenPkceEnabled() {
|
||||||
|
ClientRegistration.ClientSettings pkceEnabled = ClientRegistration.ClientSettings.builder()
|
||||||
|
.requireProofKey(true)
|
||||||
|
.build();
|
||||||
|
ClientRegistration clientWithPkceEnabled = TestClientRegistrations.clientRegistration()
|
||||||
|
.clientSettings(pkceEnabled)
|
||||||
|
.build();
|
||||||
|
given(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||||
|
.willReturn(Mono.just(clientWithPkceEnabled));
|
||||||
|
|
||||||
|
OAuth2AuthorizationRequest request = resolve(
|
||||||
|
"/oauth2/authorization/" + clientWithPkceEnabled.getRegistrationId());
|
||||||
|
assertPkceApplied(request, clientWithPkceEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
private void assertPkceApplied(OAuth2AuthorizationRequest authorizationRequest,
|
private void assertPkceApplied(OAuth2AuthorizationRequest authorizationRequest,
|
||||||
ClientRegistration clientRegistration) {
|
ClientRegistration clientRegistration) {
|
||||||
assertThat(authorizationRequest.getAdditionalParameters()).containsKey(PkceParameterNames.CODE_CHALLENGE);
|
assertThat(authorizationRequest.getAdditionalParameters()).containsKey(PkceParameterNames.CODE_CHALLENGE);
|
||||||
|
@ -111,4 +111,9 @@ public final class AuthorizationGrantType implements Serializable {
|
|||||||
return this.getValue().hashCode();
|
return this.getValue().hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AuthorizationGrantType{" + "value='" + this.value + '\'' + '}';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user