diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverter.java index c54045727d..deccbfab57 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; @@ -74,11 +75,20 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverter implements Conve MultiValueMap formParameters = new LinkedMultiValueMap<>(); formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue()); formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode()); - formParameters.add(OAuth2ParameterNames.REDIRECT_URI, authorizationExchange.getAuthorizationRequest().getRedirectUri()); - if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { + String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri(); + String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER); + if (redirectUri != null) { + formParameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri); + } + if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) { formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + } + if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); } + if (codeVerifier != null) { + formParameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier); + } return formParameters; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java index 4b2600a211..720ac2154f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -18,9 +18,12 @@ package org.springframework.security.oauth2.client.endpoint; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.util.Assert; @@ -44,6 +47,7 @@ import static org.springframework.security.oauth2.core.web.reactive.function.OAu * @see Nimbus OAuth 2.0 SDK * @see Section 4.1.3 Access Token Request (Authorization Code Grant) * @see Section 4.1.4 Access Token Response (Authorization Code Grant) + * @see Section 4.2 Client Creates the Code Challenge */ public class WebClientReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { private WebClient webClient = WebClient.builder() @@ -63,12 +67,16 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient implements Re ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration(); OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange(); String tokenUri = clientRegistration.getProviderDetails().getTokenUri(); - BodyInserters.FormInserter body = body(authorizationExchange); + BodyInserters.FormInserter body = body(authorizationExchange, clientRegistration); return this.webClient.post() .uri(tokenUri) .accept(MediaType.APPLICATION_JSON) - .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .headers(headers -> { + if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) { + headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); + } + }) .body(body) .exchange() .flatMap(response -> response.body(oauth2AccessTokenResponse())) @@ -83,14 +91,24 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient implements Re }); } - private static BodyInserters.FormInserter body(OAuth2AuthorizationExchange authorizationExchange) { + private static BodyInserters.FormInserter body(OAuth2AuthorizationExchange authorizationExchange, ClientRegistration clientRegistration) { OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse(); - String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri(); BodyInserters.FormInserter body = BodyInserters - .fromFormData("grant_type", AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) - .with("code", authorizationResponse.getCode()); + .fromFormData(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .with(OAuth2ParameterNames.CODE, authorizationResponse.getCode()); + String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri(); + String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER); if (redirectUri != null) { - body.with("redirect_uri", redirectUri); + body.with(OAuth2ParameterNames.REDIRECT_URI, redirectUri); + } + if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) { + body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + } + if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { + body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); + } + if (codeVerifier != null) { + body.with(PkceParameterNames.CODE_VERIFIER, codeVerifier); } return body; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index a5b452f47a..4ff19f88ac 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -500,6 +500,11 @@ public final class ClientRegistration implements Serializable { clientRegistration.clientId = this.clientId; clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : ""; clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType) && + !StringUtils.hasText(this.clientSecret)) { + clientRegistration.clientAuthenticationMethod = ClientAuthenticationMethod.NONE; + } + clientRegistration.authorizationGrantType = this.authorizationGrantType; clientRegistration.redirectUriTemplate = this.redirectUriTemplate; clientRegistration.scopes = this.scopes; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java index 587cd2e353..cf0ebf79cd 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -117,7 +117,10 @@ public final class ClientRegistrations { if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) { return ClientAuthenticationMethod.POST; } - throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); + if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) { + return ClientAuthenticationMethod.NONE; + } + throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); } private static List getScopes(OIDCProviderMetadata metadata) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java index 953ece1b24..88d0c27f39 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -20,14 +20,19 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator; 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.endpoint.PkceParameterNames; import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -52,6 +57,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au private final ClientRegistrationRepository clientRegistrationRepository; private final AntPathRequestMatcher authorizationRequestMatcher; private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); /** * Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters. @@ -102,9 +108,17 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); } + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); + OAuth2AuthorizationRequest.Builder builder; if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.authorizationCode(); + if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { + Map additionalParameters = new HashMap<>(); + addPkceParameters(attributes, additionalParameters); + builder.additionalParameters(additionalParameters); + } } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.implicit(); } else { @@ -115,9 +129,6 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction); - Map attributes = new HashMap<>(); - attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); - OAuth2AuthorizationRequest authorizationRequest = builder .clientId(clientRegistration.getClientId()) .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) @@ -156,4 +167,34 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au .buildAndExpand(uriVariables) .toUriString(); } + + /** + * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests + * + * @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request + * @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually, + * {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request. + * + * @since 5.2 + * @see 1.1. Protocol Flow + * @see 4.1. Client Creates a Code Verifier + * @see 4.2. Client Creates the Code Challenge + */ + private void addPkceParameters(Map attributes, Map additionalParameters) { + String codeVerifier = this.codeVerifierGenerator.generateKey(); + attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); + try { + String codeChallenge = createCodeChallenge(codeVerifier); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + } catch (NoSuchAlgorithmException e) { + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); + } + } + + private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java index 5cb18e6f90..3dfcdcf892 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java @@ -24,8 +24,10 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; 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.endpoint.PkceParameterNames; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.util.Assert; @@ -34,6 +36,9 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -68,6 +73,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolver private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + /** * Creates a new instance * @param clientRegistrationRepository the repository to resolve the {@link ClientRegistration} @@ -124,6 +131,11 @@ public class DefaultServerOAuth2AuthorizationRequestResolver OAuth2AuthorizationRequest.Builder builder; if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.authorizationCode(); + if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { + Map additionalParameters = new HashMap<>(); + addPkceParameters(attributes, additionalParameters); + builder.additionalParameters(additionalParameters); + } } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.implicit(); @@ -164,4 +176,34 @@ public class DefaultServerOAuth2AuthorizationRequestResolver .buildAndExpand(uriVariables) .toUriString(); } + + /** + * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests + * + * @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request + * @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually, + * {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request. + * + * @since 5.2 + * @see 1.1. Protocol Flow + * @see 4.1. Client Creates a Code Verifier + * @see 4.2. Client Creates the Code Challenge + */ + private void addPkceParameters(Map attributes, Map additionalParameters) { + String codeVerifier = this.codeVerifierGenerator.generateKey(); + attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); + try { + String codeChallenge = createCodeChallenge(codeVerifier); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + } catch (NoSuchAlgorithmException e) { + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); + } + } + + private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverterTests.java index 6874096c6f..84023a8818 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationCodeGrantRequestEntityConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -15,7 +15,6 @@ */ package org.springframework.security.oauth2.client.endpoint; -import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -28,8 +27,14 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExch import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.util.MultiValueMap; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; @@ -40,11 +45,8 @@ import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VAL */ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests { private OAuth2AuthorizationCodeGrantRequestEntityConverter converter = new OAuth2AuthorizationCodeGrantRequestEntityConverter(); - private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest; - - @Before - public void setup() { - ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("registration-1") + private ClientRegistration.Builder clientRegistrationBuilder = ClientRegistration + .withRegistrationId("registration-1") .clientId("client-1") .clientSecret("secret") .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) @@ -55,33 +57,31 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests { .tokenUri("https://provider.com/oauth2/token") .userInfoUri("https://provider.com/user") .userNameAttributeName("id") - .clientName("client-1") - .build(); - OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest + .clientName("client-1"); + private OAuth2AuthorizationRequest.Builder authorizationRequestBuilder = OAuth2AuthorizationRequest .authorizationCode() - .clientId(clientRegistration.getClientId()) + .clientId("client-1") .state("state-1234") - .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) - .redirectUri(clientRegistration.getRedirectUriTemplate()) - .scopes(clientRegistration.getScopes()) - .build(); - OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponse + .authorizationUri("https://provider.com/oauth2/authorize") + .redirectUri("https://client.com/callback/client-1") + .scopes(new HashSet(Arrays.asList("read", "write"))); + private OAuth2AuthorizationResponse.Builder authorizationResponseBuilder = OAuth2AuthorizationResponse .success("code-1234") .state("state-1234") - .redirectUri(clientRegistration.getRedirectUriTemplate()) - .build(); - OAuth2AuthorizationExchange authorizationExchange = - new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse); - this.authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest( - clientRegistration, authorizationExchange); - } + .redirectUri("https://client.com/callback/client-1"); @SuppressWarnings("unchecked") @Test public void convertWhenGrantRequestValidThenConverts() { - RequestEntity requestEntity = this.converter.convert(this.authorizationCodeGrantRequest); + ClientRegistration clientRegistration = clientRegistrationBuilder.build(); + OAuth2AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build(); + OAuth2AuthorizationResponse authorizationResponse = authorizationResponseBuilder.build(); + OAuth2AuthorizationExchange authorizationExchange = + new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse); + OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest( + clientRegistration, authorizationExchange); - ClientRegistration clientRegistration = this.authorizationCodeGrantRequest.getClientRegistration(); + RequestEntity requestEntity = this.converter.convert(authorizationCodeGrantRequest); assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST); assertThat(requestEntity.getUrl().toASCIIString()).isEqualTo( @@ -97,7 +97,55 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverterTests { assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo( AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); assertThat(formParameters.getFirst(OAuth2ParameterNames.CODE)).isEqualTo("code-1234"); + assertThat(formParameters.getFirst(OAuth2ParameterNames.CLIENT_ID)).isNull(); assertThat(formParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI)).isEqualTo( clientRegistration.getRedirectUriTemplate()); } + + @SuppressWarnings("unchecked") + @Test + public void convertWhenPkceGrantRequestValidThenConverts() { + ClientRegistration clientRegistration = clientRegistrationBuilder + .clientSecret(null) + .build(); + + Map attributes = new HashMap<>(); + attributes.put(PkceParameterNames.CODE_VERIFIER, "code-verifier-1234"); + + Map additionalParameters = new HashMap<>(); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge-1234"); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + + OAuth2AuthorizationRequest authorizationRequest = authorizationRequestBuilder + .attributes(attributes) + .additionalParameters(additionalParameters) + .build(); + + OAuth2AuthorizationResponse authorizationResponse = authorizationResponseBuilder.build(); + OAuth2AuthorizationExchange authorizationExchange = + new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse); + OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest = new OAuth2AuthorizationCodeGrantRequest( + clientRegistration, authorizationExchange); + + RequestEntity requestEntity = this.converter.convert(authorizationCodeGrantRequest); + + 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(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8")); + assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isNull(); + + MultiValueMap formParameters = (MultiValueMap) requestEntity.getBody(); + assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)).isEqualTo( + AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + assertThat(formParameters.getFirst(OAuth2ParameterNames.CODE)).isEqualTo("code-1234"); + assertThat(formParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI)).isEqualTo( + clientRegistration.getRedirectUriTemplate()); + assertThat(formParameters.getFirst(OAuth2ParameterNames.CLIENT_ID)).isEqualTo("client-1"); + assertThat(formParameters.getFirst(PkceParameterNames.CODE_VERIFIER)).isEqualTo("code-verifier-1234"); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index dc129a5d80..0816d7da60 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -32,9 +32,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenRespon import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.web.reactive.function.client.WebClient; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -84,6 +87,9 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests { Instant expiresAtBefore = Instant.now().plusSeconds(3600); OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest()).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + + assertThat(body).isEqualTo("grant_type=authorization_code&code=code&redirect_uri=%7BbaseUrl%7D%2F%7Baction%7D%2Foauth2%2Fcode%2F%7BregistrationId%7D"); Instant expiresAtAfter = Instant.now().plusSeconds(3600); @@ -288,4 +294,51 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests { verify(customClient, atLeastOnce()).post(); } + + @Test + public void getTokenResponseWhenOAuth2AuthorizationRequestContainsPkceParametersThenTokenRequestBodyShouldContainCodeVerifier() throws Exception { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + this.tokenResponseClient.getTokenResponse(pkceAuthorizationCodeGrantRequest()).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + + assertThat(body).isEqualTo("grant_type=authorization_code&code=code&redirect_uri=%7BbaseUrl%7D%2F%7Baction%7D%2Foauth2%2Fcode%2F%7BregistrationId%7D&client_id=client-id&code_verifier=code-verifier-1234"); + } + + private OAuth2AuthorizationCodeGrantRequest pkceAuthorizationCodeGrantRequest() { + ClientRegistration registration = this.clientRegistration + .clientSecret(null) + .build(); + + Map attributes = new HashMap<>(); + attributes.put(PkceParameterNames.CODE_VERIFIER, "code-verifier-1234"); + + Map additionalParameters = new HashMap<>(); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge-1234"); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .clientId(registration.getClientId()) + .state("state") + .authorizationUri(registration.getProviderDetails().getAuthorizationUri()) + .redirectUri(registration.getRedirectUriTemplate()) + .scopes(registration.getScopes()) + .attributes(attributes) + .additionalParameters(additionalParameters) + .build(); + OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponse + .success("code") + .state("state") + .redirectUri(registration.getRedirectUriTemplate()) + .build(); + OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(authorizationRequest, + authorizationResponse); + return new OAuth2AuthorizationCodeGrantRequest(registration, authorizationExchange); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index d3886722a5..14e547141b 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -174,6 +174,41 @@ public class ClientRegistrationTests { assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } + @Test + public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodNotProvidedAndClientSecretNullThenDefaultToNone() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(null) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE); + } + + @Test + public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodNotProvidedAndClientSecretBlankThenDefaultToNone() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(" ") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE); + assertThat(clientRegistration.getClientSecret()).isEqualTo(""); + } + @Test(expected = IllegalArgumentException.class) public void buildWhenAuthorizationCodeGrantRedirectUriIsNullThenThrowIllegalArgumentException() { ClientRegistration.withRegistrationId(REGISTRATION_ID) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java index 7c61537c51..bdad2c5fcf 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -92,7 +92,8 @@ public class ClientRegistrationsTest { + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n" + " \"token_endpoint_auth_methods_supported\": [\n" + " \"client_secret_post\", \n" - + " \"client_secret_basic\"\n" + + " \"client_secret_basic\", \n" + + " \"none\"\n" + " ], \n" + " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n" + "}"; @@ -119,7 +120,7 @@ public class ClientRegistrationsTest { @Test public void issuerWhenAllInformationThenSuccess() throws Exception { - ClientRegistration registration = registration(""); + ClientRegistration registration = registration("").build(); ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); @@ -154,7 +155,7 @@ public class ClientRegistrationsTest { public void issuerWhenScopesNullThenScopesDefaulted() throws Exception { this.response.remove("scopes_supported"); - ClientRegistration registration = registration(""); + ClientRegistration registration = registration("").build(); assertThat(registration.getScopes()).containsOnly("openid"); } @@ -163,7 +164,7 @@ public class ClientRegistrationsTest { public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception { this.response.remove("grant_types_supported"); - ClientRegistration registration = registration(""); + ClientRegistration registration = registration("").build(); assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); } @@ -184,7 +185,7 @@ public class ClientRegistrationsTest { public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception { this.response.remove("token_endpoint_auth_methods_supported"); - ClientRegistration registration = registration(""); + ClientRegistration registration = registration("").build(); assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } @@ -193,11 +194,20 @@ public class ClientRegistrationsTest { public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception { this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); - ClientRegistration registration = registration(""); + ClientRegistration registration = registration("").build(); assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST); } + @Test + public void issuerWhenTokenEndpointAuthMethodsNoneThenMethodIsNone() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("none")); + + ClientRegistration registration = registration("").build(); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.NONE); + } + /** * We currently only support client_secret_basic, so verify we have a meaningful error until we add support. * @throws Exception @@ -208,7 +218,7 @@ public class ClientRegistrationsTest { assertThatThrownBy(() -> registration("")) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]"); + .hasMessageContaining("Only ClientAuthenticationMethod.BASIC, ClientAuthenticationMethod.POST and ClientAuthenticationMethod.NONE are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]"); } @Test @@ -229,7 +239,7 @@ public class ClientRegistrationsTest { .hasMessageContaining("The Issuer \"https://example.com\" provided in the OpenID Configuration did not match the requested issuer \"" + this.issuer + "\""); } - private ClientRegistration registration(String path) throws Exception { + private ClientRegistration.Builder registration(String path) throws Exception { this.issuer = createIssuerFromServer(path); this.response.put("issuer", this.issuer); String body = this.mapper.writeValueAsString(this.response); @@ -240,8 +250,7 @@ public class ClientRegistrationsTest { return ClientRegistrations.fromOidcIssuerLocation(this.issuer) .clientId("client-id") - .clientSecret("client-secret") - .build(); + .clientSecret("client-secret"); } private String createIssuerFromServer(String path) { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java index 348f5a1a42..2ca1377e09 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java @@ -23,9 +23,11 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; 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.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -39,6 +41,7 @@ import static org.assertj.core.api.Assertions.entry; public class DefaultOAuth2AuthorizationRequestResolverTests { private ClientRegistration registration1; private ClientRegistration registration2; + private ClientRegistration pkceRegistration; private ClientRegistrationRepository clientRegistrationRepository; private String authorizationRequestBaseUri = "/oauth2/authorization"; private DefaultOAuth2AuthorizationRequestResolver resolver; @@ -47,8 +50,15 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { public void setUp() { this.registration1 = TestClientRegistrations.clientRegistration().build(); this.registration2 = TestClientRegistrations.clientRegistration2().build(); + this.pkceRegistration = TestClientRegistrations.clientRegistration() + .registrationId("pkce-client-registration-id") + .clientId("pkce-client-id") + .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) + .clientSecret(null) + .build(); + this.clientRegistrationRepository = new InMemoryClientRegistrationRepository( - this.registration1, this.registration2); + this.registration1, this.registration2, this.pkceRegistration); this.resolver = new DefaultOAuth2AuthorizationRequestResolver( this.clientRegistrationRepository, this.authorizationRequestBaseUri); } @@ -255,4 +265,40 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { "scope=read:user&state=.{15,}&" + "redirect_uri=http://localhost/login/oauth2/code/registration-id-2"); } + + @Test + public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() { + ClientRegistration clientRegistration = this.pkceRegistration; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest).isNotNull(); + assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo( + clientRegistration.getProviderDetails().getAuthorizationUri()); + assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE); + assertThat(authorizationRequest.getClientId()).isEqualTo(clientRegistration.getClientId()); + assertThat(authorizationRequest.getRedirectUri()) + .isEqualTo("http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); + assertThat(authorizationRequest.getScopes()).isEqualTo(clientRegistration.getScopes()); + assertThat(authorizationRequest.getState()).isNotNull(); + assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OAuth2ParameterNames.REGISTRATION_ID); + assertThat(authorizationRequest.getAdditionalParameters()).containsKey(PkceParameterNames.CODE_CHALLENGE); + assertThat(authorizationRequest.getAdditionalParameters()) + .contains(entry(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")); + assertThat(authorizationRequest.getAttributes()) + .contains(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + assertThat(authorizationRequest.getAttributes()) + .containsKey(PkceParameterNames.CODE_VERIFIER); + assertThat((String) authorizationRequest.getAttribute(PkceParameterNames.CODE_VERIFIER)).matches("^([a-zA-Z0-9\\-\\.\\_\\~]){128}$"); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .matches("https://example.com/login/oauth/authorize\\?" + + "response_type=code&client_id=pkce-client-id&" + + "scope=read:user&state=.{15,}&" + + "redirect_uri=http://localhost/login/oauth2/code/pkce-client-registration-id&" + + "code_challenge_method=S256&" + + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java index ba7f8ac3bf..01af6566a3 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -27,7 +27,9 @@ import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -87,4 +89,24 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests { ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path)); return this.resolver.resolve(exchange).block(); } + + @Test + public void resolveWhenAuthorizationRequestWithValidPkceClientThenResolves() { + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn( + Mono.just(TestClientRegistrations.clientRegistration() + .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) + .clientSecret(null) + .build())); + + OAuth2AuthorizationRequest request = resolve("/oauth2/authorization/registration-id"); + + assertThat((String) request.getAttribute(PkceParameterNames.CODE_VERIFIER)).matches("^([a-zA-Z0-9\\-\\.\\_\\~]){128}$"); + + assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?" + + "response_type=code&client_id=client-id&" + + "scope=read:user&state=.*?&" + + "redirect_uri=/login/oauth2/code/registration-id&" + + "code_challenge_method=S256&" + + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); + } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java index 551b26a839..4813ff2481 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -31,6 +31,12 @@ public final class ClientAuthenticationMethod implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public static final ClientAuthenticationMethod BASIC = new ClientAuthenticationMethod("basic"); public static final ClientAuthenticationMethod POST = new ClientAuthenticationMethod("post"); + + /** + * @since 5.2 + */ + public static final ClientAuthenticationMethod NONE = new ClientAuthenticationMethod("none"); + private final String value; /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceParameterNames.java new file mode 100644 index 0000000000..e3533210ab --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceParameterNames.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core.endpoint; + +/** + * Standard parameter names defined in the OAuth Parameters Registry + * and used by the authorization endpoint and token endpoint. + * + * @author Stephen Doxsee + * @author Kevin Bolduc + * @since 5.2 + * @see 6.1 OAuth Parameters Registry + */ +public interface PkceParameterNames { + + /** + * {@code code_challenge} - used in Authorization Request. + */ + String CODE_CHALLENGE = "code_challenge"; + + /** + * {@code code_challenge_method} - used in Authorization Request. + */ + String CODE_CHALLENGE_METHOD = "code_challenge_method"; + + /** + * {@code code_verifier} - used in Token Request. + */ + String CODE_VERIFIER = "code_verifier"; +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClientAuthenticationMethodTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClientAuthenticationMethodTests.java index c21d4c72c4..19b211569c 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClientAuthenticationMethodTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClientAuthenticationMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -40,4 +40,9 @@ public class ClientAuthenticationMethodTests { public void getValueWhenAuthenticationMethodPostThenReturnPost() { assertThat(ClientAuthenticationMethod.POST.getValue()).isEqualTo("post"); } + + @Test + public void getValueWhenAuthenticationMethodNoneThenReturnNone() { + assertThat(ClientAuthenticationMethod.NONE.getValue()).isEqualTo("none"); + } }