Skip to content

Commit fb89286

Browse files
committed
Remove Nimbus introspector reliance on oauth2-oidc-sdk
Signed-off-by: Andreas Svanberg <[email protected]>
1 parent b69054d commit fb89286

File tree

1 file changed

+5
-190
lines changed

1 file changed

+5
-190
lines changed

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusOpaqueTokenIntrospector.java

Lines changed: 5 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,11 @@
1616

1717
package 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-
3519
import org.springframework.core.convert.converter.Converter;
36-
import org.springframework.http.HttpHeaders;
37-
import org.springframework.http.HttpMethod;
38-
import org.springframework.http.MediaType;
3920
import 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;
4421
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
45-
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
4622
import org.springframework.util.Assert;
47-
import org.springframework.util.LinkedMultiValueMap;
48-
import org.springframework.util.MultiValueMap;
4923
import org.springframework.web.client.RestOperations;
50-
import org.springframework.web.client.RestTemplate;
5124

5225
/**
5326
* A Nimbus implementation of {@link OpaqueTokenIntrospector} that verifies and
@@ -63,13 +36,7 @@
6336
@Deprecated
6437
public 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

Comments
 (0)