Skip to content

Commit

Permalink
Simplify OAuth 2.0 Introspection Attribute Retrieval
Browse files Browse the repository at this point in the history
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
  • Loading branch information
qavid authored and jzheaux committed Jul 9, 2020
1 parent b69bcf8 commit a58278c
Show file tree
Hide file tree
Showing 10 changed files with 454 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
Expand Down Expand Up @@ -232,7 +231,7 @@ private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessR
}
}

return new DefaultOAuth2AuthenticatedPrincipal(claims, authorities);
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
}

private URL issuer(String uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import org.springframework.http.MediaType;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserters;
Expand Down Expand Up @@ -193,7 +192,7 @@ private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessR
}
}

return new DefaultOAuth2AuthenticatedPrincipal(claims, authorities);
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
}

private URL issuer(String uri) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.oauth2.server.resource.introspection;

import static org.springframework.security.core.authority.AuthorityUtils.NO_AUTHORITIES;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.util.Assert;

/**
* A domain object that wraps the attributes of OAuth 2.0 Token Introspection.
*
* @author David Kovac
* @since 5.4
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
*/
public final class OAuth2IntrospectionAuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal,
OAuth2IntrospectionClaimAccessor, Serializable {
private final Map<String, Object> attributes;
private final Collection<GrantedAuthority> authorities;
private final String name;

/**
* Constructs an {@code OAuth2IntrospectionAuthenticatedPrincipal} using the provided parameters.
*
* @param attributes the attributes of the OAuth 2.0 Token Introspection
* @param authorities the authorities of the OAuth 2.0 Token Introspection
*/
public OAuth2IntrospectionAuthenticatedPrincipal(Map<String, Object> attributes,
Collection<GrantedAuthority> authorities) {

this(null, attributes, authorities);
}

/**
* Constructs an {@code OAuth2IntrospectionAuthenticatedPrincipal} using the provided parameters.
*
* @param name the name attached to the OAuth 2.0 Token Introspection
* @param attributes the attributes of the OAuth 2.0 Token Introspection
* @param authorities the authorities of the OAuth 2.0 Token Introspection
*/
public OAuth2IntrospectionAuthenticatedPrincipal(String name, Map<String, Object> attributes,
Collection<GrantedAuthority> authorities) {

Assert.notEmpty(attributes, "attributes cannot be empty");
this.attributes = Collections.unmodifiableMap(attributes);
this.authorities = authorities == null ?
NO_AUTHORITIES : Collections.unmodifiableCollection(authorities);
this.name = name == null ? getSubject() : name;
}

/**
* Gets the attributes of the OAuth 2.0 Token Introspection in map form.
*
* @return a {@link Map} of the attribute's objects keyed by the attribute's names
*/
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}

/**
* Get the {@link Collection} of {@link GrantedAuthority}s associated
* with this OAuth 2.0 Token Introspection
*
* @return the OAuth 2.0 Token Introspection authorities
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

/**
* {@inheritDoc}
*/
@Override
public String getName() {
return this.name;
}

/**
* {@inheritDoc}
*/
@Override
public Map<String, Object> getClaims() {
return getAttributes();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.oauth2.server.resource.introspection;

import java.net.URL;
import java.time.Instant;
import java.util.List;

import org.springframework.security.oauth2.core.ClaimAccessor;

/**
* A {@link ClaimAccessor} for the &quot;claims&quot; that may be contained
* in the Introspection Response.
*
* @author David Kovac
* @since 5.4
* @see ClaimAccessor
* @see OAuth2IntrospectionClaimNames
* @see OAuth2IntrospectionAuthenticatedPrincipal
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
*/
public interface OAuth2IntrospectionClaimAccessor extends ClaimAccessor {
/**
* Returns the indicator {@code (active)} whether or not the token is currently active
*
* @return the indicator whether or not the token is currently active
*/
default boolean isActive() {
return Boolean.TRUE.equals(this.getClaimAsBoolean(OAuth2IntrospectionClaimNames.ACTIVE));
}

/**
* Returns the scopes {@code (scope)} associated with the token
*
* @return the scopes associated with the token
*/
default String getScope() {
return this.getClaimAsString(OAuth2IntrospectionClaimNames.SCOPE);
}

/**
* Returns the client identifier {@code (client_id)} for the token
*
* @return the client identifier for the token
*/
default String getClientId() {
return this.getClaimAsString(OAuth2IntrospectionClaimNames.CLIENT_ID);
}

/**
* Returns a human-readable identifier {@code (username)} for the resource owner that authorized the token
*
* @return a human-readable identifier for the resource owner that authorized the token
*/
default String getUsername() {
return this.getClaimAsString(OAuth2IntrospectionClaimNames.USERNAME);
}

/**
* Returns the type of the token {@code (token_type)}, for example {@code bearer}.
*
* @return the type of the token, for example {@code bearer}.
*/
default String getTokenType() {
return this.getClaimAsString(OAuth2IntrospectionClaimNames.TOKEN_TYPE);
}

/**
* Returns a timestamp {@code (exp)} indicating when the token expires
*
* @return a timestamp indicating when the token expires
*/
default Instant getExpiresAt() {
return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.EXPIRES_AT);
}

/**
* Returns a timestamp {@code (iat)} indicating when the token was issued
*
* @return a timestamp indicating when the token was issued
*/
default Instant getIssuedAt() {
return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.ISSUED_AT);
}

/**
* Returns a timestamp {@code (nbf)} indicating when the token is not to be used before
*
* @return a timestamp indicating when the token is not to be used before
*/
default Instant getNotBefore() {
return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.NOT_BEFORE);
}

/**
* Returns usually a machine-readable identifier {@code (sub)} of the resource owner who authorized the token
*
* @return usually a machine-readable identifier of the resource owner who authorized the token
*/
default String getSubject() {
return this.getClaimAsString(OAuth2IntrospectionClaimNames.SUBJECT);
}

/**
* Returns the intended audience {@code (aud)} for the token
*
* @return the intended audience for the token
*/
default List<String> getAudience() {
return this.getClaimAsStringList(OAuth2IntrospectionClaimNames.AUDIENCE);
}

/**
* Returns the issuer {@code (iss)} of the token
*
* @return the issuer of the token
*/
default URL getIssuer() {
return this.getClaimAsURL(OAuth2IntrospectionClaimNames.ISSUER);
}

/**
* Returns the identifier {@code (jti)} for the token
*
* @return the identifier for the token
*/
default String getId() {
return this.getClaimAsString(OAuth2IntrospectionClaimNames.JTI);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;

/**
Expand Down Expand Up @@ -56,7 +57,7 @@ public static OAuth2AuthenticatedPrincipal active(Consumer<Map<String, Object>>
Collection<GrantedAuthority> authorities =
Arrays.asList(new SimpleGrantedAuthority("SCOPE_read"),
new SimpleGrantedAuthority("SCOPE_write"), new SimpleGrantedAuthority("SCOPE_dolphin"));
return new DefaultOAuth2AuthenticatedPrincipal(attributes, authorities);
return new OAuth2IntrospectionAuthenticatedPrincipal(attributes, authorities);
}

private static URL url(String url) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
Expand Down Expand Up @@ -63,9 +63,9 @@ public void authenticateWhenActiveTokenThenOk() throws Exception {
Authentication result =
provider.authenticate(new BearerTokenAuthenticationToken("token"));

assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class);
assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class);

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

@Test
public void authenticateWhenMissingScopeAttributeThenNoAuthorities() {
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
OAuth2AuthenticatedPrincipal principal = new OAuth2IntrospectionAuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class);
when(introspector.introspect(any())).thenReturn(principal);
OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
Expand Down Expand Up @@ -66,9 +66,9 @@ public void authenticateWhenActiveTokenThenOk() throws Exception {
Authentication result =
provider.authenticate(new BearerTokenAuthenticationToken("token")).block();

assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class);
assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class);

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

@Test
public void authenticateWhenMissingScopeAttributeThenNoAuthorities() {
OAuth2AuthenticatedPrincipal authority = new DefaultOAuth2AuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
OAuth2AuthenticatedPrincipal authority = new OAuth2IntrospectionAuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null);
ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class);
when(introspector.introspect(any())).thenReturn(Mono.just(authority));
OpaqueTokenReactiveAuthenticationManager provider =
new OpaqueTokenReactiveAuthenticationManager(introspector);

Authentication result =
provider.authenticate(new BearerTokenAuthenticationToken("token")).block();
assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class);
assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class);

Map<String, Object> attributes = ((DefaultOAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
Map<String, Object> attributes = ((OAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes();
assertThat(attributes)
.isNotNull()
.doesNotContainKey(SCOPE);
Expand Down
Loading

0 comments on commit a58278c

Please sign in to comment.