Skip to content

Commit

Permalink
Add jwt-bearer authorization grant
Browse files Browse the repository at this point in the history
Closes gh-6053
  • Loading branch information
H-LREB authored and jgrandja committed Apr 9, 2021
1 parent 1a08235 commit 7694aa2
Show file tree
Hide file tree
Showing 15 changed files with 1,142 additions and 4 deletions.
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

0 comments on commit 7694aa2

Please sign in to comment.