From 0427e51bd8c551eeb22895d33cdfe03f82dde110 Mon Sep 17 00:00:00 2001 From: Visweshwar Ganesh Date: Sat, 20 Jun 2020 18:10:22 -0400 Subject: [PATCH] Adding SecretJWT authentication to Spring Security , also addresses issue #8735 --- ...activeOAuth2AccessTokenResponseClient.java | 7 +++ ...zationCodeGrantRequestEntityConverter.java | 6 +++ ...2AuthorizationGrantRequestEntityUtils.java | 48 +++++++++++++++++++ .../oauth2/client/jackson2/StdConverters.java | 4 +- .../registration/ClientRegistration.java | 26 ++++++++++ .../registration/ClientRegistrations.java | 3 ++ .../core/ClientAuthenticationMethod.java | 1 + .../ClientAssertionParameterNames.java | 44 +++++++++++++++++ .../ClientAssertionParameterValues.java | 39 +++++++++++++++ 9 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterNames.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterValues.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java index 2991b855a8d..8fb5616533d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java @@ -19,6 +19,8 @@ import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.ClientAssertionParameterNames; +import org.springframework.security.oauth2.core.endpoint.ClientAssertionParameterValues; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.util.Assert; @@ -124,6 +126,11 @@ BodyInserters.FormInserter populateTokenRequestBody(T grantRequest, Body if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); } + if(ClientAuthenticationMethod.SECRET_JWT.equals(clientRegistration.getClientAuthenticationMethod())){ + body.with(ClientAssertionParameterNames.CLIENT_ASSERTION_TYPE, ClientAssertionParameterValues.CLIENT_ASSERTION_TYPE_JWT_BEARER); + body.with(ClientAssertionParameterNames.CLIENT_ASSERTION, OAuth2AuthorizationGrantRequestEntityUtils.getClientSecretAssertion(clientRegistration).serialize()); + + } Set scopes = scopes(grantRequest); if (!CollectionUtils.isEmpty(scopes)) { body.with(OAuth2ParameterNames.SCOPE, 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 a8a088a77a2..2974901bae2 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 @@ -24,6 +24,8 @@ 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.security.oauth2.core.endpoint.ClientAssertionParameterValues; +import org.springframework.security.oauth2.core.endpoint.ClientAssertionParameterNames; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; @@ -86,6 +88,10 @@ private MultiValueMap buildFormParameters(OAuth2AuthorizationCod if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); } + if(ClientAuthenticationMethod.SECRET_JWT.equals(clientRegistration.getClientAuthenticationMethod())){ + formParameters.add(ClientAssertionParameterNames.CLIENT_ASSERTION_TYPE, ClientAssertionParameterValues.CLIENT_ASSERTION_TYPE_JWT_BEARER); + formParameters.add(ClientAssertionParameterNames.CLIENT_ASSERTION, OAuth2AuthorizationGrantRequestEntityUtils.getClientSecretAssertion(clientRegistration).serialize()); + } if (codeVerifier != null) { formParameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationGrantRequestEntityUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationGrantRequestEntityUtils.java index a1ed924307d..f2255a487cb 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationGrantRequestEntityUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2AuthorizationGrantRequestEntityUtils.java @@ -15,6 +15,14 @@ */ package org.springframework.security.oauth2.client.endpoint; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.JWT; +import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import java.net.URI; +import java.net.URISyntaxException; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -23,6 +31,8 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import java.util.Collections; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; @@ -38,6 +48,7 @@ * @see OAuth2ClientCredentialsGrantRequestEntityConverter */ final class OAuth2AuthorizationGrantRequestEntityUtils { + private static HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders(); static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) { @@ -56,4 +67,41 @@ private static HttpHeaders getDefaultTokenRequestHeaders() { headers.setContentType(contentType); return headers; } + + /* + Adding support for client assertion authentication + https://tools.ietf.org/html/rfc7521#section-6.1 + */ + + static JWT getClientSecretAssertion(ClientRegistration clientRegistration){ + + JWT clientAssertion = null; + + if(ClientAuthenticationMethod.SECRET_JWT.equals(clientRegistration.getClientAuthenticationMethod())) { + + try { + ClientID clientID = new ClientID(clientRegistration.getClientId()); + URI audience = new URI(clientRegistration.getProviderDetails().getTokenUri()); + Secret secret = new Secret(clientRegistration.getClientSecret()); + JWSAlgorithm jwsAlgorithm = new JWSAlgorithm(clientRegistration.getClientAssertionSigningAlgorithm()); + + //Generate a client secret JWT using nimbus libraries. + clientAssertion = new ClientSecretJWT(clientID, + audience + , jwsAlgorithm + , secret).getClientAssertion(); + } catch (JOSEException e) { + OAuth2Error oauth2Error = new OAuth2Error("Client_secret_jwt", + "Encountered an error generating a client secret JWT",null); + throw new OAuth2AuthenticationException(oauth2Error,e.getMessage()); + + } catch(URISyntaxException e){ + OAuth2Error oauth2Error = new OAuth2Error("token_endpoint", + "The token endpoint provided or configured doesn't conform to a standard URI Pattern",null); + throw new OAuth2AuthenticationException(oauth2Error,e.getMessage()); + } + } + + return clientAssertion; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java index 10510e5bafa..37b9f7ba916 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java @@ -50,7 +50,9 @@ public ClientAuthenticationMethod convert(JsonNode jsonNode) { if (ClientAuthenticationMethod.BASIC.getValue().equalsIgnoreCase(value)) { return ClientAuthenticationMethod.BASIC; } else if (ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) { - return ClientAuthenticationMethod.POST; + return ClientAuthenticationMethod.POST;} + else if (ClientAuthenticationMethod.SECRET_JWT.getValue().equalsIgnoreCase(value)) { + return ClientAuthenticationMethod.SECRET_JWT; } else if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) { return ClientAuthenticationMethod.NONE; } 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 2051fdcf2d4..6e0d6126a27 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 @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.client.registration; +import com.nimbusds.jose.JWSAlgorithm; import java.io.Serializable; import java.util.Arrays; import java.util.Collection; @@ -30,6 +31,7 @@ import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -47,6 +49,7 @@ public final class ClientRegistration implements Serializable { private String registrationId; private String clientId; private String clientSecret; + private String clientAssertionSigningAlgorithm = JwsAlgorithms.HS256; private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.BASIC; private AuthorizationGrantType authorizationGrantType; private String redirectUriTemplate; @@ -139,6 +142,15 @@ public String getClientName() { return this.clientName; } + /** + * Returns the Signing Algorithm used for Client Assertion + * + * @return the {@link String} + */ + public String getClientAssertionSigningAlgorithm() { + return this.clientAssertionSigningAlgorithm; + } + @Override public String toString() { return "ClientRegistration{" @@ -151,6 +163,7 @@ public String toString() { + ", scopes=" + this.scopes + ", providerDetails=" + this.providerDetails + ", clientName='" + this.clientName + + ", clientAssertionSigningAlgorithm='" + this.clientAssertionSigningAlgorithm + '\'' + '}'; } @@ -311,6 +324,7 @@ public static class Builder implements Serializable { private String issuerUri; private Map configurationMetadata = Collections.emptyMap(); private String clientName; + private String clientAssertionSigningAlgorithm = JwsAlgorithms.HS256; private Builder(String registrationId) { this.registrationId = registrationId; @@ -336,6 +350,7 @@ private Builder(ClientRegistration clientRegistration) { this.configurationMetadata = new HashMap<>(configurationMetadata); } this.clientName = clientRegistration.clientName; + this.clientAssertionSigningAlgorithm = clientRegistration.clientAssertionSigningAlgorithm; } /** @@ -537,6 +552,16 @@ public Builder clientName(String clientName) { this.clientName = clientName; return this; } + /** + * Sets the Client Assertion Signature Algorithm + * + * @param clientAssertionSigningAlgorithm the client or registration name + * @return the {@link Builder} + */ + public Builder clientAssertionSigningAlgorithm(String clientAssertionSigningAlgorithm) { + this.clientAssertionSigningAlgorithm = clientAssertionSigningAlgorithm; + return this; + } /** * Builds a new {@link ClientRegistration}. @@ -588,6 +613,7 @@ private ClientRegistration create() { clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName : this.registrationId; + clientRegistration.clientAssertionSigningAlgorithm = this.clientAssertionSigningAlgorithm; return clientRegistration; } 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 857b150db09..724b532b076 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 @@ -261,6 +261,9 @@ private static ClientAuthenticationMethod getClientAuthenticationMethod(String i if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) { return ClientAuthenticationMethod.POST; } + if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + return ClientAuthenticationMethod.SECRET_JWT; + } if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE)) { return ClientAuthenticationMethod.NONE; } 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 48ecaaa0ca2..3630b8f3a06 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 @@ -31,6 +31,7 @@ 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"); + public static final ClientAuthenticationMethod SECRET_JWT = new ClientAuthenticationMethod("secret_jwt"); /** * @since 5.2 diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterNames.java new file mode 100644 index 00000000000..7a6f85e771c --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterNames.java @@ -0,0 +1,44 @@ +/* + * + * + * * Copyright 2020 Paychex, Inc. + * * + * * 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 + * * + * * https://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; + +/** + * @author visweshwarganesh + * @Created 06/20/2020 - 9:13 AM + * RFC-7521 Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants + * https://tools.ietf.org/html/rfc7521#section-9 + */ +public interface ClientAssertionParameterNames { + + /** + * {@code assertion} - used in Access Token Request. + */ + String ASSERTION = "assertion"; + + /** + * {@code client_assertion} - used in Access Token Request. + */ + String CLIENT_ASSERTION = "client_assertion"; + + /** + * {@code client_assertion_type} - used in Access Token Request. + */ + String CLIENT_ASSERTION_TYPE = "client_assertion_type"; +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterValues.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterValues.java new file mode 100644 index 00000000000..7ad7fad13a2 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ClientAssertionParameterValues.java @@ -0,0 +1,39 @@ +/* + * + * + * * Copyright 2020 Paychex, Inc. + * * + * * 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 + * * + * * https://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; + +/** + * @author visweshwarganesh + * @Created 06/20/2020 - 9:21 AM + * RFC-7523 JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants + * https://tools.ietf.org/html/rfc7523#section-8 + */ +public interface ClientAssertionParameterValues { + + /** + * {@code urn:ietf:params:oauth:client-assertion-type:jwt-bearer} - used in Access Token Request. + */ + String CLIENT_ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + + /** + * {@code urn:ietf:params:oauth:grant-type:jwt-bearer} - used in Access Token Request. + */ + String CLIENT_GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; +}