Skip to content

Commit 7694aa2

Browse files
H-LREBjgrandja
authored andcommitted
Add jwt-bearer authorization grant
Closes gh-6053
1 parent 1a08235 commit 7694aa2

15 files changed

+1142
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.client;
18+
19+
import java.time.Clock;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient;
25+
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
26+
import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest;
27+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
28+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
29+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
30+
import org.springframework.security.oauth2.jwt.Jwt;
31+
import org.springframework.util.Assert;
32+
33+
/**
34+
* An implementation of an {@link OAuth2AuthorizedClientProvider} for the
35+
* {@link OAuth2JwtBearerGrantRequest#JWT_BEARER_GRANT_TYPE jwt-bearer} grant.
36+
*
37+
* @author Joe Grandja
38+
* @since 5.5
39+
* @see OAuth2AuthorizedClientProvider
40+
* @see DefaultJwtBearerTokenResponseClient
41+
*/
42+
public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
43+
44+
private OAuth2AccessTokenResponseClient<OAuth2JwtBearerGrantRequest> accessTokenResponseClient = new DefaultJwtBearerTokenResponseClient();
45+
46+
private Duration clockSkew = Duration.ofSeconds(60);
47+
48+
private Clock clock = Clock.systemUTC();
49+
50+
/**
51+
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration()
52+
* client} in the provided {@code context}. Returns {@code null} if authorization is
53+
* not supported, e.g. the client's
54+
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant type} is
55+
* not {@link OAuth2JwtBearerGrantRequest#JWT_BEARER_GRANT_TYPE jwt-bearer}.
56+
* @param context the context that holds authorization-specific state for the client
57+
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not
58+
* supported
59+
*/
60+
@Override
61+
@Nullable
62+
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
63+
Assert.notNull(context, "context cannot be null");
64+
65+
ClientRegistration clientRegistration = context.getClientRegistration();
66+
if (!OAuth2JwtBearerGrantRequest.JWT_BEARER_GRANT_TYPE.equals(clientRegistration.getAuthorizationGrantType())) {
67+
return null;
68+
}
69+
70+
Jwt jwt = context.getAttribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME);
71+
if (jwt == null) {
72+
return null;
73+
}
74+
75+
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
76+
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
77+
// If client is already authorized but access token is NOT expired than no
78+
// need for re-authorization
79+
return null;
80+
}
81+
82+
OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, jwt);
83+
OAuth2AccessTokenResponse tokenResponse = this.accessTokenResponseClient
84+
.getTokenResponse(jwtBearerGrantRequest);
85+
86+
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
87+
tokenResponse.getAccessToken());
88+
}
89+
90+
private boolean hasTokenExpired(AbstractOAuth2Token token) {
91+
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
92+
}
93+
94+
/**
95+
* Sets the client used when requesting an access token credential at the Token
96+
* Endpoint for the {@code jwt-bearer} grant.
97+
* @param accessTokenResponseClient the client used when requesting an access token
98+
* credential at the Token Endpoint for the {@code jwt-bearer} grant
99+
*/
100+
public void setAccessTokenResponseClient(
101+
OAuth2AccessTokenResponseClient<OAuth2JwtBearerGrantRequest> accessTokenResponseClient) {
102+
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
103+
this.accessTokenResponseClient = accessTokenResponseClient;
104+
}
105+
106+
/**
107+
* Sets the maximum acceptable clock skew, which is used when checking the
108+
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
109+
* 60 seconds. An access token is considered expired if it's before
110+
* {@code Instant.now(this.clock) - clockSkew}.
111+
* @param clockSkew the maximum acceptable clock skew
112+
*/
113+
public void setClockSkew(Duration clockSkew) {
114+
Assert.notNull(clockSkew, "clockSkew cannot be null");
115+
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
116+
this.clockSkew = clockSkew;
117+
}
118+
119+
/**
120+
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
121+
* token expiry.
122+
* @param clock the clock
123+
*/
124+
public void setClock(Clock clock) {
125+
Assert.notNull(clock, "clock cannot be null");
126+
this.clock = clock;
127+
}
128+
129+
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -60,6 +60,12 @@ public final class OAuth2AuthorizationContext {
6060
*/
6161
public static final String PASSWORD_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".PASSWORD");
6262

63+
/**
64+
* The name of the {@link #getAttribute(String) attribute} in the context associated
65+
* to the value for the JWT Bearer token.
66+
*/
67+
public static final String JWT_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".JWT");
68+
6369
private ClientRegistration clientRegistration;
6470

6571
private OAuth2AuthorizedClient authorizedClient;

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java

+97-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727

2828
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
2929
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
30+
import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest;
3031
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
3132
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
3233
import org.springframework.util.Assert;
@@ -156,6 +157,29 @@ public OAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuil
156157
return OAuth2AuthorizedClientProviderBuilder.this;
157158
}
158159

160+
/**
161+
* Configures support for the {@code jwt_bearer} grant.
162+
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
163+
*/
164+
public OAuth2AuthorizedClientProviderBuilder jwtBearer() {
165+
this.builders.computeIfAbsent(JwtBearerOAuth2AuthorizedClientProvider.class,
166+
(k) -> new JwtBearerGrantBuilder());
167+
return OAuth2AuthorizedClientProviderBuilder.this;
168+
}
169+
170+
/**
171+
* Configures support for the {@code jwt_bearer} grant.
172+
* @param builderConsumer a {@code Consumer} of {@link JwtBearerGrantBuilder} used for
173+
* further configuration
174+
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
175+
*/
176+
public OAuth2AuthorizedClientProviderBuilder jwtBearer(Consumer<JwtBearerGrantBuilder> builderConsumer) {
177+
JwtBearerGrantBuilder builder = (JwtBearerGrantBuilder) this.builders
178+
.computeIfAbsent(JwtBearerOAuth2AuthorizedClientProvider.class, (k) -> new JwtBearerGrantBuilder());
179+
builderConsumer.accept(builder);
180+
return OAuth2AuthorizedClientProviderBuilder.this;
181+
}
182+
159183
/**
160184
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider} composed of
161185
* one or more {@link OAuth2AuthorizedClientProvider}(s).
@@ -205,7 +229,7 @@ public PasswordGrantBuilder accessTokenResponseClient(
205229
/**
206230
* Sets the maximum acceptable clock skew, which is used when checking the access
207231
* token expiry. An access token is considered expired if it's before
208-
* {@code Instant.now(this.clock) - clockSkew}.
232+
* {@code Instant.now(this.clock) + clockSkew}.
209233
* @param clockSkew the maximum acceptable clock skew
210234
* @return the {@link PasswordGrantBuilder}
211235
*/
@@ -246,6 +270,77 @@ public OAuth2AuthorizedClientProvider build() {
246270

247271
}
248272

273+
/**
274+
* A builder for the {@code jwt_bearer} grant.
275+
*/
276+
public final class JwtBearerGrantBuilder implements Builder {
277+
278+
private OAuth2AccessTokenResponseClient<OAuth2JwtBearerGrantRequest> accessTokenResponseClient;
279+
280+
private Duration clockSkew;
281+
282+
private Clock clock;
283+
284+
private JwtBearerGrantBuilder() {
285+
}
286+
287+
/**
288+
* Sets the client used when requesting an access token credential at the Token
289+
* Endpoint.
290+
* @param accessTokenResponseClient the client used when requesting an access
291+
* token credential at the Token Endpoint
292+
* @return the {@link JwtBearerGrantBuilder}
293+
*/
294+
public JwtBearerGrantBuilder accessTokenResponseClient(
295+
OAuth2AccessTokenResponseClient<OAuth2JwtBearerGrantRequest> accessTokenResponseClient) {
296+
this.accessTokenResponseClient = accessTokenResponseClient;
297+
return this;
298+
}
299+
300+
/**
301+
* Sets the maximum acceptable clock skew, which is used when checking the access
302+
* token expiry. An access token is considered expired if it's before
303+
* {@code Instant.now(this.clock) + clockSkew}.
304+
* @param clockSkew the maximum acceptable clock skew
305+
* @return the {@link JwtBearerGrantBuilder}
306+
*/
307+
public JwtBearerGrantBuilder clockSkew(Duration clockSkew) {
308+
this.clockSkew = clockSkew;
309+
return this;
310+
}
311+
312+
/**
313+
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
314+
* access token expiry.
315+
* @param clock the clock
316+
* @return the {@link JwtBearerGrantBuilder}
317+
*/
318+
public JwtBearerGrantBuilder clock(Clock clock) {
319+
this.clock = clock;
320+
return this;
321+
}
322+
323+
/**
324+
* Builds an instance of {@link JwtBearerOAuth2AuthorizedClientProvider}.
325+
* @return the {@link JwtBearerOAuth2AuthorizedClientProvider}
326+
*/
327+
@Override
328+
public OAuth2AuthorizedClientProvider build() {
329+
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
330+
if (this.accessTokenResponseClient != null) {
331+
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
332+
}
333+
if (this.clockSkew != null) {
334+
authorizedClientProvider.setClockSkew(this.clockSkew);
335+
}
336+
if (this.clock != null) {
337+
authorizedClientProvider.setClock(this.clock);
338+
}
339+
return authorizedClientProvider;
340+
}
341+
342+
}
343+
249344
/**
250345
* A builder for the {@code client_credentials} grant.
251346
*/

0 commit comments

Comments
 (0)