Skip to content

Add jwt-bearer authorization grant #9535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2002-2021 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
*
* 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.client;

import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;

/**
* An implementation of an {@link OAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#JWT_BEARER jwt-bearer} grant.
*
* @author Joe Grandja
* @since 5.5
* @see OAuth2AuthorizedClientProvider
* @see DefaultJwtBearerTokenResponseClient
*/
public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {

private OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = new DefaultJwtBearerTokenResponseClient();

/**
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration()
* client} in the provided {@code context}. Returns {@code null} if authorization is
* not supported, e.g. the client's
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant type} is
* not {@link AuthorizationGrantType#JWT_BEARER jwt-bearer}.
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not
* supported
*/
@Override
@Nullable
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
ClientRegistration clientRegistration = context.getClientRegistration();
if (!AuthorizationGrantType.JWT_BEARER.equals(clientRegistration.getAuthorizationGrantType())) {
return null;
}
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient != null) {
return null;
}
if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) {
return null;
}
Jwt jwt = (Jwt) context.getPrincipal().getPrincipal();
// As per spec, in section 4.1 Using Assertions as Authorization Grants
// https://tools.ietf.org/html/rfc7521#section-4.1
//
// An assertion used in this context is generally a short-lived
// representation of the authorization grant, and authorization servers
// SHOULD NOT issue access tokens with a lifetime that exceeds the
// validity period of the assertion by a significant period. In
// practice, that will usually mean that refresh tokens are not issued
// in response to assertion grant requests, and access tokens will be
// issued with a reasonably short lifetime. Clients can refresh an
// expired access token by requesting a new one using the same
// assertion, if it is still valid, or with a new assertion.
JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, jwt);
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, jwtBearerGrantRequest);
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken());
}

private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration,
JwtBearerGrantRequest jwtBearerGrantRequest) {
try {
return this.accessTokenResponseClient.getTokenResponse(jwtBearerGrantRequest);
}
catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
}
}

/**
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code jwt-bearer} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code jwt-bearer} grant
*/
public void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2002-2021 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
*
* 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.client.endpoint;

import java.util.Arrays;

import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

/**
* The default implementation of an {@link OAuth2AccessTokenResponseClient} for the
* {@link AuthorizationGrantType#JWT_BEARER jwt-bearer} grant. This implementation uses a
* {@link RestOperations} when requesting an access token credential at the Authorization
* Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.5
* @see OAuth2AccessTokenResponseClient
* @see JwtBearerGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7523#section-2.1">Section
* 2.1 Using JWTs as Authorization Grants</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7521#section-4.1">Section
* 4.1 Using Assertions as Authorization Grants</a>
*/
public final class DefaultJwtBearerTokenResponseClient
implements OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {

private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";

private Converter<JwtBearerGrantRequest, RequestEntity<?>> requestEntityConverter = new JwtBearerGrantRequestEntityConverter();

private RestOperations restOperations;

public DefaultJwtBearerTokenResponseClient() {
RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}

@Override
public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest jwtBearerGrantRequest) {
Assert.notNull(jwtBearerGrantRequest, "jwtBearerGrantRequest cannot be null");
RequestEntity<?> request = this.requestEntityConverter.convert(jwtBearerGrantRequest);
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
OAuth2AccessTokenResponse tokenResponse = response.getBody();
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
// As per spec, in Section 5.1 Successful Access Token Response
// https://tools.ietf.org/html/rfc6749#section-5.1
// If AccessTokenResponse.scope is empty, then default to the scope
// originally requested by the client in the Token Request
// @formatter:off
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
.scopes(jwtBearerGrantRequest.getClientRegistration().getScopes())
.build();
// @formatter:on
}
return tokenResponse;
}

private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
}
catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(),
null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}

/**
* Sets the {@link Converter} used for converting the {@link JwtBearerGrantRequest} to
* a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
* @param requestEntityConverter the {@link Converter} used for converting to a
* {@link RequestEntity} representation of the Access Token Request
*/
public void setRequestEntityConverter(Converter<JwtBearerGrantRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}

/**
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token
* Response.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured
* with the following:
* <ol>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and
* {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
* @param restOperations the {@link RestOperations} used when requesting the Access
* Token Response
*/
public void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2002-2021 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
*
* 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.client.endpoint;

import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;

/**
* A JWT Bearer Grant request that holds a {@link Jwt} assertion.
*
* @author Joe Grandja
* @since 5.5
* @see AbstractOAuth2AuthorizationGrantRequest
* @see ClientRegistration
* @see Jwt
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7523#section-2.1">Section
* 2.1 Using JWTs as Authorization Grants</a>
*/
public class JwtBearerGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {

private final Jwt jwt;

/**
* Constructs a {@code JwtBearerGrantRequest} using the provided parameters.
* @param clientRegistration the client registration
* @param jwt the JWT assertion
*/
public JwtBearerGrantRequest(ClientRegistration clientRegistration, Jwt jwt) {
super(AuthorizationGrantType.JWT_BEARER, clientRegistration);
Assert.isTrue(AuthorizationGrantType.JWT_BEARER.equals(clientRegistration.getAuthorizationGrantType()),
"clientRegistration.authorizationGrantType must be AuthorizationGrantType.JWT_BEARER");
Assert.notNull(jwt, "jwt cannot be null");
this.jwt = jwt;
}

/**
* Returns the {@link Jwt JWT} assertion.
* @return the {@link Jwt} assertion
*/
public Jwt getJwt() {
return this.jwt;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2002-2021 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
*
* 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.client.endpoint;

import org.springframework.http.RequestEntity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

/**
* An implementation of an {@link AbstractOAuth2AuthorizationGrantRequestEntityConverter}
* that converts the provided {@link JwtBearerGrantRequest} to a {@link RequestEntity}
* representation of an OAuth 2.0 Access Token Request for the JWT Bearer Grant.
*
* @author Joe Grandja
* @since 5.5
* @see AbstractOAuth2AuthorizationGrantRequestEntityConverter
* @see JwtBearerGrantRequest
* @see RequestEntity
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7523#section-2.1">Section
* 2.1 Using JWTs as Authorization Grants</a>
*/
public class JwtBearerGrantRequestEntityConverter
extends AbstractOAuth2AuthorizationGrantRequestEntityConverter<JwtBearerGrantRequest> {

@Override
protected MultiValueMap<String, String> createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) {
ClientRegistration clientRegistration = jwtBearerGrantRequest.getClientRegistration();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add(OAuth2ParameterNames.GRANT_TYPE, jwtBearerGrantRequest.getGrantType().getValue());
parameters.add(OAuth2ParameterNames.ASSERTION, jwtBearerGrantRequest.getJwt().getTokenValue());
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
parameters.add(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
}
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())
|| ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
}
return parameters;
}

}
Loading