1616
1717package org .springframework .security .oauth2 .server .resource .introspection ;
1818
19- import java .net .URI ;
20- import java .time .Instant ;
21- import java .util .ArrayList ;
22- import java .util .Collection ;
23- import java .util .Collections ;
24- import java .util .List ;
25- import java .util .Map ;
26-
27- import com .nimbusds .oauth2 .sdk .ErrorObject ;
28- import com .nimbusds .oauth2 .sdk .TokenIntrospectionResponse ;
29- import com .nimbusds .oauth2 .sdk .TokenIntrospectionSuccessResponse ;
30- import com .nimbusds .oauth2 .sdk .http .HTTPResponse ;
31- import com .nimbusds .oauth2 .sdk .id .Audience ;
32- import org .apache .commons .logging .Log ;
33- import org .apache .commons .logging .LogFactory ;
34-
3519import org .springframework .core .convert .converter .Converter ;
36- import org .springframework .http .HttpHeaders ;
37- import org .springframework .http .HttpMethod ;
38- import org .springframework .http .MediaType ;
3920import org .springframework .http .RequestEntity ;
40- import org .springframework .http .ResponseEntity ;
41- import org .springframework .http .client .support .BasicAuthenticationInterceptor ;
42- import org .springframework .security .core .GrantedAuthority ;
43- import org .springframework .security .core .authority .SimpleGrantedAuthority ;
4421import org .springframework .security .oauth2 .core .OAuth2AuthenticatedPrincipal ;
45- import org .springframework .security .oauth2 .core .OAuth2TokenIntrospectionClaimNames ;
4622import org .springframework .util .Assert ;
47- import org .springframework .util .LinkedMultiValueMap ;
48- import org .springframework .util .MultiValueMap ;
4923import org .springframework .web .client .RestOperations ;
50- import org .springframework .web .client .RestTemplate ;
5124
5225/**
5326 * A Nimbus implementation of {@link OpaqueTokenIntrospector} that verifies and
6336@ Deprecated
6437public class NimbusOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
6538
66- private static final String AUTHORITY_PREFIX = "SCOPE_" ;
67-
68- private final Log logger = LogFactory .getLog (getClass ());
69-
70- private final RestOperations restOperations ;
71-
72- private Converter <String , RequestEntity <?>> requestEntityConverter ;
39+ private final SpringOpaqueTokenIntrospector delegate ;
7340
7441 /**
7542 * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
@@ -81,10 +48,7 @@ public NimbusOpaqueTokenIntrospector(String introspectionUri, String clientId, S
8148 Assert .notNull (introspectionUri , "introspectionUri cannot be null" );
8249 Assert .notNull (clientId , "clientId cannot be null" );
8350 Assert .notNull (clientSecret , "clientSecret cannot be null" );
84- this .requestEntityConverter = this .defaultRequestEntityConverter (URI .create (introspectionUri ));
85- RestTemplate restTemplate = new RestTemplate ();
86- restTemplate .getInterceptors ().add (new BasicAuthenticationInterceptor (clientId , clientSecret ));
87- this .restOperations = restTemplate ;
51+ this .delegate = new SpringOpaqueTokenIntrospector (introspectionUri , clientId , clientSecret );
8852 }
8953
9054 /**
@@ -98,47 +62,12 @@ public NimbusOpaqueTokenIntrospector(String introspectionUri, String clientId, S
9862 public NimbusOpaqueTokenIntrospector (String introspectionUri , RestOperations restOperations ) {
9963 Assert .notNull (introspectionUri , "introspectionUri cannot be null" );
10064 Assert .notNull (restOperations , "restOperations cannot be null" );
101- this .requestEntityConverter = this .defaultRequestEntityConverter (URI .create (introspectionUri ));
102- this .restOperations = restOperations ;
103- }
104-
105- private Converter <String , RequestEntity <?>> defaultRequestEntityConverter (URI introspectionUri ) {
106- return (token ) -> {
107- HttpHeaders headers = requestHeaders ();
108- MultiValueMap <String , String > body = requestBody (token );
109- return new RequestEntity <>(body , headers , HttpMethod .POST , introspectionUri );
110- };
111- }
112-
113- private HttpHeaders requestHeaders () {
114- HttpHeaders headers = new HttpHeaders ();
115- headers .setAccept (Collections .singletonList (MediaType .APPLICATION_JSON ));
116- return headers ;
117- }
118-
119- private MultiValueMap <String , String > requestBody (String token ) {
120- MultiValueMap <String , String > body = new LinkedMultiValueMap <>();
121- body .add ("token" , token );
122- return body ;
65+ this .delegate = new SpringOpaqueTokenIntrospector (introspectionUri , restOperations );
12366 }
12467
12568 @ Override
12669 public OAuth2AuthenticatedPrincipal introspect (String token ) {
127- RequestEntity <?> requestEntity = this .requestEntityConverter .convert (token );
128- if (requestEntity == null ) {
129- throw new OAuth2IntrospectionException ("requestEntityConverter returned a null entity" );
130- }
131- ResponseEntity <String > responseEntity = makeRequest (requestEntity );
132- HTTPResponse httpResponse = adaptToNimbusResponse (responseEntity );
133- TokenIntrospectionResponse introspectionResponse = parseNimbusResponse (httpResponse );
134- TokenIntrospectionSuccessResponse introspectionSuccessResponse = castToNimbusSuccess (introspectionResponse );
135- // relying solely on the authorization server to validate this token (not checking
136- // 'exp', for example)
137- if (!introspectionSuccessResponse .isActive ()) {
138- this .logger .trace ("Did not validate token since it is inactive" );
139- throw new BadOpaqueTokenException ("Provided token isn't active" );
140- }
141- return convertClaimsSet (introspectionSuccessResponse );
70+ return this .delegate .introspect (token );
14271 }
14372
14473 /**
@@ -149,121 +78,7 @@ public OAuth2AuthenticatedPrincipal introspect(String token) {
14978 */
15079 public void setRequestEntityConverter (Converter <String , RequestEntity <?>> requestEntityConverter ) {
15180 Assert .notNull (requestEntityConverter , "requestEntityConverter cannot be null" );
152- this .requestEntityConverter = requestEntityConverter ;
153- }
154-
155- private ResponseEntity <String > makeRequest (RequestEntity <?> requestEntity ) {
156- try {
157- return this .restOperations .exchange (requestEntity , String .class );
158- }
159- catch (Exception ex ) {
160- throw new OAuth2IntrospectionException (ex .getMessage (), ex );
161- }
162- }
163-
164- private HTTPResponse adaptToNimbusResponse (ResponseEntity <String > responseEntity ) {
165- MediaType contentType = responseEntity .getHeaders ().getContentType ();
166-
167- if (contentType == null ) {
168- this .logger .trace ("Did not receive Content-Type from introspection endpoint in response" );
169-
170- throw new OAuth2IntrospectionException (
171- "Introspection endpoint response was invalid, as no Content-Type header was provided" );
172- }
173-
174- // Nimbus expects JSON, but does not appear to validate this header first.
175- if (!contentType .isCompatibleWith (MediaType .APPLICATION_JSON )) {
176- this .logger .trace ("Did not receive JSON-compatible Content-Type from introspection endpoint in response" );
177-
178- throw new OAuth2IntrospectionException ("Introspection endpoint response was invalid, as content type '"
179- + contentType + "' is not compatible with JSON" );
180- }
181-
182- HTTPResponse response = new HTTPResponse (responseEntity .getStatusCode ().value ());
183- response .setHeader (HttpHeaders .CONTENT_TYPE , contentType .toString ());
184- response .setContent (responseEntity .getBody ());
185-
186- if (response .getStatusCode () != HTTPResponse .SC_OK ) {
187- this .logger .trace ("Introspection endpoint returned non-OK status code" );
188-
189- throw new OAuth2IntrospectionException (
190- "Introspection endpoint responded with HTTP status code " + response .getStatusCode ());
191- }
192- return response ;
193- }
194-
195- private TokenIntrospectionResponse parseNimbusResponse (HTTPResponse response ) {
196- try {
197- return TokenIntrospectionResponse .parse (response );
198- }
199- catch (Exception ex ) {
200- throw new OAuth2IntrospectionException (ex .getMessage (), ex );
201- }
202- }
203-
204- private TokenIntrospectionSuccessResponse castToNimbusSuccess (TokenIntrospectionResponse introspectionResponse ) {
205- if (!introspectionResponse .indicatesSuccess ()) {
206- ErrorObject errorObject = introspectionResponse .toErrorResponse ().getErrorObject ();
207- String message = "Token introspection failed with response " + errorObject .toJSONObject ().toJSONString ();
208- this .logger .trace (message );
209- throw new OAuth2IntrospectionException (message );
210- }
211- return (TokenIntrospectionSuccessResponse ) introspectionResponse ;
212- }
213-
214- private OAuth2AuthenticatedPrincipal convertClaimsSet (TokenIntrospectionSuccessResponse response ) {
215- Collection <GrantedAuthority > authorities = new ArrayList <>();
216- Map <String , Object > claims = response .toJSONObject ();
217- if (response .getAudience () != null ) {
218- List <String > audiences = new ArrayList <>();
219- for (Audience audience : response .getAudience ()) {
220- audiences .add (audience .getValue ());
221- }
222- claims .put (OAuth2TokenIntrospectionClaimNames .AUD , Collections .unmodifiableList (audiences ));
223- }
224- if (response .getClientID () != null ) {
225- claims .put (OAuth2TokenIntrospectionClaimNames .CLIENT_ID , response .getClientID ().getValue ());
226- }
227- if (response .getExpirationTime () != null ) {
228- Instant exp = response .getExpirationTime ().toInstant ();
229- claims .put (OAuth2TokenIntrospectionClaimNames .EXP , exp );
230- }
231- if (response .getIssueTime () != null ) {
232- Instant iat = response .getIssueTime ().toInstant ();
233- claims .put (OAuth2TokenIntrospectionClaimNames .IAT , iat );
234- }
235- if (response .getIssuer () != null ) {
236- // RFC-7662 page 7 directs users to RFC-7519 for defining the values of these
237- // issuer fields.
238- // https://datatracker.ietf.org/doc/html/rfc7662#page-7
239- //
240- // RFC-7519 page 9 defines issuer fields as being 'case-sensitive' strings
241- // containing
242- // a 'StringOrURI', which is defined on page 5 as being any string, but
243- // strings containing ':'
244- // should be treated as valid URIs.
245- // https://datatracker.ietf.org/doc/html/rfc7519#section-2
246- //
247- // It is not defined however as to whether-or-not normalized URIs should be
248- // treated as the same literal
249- // value. It only defines validation itself, so to avoid potential ambiguity
250- // or unwanted side effects that
251- // may be awkward to debug, we do not want to manipulate this value. Previous
252- // versions of Spring Security
253- // would *only* allow valid URLs, which is not what we wish to achieve here.
254- claims .put (OAuth2TokenIntrospectionClaimNames .ISS , response .getIssuer ().getValue ());
255- }
256- if (response .getNotBeforeTime () != null ) {
257- claims .put (OAuth2TokenIntrospectionClaimNames .NBF , response .getNotBeforeTime ().toInstant ());
258- }
259- if (response .getScope () != null ) {
260- List <String > scopes = Collections .unmodifiableList (response .getScope ().toStringList ());
261- claims .put (OAuth2TokenIntrospectionClaimNames .SCOPE , scopes );
262- for (String scope : scopes ) {
263- authorities .add (new SimpleGrantedAuthority (AUTHORITY_PREFIX + scope ));
264- }
265- }
266- return new OAuth2IntrospectionAuthenticatedPrincipal (claims , authorities );
81+ this .delegate .setRequestEntityConverter (requestEntityConverter );
26782 }
26883
26984}
0 commit comments