Skip to content

Commit cc3b024

Browse files
committed
Implement Token Introspection endpoint
1 parent f97b8b2 commit cc3b024

18 files changed

+1974
-16
lines changed

oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dependencies {
77
compile 'org.springframework.security:spring-security-oauth2-jose'
88
compile springCoreDependency
99
compile 'com.nimbusds:nimbus-jose-jwt'
10+
compile 'com.nimbusds:oauth2-oidc-sdk'
1011
compile 'com.fasterxml.jackson.core:jackson-databind'
1112

1213
testCompile 'org.springframework.security:spring-security-test'
@@ -15,6 +16,7 @@ dependencies {
1516
testCompile 'org.assertj:assertj-core'
1617
testCompile 'org.mockito:mockito-core'
1718
testCompile 'com.jayway.jsonpath:json-path'
19+
testCompile 'org.hamcrest:hamcrest:2.2'
1820

1921
provided 'javax.servlet:javax.servlet-api'
2022
}

oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java

+45-3
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,28 @@
2424
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2525
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
2626
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
27+
import org.springframework.security.crypto.key.AsymmetricKey;
28+
import org.springframework.security.crypto.key.CryptoKey;
2729
import org.springframework.security.crypto.key.CryptoKeySource;
30+
import org.springframework.security.crypto.key.SymmetricKey;
2831
import org.springframework.security.oauth2.jose.jws.NimbusJwsEncoder;
32+
import org.springframework.security.oauth2.jwt.JwtDecoder;
33+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
2934
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
3035
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
3136
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
3237
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
3338
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
3439
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
40+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
3541
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
3642
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
3743
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
3844
import org.springframework.security.oauth2.server.authorization.web.JwkSetEndpointFilter;
3945
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
4046
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
4147
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
48+
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
4249
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
4350
import org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter;
4451
import org.springframework.security.web.AuthenticationEntryPoint;
@@ -55,7 +62,11 @@
5562
import org.springframework.util.StringUtils;
5663

5764
import java.net.URI;
65+
import java.security.Key;
66+
import java.security.interfaces.RSAPublicKey;
67+
import java.util.ArrayList;
5868
import java.util.Arrays;
69+
import java.util.Collection;
5970
import java.util.LinkedHashMap;
6071
import java.util.List;
6172
import java.util.Map;
@@ -65,12 +76,14 @@
6576
*
6677
* @author Joe Grandja
6778
* @author Daniel Garnier-Moiroux
79+
* @author Gerardo Roza
6880
* @since 0.0.1
6981
* @see AbstractHttpConfigurer
7082
* @see RegisteredClientRepository
7183
* @see OAuth2AuthorizationService
7284
* @see OAuth2AuthorizationEndpointFilter
7385
* @see OAuth2TokenEndpointFilter
86+
* @see OAuth2TokenIntrospectionEndpointFilter
7487
* @see OAuth2TokenRevocationEndpointFilter
7588
* @see JwkSetEndpointFilter
7689
* @see OidcProviderConfigurationEndpointFilter
@@ -88,6 +101,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
88101
HttpMethod.POST.name()));
89102
private final RequestMatcher tokenEndpointMatcher = new AntPathRequestMatcher(
90103
OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI, HttpMethod.POST.name());
104+
private final RequestMatcher tokenIntrospectionMatcher = new AntPathRequestMatcher(
105+
OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI, HttpMethod.POST.name());
91106
private final RequestMatcher tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
92107
OAuth2TokenRevocationEndpointFilter.DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI, HttpMethod.POST.name());
93108
private final RequestMatcher jwkSetEndpointMatcher = new AntPathRequestMatcher(
@@ -152,7 +167,7 @@ public List<RequestMatcher> getEndpointMatchers() {
152167
// TODO Initialize matchers using URI's from ProviderSettings
153168
return Arrays.asList(this.authorizationEndpointMatcher, this.tokenEndpointMatcher,
154169
this.tokenRevocationEndpointMatcher, this.jwkSetEndpointMatcher,
155-
this.oidcProviderConfigurationEndpointMatcher);
170+
this.oidcProviderConfigurationEndpointMatcher, this.tokenIntrospectionMatcher);
156171
}
157172

158173
@Override
@@ -192,11 +207,17 @@ public void init(B builder) {
192207
getAuthorizationService(builder));
193208
builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
194209

210+
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
211+
new OAuth2TokenIntrospectionAuthenticationProvider(
212+
getAuthorizationService(builder), getJwtDecoders(builder));
213+
builder.authenticationProvider(postProcess(tokenIntrospectionAuthenticationProvider));
214+
195215
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
196216
if (exceptionHandling != null) {
197217
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
198218
entryPoints.put(
199-
new OrRequestMatcher(this.tokenEndpointMatcher, this.tokenRevocationEndpointMatcher),
219+
new OrRequestMatcher(this.tokenEndpointMatcher, this.tokenRevocationEndpointMatcher,
220+
this.tokenIntrospectionMatcher),
200221
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
201222
DelegatingAuthenticationEntryPoint authenticationEntryPoint =
202223
new DelegatingAuthenticationEntryPoint(entryPoints);
@@ -229,7 +250,8 @@ public void configure(B builder) {
229250
OAuth2ClientAuthenticationFilter clientAuthenticationFilter =
230251
new OAuth2ClientAuthenticationFilter(
231252
authenticationManager,
232-
new OrRequestMatcher(this.tokenEndpointMatcher, this.tokenRevocationEndpointMatcher));
253+
new OrRequestMatcher(this.tokenEndpointMatcher, this.tokenRevocationEndpointMatcher,
254+
this.tokenIntrospectionMatcher));
233255
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
234256

235257
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
@@ -251,6 +273,12 @@ public void configure(B builder) {
251273
authenticationManager,
252274
providerSettings.tokenRevocationEndpoint());
253275
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenEndpointFilter.class);
276+
277+
OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
278+
new OAuth2TokenIntrospectionEndpointFilter(
279+
authenticationManager,
280+
providerSettings.tokenIntrospectionEndpoint());
281+
builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), OAuth2TokenEndpointFilter.class);
254282
}
255283

256284
private static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepository(B builder) {
@@ -278,6 +306,20 @@ private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService get
278306
return authorizationService;
279307
}
280308

309+
private static <B extends HttpSecurityBuilder<B>> Collection<JwtDecoder> getJwtDecoders(B builder) {
310+
Collection<JwtDecoder> jwtDecoders = new ArrayList<>();
311+
for (CryptoKey<? extends Key> cryptoKey : getKeySource(builder).getKeys()) {
312+
if (AsymmetricKey.class.isAssignableFrom(cryptoKey.getClass())
313+
&& RSAPublicKey.class.isAssignableFrom(((AsymmetricKey) cryptoKey).getPublicKey().getClass())) {
314+
jwtDecoders.add(NimbusJwtDecoder
315+
.withPublicKey((RSAPublicKey) ((AsymmetricKey) cryptoKey).getPublicKey()).build());
316+
} else if (SymmetricKey.class.isAssignableFrom(cryptoKey.getClass())) {
317+
jwtDecoders.add(NimbusJwtDecoder.withSecretKey(((SymmetricKey) cryptoKey).getKey()).build());
318+
}
319+
}
320+
return jwtDecoders;
321+
}
322+
281323
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationServiceBean(B builder) {
282324
Map<String, OAuth2AuthorizationService> authorizationServiceMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
283325
builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizationService.class);

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/TokenType.java

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public final class TokenType implements Serializable {
2626
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
2727
public static final TokenType ACCESS_TOKEN = new TokenType("access_token");
2828
public static final TokenType REFRESH_TOKEN = new TokenType("refresh_token");
29+
public static final TokenType ID_TOKEN = new TokenType("id_token");
2930
public static final TokenType AUTHORIZATION_CODE = new TokenType("authorization_code");
3031
private final String value;
3132

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright 2020 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+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
19+
20+
import java.util.Collection;
21+
import java.util.stream.Collectors;
22+
23+
import org.springframework.security.authentication.AuthenticationProvider;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.core.AuthenticationException;
26+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
27+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
28+
import org.springframework.security.oauth2.core.OAuth2Error;
29+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes2;
30+
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
31+
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
32+
import org.springframework.security.oauth2.jwt.Jwt;
33+
import org.springframework.security.oauth2.jwt.JwtDecoder;
34+
import org.springframework.security.oauth2.jwt.JwtException;
35+
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
36+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
37+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
38+
import org.springframework.security.oauth2.server.authorization.TokenType;
39+
import org.springframework.security.oauth2.server.authorization.Version;
40+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
41+
import org.springframework.security.oauth2.server.authorization.token.TokenExpirationValidator;
42+
import org.springframework.util.Assert;
43+
import org.springframework.util.StringUtils;
44+
45+
/**
46+
* An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Introspection.
47+
*
48+
* @author Gerardo Roza
49+
* @since 0.0.4
50+
* @see OAuth2TokenIntrospectionAuthenticationToken
51+
* @see OAuth2AuthorizationService
52+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section 2.1 - Introspection Request</a>
53+
*/
54+
public class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider {
55+
private final OAuth2AuthorizationService authorizationService;
56+
private final Collection<JwtDecoder> jwtDecoders;
57+
58+
/**
59+
* Constructs an {@code OAuth2TokenIntrospectionAuthenticationProvider} using the provided parameters.
60+
*
61+
* @param authorizationService the authorization service
62+
* @param jwtDecoders all available decoders that might be used to parse the token as a JWT-encoded token
63+
*/
64+
public OAuth2TokenIntrospectionAuthenticationProvider(OAuth2AuthorizationService authorizationService,
65+
Collection<JwtDecoder> jwtDecoders) {
66+
Assert.notNull(authorizationService, "authorizationService cannot be null");
67+
this.authorizationService = authorizationService;
68+
this.jwtDecoders = jwtDecoders;
69+
}
70+
71+
@Override
72+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
73+
OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = (OAuth2TokenIntrospectionAuthenticationToken) authentication;
74+
75+
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
76+
tokenIntrospectionAuthentication);
77+
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
78+
79+
TokenType tokenType = null;
80+
81+
try {
82+
String tokenTypeHint = tokenIntrospectionAuthentication.getTokenTypeHint();
83+
if (StringUtils.hasText(tokenTypeHint)) {
84+
if (TokenType.REFRESH_TOKEN.getValue().equals(tokenTypeHint)) {
85+
tokenType = TokenType.REFRESH_TOKEN;
86+
} else if (TokenType.ACCESS_TOKEN.getValue().equals(tokenTypeHint)) {
87+
tokenType = TokenType.ACCESS_TOKEN;
88+
} else if (TokenType.ID_TOKEN.getValue().equals(tokenTypeHint)) {
89+
tokenType = TokenType.ID_TOKEN;
90+
} else {
91+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes2.UNSUPPORTED_TOKEN_TYPE));
92+
}
93+
}
94+
95+
OAuth2Authorization authorization = this.authorizationService
96+
.findByToken(tokenIntrospectionAuthentication.getTokenValue(), tokenType);
97+
if (authorization == null) {
98+
throw new IntrospectionTokenException("Token not found");
99+
}
100+
101+
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
102+
throw new IntrospectionTokenException("Invalid client");
103+
}
104+
105+
AbstractOAuth2Token token = authorization.getTokens().getToken(tokenIntrospectionAuthentication.getTokenValue())
106+
.get();
107+
108+
if (authorization.getTokens().getTokenMetadata(token).isInvalidated()) {
109+
throw new IntrospectionTokenException("Token has been invalidated");
110+
}
111+
112+
token = parseJwt(token);
113+
114+
validateToken(token);
115+
116+
return new OAuth2TokenIntrospectionAuthenticationToken(clientPrincipal, registeredClient.getClientId(), token);
117+
} catch (IntrospectionTokenException exception) {
118+
return new OAuth2TokenIntrospectionAuthenticationToken(clientPrincipal, registeredClient.getClientId(), null);
119+
}
120+
}
121+
122+
@Override
123+
public boolean supports(Class<?> authentication) {
124+
return OAuth2TokenIntrospectionAuthenticationToken.class.isAssignableFrom(authentication);
125+
}
126+
127+
private AbstractOAuth2Token parseJwt(AbstractOAuth2Token token) {
128+
for (JwtDecoder jwtDecoder : this.jwtDecoders) {
129+
try {
130+
return jwtDecoder.decode(token.getTokenValue());
131+
} catch (JwtException ex) {
132+
// token might not be a JWT, or can't be processed with this decoder.
133+
}
134+
}
135+
return token;
136+
}
137+
138+
@SuppressWarnings("unchecked")
139+
private <T extends AbstractOAuth2Token> void validateToken(T token) {
140+
OAuth2TokenValidator<T> tokenValidator = (token instanceof Jwt) ? (OAuth2TokenValidator<T>) new JwtTimestampValidator()
141+
: (OAuth2TokenValidator<T>) new TokenExpirationValidator();
142+
OAuth2TokenValidatorResult result = tokenValidator.validate(token);
143+
if (result.hasErrors()) {
144+
String errorMessages = result.getErrors().stream().map(OAuth2Error::getErrorCode)
145+
.collect(Collectors.joining(", ", "Invalid Token:", "."));
146+
throw new IntrospectionTokenException(errorMessages);
147+
}
148+
}
149+
150+
/**
151+
* Exception that can be triggered when a token is found invalid.
152+
*
153+
* @author Gerardo Roza
154+
*/
155+
private static class IntrospectionTokenException extends RuntimeException {
156+
157+
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
158+
159+
/**
160+
* Construct an instance of {@link IntrospectionTokenException} given the provided description.
161+
*
162+
* @param description the description
163+
*/
164+
IntrospectionTokenException(String description) {
165+
this(description, null);
166+
}
167+
168+
/**
169+
* Construct an instance of {@link IntrospectionTokenException} given the provided description and cause
170+
*
171+
* @param description the description
172+
* @param cause the causing exception
173+
*/
174+
IntrospectionTokenException(String description, Throwable cause) {
175+
super(description, cause);
176+
}
177+
}
178+
179+
}

0 commit comments

Comments
 (0)