Skip to content

Commit a58278c

Browse files
qavidjzheaux
authored andcommitted
Simplify OAuth 2.0 Introspection Attribute Retrieval
In order to simplify retrieving of OAuth 2.0 Introspection specific attributes, OAuth2IntrospectionClaimAccessor interface was introduced and also new OAuth2AuthenticatedPrincipal implementing this new interface (OAuth2IntrospectionAuthenticatedPrincipal). Also DefaultOAuth2AuthenticatedPrincipal was replaced by OAuth2IntrospectionAuthenticatedPrincipal in cases where OAuth 2.0 Introspection is performed (NimbusOpaqueTokenIntrospector, NimbusReactiveOpaqueTokenIntrospector). DefaultOAuth2AuthenticatedPrincipal can be still used by applications that introspected the token without OAuth 2.0 Introspection. OAuth2IntrospectionAuthenticatedPrincipal will also be used as a default principal in tests where request is post-processed/mutated by OpaqueTokenRequestPostProcessor/OpaqueTokenMutator. Closes gh-6489
1 parent b69bcf8 commit a58278c

File tree

10 files changed

+454
-19
lines changed

10 files changed

+454
-19
lines changed

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
4040
import org.springframework.security.core.GrantedAuthority;
4141
import org.springframework.security.core.authority.SimpleGrantedAuthority;
42-
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
4342
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
4443
import org.springframework.util.Assert;
4544
import org.springframework.util.LinkedMultiValueMap;
@@ -232,7 +231,7 @@ private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessR
232231
}
233232
}
234233

235-
return new DefaultOAuth2AuthenticatedPrincipal(claims, authorities);
234+
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
236235
}
237236

238237
private URL issuer(String uri) {

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.springframework.http.MediaType;
3838
import org.springframework.security.core.GrantedAuthority;
3939
import org.springframework.security.core.authority.SimpleGrantedAuthority;
40-
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
4140
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
4241
import org.springframework.util.Assert;
4342
import org.springframework.web.reactive.function.BodyInserters;
@@ -193,7 +192,7 @@ private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessR
193192
}
194193
}
195194

196-
return new DefaultOAuth2AuthenticatedPrincipal(claims, authorities);
195+
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
197196
}
198197

199198
private URL issuer(String uri) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2002-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+
17+
package org.springframework.security.oauth2.server.resource.introspection;
18+
19+
import static org.springframework.security.core.authority.AuthorityUtils.NO_AUTHORITIES;
20+
21+
import java.io.Serializable;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.Map;
25+
26+
import org.springframework.security.core.GrantedAuthority;
27+
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* A domain object that wraps the attributes of OAuth 2.0 Token Introspection.
32+
*
33+
* @author David Kovac
34+
* @since 5.4
35+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
36+
*/
37+
public final class OAuth2IntrospectionAuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal,
38+
OAuth2IntrospectionClaimAccessor, Serializable {
39+
private final Map<String, Object> attributes;
40+
private final Collection<GrantedAuthority> authorities;
41+
private final String name;
42+
43+
/**
44+
* Constructs an {@code OAuth2IntrospectionAuthenticatedPrincipal} using the provided parameters.
45+
*
46+
* @param attributes the attributes of the OAuth 2.0 Token Introspection
47+
* @param authorities the authorities of the OAuth 2.0 Token Introspection
48+
*/
49+
public OAuth2IntrospectionAuthenticatedPrincipal(Map<String, Object> attributes,
50+
Collection<GrantedAuthority> authorities) {
51+
52+
this(null, attributes, authorities);
53+
}
54+
55+
/**
56+
* Constructs an {@code OAuth2IntrospectionAuthenticatedPrincipal} using the provided parameters.
57+
*
58+
* @param name the name attached to the OAuth 2.0 Token Introspection
59+
* @param attributes the attributes of the OAuth 2.0 Token Introspection
60+
* @param authorities the authorities of the OAuth 2.0 Token Introspection
61+
*/
62+
public OAuth2IntrospectionAuthenticatedPrincipal(String name, Map<String, Object> attributes,
63+
Collection<GrantedAuthority> authorities) {
64+
65+
Assert.notEmpty(attributes, "attributes cannot be empty");
66+
this.attributes = Collections.unmodifiableMap(attributes);
67+
this.authorities = authorities == null ?
68+
NO_AUTHORITIES : Collections.unmodifiableCollection(authorities);
69+
this.name = name == null ? getSubject() : name;
70+
}
71+
72+
/**
73+
* Gets the attributes of the OAuth 2.0 Token Introspection in map form.
74+
*
75+
* @return a {@link Map} of the attribute's objects keyed by the attribute's names
76+
*/
77+
@Override
78+
public Map<String, Object> getAttributes() {
79+
return this.attributes;
80+
}
81+
82+
/**
83+
* Get the {@link Collection} of {@link GrantedAuthority}s associated
84+
* with this OAuth 2.0 Token Introspection
85+
*
86+
* @return the OAuth 2.0 Token Introspection authorities
87+
*/
88+
@Override
89+
public Collection<? extends GrantedAuthority> getAuthorities() {
90+
return this.authorities;
91+
}
92+
93+
/**
94+
* {@inheritDoc}
95+
*/
96+
@Override
97+
public String getName() {
98+
return this.name;
99+
}
100+
101+
/**
102+
* {@inheritDoc}
103+
*/
104+
@Override
105+
public Map<String, Object> getClaims() {
106+
return getAttributes();
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2002-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+
17+
package org.springframework.security.oauth2.server.resource.introspection;
18+
19+
import java.net.URL;
20+
import java.time.Instant;
21+
import java.util.List;
22+
23+
import org.springframework.security.oauth2.core.ClaimAccessor;
24+
25+
/**
26+
* A {@link ClaimAccessor} for the &quot;claims&quot; that may be contained
27+
* in the Introspection Response.
28+
*
29+
* @author David Kovac
30+
* @since 5.4
31+
* @see ClaimAccessor
32+
* @see OAuth2IntrospectionClaimNames
33+
* @see OAuth2IntrospectionAuthenticatedPrincipal
34+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
35+
*/
36+
public interface OAuth2IntrospectionClaimAccessor extends ClaimAccessor {
37+
/**
38+
* Returns the indicator {@code (active)} whether or not the token is currently active
39+
*
40+
* @return the indicator whether or not the token is currently active
41+
*/
42+
default boolean isActive() {
43+
return Boolean.TRUE.equals(this.getClaimAsBoolean(OAuth2IntrospectionClaimNames.ACTIVE));
44+
}
45+
46+
/**
47+
* Returns the scopes {@code (scope)} associated with the token
48+
*
49+
* @return the scopes associated with the token
50+
*/
51+
default String getScope() {
52+
return this.getClaimAsString(OAuth2IntrospectionClaimNames.SCOPE);
53+
}
54+
55+
/**
56+
* Returns the client identifier {@code (client_id)} for the token
57+
*
58+
* @return the client identifier for the token
59+
*/
60+
default String getClientId() {
61+
return this.getClaimAsString(OAuth2IntrospectionClaimNames.CLIENT_ID);
62+
}
63+
64+
/**
65+
* Returns a human-readable identifier {@code (username)} for the resource owner that authorized the token
66+
*
67+
* @return a human-readable identifier for the resource owner that authorized the token
68+
*/
69+
default String getUsername() {
70+
return this.getClaimAsString(OAuth2IntrospectionClaimNames.USERNAME);
71+
}
72+
73+
/**
74+
* Returns the type of the token {@code (token_type)}, for example {@code bearer}.
75+
*
76+
* @return the type of the token, for example {@code bearer}.
77+
*/
78+
default String getTokenType() {
79+
return this.getClaimAsString(OAuth2IntrospectionClaimNames.TOKEN_TYPE);
80+
}
81+
82+
/**
83+
* Returns a timestamp {@code (exp)} indicating when the token expires
84+
*
85+
* @return a timestamp indicating when the token expires
86+
*/
87+
default Instant getExpiresAt() {
88+
return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.EXPIRES_AT);
89+
}
90+
91+
/**
92+
* Returns a timestamp {@code (iat)} indicating when the token was issued
93+
*
94+
* @return a timestamp indicating when the token was issued
95+
*/
96+
default Instant getIssuedAt() {
97+
return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.ISSUED_AT);
98+
}
99+
100+
/**
101+
* Returns a timestamp {@code (nbf)} indicating when the token is not to be used before
102+
*
103+
* @return a timestamp indicating when the token is not to be used before
104+
*/
105+
default Instant getNotBefore() {
106+
return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.NOT_BEFORE);
107+
}
108+
109+
/**
110+
* Returns usually a machine-readable identifier {@code (sub)} of the resource owner who authorized the token
111+
*
112+
* @return usually a machine-readable identifier of the resource owner who authorized the token
113+
*/
114+
default String getSubject() {
115+
return this.getClaimAsString(OAuth2IntrospectionClaimNames.SUBJECT);
116+
}
117+
118+
/**
119+
* Returns the intended audience {@code (aud)} for the token
120+
*
121+
* @return the intended audience for the token
122+
*/
123+
default List<String> getAudience() {
124+
return this.getClaimAsStringList(OAuth2IntrospectionClaimNames.AUDIENCE);
125+
}
126+
127+
/**
128+
* Returns the issuer {@code (iss)} of the token
129+
*
130+
* @return the issuer of the token
131+
*/
132+
default URL getIssuer() {
133+
return this.getClaimAsURL(OAuth2IntrospectionClaimNames.ISSUER);
134+
}
135+
136+
/**
137+
* Returns the identifier {@code (jti)} for the token
138+
*
139+
* @return the identifier for the token
140+
*/
141+
default String getId() {
142+
return this.getClaimAsString(OAuth2IntrospectionClaimNames.JTI);
143+
}
144+
}

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AuthenticatedPrincipals.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.security.core.GrantedAuthority;
3030
import org.springframework.security.core.authority.SimpleGrantedAuthority;
31+
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
3132
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
3233

3334
/**
@@ -56,7 +57,7 @@ public static OAuth2AuthenticatedPrincipal active(Consumer<Map<String, Object>>
5657
Collection<GrantedAuthority> authorities =
5758
Arrays.asList(new SimpleGrantedAuthority("SCOPE_read"),
5859
new SimpleGrantedAuthority("SCOPE_write"), new SimpleGrantedAuthority("SCOPE_dolphin"));
59-
return new DefaultOAuth2AuthenticatedPrincipal(attributes, authorities);
60+
return new OAuth2IntrospectionAuthenticatedPrincipal(attributes, authorities);
6061
}
6162

6263
private static URL url(String url) {

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525

2626
import org.springframework.security.authentication.AuthenticationServiceException;
2727
import org.springframework.security.core.Authentication;
28-
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
2928
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
3029
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
30+
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
3131
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
3232
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
3333
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
@@ -63,9 +63,9 @@ public void authenticateWhenActiveTokenThenOk() throws Exception {
6363
Authentication result =
6464
provider.authenticate(new BearerTokenAuthenticationToken("token"));
6565

66-
assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class);
66+
assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class);
6767

68-
Map<String, Object> attributes = ((DefaultOAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
68+
Map<String, Object> attributes = ((OAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
6969
assertThat(attributes)
7070
.isNotNull()
7171
.containsEntry(ACTIVE, true)
@@ -85,7 +85,7 @@ public void authenticateWhenActiveTokenThenOk() throws Exception {
8585

8686
@Test
8787
public void authenticateWhenMissingScopeAttributeThenNoAuthorities() {
88-
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
88+
OAuth2AuthenticatedPrincipal principal = new OAuth2IntrospectionAuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
8989
OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class);
9090
when(introspector.introspect(any())).thenReturn(principal);
9191
OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector);

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727

2828
import org.springframework.security.authentication.AuthenticationServiceException;
2929
import org.springframework.security.core.Authentication;
30-
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
3130
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
3231
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
32+
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
3333
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
3434
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
3535
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
@@ -66,9 +66,9 @@ public void authenticateWhenActiveTokenThenOk() throws Exception {
6666
Authentication result =
6767
provider.authenticate(new BearerTokenAuthenticationToken("token")).block();
6868

69-
assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class);
69+
assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class);
7070

71-
Map<String, Object> attributes = ((DefaultOAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
71+
Map<String, Object> attributes = ((OAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
7272
assertThat(attributes)
7373
.isNotNull()
7474
.containsEntry(ACTIVE, true)
@@ -88,17 +88,17 @@ public void authenticateWhenActiveTokenThenOk() throws Exception {
8888

8989
@Test
9090
public void authenticateWhenMissingScopeAttributeThenNoAuthorities() {
91-
OAuth2AuthenticatedPrincipal authority = new DefaultOAuth2AuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
91+
OAuth2AuthenticatedPrincipal authority = new OAuth2IntrospectionAuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
9292
ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class);
9393
when(introspector.introspect(any())).thenReturn(Mono.just(authority));
9494
OpaqueTokenReactiveAuthenticationManager provider =
9595
new OpaqueTokenReactiveAuthenticationManager(introspector);
9696

9797
Authentication result =
9898
provider.authenticate(new BearerTokenAuthenticationToken("token")).block();
99-
assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class);
99+
assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class);
100100

101-
Map<String, Object> attributes = ((DefaultOAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
101+
Map<String, Object> attributes = ((OAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
102102
assertThat(attributes)
103103
.isNotNull()
104104
.doesNotContainKey(SCOPE);

0 commit comments

Comments
 (0)