Skip to content

Gh 6053 support jwt bearer grant type #9505

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
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,129 @@
/*
* 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 java.time.Clock;
import java.time.Duration;
import java.time.Instant;

import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
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 OAuth2JwtBearerGrantRequest#JWT_BEARER_GRANT_TYPE jwt-bearer} grant.
*
* @author Joe Grandja
* @since 5.5
* @see OAuth2AuthorizedClientProvider
* @see DefaultJwtBearerTokenResponseClient
*/
public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {

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

private Duration clockSkew = Duration.ofSeconds(60);

private Clock clock = Clock.systemUTC();

/**
* 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 OAuth2JwtBearerGrantRequest#JWT_BEARER_GRANT_TYPE 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 (!OAuth2JwtBearerGrantRequest.JWT_BEARER_GRANT_TYPE.equals(clientRegistration.getAuthorizationGrantType())) {
return null;
}

Jwt jwt = context.getAttribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME);
if (jwt == null) {
return null;
}

OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
// If client is already authorized but access token is NOT expired than no
// need for re-authorization
return null;
}

OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, jwt);
OAuth2AccessTokenResponse tokenResponse = this.accessTokenResponseClient
.getTokenResponse(jwtBearerGrantRequest);

return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken());
}

private boolean hasTokenExpired(AbstractOAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}

/**
* 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<OAuth2JwtBearerGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}

/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
* 60 seconds. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
Assert.notNull(clockSkew, "clockSkew cannot be null");
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
this.clockSkew = clockSkew;
}

/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
* token expiry.
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
Expand Down Expand Up @@ -60,6 +60,12 @@ public final class OAuth2AuthorizationContext {
*/
public static final String PASSWORD_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".PASSWORD");

/**
* The name of the {@link #getAttribute(String) attribute} in the context associated
* to the value for the JWT Bearer token.
*/
public static final String JWT_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".JWT");

private ClientRegistration clientRegistration;

private OAuth2AuthorizedClient authorizedClient;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
Expand Down Expand Up @@ -27,6 +27,7 @@

import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -156,6 +157,29 @@ public OAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuil
return OAuth2AuthorizedClientProviderBuilder.this;
}

/**
* Configures support for the {@code jwt_bearer} grant.
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder jwtBearer() {
this.builders.computeIfAbsent(JwtBearerOAuth2AuthorizedClientProvider.class,
(k) -> new JwtBearerGrantBuilder());
return OAuth2AuthorizedClientProviderBuilder.this;
}

/**
* Configures support for the {@code jwt_bearer} grant.
* @param builderConsumer a {@code Consumer} of {@link JwtBearerGrantBuilder} used for
* further configuration
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder jwtBearer(Consumer<JwtBearerGrantBuilder> builderConsumer) {
JwtBearerGrantBuilder builder = (JwtBearerGrantBuilder) this.builders
.computeIfAbsent(JwtBearerOAuth2AuthorizedClientProvider.class, (k) -> new JwtBearerGrantBuilder());
builderConsumer.accept(builder);
return OAuth2AuthorizedClientProviderBuilder.this;
}

/**
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider} composed of
* one or more {@link OAuth2AuthorizedClientProvider}(s).
Expand Down Expand Up @@ -205,7 +229,7 @@ public PasswordGrantBuilder accessTokenResponseClient(
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* {@code Instant.now(this.clock) + clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link PasswordGrantBuilder}
*/
Expand Down Expand Up @@ -246,6 +270,77 @@ public OAuth2AuthorizedClientProvider build() {

}

/**
* A builder for the {@code jwt_bearer} grant.
*/
public final class JwtBearerGrantBuilder implements Builder {

private OAuth2AccessTokenResponseClient<OAuth2JwtBearerGrantRequest> accessTokenResponseClient;

private Duration clockSkew;

private Clock clock;

private JwtBearerGrantBuilder() {
}

/**
* Sets the client used when requesting an access token credential at the Token
* Endpoint.
* @param accessTokenResponseClient the client used when requesting an access
* token credential at the Token Endpoint
* @return the {@link JwtBearerGrantBuilder}
*/
public JwtBearerGrantBuilder accessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2JwtBearerGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}

/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) + clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link JwtBearerGrantBuilder}
*/
public JwtBearerGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}

/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
* access token expiry.
* @param clock the clock
* @return the {@link JwtBearerGrantBuilder}
*/
public JwtBearerGrantBuilder clock(Clock clock) {
this.clock = clock;
return this;
}

/**
* Builds an instance of {@link JwtBearerOAuth2AuthorizedClientProvider}.
* @return the {@link JwtBearerOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
return authorizedClientProvider;
}

}

/**
* A builder for the {@code client_credentials} grant.
*/
Expand Down
Loading