22
22
import org .springframework .security .oauth2 .client .registration .ClientRegistration ;
23
23
import org .springframework .security .oauth2 .client .registration .ClientRegistrationRepository ;
24
24
import org .springframework .security .oauth2 .client .web .OAuth2AuthorizedClientRepository ;
25
+ import org .springframework .security .oauth2 .core .AbstractOAuth2Token ;
25
26
import org .springframework .security .oauth2 .core .AuthorizationGrantType ;
26
27
import org .springframework .security .oauth2 .core .endpoint .OAuth2AccessTokenResponse ;
27
28
import org .springframework .util .Assert ;
28
29
29
30
import javax .servlet .http .HttpServletRequest ;
30
31
import javax .servlet .http .HttpServletResponse ;
32
+ import java .time .Duration ;
33
+ import java .time .Instant ;
31
34
32
35
/**
33
36
* An implementation of an {@link OAuth2AuthorizedClientProvider}
@@ -45,6 +48,7 @@ public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OA
45
48
private final OAuth2AuthorizedClientRepository authorizedClientRepository ;
46
49
private OAuth2AccessTokenResponseClient <OAuth2ClientCredentialsGrantRequest > accessTokenResponseClient =
47
50
new DefaultClientCredentialsTokenResponseClient ();
51
+ private Duration clockSkew = Duration .ofSeconds (60 );
48
52
49
53
/**
50
54
* Constructs a {@code ClientCredentialsOAuth2AuthorizedClientProvider} using the provided parameters.
@@ -61,10 +65,11 @@ public ClientCredentialsOAuth2AuthorizedClientProvider(ClientRegistrationReposit
61
65
}
62
66
63
67
/**
64
- * Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration () client} in the provided {@code context}.
68
+ * Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistrationId () client} in the provided {@code context}.
65
69
* Returns {@code null} if authorization (or re-authorization) is not supported,
66
70
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
67
- * is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials}.
71
+ * is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} OR
72
+ * the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
68
73
*
69
74
* <p>
70
75
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
@@ -80,15 +85,26 @@ public ClientCredentialsOAuth2AuthorizedClientProvider(ClientRegistrationReposit
80
85
@ Nullable
81
86
public OAuth2AuthorizedClient authorize (OAuth2AuthorizationContext context ) {
82
87
Assert .notNull (context , "context cannot be null" );
83
- if (!AuthorizationGrantType .CLIENT_CREDENTIALS .equals (context .getClientRegistration ().getAuthorizationGrantType ())) {
84
- return null ;
85
- }
86
88
87
89
HttpServletRequest request = context .getAttribute (HTTP_SERVLET_REQUEST_ATTRIBUTE_NAME );
88
90
HttpServletResponse response = context .getAttribute (HTTP_SERVLET_RESPONSE_ATTRIBUTE_NAME );
89
91
Assert .notNull (request , "The context attribute cannot be null '" + HTTP_SERVLET_REQUEST_ATTRIBUTE_NAME + "'" );
90
92
Assert .notNull (response , "The context attribute cannot be null '" + HTTP_SERVLET_RESPONSE_ATTRIBUTE_NAME + "'" );
91
93
94
+ String clientRegistrationId = context .getClientRegistrationId ();
95
+ ClientRegistration clientRegistration = this .clientRegistrationRepository .findByRegistrationId (clientRegistrationId );
96
+ Assert .notNull (clientRegistration , "Could not find ClientRegistration with id '" + clientRegistrationId + "'" );
97
+
98
+ if (!AuthorizationGrantType .CLIENT_CREDENTIALS .equals (clientRegistration .getAuthorizationGrantType ())) {
99
+ return null ;
100
+ }
101
+
102
+ OAuth2AuthorizedClient authorizedClient = this .authorizedClientRepository .loadAuthorizedClient (
103
+ clientRegistrationId , context .getPrincipal (), request );
104
+ if (authorizedClient != null && !hasTokenExpired (authorizedClient .getAccessToken ())) {
105
+ return null ;
106
+ }
107
+
92
108
// As per spec, in section 4.4.3 Access Token Response
93
109
// https://tools.ietf.org/html/rfc6749#section-4.4.3
94
110
// A refresh token SHOULD NOT be included.
@@ -97,24 +113,23 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
97
113
// is the same as acquiring a new access token (authorization).
98
114
99
115
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest =
100
- new OAuth2ClientCredentialsGrantRequest (context . getClientRegistration () );
116
+ new OAuth2ClientCredentialsGrantRequest (clientRegistration );
101
117
OAuth2AccessTokenResponse tokenResponse =
102
118
this .accessTokenResponseClient .getTokenResponse (clientCredentialsGrantRequest );
103
119
104
- OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient (
105
- context .getClientRegistration (),
106
- context .getPrincipal ().getName (),
107
- tokenResponse .getAccessToken ());
120
+ authorizedClient = new OAuth2AuthorizedClient (
121
+ clientRegistration , context .getPrincipal ().getName (), tokenResponse .getAccessToken ());
108
122
109
123
this .authorizedClientRepository .saveAuthorizedClient (
110
- authorizedClient ,
111
- context .getPrincipal (),
112
- request ,
113
- response );
124
+ authorizedClient , context .getPrincipal (), request , response );
114
125
115
126
return authorizedClient ;
116
127
}
117
128
129
+ private boolean hasTokenExpired (AbstractOAuth2Token token ) {
130
+ return token .getExpiresAt ().isBefore (Instant .now ().minus (this .clockSkew ));
131
+ }
132
+
118
133
/**
119
134
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant.
120
135
*
@@ -124,4 +139,17 @@ public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2C
124
139
Assert .notNull (accessTokenResponseClient , "accessTokenResponseClient cannot be null" );
125
140
this .accessTokenResponseClient = accessTokenResponseClient ;
126
141
}
142
+
143
+ /**
144
+ * Sets the maximum acceptable clock skew, which is used when checking the
145
+ * {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
146
+ * An access token is considered expired if it's before {@code Instant.now() - clockSkew}.
147
+ *
148
+ * @param clockSkew the maximum acceptable clock skew
149
+ */
150
+ public void setClockSkew (Duration clockSkew ) {
151
+ Assert .notNull (clockSkew , "clockSkew cannot be null" );
152
+ Assert .isTrue (clockSkew .getSeconds () >= 0 , "clockSkew must be >= 0" );
153
+ this .clockSkew = clockSkew ;
154
+ }
127
155
}
0 commit comments