Skip to content

Commit

Permalink
Closes gh-11440
Browse files Browse the repository at this point in the history
  • Loading branch information
Crain-32 committed Apr 5, 2024
1 parent 8f87732 commit 8e1c360
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
----
======

[NOTE]
If you're using the `client-authentication-method: client_secret_basic` and you need to skip URL encoding,
create a new `DefaultOAuth2TokenRequestHeadersConverter` and set it in the Request Entity Converter above.

=== Authenticate using `client_secret_jwt`

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-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.
Expand Down Expand Up @@ -42,11 +42,7 @@
abstract class AbstractOAuth2AuthorizationGrantRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
implements Converter<T, RequestEntity<?>> {

// @formatter:off
private Converter<T, HttpHeaders> headersConverter =
(authorizationGrantRequest) -> OAuth2AuthorizationGrantRequestEntityUtils
.getTokenRequestHeaders(authorizationGrantRequest.getClientRegistration());
// @formatter:on
private Converter<T, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();

private Converter<T, MultiValueMap<String, String>> parametersConverter = this::createParameters;

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-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.
Expand All @@ -16,10 +16,9 @@

package org.springframework.security.oauth2.client.endpoint;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.nio.charset.StandardCharsets;

import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
Expand All @@ -28,51 +27,53 @@
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;


/**
* Utility methods used by the {@link Converter}'s that convert from an implementation of
* an {@link AbstractOAuth2AuthorizationGrantRequest} to a {@link RequestEntity}
* representation of an OAuth 2.0 Access Token Request for the specific Authorization
* Grant.
* Default Converter used by the {@link OAuth2AuthorizationCodeGrantRequestEntityConverter}
* that convert from an implementation of an {@link AbstractOAuth2AuthorizationGrantRequest}
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the
* specific Authorization Grant.
*
* @author Peter Eastham
* @author Joe Grandja
* @since 5.1
* @see OAuth2AuthorizationCodeGrantRequestEntityConverter
* @since 6.3
* @see OAuth2ClientCredentialsGrantRequestEntityConverter
*/
final class OAuth2AuthorizationGrantRequestEntityUtils {
public class DefaultOAuth2TokenRequestHeadersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
implements Converter<T, HttpHeaders> {

private static HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();
private static final HttpHeaders DEFAULT_TOKEN_HEADERS = getDefaultTokenRequestHeaders();
private boolean encodeClientCredentials = true;

private OAuth2AuthorizationGrantRequestEntityUtils() {
private static HttpHeaders getDefaultTokenRequestHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
headers.setContentType(contentType);
return headers;
}

static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {

@Override
public HttpHeaders convert(T source) {
HttpHeaders headers = new HttpHeaders();
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
headers.addAll(DEFAULT_TOKEN_HEADERS);
ClientRegistration clientRegistration = source.getClientRegistration();
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
String clientId = encodeClientCredential(clientRegistration.getClientId());
String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
String clientId = this.encodeClientCredentials ?
encodeClientCredential(clientRegistration.getClientId()) : clientRegistration.getClientId();
String clientSecret = this.encodeClientCredentials ?
encodeClientCredential(clientRegistration.getClientSecret()) : clientRegistration.getClientSecret();
headers.setBasicAuth(clientId, clientSecret);
}
return headers;
}

private static String encodeClientCredential(String clientCredential) {
try {
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
}
catch (UnsupportedEncodingException ex) {
// Will not happen since UTF-8 is a standard charset
throw new IllegalArgumentException(ex);
}
return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8);
}

private static HttpHeaders getDefaultTokenRequestHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
headers.setContentType(contentType);
return headers;
public void setEncodeClientCredentials(boolean encodeClientCredentials) {
this.encodeClientCredentials = encodeClientCredentials;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-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.
Expand Down Expand Up @@ -110,9 +110,11 @@ public void convertWhenParametersConverterSetThenCalled() {
@SuppressWarnings("unchecked")
@Test
public void convertWhenGrantRequestValidThenConverts() {
ClientRegistration clientRegistration = TestClientRegistrations.password().build();
ClientRegistration clientRegistration = TestClientRegistrations.password().clientId("clientId").clientSecret("clientSecret=").build();
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
"password");
Converter<OAuth2PasswordGrantRequest, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();
this.converter.setHeadersConverter(headersConverter);
RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
assertThat(requestEntity.getUrl().toASCIIString())
Expand All @@ -121,7 +123,7 @@ public void convertWhenGrantRequestValidThenConverts() {
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
assertThat(headers.getContentType())
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0JTNE");
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
Expand All @@ -130,4 +132,29 @@ public void convertWhenGrantRequestValidThenConverts() {
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
}

@SuppressWarnings("unchecked")
@Test
public void convertWhenGrantRequestValidThenConvertsWithoutUrlEncoding() {
ClientRegistration clientRegistration = TestClientRegistrations.password().clientId("clientId").clientSecret("clientSecret=").build();
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, "user1",
"password=");
var headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<OAuth2PasswordGrantRequest>();
headersConverter.setEncodeClientCredentials(false);
this.converter.setHeadersConverter(headersConverter);
RequestEntity<?> requestEntity = this.converter.convert(passwordGrantRequest);
assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST);
assertThat(requestEntity.getUrl().toASCIIString())
.isEqualTo(clientRegistration.getProviderDetails().getTokenUri());
HttpHeaders headers = requestEntity.getHeaders();
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
assertThat(headers.getContentType())
.isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0PQ==");
MultiValueMap<String, String> formParameters = (MultiValueMap<String, String>) requestEntity.getBody();
assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE))
.isEqualTo(AuthorizationGrantType.PASSWORD.getValue());
assertThat(formParameters.getFirst(OAuth2ParameterNames.USERNAME)).isEqualTo("user1");
assertThat(formParameters.getFirst(OAuth2ParameterNames.PASSWORD)).isEqualTo("password=");
assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).contains(clientRegistration.getScopes());
}
}

0 comments on commit 8e1c360

Please sign in to comment.