diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index 28028414..baeb6849 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -184,16 +184,17 @@ public final class OAuth2AuthorizationServerConfigurer exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling != null) { exceptionHandling.defaultAuthenticationEntryPointFor( @@ -245,17 +246,17 @@ public final class OAuth2AuthorizationServerConfigurerSection 2.2 Introspection Response + */ +public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionClaimAccessor, Serializable { + private static final long serialVersionUID = Version.SERIAL_VERSION_UID; + private final Map claims; + + private OAuth2TokenIntrospection(Map claims) { + this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + } + + /** + * Returns the claims in the Token Introspection Response. + * + * @return a {@code Map} of the claims + */ + @Override + public Map getClaims() { + return this.claims; + } + + /** + * Constructs a new {@link Builder} initialized with the {@link #isActive() active} claim to {@code false}. + * + * @return the {@link Builder} + */ + public static Builder builder() { + return builder(false); + } + + /** + * Constructs a new {@link Builder} initialized with the provided {@link #isActive() active} claim. + * + * @param active {@code true} if the token is currently active, {@code false} otherwise + * @return the {@link Builder} + */ + public static Builder builder(boolean active) { + return new Builder(active); + } + + /** + * Constructs a new {@link Builder} initialized with the provided claims. + * + * @param claims the claims to initialize the builder + * @return the {@link Builder} + */ + public static Builder withClaims(Map claims) { + Assert.notEmpty(claims, "claims cannot be empty"); + return builder().claims(c -> c.putAll(claims)); + } + + /** + * A builder for {@link OAuth2TokenIntrospection}. + */ + public static class Builder { + private final Map claims = new LinkedHashMap<>(); + + private Builder(boolean active) { + active(active); + } + + /** + * Sets the indicator of whether or not the presented token is currently active, REQUIRED. + * + * @param active {@code true} if the token is currently active, {@code false} otherwise + * @return the {@link Builder} for further configuration + */ + public Builder active(boolean active) { + return claim(OAuth2TokenIntrospectionClaimNames.ACTIVE, active); + } + + /** + * Add the scope associated with this token, OPTIONAL. + * + * @param scope the scope associated with this token + * @return the {@link Builder} for further configuration + */ + public Builder scope(String scope) { + addClaimToClaimList(OAuth2TokenIntrospectionClaimNames.SCOPE, scope); + return this; + } + + /** + * A {@code Consumer} of the scope(s) associated with this token, + * allowing the ability to add, replace, or remove, OPTIONAL. + * + * @param scopesConsumer a {@code Consumer} of the scope(s) associated with this token + * @return the {@link Builder} for further configuration + */ + public Builder scopes(Consumer> scopesConsumer) { + acceptClaimValues(OAuth2TokenIntrospectionClaimNames.SCOPE, scopesConsumer); + return this; + } + + /** + * Sets the client identifier for the OAuth 2.0 client that requested this token, OPTIONAL. + * + * @param clientId the client identifier for the OAuth 2.0 client that requested this token + * @return the {@link Builder} for further configuration + */ + public Builder clientId(String clientId) { + return claim(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, clientId); + } + + /** + * Sets the human-readable identifier for the resource owner who authorized this token, OPTIONAL. + * + * @param username the human-readable identifier for the resource owner who authorized this token + * @return the {@link Builder} for further configuration + */ + public Builder username(String username) { + return claim(OAuth2TokenIntrospectionClaimNames.USERNAME, username); + } + + /** + * Sets the token type (e.g. bearer), OPTIONAL. + * + * @param tokenType the token type + * @return the {@link Builder} for further configuration + */ + public Builder tokenType(String tokenType) { + return claim(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, tokenType); + } + + /** + * Sets the time indicating when this token will expire, OPTIONAL. + * + * @param expiresAt the time indicating when this token will expire + * @return the {@link Builder} for further configuration + */ + public Builder expiresAt(Instant expiresAt) { + return claim(OAuth2TokenIntrospectionClaimNames.EXP, expiresAt); + } + + /** + * Sets the time indicating when this token was originally issued, OPTIONAL. + * + * @param issuedAt the time indicating when this token was originally issued + * @return the {@link Builder} for further configuration + */ + public Builder issuedAt(Instant issuedAt) { + return claim(OAuth2TokenIntrospectionClaimNames.IAT, issuedAt); + } + + /** + * Sets the time indicating when this token is not to be used before, OPTIONAL. + * + * @param notBefore the time indicating when this token is not to be used before + * @return the {@link Builder} for further configuration + */ + public Builder notBefore(Instant notBefore) { + return claim(OAuth2TokenIntrospectionClaimNames.NBF, notBefore); + } + + /** + * Sets the subject of the token, usually a machine-readable identifier + * of the resource owner who authorized this token, OPTIONAL. + * + * @param subject the subject of the token + * @return the {@link Builder} for further configuration + */ + public Builder subject(String subject) { + return claim(OAuth2TokenIntrospectionClaimNames.SUB, subject); + } + + /** + * Add the identifier representing the intended audience for this token, OPTIONAL. + * + * @param audience the identifier representing the intended audience for this token + * @return the {@link Builder} for further configuration + */ + public Builder audience(String audience) { + addClaimToClaimList(OAuth2TokenIntrospectionClaimNames.AUD, audience); + return this; + } + + /** + * A {@code Consumer} of the intended audience(s) for this token, + * allowing the ability to add, replace, or remove, OPTIONAL. + * + * @param audiencesConsumer a {@code Consumer} of the intended audience(s) for this token + * @return the {@link Builder} for further configuration + */ + public Builder audiences(Consumer> audiencesConsumer) { + acceptClaimValues(OAuth2TokenIntrospectionClaimNames.AUD, audiencesConsumer); + return this; + } + + /** + * Sets the issuer of this token, OPTIONAL. + * + * @param issuer the issuer of this token + * @return the {@link Builder} for further configuration + */ + public Builder issuer(String issuer) { + return claim(OAuth2TokenIntrospectionClaimNames.ISS, issuer); + } + + /** + * Sets the identifier for the token, OPTIONAL. + * + * @param jti the identifier for the token + * @return the {@link Builder} for further configuration + */ + public Builder id(String jti) { + return claim(OAuth2TokenIntrospectionClaimNames.JTI, jti); + } + + /** + * Sets the claim. + * + * @param name the claim name + * @param value the claim value + * @return the {@link Builder} for further configuration + */ + public Builder claim(String name, Object value) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(value, "value cannot be null"); + this.claims.put(name, value); + return this; + } + + /** + * Provides access to every {@link #claim(String, Object)} declared so far with + * the possibility to add, replace, or remove. + * + * @param claimsConsumer a {@code Consumer} of the claims + * @return the {@link Builder} for further configurations + */ + public Builder claims(Consumer> claimsConsumer) { + claimsConsumer.accept(this.claims); + return this; + } + + /** + * Validate the claims and build the {@link OAuth2TokenIntrospection}. + *

+ * The following claims are REQUIRED: {@code active} + * + * @return the {@link OAuth2TokenIntrospection} + */ + public OAuth2TokenIntrospection build() { + validateClaims(); + return new OAuth2TokenIntrospection(this.claims); + } + + private void validateClaims() { + Assert.notNull(this.claims.get(OAuth2TokenIntrospectionClaimNames.ACTIVE), "active cannot be null"); + Assert.isInstanceOf(Boolean.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.ACTIVE), "active must be of type boolean"); + if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.SCOPE)) { + Assert.isInstanceOf(List.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.SCOPE), "scope must be of type List"); + } + if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.EXP)) { + Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.EXP), "exp must be of type Instant"); + } + if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.IAT)) { + Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.IAT), "iat must be of type Instant"); + } + if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.NBF)) { + Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.NBF), "nbf must be of type Instant"); + } + if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.AUD)) { + Assert.isInstanceOf(List.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.AUD), "aud must be of type List"); + } + if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.ISS)) { + validateURL(this.claims.get(OAuth2TokenIntrospectionClaimNames.ISS), "iss must be a valid URL"); + } + } + + @SuppressWarnings("unchecked") + private void addClaimToClaimList(String name, String value) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(value, "value cannot be null"); + this.claims.computeIfAbsent(name, k -> new LinkedList()); + ((List) this.claims.get(name)).add(value); + } + + @SuppressWarnings("unchecked") + private void acceptClaimValues(String name, Consumer> valuesConsumer) { + Assert.hasText(name, "name cannot be empty"); + Assert.notNull(valuesConsumer, "valuesConsumer cannot be null"); + this.claims.computeIfAbsent(name, k -> new LinkedList()); + List values = (List) this.claims.get(name); + valuesConsumer.accept(values); + } + + private static void validateURL(Object url, String errorMessage) { + if (URL.class.isAssignableFrom(url.getClass())) { + return; + } + + try { + new URI(url.toString()).toURL(); + } catch (Exception ex) { + throw new IllegalArgumentException(errorMessage, ex); + } + } + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java index a99f54bf..e26931f3 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * 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. @@ -16,147 +16,133 @@ package org.springframework.security.oauth2.core; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; - import java.net.URL; import java.time.Instant; import java.util.List; -/** - * TODO This class is a copy from Spring Security (Resource Server) with the difference that we rely on the existing - * {@code OAuth2ParameterNames} and {@code JwtClaimNames} claims. It should be consolidated when merging this codebase into Spring - * Security. +/* + * TODO + * This class is "mostly" a copy from Spring Security and should be removed after upgrading to Spring Security 5.6.0 GA. + * The major changes made between the Spring Security class and this one are: + * 1) Class renamed from `OAuth2IntrospectionClaimAccessor` to `OAuth2TokenIntrospectionClaimAccessor` + * 2) Moved from package `org.springframework.security.oauth2.server.resource.introspection` to `org.springframework.security.oauth2.core` * - * A {@link ClaimAccessor} for the "claims" that may be contained in the Introspection Response. + * gh-9647 Move and rename OAuth2IntrospectionClaimAccessor/Names + * https://github.com/spring-projects/spring-security/issues/9647 + */ + +/** + * A {@link ClaimAccessor} for the "claims" that may be contained in the + * Introspection Response. * * @author David Kovac - * @author Gerardo Roza - * @since 0.1.1 + * @since 5.4 * @see ClaimAccessor - * @see OAuth2IntrospectionClaimNames - * @see OAuth2IntrospectionAuthenticatedPrincipal - * @see Introspection Response + * @see OAuth2TokenIntrospectionClaimNames + * @see Introspection Response */ public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor { - String ACTIVE = "active"; - /** * 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(ACTIVE)); + return Boolean.TRUE.equals(getClaimAsBoolean(OAuth2TokenIntrospectionClaimNames.ACTIVE)); } /** * Returns the scopes {@code (scope)} associated with the token - * * @return the scopes associated with the token */ - default String getScope() { - return this.getClaimAsString(SCOPE); + default List getScope() { + return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.SCOPE); } /** * Returns the client identifier {@code (client_id)} for the token - * * @return the client identifier for the token */ default String getClientId() { - return this.getClaimAsString(CLIENT_ID); + return getClaimAsString(OAuth2TokenIntrospectionClaimNames.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 + * 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(USERNAME); + return getClaimAsString(OAuth2TokenIntrospectionClaimNames.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(TOKEN_TYPE); + return getClaimAsString(OAuth2TokenIntrospectionClaimNames.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(EXP); + return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.EXP); } /** * 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(IAT); + return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.IAT); } /** - * Returns a timestamp {@code (nbf)} indicating when the token is not to be used before - * + * 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(NBF); + return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.NBF); } /** - * 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 + * 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(SUB); + return getClaimAsString(OAuth2TokenIntrospectionClaimNames.SUB); } /** * Returns the intended audience {@code (aud)} for the token - * * @return the intended audience for the token */ default List getAudience() { - return this.getClaimAsStringList(AUD); + return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.AUD); } /** * Returns the issuer {@code (iss)} of the token - * * @return the issuer of the token */ default URL getIssuer() { - return this.getClaimAsURL(ISS); + return getClaimAsURL(OAuth2TokenIntrospectionClaimNames.ISS); } /** * Returns the identifier {@code (jti)} for the token - * * @return the identifier for the token */ default String getId() { - return this.getClaimAsString(JTI); + return getClaimAsString(OAuth2TokenIntrospectionClaimNames.JTI); } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimNames.java new file mode 100644 index 00000000..af5715ca --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimNames.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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.core; + +/* + * TODO + * This class is "mostly" a copy from Spring Security and should be removed after upgrading to Spring Security 5.6.0 GA. + * The major changes made between the Spring Security class and this one are: + * 1) Class renamed from `OAuth2IntrospectionClaimNames` to `OAuth2TokenIntrospectionClaimNames` + * 2) Moved from package `org.springframework.security.oauth2.server.resource.introspection` to `org.springframework.security.oauth2.core` + * + * gh-9647 Move and rename OAuth2IntrospectionClaimAccessor/Names + * https://github.com/spring-projects/spring-security/issues/9647 + */ + +/** + * The names of the "Introspection Claims" defined by an + * Introspection + * Response. + * + * @author Josh Cummings + * @since 5.2 + */ +public interface OAuth2TokenIntrospectionClaimNames { + + /** + * {@code active} - Indicator whether or not the token is currently active + */ + String ACTIVE = "active"; + + /** + * {@code scope} - The scopes for the token + */ + String SCOPE = "scope"; + + /** + * {@code client_id} - The Client identifier for the token + */ + String CLIENT_ID = "client_id"; + + /** + * {@code username} - A human-readable identifier for the resource owner that + * authorized the token + */ + String USERNAME = "username"; + + /** + * {@code token_type} - The type of the token, for example {@code bearer}. + */ + String TOKEN_TYPE = "token_type"; + + /** + * {@code exp} - A timestamp indicating when the token expires + */ + String EXP = "exp"; + + /** + * {@code iat} - A timestamp indicating when the token was issued + */ + String IAT = "iat"; + + /** + * {@code nbf} - A timestamp indicating when the token is not to be used before + */ + String NBF = "nbf"; + + /** + * {@code sub} - Usually a machine-readable identifier of the resource owner who + * authorized the token + */ + String SUB = "sub"; + + /** + * {@code aud} - The intended audience for the token + */ + String AUD = "aud"; + + /** + * {@code iss} - The issuer of the token + */ + String ISS = "iss"; + + /** + * {@code jti} - The identifier for the token + */ + String JTI = "jti"; + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaims.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaims.java deleted file mode 100644 index 8fb29048..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaims.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright 2020-2021 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.core; - -import static java.util.stream.Collectors.toMap; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; - -import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; -import org.springframework.util.Assert; - -import java.io.Serializable; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; - -/** - * A representation of an OAuth 2.0 Introspection Token Response Claims. - * - * @author Gerardo Roza - * @since 0.1.1 - * @see Section 2.2 - Introspection Response - */ -public class OAuth2TokenIntrospectionClaims implements OAuth2TokenIntrospectionClaimAccessor, Serializable { - private static final long serialVersionUID = Version.SERIAL_VERSION_UID; - private static final Collection SUPPORTED_FIELDS = Arrays - .asList(ACTIVE, CLIENT_ID, SCOPE, TOKEN_TYPE, USERNAME, AUD, EXP, IAT, ISS, JTI, NBF, SUB); - - private final Map claims; - - private OAuth2TokenIntrospectionClaims(Map claims) { - this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); - } - - /** - * Returns the populated Token Introspection Response claims. - * - * @return the claims - */ - @Override - public Map getClaims() { - return this.claims; - } - - /** - * Constructs a new {@link Builder} with the provided parameters. - * - * @param claims the params to initialize the builder - */ - public static Builder withClaims(Map claims) { - Assert.notEmpty(claims, "claims cannot be empty"); - Builder builder = new Builder().claims(c -> c.putAll(claims)); - Optional.ofNullable(claims.get(IAT)).filter(Instant.class::isInstance).map(Instant.class::cast) - .ifPresent(builder::issuedAt); - Optional.ofNullable(claims.get(EXP)).filter(Instant.class::isInstance).map(Instant.class::cast) - .ifPresent(builder::expirationTime); - Optional.ofNullable(claims.get(NBF)).filter(Instant.class::isInstance).map(Instant.class::cast) - .ifPresent(builder::notBefore); - Optional.ofNullable(claims.get(TOKEN_TYPE)).filter(TokenType.class::isInstance).map(TokenType.class::cast) - .ifPresent(builder::tokenType); - return builder; - } - - /** - * Constructs a new {@link Builder} with the required active field. - * - * @param active boolean indicating whether the introspected token is active or not to initialize the builder - */ - public static Builder builder(boolean active) { - return new Builder(active); - } - - /** - * A builder for {@link OAuth2TokenIntrospectionClaims}. - */ - public static final class Builder { - - private final Map claims = new LinkedHashMap<>(); - - private Builder() { - } - - /** - * Helps configure a basic {@link OAuth2TokenIntrospectionClaims}. - * - * @param active boolean indicating whether the introspected token is active or not - */ - public Builder(boolean active) { - claim(ACTIVE, active); - } - - /** - * Populates the 'active' field. - * - * @param active boolean indicating whether the introspected token is active or not - * @return the {@link Builder} for further configurations - */ - public Builder active(boolean active) { - claim(ACTIVE, active); - return this; - } - - /** - * Populates the 'scope' field. - * - * @param scope string containing a space-separated list of scopes associated with this token - * @return the {@link Builder} for further configurations - */ - public Builder scope(String scope) { - claim(SCOPE, scope); - return this; - } - - /** - * Populates the 'client_id' field. - * - * @param clientId identifier for the OAuth 2.0 client that requested this token - * @return the {@link Builder} for further configurations - */ - public Builder clientId(String clientId) { - claim(CLIENT_ID, clientId); - return this; - } - - /** - * Populates the 'username' field. - * - * @param username Human-readable identifier for the resource owner who authorized this token - * @return the {@link Builder} for further configurations - */ - public Builder username(String username) { - claim(USERNAME, username); - return this; - } - - /** - * Populates the 'token_type' field. - * - * @param tokenType {@link TokenType} indicating the type of the token - * @return the {@link Builder} for further configurations - */ - public Builder tokenType(OAuth2AccessToken.TokenType tokenType) { - claim(TOKEN_TYPE, tokenType.getValue()); - return this; - } - - /** - * Populates the 'exp' (Expiration Time) field. - * - * @param expirationTime {@link Instant} indicating when this token will expire - * @return the {@link Builder} for further configurations - */ - public Builder expirationTime(Instant expirationTime) { - claim(EXP, expirationTime.getEpochSecond()); - return this; - } - - /** - * Populates the 'iat' (Issued At) field. - * - * @param issuedAt {@link Instant} indicating when this token was originally issued - * @return the {@link Builder} for further configurations - */ - public Builder issuedAt(Instant issuedAt) { - claim(IAT, issuedAt.getEpochSecond()); - return this; - } - - /** - * Populates the 'nbf' (Not Before) field. - * - * @param notBefore {@link Instant} indicating when this token is not to be used before - * @return the {@link Builder} for further configurations - */ - public Builder notBefore(Instant notBefore) { - claim(NBF, notBefore.getEpochSecond()); - return this; - } - - /** - * Populates the 'sub' (Subject) field. - * - * @param subject usually a machine-readable identifier of the resource owner who authorized this token - * @return the {@link Builder} for further configurations - */ - public Builder subject(String subject) { - claim(SUB, subject); - return this; - } - - /** - * Populates the 'aud' (Audience) field. - * - * @param audience service-specific string identifier or list of string identifiers representing the intended audience for this - * token - * @return the {@link Builder} for further configurations - */ - public Builder audience(List audience) { - claim(AUD, audience); - return this; - } - - /** - * Populates the 'iss' (Issuer) field. - * - * @param issuer of this token - * @return the {@link Builder} for further configurations - */ - public Builder issuer(String issuer) { - claim(ISS, issuer); - return this; - } - - /** - * Populates the 'jti' (JWT ID) field. - * - * @param jwtId identifier for the token - * @return the {@link Builder} for further configurations - */ - public Builder jwtId(String jwtId) { - claim(JTI, jwtId); - return this; - } - - /** - * Use this claim in the resulting {@link OAuth2TokenIntrospectionClaims}. - * - * @param name the claim name - * @param value the claim value - * @return the {@link Builder} for further configuration - */ - public Builder claim(String name, Object value) { - Assert.hasText(name, "name cannot be empty"); - Assert.notNull(value, "value cannot be null"); - this.claims.put(name, value); - return this; - } - - /** - * Provides access to every {@link #claim(String, Object)} declared so far with the possibility to add, replace, or remove. - * - * @param claimsConsumer a {@code Consumer} of the claims - * @return the {@link Builder} for further configurations - */ - public Builder claims(Consumer> claimsConsumer) { - claimsConsumer.accept(this.claims); - return this; - } - - private void validateClaims() { - Assert.notNull(this.claims.get(ACTIVE), "active cannot be null"); - } - - /** - * Build the {@link OAuth2TokenIntrospectionClaims} - * - * @return The constructed {@link OAuth2TokenIntrospectionClaims} - */ - public OAuth2TokenIntrospectionClaims build() { - Map responseClaims = this.claims.entrySet().stream() - .filter(entry -> SUPPORTED_FIELDS.contains(entry.getKey())) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); - validateClaims(); - return new OAuth2TokenIntrospectionClaims(responseClaims); - } - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverter.java deleted file mode 100644 index 451d7b5f..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverter.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2020-2021 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.core.http.converter; - -import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims; -import org.springframework.security.oauth2.core.converter.ClaimConversionService; -import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; -import org.springframework.util.Assert; - -import java.time.Instant; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -/** - * A {@link HttpMessageConverter} for an {@link OAuth2TokenIntrospectionClaims} Token Introspection Response. - * - * @author Gerardo Roza - * @since 0.1.1 - * @see AbstractHttpMessageConverter - * @see OAuth2TokenIntrospectionClaims - */ -public class OAuth2TokenIntrospectionClaimsHttpMessageConverter - extends AbstractHttpMessageConverter { - - private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { - }; - - private GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); - - private Converter, OAuth2TokenIntrospectionClaims> tokenIntrospectionResponseConverter = new OAuth2TokenIntrospectionResponseConverter(); - private Converter> tokenIntrospectionResponseParametersConverter = OAuth2TokenIntrospectionClaims::getClaims; - - public OAuth2TokenIntrospectionClaimsHttpMessageConverter() { - super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); - } - - @Override - protected boolean supports(Class clazz) { - return OAuth2TokenIntrospectionClaims.class.isAssignableFrom(clazz); - } - - @Override - @SuppressWarnings("unchecked") - protected OAuth2TokenIntrospectionClaims readInternal(Class clazz, - HttpInputMessage inputMessage) throws HttpMessageNotReadableException { - try { - Map tokenIntrospectionResponseParameters = (Map) this.jsonMessageConverter - .read(STRING_OBJECT_MAP.getType(), null, inputMessage); - return this.tokenIntrospectionResponseConverter.convert(tokenIntrospectionResponseParameters); - } catch (Exception ex) { - throw new HttpMessageNotReadableException( - "An error occurred reading the Token Introspection Response: " + ex.getMessage(), ex, inputMessage); - } - } - - @Override - protected void writeInternal(OAuth2TokenIntrospectionClaims tokenIntrospectionResponse, - HttpOutputMessage outputMessage) { - try { - - Map tokenIntrospectionResponseParameters = this.tokenIntrospectionResponseParametersConverter - .convert(tokenIntrospectionResponse); - this.jsonMessageConverter.write( - tokenIntrospectionResponseParameters, STRING_OBJECT_MAP.getType(), MediaType.APPLICATION_JSON, - outputMessage); - } catch (Exception ex) { - throw new HttpMessageNotWritableException( - "An error occurred writing the Token Introspection Response: " + ex.getMessage(), ex); - } - } - - /** - * Sets the {@link Converter} used for converting the Token Introspection parameters to an {@link OAuth2TokenIntrospectionClaims}. - * - * @param tokenIntrospectionResponseConverter the {@link Converter} used for converting to an - * {@link OAuth2TokenIntrospectionClaims} - */ - public void setTokenIntrospectionResponseConverter( - Converter, OAuth2TokenIntrospectionClaims> tokenIntrospectionResponseConverter) { - Assert.notNull(tokenIntrospectionResponseConverter, "tokenIntrospectionResponseConverter cannot be null"); - this.tokenIntrospectionResponseConverter = tokenIntrospectionResponseConverter; - } - - /** - * Sets the {@link Converter} used for converting the {@link OAuth2TokenIntrospectionClaims} to a {@code Map} representation of - * the Token Introspection Response. - * - * @param tokenIntrospectionResponseParametersConverter the {@link Converter} used for converting to a {@code Map} representation - * of the Token Introspection Response - */ - public void setTokenIntrospectionResponseParametersConverter( - Converter> tokenIntrospectionResponseParametersConverter) { - Assert.notNull( - tokenIntrospectionResponseParametersConverter, - "tokenIntrospectionResponseParametersConverter cannot be null"); - this.tokenIntrospectionResponseParametersConverter = tokenIntrospectionResponseParametersConverter; - } - - private static final class OAuth2TokenIntrospectionResponseConverter - implements Converter, OAuth2TokenIntrospectionClaims> { - private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService - .getSharedInstance(); - private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); - private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class); - private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class); - private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); - private final ClaimTypeConverter parameterTypeConverter; - - private OAuth2TokenIntrospectionResponseConverter() { - Converter collectionStringConverter = getConverter( - TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR)); - Converter stringConverter = getConverter(STRING_TYPE_DESCRIPTOR); - Converter booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR); - Converter instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR); - - Map> claimConverters = new HashMap<>(); - claimConverters.put(ACTIVE, booleanConverter); - claimConverters.put(SCOPE, stringConverter); - claimConverters.put(CLIENT_ID, stringConverter); - claimConverters.put(TOKEN_TYPE, stringConverter); - claimConverters.put(USERNAME, stringConverter); - claimConverters.put(AUD, collectionStringConverter); - claimConverters.put(EXP, instantConverter); - claimConverters.put(IAT, instantConverter); - claimConverters.put(ISS, stringConverter); - claimConverters.put(JTI, stringConverter); - claimConverters.put(NBF, instantConverter); - claimConverters.put(SUB, stringConverter); - this.parameterTypeConverter = new ClaimTypeConverter(claimConverters); - } - - @Override - public OAuth2TokenIntrospectionClaims convert(Map source) { - Map parsedClaims = this.parameterTypeConverter.convert(source); - return OAuth2TokenIntrospectionClaims.withClaims(parsedClaims).build(); - } - - private static Converter getConverter(TypeDescriptor targetDescriptor) { - return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor); - } - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverter.java new file mode 100644 index 00000000..a27ed7e0 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverter.java @@ -0,0 +1,203 @@ +/* + * Copyright 2020-2021 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.core.http.converter; + +import java.net.URL; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.core.converter.ClaimConversionService; +import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * A {@link HttpMessageConverter} for an {@link OAuth2TokenIntrospection OAuth 2.0 Token Introspection Response}. + * + * @author Gerardo Roza + * @author Joe Grandja + * @since 0.1.1 + * @see AbstractHttpMessageConverter + * @see OAuth2TokenIntrospection + */ +public class OAuth2TokenIntrospectionHttpMessageConverter extends AbstractHttpMessageConverter { + + private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { + }; + + private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); + + private Converter, OAuth2TokenIntrospection> tokenIntrospectionConverter = new MapOAuth2TokenIntrospectionConverter(); + private Converter> tokenIntrospectionParametersConverter = new OAuth2TokenIntrospectionMapConverter(); + + public OAuth2TokenIntrospectionHttpMessageConverter() { + super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + } + + @Override + protected boolean supports(Class clazz) { + return OAuth2TokenIntrospection.class.isAssignableFrom(clazz); + } + + @Override + @SuppressWarnings("unchecked") + protected OAuth2TokenIntrospection readInternal(Class clazz, HttpInputMessage inputMessage) + throws HttpMessageNotReadableException { + try { + Map tokenIntrospectionParameters = (Map) this.jsonMessageConverter + .read(STRING_OBJECT_MAP.getType(), null, inputMessage); + return this.tokenIntrospectionConverter.convert(tokenIntrospectionParameters); + } catch (Exception ex) { + throw new HttpMessageNotReadableException( + "An error occurred reading the Token Introspection Response: " + ex.getMessage(), ex, inputMessage); + } + } + + @Override + protected void writeInternal(OAuth2TokenIntrospection tokenIntrospection, HttpOutputMessage outputMessage) + throws HttpMessageNotWritableException { + try { + Map tokenIntrospectionResponseParameters = this.tokenIntrospectionParametersConverter + .convert(tokenIntrospection); + this.jsonMessageConverter.write(tokenIntrospectionResponseParameters, STRING_OBJECT_MAP.getType(), + MediaType.APPLICATION_JSON, outputMessage); + } catch (Exception ex) { + throw new HttpMessageNotWritableException( + "An error occurred writing the Token Introspection Response: " + ex.getMessage(), ex); + } + } + + /** + * Sets the {@link Converter} used for converting the Token Introspection Response parameters to an {@link OAuth2TokenIntrospection}. + * + * @param tokenIntrospectionConverter the {@link Converter} used for converting to an {@link OAuth2TokenIntrospection} + */ + public final void setTokenIntrospectionConverter( + Converter, OAuth2TokenIntrospection> tokenIntrospectionConverter) { + Assert.notNull(tokenIntrospectionConverter, "tokenIntrospectionConverter cannot be null"); + this.tokenIntrospectionConverter = tokenIntrospectionConverter; + } + + /** + * Sets the {@link Converter} used for converting an {@link OAuth2TokenIntrospection} + * to a {@code Map} representation of the Token Introspection Response parameters. + * + * @param tokenIntrospectionParametersConverter the {@link Converter} used for converting to a + * {@code Map} representation of the Token Introspection Response parameters + */ + public final void setTokenIntrospectionParametersConverter( + Converter> tokenIntrospectionParametersConverter) { + Assert.notNull(tokenIntrospectionParametersConverter, "tokenIntrospectionParametersConverter cannot be null"); + this.tokenIntrospectionParametersConverter = tokenIntrospectionParametersConverter; + } + + private static final class MapOAuth2TokenIntrospectionConverter + implements Converter, OAuth2TokenIntrospection> { + + private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance(); + private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class); + private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class); + private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); + private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class); + private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class); + private final ClaimTypeConverter claimTypeConverter; + + private MapOAuth2TokenIntrospectionConverter() { + Converter booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR); + Converter stringConverter = getConverter(STRING_TYPE_DESCRIPTOR); + Converter instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR); + Converter collectionStringConverter = getConverter( + TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR)); + Converter urlConverter = getConverter(URL_TYPE_DESCRIPTOR); + + Map> claimConverters = new HashMap<>(); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.ACTIVE, booleanConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.SCOPE, MapOAuth2TokenIntrospectionConverter::convertScope); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, stringConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.USERNAME, stringConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, stringConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.EXP, instantConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.IAT, instantConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.NBF, instantConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.SUB, stringConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.AUD, collectionStringConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.ISS, urlConverter); + claimConverters.put(OAuth2TokenIntrospectionClaimNames.JTI, stringConverter); + this.claimTypeConverter = new ClaimTypeConverter(claimConverters); + } + + @Override + public OAuth2TokenIntrospection convert(Map source) { + Map parsedClaims = this.claimTypeConverter.convert(source); + return OAuth2TokenIntrospection.withClaims(parsedClaims).build(); + } + + private static Converter getConverter(TypeDescriptor targetDescriptor) { + return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor); + } + + private static List convertScope(Object scope) { + if (scope == null) { + return Collections.emptyList(); + } + return Arrays.asList(StringUtils.delimitedListToStringArray(scope.toString(), " ")); + } + } + + private static final class OAuth2TokenIntrospectionMapConverter + implements Converter> { + + @Override + public Map convert(OAuth2TokenIntrospection source) { + Map responseClaims = new LinkedHashMap<>(source.getClaims()); + if (!CollectionUtils.isEmpty(source.getScope())) { + responseClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, StringUtils.collectionToDelimitedString(source.getScope(), " ")); + } + if (source.getExpiresAt() != null) { + responseClaims.put(OAuth2TokenIntrospectionClaimNames.EXP, source.getExpiresAt().getEpochSecond()); + } + if (source.getIssuedAt() != null) { + responseClaims.put(OAuth2TokenIntrospectionClaimNames.IAT, source.getIssuedAt().getEpochSecond()); + } + if (source.getNotBefore() != null) { + responseClaims.put(OAuth2TokenIntrospectionClaimNames.NBF, source.getNotBefore().getEpochSecond()); + } + return responseClaims; + } + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java index 904932cd..b9cc4ee3 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.authorization; import java.io.Serializable; +import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -32,6 +33,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken2; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -280,6 +282,37 @@ public class OAuth2Authorization implements Serializable { return Boolean.TRUE.equals(getMetadata(INVALIDATED_METADATA_NAME)); } + /** + * Returns {@code true} if the token has expired. + * + * @return {@code true} if the token has expired, {@code false} otherwise + */ + public boolean isExpired() { + return getToken().getExpiresAt() != null && Instant.now().isAfter(getToken().getExpiresAt()); + } + + /** + * Returns {@code true} if the token is before the time it can be used. + * + * @return {@code true} if the token is before the time it can be used, {@code false} otherwise + */ + public boolean isBeforeUse() { + Instant notBefore = null; + if (!CollectionUtils.isEmpty(getClaims())) { + notBefore = (Instant) getClaims().get("nbf"); + } + return notBefore != null && Instant.now().isBefore(notBefore); + } + + /** + * Returns {@code true} if the token is currently active. + * + * @return {@code true} if the token is currently active, {@code false} otherwise + */ + public boolean isActive() { + return !isInvalidated() && !isExpired() && !isBeforeUse(); + } + /** * Returns the claims associated to the token. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java index 7ef50eae..0e8039c7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java @@ -15,85 +15,83 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; -import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; -import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; +import java.time.Instant; +import java.util.List; +import java.util.Map; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; +import org.springframework.security.oauth2.jwt.JwtClaimAccessor; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; -import org.springframework.security.oauth2.server.authorization.OAuth2Authorization.Token; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; -import java.time.Instant; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; +import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; /** * An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Introspection. * * @author Gerardo Roza + * @author Joe Grandja * @since 0.1.1 * @see OAuth2TokenIntrospectionAuthenticationToken + * @see RegisteredClientRepository * @see OAuth2AuthorizationService - * @see Section 2.1 - Introspection Request + * @see Section 2.1 Introspection Request */ public class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider { + private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; /** * Constructs an {@code OAuth2TokenIntrospectionAuthenticationProvider} using the provided parameters. * + * @param registeredClientRepository the repository of registered clients * @param authorizationService the authorization service */ - public OAuth2TokenIntrospectionAuthenticationProvider(OAuth2AuthorizationService authorizationService) { + public OAuth2TokenIntrospectionAuthenticationProvider(RegisteredClientRepository registeredClientRepository, + OAuth2AuthorizationService authorizationService) { + Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); Assert.notNull(authorizationService, "authorizationService cannot be null"); + this.registeredClientRepository = registeredClientRepository; this.authorizationService = authorizationService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = (OAuth2TokenIntrospectionAuthenticationToken) authentication; + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = + (OAuth2TokenIntrospectionAuthenticationToken) authentication; - OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient( - tokenIntrospectionAuthentication); - RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + OAuth2ClientAuthenticationToken clientPrincipal = + getAuthenticatedClientElseThrowInvalidClient(tokenIntrospectionAuthentication); - OAuth2Authorization authorization = this.authorizationService - .findByToken(tokenIntrospectionAuthentication.getTokenValue(), null); + OAuth2Authorization authorization = this.authorizationService.findByToken( + tokenIntrospectionAuthentication.getToken(), null); if (authorization == null) { - return generateAuthenticationTokenForInvalidToken(clientPrincipal, registeredClient); + // Return the authentication request when token not found + return tokenIntrospectionAuthentication; } - Token tokenHolder = authorization - .getToken(tokenIntrospectionAuthentication.getTokenValue()); - - if (tokenHolder.isInvalidated()) { - return generateAuthenticationTokenForInvalidToken(clientPrincipal, registeredClient); + OAuth2Authorization.Token authorizedToken = + authorization.getToken(tokenIntrospectionAuthentication.getToken()); + if (!authorizedToken.isActive()) { + return new OAuth2TokenIntrospectionAuthenticationToken(tokenIntrospectionAuthentication.getToken(), + clientPrincipal, OAuth2TokenIntrospection.builder().build()); } - if (isExpired(tokenHolder.getToken()) - || (tokenHolder.getClaims() != null && hasInvalidClaims(tokenHolder.getClaims()))) { - return generateAuthenticationTokenForInvalidToken(clientPrincipal, registeredClient); - } + RegisteredClient authorizedClient = this.registeredClientRepository.findById(authorization.getRegisteredClientId()); + OAuth2TokenIntrospection tokenClaims = withActiveTokenClaims(authorizedToken, authorizedClient); - Map claims = generateTokenIntrospectionClaims( - tokenHolder, registeredClient.getClientId(), authorization.getPrincipalName()); - - return new OAuth2TokenIntrospectionAuthenticationToken(clientPrincipal, claims); + return new OAuth2TokenIntrospectionAuthenticationToken(authorizedToken.getToken().getTokenValue(), + clientPrincipal, tokenClaims); } @Override @@ -101,42 +99,49 @@ public class OAuth2TokenIntrospectionAuthenticationProvider implements Authentic return OAuth2TokenIntrospectionAuthenticationToken.class.isAssignableFrom(authentication); } - private boolean isExpired(AbstractOAuth2Token token) { - Instant expiry = token.getExpiresAt(); - return (expiry != null && Instant.now().isAfter(expiry)); - } + private static OAuth2TokenIntrospection withActiveTokenClaims( + OAuth2Authorization.Token authorizedToken, RegisteredClient authorizedClient) { - private boolean hasInvalidClaims(Map claims) { - Object notBeforeValue = claims.get(JwtClaimNames.NBF); - if (notBeforeValue != null && Instant.class.isAssignableFrom(notBeforeValue.getClass())) { - Instant notBefore = (Instant) notBeforeValue; - return Instant.now().isBefore(notBefore); + OAuth2TokenIntrospection.Builder tokenClaims = OAuth2TokenIntrospection.builder(true) + .clientId(authorizedClient.getClientId()); + + // TODO Set "username" + + AbstractOAuth2Token token = authorizedToken.getToken(); + if (token.getIssuedAt() != null) { + tokenClaims.issuedAt(token.getIssuedAt()); + } + if (token.getExpiresAt() != null) { + tokenClaims.expiresAt(token.getExpiresAt()); } - return false; - } - private Map generateTokenIntrospectionClaims(Token tokenHolder, - String clientId, String username) { - Map claims = Optional.ofNullable(tokenHolder.getClaims()).map(LinkedHashMap::new) - .orElse(new LinkedHashMap<>()); - AbstractOAuth2Token token = tokenHolder.getToken(); - claims.put(ACTIVE, true); - claims.put(CLIENT_ID, clientId); - claims.put(USERNAME, username); - Optional.ofNullable(token.getIssuedAt()).ifPresent(iat -> claims.put(IAT, iat)); - Optional.ofNullable(token.getExpiresAt()).ifPresent(exp -> claims.put(EXP, exp)); if (OAuth2AccessToken.class.isAssignableFrom(token.getClass())) { - Collection scopes = ((OAuth2AccessToken) token).getScopes(); - if (!scopes.isEmpty()) { - claims.put(SCOPE, String.join(" ", scopes)); - } - claims.put(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER); - } - return claims; - } + OAuth2AccessToken accessToken = (OAuth2AccessToken) token; + tokenClaims.scopes(scopes -> scopes.addAll(accessToken.getScopes())); + tokenClaims.tokenType(accessToken.getTokenType().getValue()); - private OAuth2TokenIntrospectionAuthenticationToken generateAuthenticationTokenForInvalidToken( - Authentication clientPrincipal, RegisteredClient registeredClient) { - return new OAuth2TokenIntrospectionAuthenticationToken(clientPrincipal, null); + Map claims = authorizedToken.getClaims(); + if (!CollectionUtils.isEmpty(claims)) { + // Assuming JWT as it's the only (currently) supported access token format + JwtClaimAccessor jwtClaims = () -> claims; + + Instant notBefore = jwtClaims.getNotBefore(); + if (notBefore != null) { + tokenClaims.notBefore(notBefore); + } + tokenClaims.subject(jwtClaims.getSubject()); + List audience = jwtClaims.getAudience(); + if (!CollectionUtils.isEmpty(audience)) { + tokenClaims.audiences(audiences -> audiences.addAll(audience)); + } + tokenClaims.issuer(jwtClaims.getIssuer().toExternalForm()); + String jti = jwtClaims.getId(); + if (StringUtils.hasText(jti)) { + tokenClaims.id(jti); + } + } + } + + return tokenClaims.build(); } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java index 1ce35da3..c99eb0e9 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java @@ -15,66 +15,76 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import org.springframework.lang.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; import org.springframework.security.oauth2.core.Version; import org.springframework.util.Assert; -import java.util.Collections; -import java.util.Map; - /** * An {@link Authentication} implementation used for OAuth 2.0 Token Introspection. * * @author Gerardo Roza + * @author Joe Grandja * @since 0.1.1 * @see AbstractAuthenticationToken + * @see OAuth2TokenIntrospection * @see OAuth2TokenIntrospectionAuthenticationProvider */ public class OAuth2TokenIntrospectionAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = Version.SERIAL_VERSION_UID; - private final String tokenValue; + private final String token; private final Authentication clientPrincipal; private final String tokenTypeHint; - private Map claims; + private final Map additionalParameters; + private final OAuth2TokenIntrospection tokenClaims; /** * Constructs an {@code OAuth2TokenIntrospectionAuthenticationToken} using the provided parameters. * - * @param tokenValue the token + * @param token the token * @param clientPrincipal the authenticated client principal * @param tokenTypeHint the token type hint + * @param additionalParameters the additional parameters */ - public OAuth2TokenIntrospectionAuthenticationToken(String tokenValue, Authentication clientPrincipal, - @Nullable String tokenTypeHint) { + public OAuth2TokenIntrospectionAuthenticationToken(String token, Authentication clientPrincipal, + @Nullable String tokenTypeHint, @Nullable Map additionalParameters) { super(Collections.emptyList()); - Assert.hasText(tokenValue, "token cannot be empty"); + Assert.hasText(token, "token cannot be empty"); Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); - this.tokenValue = tokenValue; + this.token = token; this.clientPrincipal = clientPrincipal; this.tokenTypeHint = tokenTypeHint; - this.claims = null; + this.additionalParameters = Collections.unmodifiableMap( + additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap()); + this.tokenClaims = OAuth2TokenIntrospection.builder().build(); } /** * Constructs an {@code OAuth2TokenIntrospectionAuthenticationToken} using the provided parameters. * - * The {@code claims} should be provided only if the token is active. - * - * @param claims the claims obtained from the introspected active token + * @param token the token * @param clientPrincipal the authenticated client principal + * @param tokenClaims the token claims */ - public OAuth2TokenIntrospectionAuthenticationToken(Authentication clientPrincipal, - @Nullable Map claims) { + public OAuth2TokenIntrospectionAuthenticationToken(String token, Authentication clientPrincipal, + OAuth2TokenIntrospection tokenClaims) { super(Collections.emptyList()); + Assert.hasText(token, "token cannot be empty"); Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); - this.claims = claims; + Assert.notNull(tokenClaims, "tokenClaims cannot be null"); + this.token = token; this.clientPrincipal = clientPrincipal; this.tokenTypeHint = null; - this.tokenValue = null; - setAuthenticated(true); // Indicates that the request was authenticated, even though the introspected token might not be - // active + this.additionalParameters = Collections.emptyMap(); + this.tokenClaims = tokenClaims; + // Indicates that the request was authenticated, even though the token might not be active + setAuthenticated(true); } @Override @@ -88,12 +98,12 @@ public class OAuth2TokenIntrospectionAuthenticationToken extends AbstractAuthent } /** - * Returns the token value. + * Returns the token. * - * @return the token value + * @return the token */ - public String getTokenValue() { - return this.tokenValue; + public String getToken() { + return this.token; } /** @@ -107,22 +117,21 @@ public class OAuth2TokenIntrospectionAuthenticationToken extends AbstractAuthent } /** - * Returns the introspection claims. + * Returns the additional parameters. * - * @return the claims + * @return the additional parameters */ - public Map getClaims() { - return claims; + public Map getAdditionalParameters() { + return this.additionalParameters; } /** - * Returns whether the introspected token is active, having in mind only active tokens' claims should be passed to the - * constructor. + * Returns the token claims. * - * @return whether the introspected token is active or not + * @return the {@link OAuth2TokenIntrospection} */ - public boolean isTokenActive() { - return this.claims != null; + public OAuth2TokenIntrospection getTokenClaims() { + return this.tokenClaims; } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java index 4d4e4a1a..73c87d96 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java @@ -15,6 +15,10 @@ */ package org.springframework.security.oauth2.server.authorization.web; +import java.io.IOException; +import java.util.Map; +import java.util.stream.Collectors; + import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -31,10 +35,11 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; -import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter; +import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -43,18 +48,17 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; - /** * A {@code Filter} for the OAuth 2.0 Token Introspection endpoint. * * @author Gerardo Roza - * @see Section 2 - Introspection Endpoint - * @see Section 2.1 - Introspection Request + * @author Joe Grandja + * @see OAuth2TokenIntrospectionAuthenticationProvider + * @see Section 2 Introspection Endpoint + * @see Section 2.1 Introspection Request * @since 0.1.1 */ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter { - /** * The default endpoint {@code URI} for token introspection requests. */ @@ -62,9 +66,10 @@ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter private final AuthenticationManager authenticationManager; private final RequestMatcher tokenIntrospectionEndpointMatcher; - private final Converter tokenIntrospectionAuthenticationConverter = new DefaultTokenIntrospectionAuthenticationConverter(); - private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter(); - + private final Converter tokenIntrospectionAuthenticationConverter = + new DefaultTokenIntrospectionAuthenticationConverter(); + private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = + new OAuth2TokenIntrospectionHttpMessageConverter(); private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); /** @@ -101,40 +106,34 @@ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter } try { + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = + (OAuth2TokenIntrospectionAuthenticationToken) this.tokenIntrospectionAuthenticationConverter.convert(request); - Authentication authentication = this.tokenIntrospectionAuthenticationConverter.convert(request); + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult = + (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationManager.authenticate(tokenIntrospectionAuthentication); - OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationManager - .authenticate(authentication); + OAuth2TokenIntrospection tokenClaims = tokenIntrospectionAuthenticationResult.getTokenClaims(); + sendTokenIntrospectionResponse(response, tokenClaims); - OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = tokenIntrospectionAuthentication - .isTokenActive() - ? OAuth2TokenIntrospectionClaims.withClaims(tokenIntrospectionAuthentication.getClaims()) - .build() - : OAuth2TokenIntrospectionClaims.builder(false).build(); - - sendTokenIntrospectionResponse(response, tokenIntrospectionResponse); } catch (OAuth2AuthenticationException ex) { SecurityContextHolder.clearContext(); sendErrorResponse(response, ex.getError()); } } + private void sendTokenIntrospectionResponse(HttpServletResponse response, OAuth2TokenIntrospection tokenClaims) throws IOException { + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); + this.tokenIntrospectionHttpResponseConverter.write(tokenClaims, null, httpResponse); + } + private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException { ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); this.errorHttpResponseConverter.write(error, null, httpResponse); } - private void sendTokenIntrospectionResponse(HttpServletResponse response, - OAuth2TokenIntrospectionClaims tokenIntrospectionResponse) throws IOException { - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - this.tokenIntrospectionHttpResponseConverter.write(tokenIntrospectionResponse, null, httpResponse); - } - private static void throwError(String errorCode, String parameterName) { - OAuth2Error error = new OAuth2Error( - errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName, + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName, "https://tools.ietf.org/html/rfc7662#section-2.1"); throw new OAuth2AuthenticationException(error); } @@ -150,18 +149,29 @@ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter // token (REQUIRED) String token = parameters.getFirst(OAuth2ParameterNames2.TOKEN); - if (!StringUtils.hasText(token) || parameters.get(OAuth2ParameterNames2.TOKEN).size() != 1) { + if (!StringUtils.hasText(token) || + parameters.get(OAuth2ParameterNames2.TOKEN).size() != 1) { throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN); } // token_type_hint (OPTIONAL) String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames2.TOKEN_TYPE_HINT); - if (StringUtils.hasText(tokenTypeHint) - && parameters.get(OAuth2ParameterNames2.TOKEN_TYPE_HINT).size() != 1) { + if (StringUtils.hasText(tokenTypeHint) && + parameters.get(OAuth2ParameterNames2.TOKEN_TYPE_HINT).size() != 1) { throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN_TYPE_HINT); } - return new OAuth2TokenIntrospectionAuthenticationToken(token, clientPrincipal, tokenTypeHint); + // @formatter:off + Map additionalParameters = parameters + .entrySet() + .stream() + .filter(e -> !e.getKey().equals(OAuth2ParameterNames2.TOKEN) && + !e.getKey().equals(OAuth2ParameterNames2.TOKEN_TYPE_HINT)) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); + // @formatter:on + + return new OAuth2TokenIntrospectionAuthenticationToken( + token, clientPrincipal, tokenTypeHint, additionalParameters); } } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java index eb7533ad..7aa36ded 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java @@ -15,6 +15,52 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; +import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2; +import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter; +import org.springframework.security.oauth2.jose.TestJwks; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.TestJwtClaimsSets; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; @@ -22,55 +68,22 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; -import org.springframework.security.config.test.SpringTestRule; -import org.springframework.security.oauth2.core.AbstractOAuth2Token; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2RefreshToken; -import org.springframework.security.oauth2.core.OAuth2TokenType; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2; -import org.springframework.security.oauth2.jose.TestJwks; -import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; -import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; -import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; - -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.HashSet; - /** * Integration tests for the OAuth 2.0 Token Introspection endpoint. * * @author Gerardo Roza + * @author Joe Grandja */ public class OAuth2TokenIntrospectionTests { private static RegisteredClientRepository registeredClientRepository; private static OAuth2AuthorizationService authorizationService; private static JWKSource jwkSource; + private static ProviderSettings providerSettings; + private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = + new OAuth2TokenIntrospectionHttpMessageConverter(); @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -84,6 +97,7 @@ public class OAuth2TokenIntrospectionTests { authorizationService = mock(OAuth2AuthorizationService.class); JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); + providerSettings = new ProviderSettings().tokenIntrospectionEndpoint("/test/introspect"); } @Before @@ -93,110 +107,101 @@ public class OAuth2TokenIntrospectionTests { } @Test - public void requestWhenIntrospectValidRefreshTokenThenActiveResponse() throws Exception { + public void requestWhenIntrospectValidAccessTokenThenActive() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); - - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); - OAuth2RefreshToken token = authorization.getRefreshToken().getToken(); - OAuth2TokenType tokenType = OAuth2TokenType.REFRESH_TOKEN; - when(authorizationService.findByToken(eq(token.getTokenValue()), isNull())).thenReturn(authorization); - - // @formatter:off - this.mvc.perform( - MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI) - .params(getTokenIntrospectionRequestParameters(token, tokenType)) - .with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret()))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.active").value(true)) - .andExpect(jsonPath("$.client_id").value("client-1")) - .andExpect(jsonPath("$.iat").isNotEmpty()) - .andExpect(jsonPath("$.exp").isNotEmpty()) - .andExpect(jsonPath("$.username").value("principal")); - // @formatter:on - - verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId())); - verify(authorizationService).findByToken(eq(token.getTokenValue()), isNull()); - } - - @Test - public void requestWhenIntrospectValidAccessTokenThenActiveResponse() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); - - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); + RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build(); + when(registeredClientRepository.findByClientId(eq(introspectRegisteredClient.getClientId()))) + .thenReturn(introspectRegisteredClient); + RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build(); Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(Duration.ofHours(1)); OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt, - new HashSet<>(Arrays.asList("scope1", "Scope2"))); - OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN; - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken) + OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt, + new HashSet<>(Arrays.asList("scope1", "scope2"))); + JwtClaimsSet tokenClaims = TestJwtClaimsSets.jwtClaimsSet().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(authorizedRegisteredClient, accessToken, tokenClaims.getClaims()) .build(); - - when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())).thenReturn(authorization); + when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) + .thenReturn(authorization); + when(registeredClientRepository.findById(eq(authorizedRegisteredClient.getId()))) + .thenReturn(authorizedRegisteredClient); // @formatter:off - this.mvc.perform( - MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI) - .params(getTokenIntrospectionRequestParameters(accessToken, tokenType)) - .with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret()))) + MvcResult mvcResult = this.mvc.perform(post(providerSettings.tokenIntrospectionEndpoint()) + .params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN)) + .with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret()))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.active").value(true)) - .andExpect(jsonPath("$.client_id").value("client-1")) - .andExpect(jsonPath("$.scope").isNotEmpty()) - .andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.BEARER.getValue())) - .andExpect(jsonPath("$.iat").isNotEmpty()) - .andExpect(jsonPath("$.exp").isNotEmpty()) - .andExpect(jsonPath("$.username").value("principal")); + .andReturn(); // @formatter:on - verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId())); + verify(registeredClientRepository).findByClientId(eq(introspectRegisteredClient.getClientId())); verify(authorizationService).findByToken(eq(accessToken.getTokenValue()), isNull()); + verify(registeredClientRepository).findById(eq(authorizedRegisteredClient.getId())); + + OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult); + assertThat(tokenIntrospectionResponse.isActive()).isTrue(); + assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId()); + assertThat(tokenIntrospectionResponse.getUsername()).isNull(); + assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween( + accessToken.getIssuedAt().minusSeconds(1), accessToken.getIssuedAt().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween( + accessToken.getExpiresAt().minusSeconds(1), accessToken.getExpiresAt().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getScope()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes()); + assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(accessToken.getTokenType().getValue()); + assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween( + tokenClaims.getNotBefore().minusSeconds(1), tokenClaims.getNotBefore().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(tokenClaims.getSubject()); + assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(tokenClaims.getAudience()); + assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(tokenClaims.getIssuer()); + assertThat(tokenIntrospectionResponse.getId()).isEqualTo(tokenClaims.getId()); } @Test - public void requestWhenIntrospectTokenIssuedToDifferentClientThenActiveResponse() throws Exception { + public void requestWhenIntrospectValidRefreshTokenThenActive() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) - .thenReturn(registeredClient); + RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build(); + when(registeredClientRepository.findByClientId(eq(introspectRegisteredClient.getClientId()))) + .thenReturn(introspectRegisteredClient); - RegisteredClient registeredClient2 = TestRegisteredClients.registeredClient2().build(); - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(Duration.ofHours(1)); - OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt, - new HashSet<>(Arrays.asList("scope1", "Scope2"))); - OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN; - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient2).token(accessToken) - .build(); - - when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())).thenReturn(authorization); + RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build(); + OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken(); + when(authorizationService.findByToken(eq(refreshToken.getTokenValue()), isNull())) + .thenReturn(authorization); + when(registeredClientRepository.findById(eq(authorizedRegisteredClient.getId()))) + .thenReturn(authorizedRegisteredClient); // @formatter:off - this.mvc.perform( - MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI) - .params(getTokenIntrospectionRequestParameters(accessToken, tokenType)) - .with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret()))) + MvcResult mvcResult = this.mvc.perform(post(providerSettings.tokenIntrospectionEndpoint()) + .params(getTokenIntrospectionRequestParameters(refreshToken, OAuth2TokenType.REFRESH_TOKEN)) + .with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret()))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.active").value(true)) - .andExpect(jsonPath("$.client_id").value("client-1")) - .andExpect(jsonPath("$.scope").isNotEmpty()) - .andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.BEARER.getValue())) - .andExpect(jsonPath("$.iat").isNotEmpty()) - .andExpect(jsonPath("$.exp").isNotEmpty()) - .andExpect(jsonPath("$.username").value("principal")); + .andReturn(); // @formatter:on - verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId())); - verify(authorizationService).findByToken(eq(accessToken.getTokenValue()), isNull()); + verify(registeredClientRepository).findByClientId(eq(introspectRegisteredClient.getClientId())); + verify(authorizationService).findByToken(eq(refreshToken.getTokenValue()), isNull()); + verify(registeredClientRepository).findById(eq(authorizedRegisteredClient.getId())); + + OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult); + assertThat(tokenIntrospectionResponse.isActive()).isTrue(); + assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId()); + assertThat(tokenIntrospectionResponse.getUsername()).isNull(); + assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween( + refreshToken.getIssuedAt().minusSeconds(1), refreshToken.getIssuedAt().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween( + refreshToken.getExpiresAt().minusSeconds(1), refreshToken.getExpiresAt().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getScope()).isNull(); + assertThat(tokenIntrospectionResponse.getTokenType()).isNull(); + assertThat(tokenIntrospectionResponse.getNotBefore()).isNull(); + assertThat(tokenIntrospectionResponse.getSubject()).isNull(); + assertThat(tokenIntrospectionResponse.getAudience()).isNull(); + assertThat(tokenIntrospectionResponse.getIssuer()).isNull(); + assertThat(tokenIntrospectionResponse.getId()).isNull(); } private static MultiValueMap getTokenIntrospectionRequestParameters(AbstractOAuth2Token token, @@ -207,6 +212,13 @@ public class OAuth2TokenIntrospectionTests { return parameters; } + private OAuth2TokenIntrospection readTokenIntrospectionResponse(MvcResult mvcResult) throws Exception { + MockHttpServletResponse servletResponse = mvcResult.getResponse(); + MockClientHttpResponse httpResponse = new MockClientHttpResponse( + servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus())); + return this.tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospection.class, httpResponse); + } + @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfiguration { @@ -225,5 +237,10 @@ public class OAuth2TokenIntrospectionTests { JWKSource jwkSource() { return jwkSource; } + + @Bean + ProviderSettings providerSettings() { + return providerSettings; + } } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverterTests.java deleted file mode 100644 index ad3aea60..00000000 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverterTests.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2020-2021 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.core.http.converter; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB; - -import org.assertj.core.api.Condition; -import org.junit.Test; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpStatus; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.mock.http.MockHttpOutputMessage; -import org.springframework.mock.http.client.MockClientHttpResponse; -import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; -import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter; -import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims; - -import java.time.Instant; -import java.util.Arrays; -import java.util.Collection; -import java.util.Map; - -/** - * Tests for {@link OAuth2TokenIntrospectionClaimsHttpMessageConverter} - * - * @author Gerardo Roza - */ -public class OAuth2TokenIntrospectionClaimsHttpMessageConverterTests { - private final OAuth2TokenIntrospectionClaimsHttpMessageConverter messageConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter(); - - @Test - public void supportsWhenOidcProviderConfigurationThenTrue() { - assertThat(this.messageConverter.supports(OAuth2TokenIntrospectionClaims.class)).isTrue(); - } - - @Test - public void setProviderConfigurationParametersConverterWhenNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.messageConverter.setTokenIntrospectionResponseParametersConverter(null)); - } - - @Test - public void setProviderConfigurationConverterWhenNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.messageConverter.setTokenIntrospectionResponseConverter(null)); - } - - @SuppressWarnings("unchecked") - @Test - public void readInternalWhenValidParametersThenSuccess() throws Exception { - // @formatter:off - String tokenIntrospectionResponseBody = "{\n" - + " \"active\": true,\n" - + " \"iss\": \"https://example.com/issuer1\",\n" - + " \"scope\": \"scope1 Scope2\",\n" - + " \"client_id\": \"clientId1\",\n" - + " \"token_type\": \"Bearer\",\n" - + " \"username\": \"username1\",\n" - + " \"aud\": [\"audience1\", \"audience2\"],\n" - + " \"exp\": 1607637467,\n" - + " \"iat\": 1607633867,\n" - + " \"nbf\": 1607633867,\n" - + " \"sub\": \"subject1\",\n" - + " \"jti\": \"jwtId1\"\n" - + "}\n"; - // @formatter:on - MockClientHttpResponse response = new MockClientHttpResponse( - tokenIntrospectionResponseBody.getBytes(), HttpStatus.OK); - OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = this.messageConverter - .readInternal(OAuth2TokenIntrospectionClaims.class, response); - Map responseParameters = tokenIntrospectionResponse.getClaims(); - Condition collectionContainsCondition = new Condition<>( - collection -> Collection.class.isAssignableFrom(collection.getClass()) - && ((Collection) collection).contains("audience1") - && ((Collection) collection).contains("audience2"), - "collection contains entries"); - - // @formatter:off - assertThat(responseParameters) - .containsEntry(ACTIVE, true) - .containsEntry(SCOPE, "scope1 Scope2") - .containsEntry(CLIENT_ID, "clientId1") - .containsEntry(TOKEN_TYPE, "Bearer") - .containsEntry(USERNAME, "username1") - .hasEntrySatisfying(AUD, collectionContainsCondition) - .containsEntry(EXP, 1607637467L) - .containsEntry(IAT, 1607633867L) - .containsEntry(NBF, 1607633867L) - .containsEntry(ISS, "https://example.com/issuer1") - .containsEntry(JTI, "jwtId1") - .containsEntry(SUB, "subject1"); - // @formatter:on - } - - @Test - public void readInternalWhenFailingConverterThenThrowException() { - String errorMessage = "this is not a valid converter"; - this.messageConverter.setTokenIntrospectionResponseConverter(source -> { - throw new RuntimeException(errorMessage); - }); - MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK); - - assertThatExceptionOfType(HttpMessageNotReadableException.class) - .isThrownBy(() -> this.messageConverter.readInternal(OAuth2TokenIntrospectionClaims.class, response)) - .withMessageContaining("An error occurred reading the Token Introspection Response") - .withMessageContaining(errorMessage); - } - - @Test - public void writeInternalWhenTokenIntrospectionResponseThenSuccess() { - // @formatter:off - OAuth2TokenIntrospectionClaims providerConfiguration = OAuth2TokenIntrospectionClaims.builder(true) - .issuer("https://example.com/issuer1") - .scope("scope1 Scope2") - .clientId("clientId1") - .tokenType(TokenType.BEARER) - .username("username1") - .audience(Arrays.asList("audience1", "audience2")) - .expirationTime(Instant.ofEpochSecond(1607637467)) - .issuedAt(Instant.ofEpochSecond(1607633867)) - .notBefore(Instant.ofEpochSecond(1607633867)) - .jwtId("jwtId1") - .subject("subject1").build(); - // @formatter:on - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - - this.messageConverter.writeInternal(providerConfiguration, outputMessage); - - String providerConfigurationResponse = outputMessage.getBodyAsString(); - // @formatter:off - assertThat(providerConfigurationResponse) - .contains("\"iss\":\"https://example.com/issuer1\"") - .contains("\"active\":true") - .contains("\"scope\":\"scope1 Scope2\"") - .contains("\"client_id\":\"clientId1\"") - .contains("\"token_type\":\"Bearer\"") - .contains("\"username\":\"username1\"") - .contains("\"aud\":[\"audience1\",\"audience2\"]") - .contains("\"exp\":1607637467") - .contains("\"iat\":1607633867") - .contains("\"nbf\":1607633867") - .contains("\"jti\":\"jwtId1\"") - .contains("\"sub\":\"subject1\""); - // @formatter:on - } - - @Test - public void writeInternalWhenWriteFailsThenThrowsException() { - String errorMessage = "this is not a valid converter"; - Converter> failingConverter = source -> { - throw new RuntimeException(errorMessage); - }; - this.messageConverter.setTokenIntrospectionResponseParametersConverter(failingConverter); - - OAuth2TokenIntrospectionClaims providerConfiguration = OAuth2TokenIntrospectionClaims.builder(true).build(); - - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - - assertThatThrownBy(() -> this.messageConverter.writeInternal(providerConfiguration, outputMessage)) - .isInstanceOf(HttpMessageNotWritableException.class) - .hasMessageContaining("An error occurred writing the Token Introspection Response") - .hasMessageContaining(errorMessage); - } -} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverterTests.java new file mode 100644 index 00000000..3e0af387 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverterTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2020-2021 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.core.http.converter; + +import java.net.URL; +import java.time.Instant; +import java.util.Arrays; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link OAuth2TokenIntrospectionHttpMessageConverter} + * + * @author Gerardo Roza + * @author Joe Grandja + */ +public class OAuth2TokenIntrospectionHttpMessageConverterTests { + private final OAuth2TokenIntrospectionHttpMessageConverter messageConverter = new OAuth2TokenIntrospectionHttpMessageConverter(); + + @Test + public void supportsWhenOAuth2TokenIntrospectionThenTrue() { + assertThat(this.messageConverter.supports(OAuth2TokenIntrospection.class)).isTrue(); + } + + @Test + public void setTokenIntrospectionParametersConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.messageConverter.setTokenIntrospectionParametersConverter(null)); + } + + @Test + public void setTokenIntrospectionConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.messageConverter.setTokenIntrospectionConverter(null)); + } + + @Test + public void readInternalWhenValidParametersThenSuccess() throws Exception { + // @formatter:off + String tokenIntrospectionResponseBody = "{\n" + + " \"active\": true,\n" + + " \"client_id\": \"clientId1\",\n" + + " \"username\": \"username1\",\n" + + " \"iat\": 1607633867,\n" + + " \"exp\": 1607637467,\n" + + " \"scope\": \"scope1 scope2\",\n" + + " \"token_type\": \"Bearer\",\n" + + " \"nbf\": 1607633867,\n" + + " \"sub\": \"subject1\",\n" + + " \"aud\": [\"audience1\", \"audience2\"],\n" + + " \"iss\": \"https://example.com/issuer1\",\n" + + " \"jti\": \"jwtId1\"\n" + + "}\n"; + // @formatter:on + MockClientHttpResponse response = new MockClientHttpResponse( + tokenIntrospectionResponseBody.getBytes(), HttpStatus.OK); + OAuth2TokenIntrospection tokenIntrospectionResponse = this.messageConverter + .readInternal(OAuth2TokenIntrospection.class, response); + + assertThat(tokenIntrospectionResponse.isActive()).isTrue(); + assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo("clientId1"); + assertThat(tokenIntrospectionResponse.getUsername()).isEqualTo("username1"); + assertThat(tokenIntrospectionResponse.getIssuedAt()).isEqualTo(Instant.ofEpochSecond(1607633867L)); + assertThat(tokenIntrospectionResponse.getExpiresAt()).isEqualTo(Instant.ofEpochSecond(1607637467L)); + assertThat(tokenIntrospectionResponse.getScope()).containsExactlyInAnyOrderElementsOf(Arrays.asList("scope1", "scope2")); + assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo("Bearer"); + assertThat(tokenIntrospectionResponse.getNotBefore()).isEqualTo(Instant.ofEpochSecond(1607633867L)); + assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo("subject1"); + assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(Arrays.asList("audience1", "audience2")); + assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(new URL("https://example.com/issuer1")); + assertThat(tokenIntrospectionResponse.getId()).isEqualTo("jwtId1"); + } + + @Test + public void readInternalWhenFailingConverterThenThrowException() { + String errorMessage = "this is not a valid converter"; + this.messageConverter.setTokenIntrospectionConverter(source -> { + throw new RuntimeException(errorMessage); + }); + MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK); + + assertThatExceptionOfType(HttpMessageNotReadableException.class) + .isThrownBy(() -> this.messageConverter.readInternal(OAuth2TokenIntrospection.class, response)) + .withMessageContaining("An error occurred reading the Token Introspection Response") + .withMessageContaining(errorMessage); + } + + @Test + public void writeInternalWhenTokenIntrospectionThenSuccess() { + // @formatter:off + OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder(true) + .clientId("clientId1") + .username("username1") + .issuedAt(Instant.ofEpochSecond(1607633867)) + .expiresAt(Instant.ofEpochSecond(1607637467)) + .scope("scope1 scope2") + .tokenType(TokenType.BEARER.getValue()) + .notBefore(Instant.ofEpochSecond(1607633867)) + .subject("subject1") + .audience("audience1") + .audience("audience2") + .issuer("https://example.com/issuer1") + .id("jwtId1") + .build(); + // @formatter:on + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + + this.messageConverter.writeInternal(tokenClaims, outputMessage); + + String tokenIntrospectionResponse = outputMessage.getBodyAsString(); + assertThat(tokenIntrospectionResponse).contains("\"active\":true"); + assertThat(tokenIntrospectionResponse).contains("\"client_id\":\"clientId1\""); + assertThat(tokenIntrospectionResponse).contains("\"username\":\"username1\""); + assertThat(tokenIntrospectionResponse).contains("\"iat\":1607633867"); + assertThat(tokenIntrospectionResponse).contains("\"exp\":1607637467"); + assertThat(tokenIntrospectionResponse).contains("\"scope\":\"scope1 scope2\""); + assertThat(tokenIntrospectionResponse).contains("\"token_type\":\"Bearer\""); + assertThat(tokenIntrospectionResponse).contains("\"nbf\":1607633867"); + assertThat(tokenIntrospectionResponse).contains("\"sub\":\"subject1\""); + assertThat(tokenIntrospectionResponse).contains("\"aud\":[\"audience1\",\"audience2\"]"); + assertThat(tokenIntrospectionResponse).contains("\"iss\":\"https://example.com/issuer1\""); + assertThat(tokenIntrospectionResponse).contains("\"jti\":\"jwtId1\""); + } + + @Test + public void writeInternalWhenWriteFailsThenThrowsException() { + String errorMessage = "this is not a valid converter"; + Converter> failingConverter = source -> { + throw new RuntimeException(errorMessage); + }; + this.messageConverter.setTokenIntrospectionParametersConverter(failingConverter); + + OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder().build(); + + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + + assertThatThrownBy(() -> this.messageConverter.writeInternal(tokenClaims, outputMessage)) + .isInstanceOf(HttpMessageNotWritableException.class) + .hasMessageContaining("An error occurred writing the Token Introspection Response") + .hasMessageContaining(errorMessage); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/TestOAuth2Authorizations.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/TestOAuth2Authorizations.java index 9e71562b..fa5da747 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/TestOAuth2Authorizations.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/TestOAuth2Authorizations.java @@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken2; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.util.CollectionUtils; /** * @author Joe Grandja @@ -46,11 +47,22 @@ public class TestOAuth2Authorizations { } public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient, + OAuth2AccessToken accessToken, Map accessTokenClaims) { + return authorization(registeredClient, accessToken, accessTokenClaims, Collections.emptyMap()); + } + + public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient, + Map authorizationRequestAdditionalParameters) { + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300)); + return authorization(registeredClient, accessToken, Collections.emptyMap(), authorizationRequestAdditionalParameters); + } + + private static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient, + OAuth2AccessToken accessToken, Map accessTokenClaims, Map authorizationRequestAdditionalParameters) { OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode( "code", Instant.now(), Instant.now().plusSeconds(120)); - OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300)); OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2( "refresh-token", Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS)); OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() @@ -66,7 +78,7 @@ public class TestOAuth2Authorizations { .principalName("principal") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .token(authorizationCode) - .token(accessToken, (metadata) -> metadata.putAll(tokenMetadata())) + .token(accessToken, (metadata) -> metadata.putAll(tokenMetadata(accessTokenClaims))) .refreshToken(refreshToken) .attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest) .attribute(Principal.class.getName(), @@ -74,14 +86,21 @@ public class TestOAuth2Authorizations { .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes()); } - private static Map tokenMetadata() { + private static Map tokenMetadata(Map tokenClaims) { Map tokenMetadata = new HashMap<>(); tokenMetadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false); + if (CollectionUtils.isEmpty(tokenClaims)) { + tokenClaims = defaultTokenClaims(); + } + tokenMetadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, tokenClaims); + return tokenMetadata; + } + + private static Map defaultTokenClaims() { Map claims = new HashMap<>(); claims.put("claim1", "value1"); claims.put("claim2", "value2"); claims.put("claim3", "value3"); - tokenMetadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claims); - return tokenMetadata; + return claims; } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java index c4225d1a..2b7bc654 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java @@ -15,62 +15,72 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; -import static java.time.Duration.ofHours; -import static java.time.Instant.now; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; import org.junit.Before; import org.junit.Test; + import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; -import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.TestJwtClaimsSets; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Tests for {@link OAuth2TokenIntrospectionAuthenticationProvider}. * * @author Gerardo Roza + * @author Joe Grandja */ public class OAuth2TokenIntrospectionAuthenticationProviderTests { + private RegisteredClientRepository registeredClientRepository; private OAuth2AuthorizationService authorizationService; private OAuth2TokenIntrospectionAuthenticationProvider authenticationProvider; @Before public void setUp() { + this.registeredClientRepository = mock(RegisteredClientRepository.class); this.authorizationService = mock(OAuth2AuthorizationService.class); - this.authenticationProvider = new OAuth2TokenIntrospectionAuthenticationProvider(this.authorizationService); + this.authenticationProvider = new OAuth2TokenIntrospectionAuthenticationProvider( + this.registeredClientRepository, this.authorizationService); + } + + @Test + public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationProvider(null, this.authorizationService)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("registeredClientRepository cannot be null"); } @Test public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationProvider(null)) - .isInstanceOf(IllegalArgumentException.class).hasMessage("authorizationService cannot be null"); + assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationProvider(this.registeredClientRepository, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationService cannot be null"); } @Test @@ -83,8 +93,10 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); TestingAuthenticationToken clientPrincipal = new TestingAuthenticationToken( registeredClient.getClientId(), registeredClient.getClientSecret()); + OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - "token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue()); + "token", clientPrincipal, null, null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") @@ -95,10 +107,11 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, - null); + registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null); + OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - "token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue()); + "token", clientPrincipal, null, null); + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode") @@ -106,144 +119,157 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { } @Test - public void authenticateWhenInvalidOAuth2TokenTypeThenThrowOAuth2AuthenticationException() { + public void authenticateWhenInvalidTokenThenNotActive() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); + OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - "token", clientPrincipal, "unsupportedOAuth2TokenType"); - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider - .authenticate(authentication); - assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.isTokenActive()).isFalse(); + "token", clientPrincipal, null, null); + OAuth2TokenIntrospectionAuthenticationToken authenticationResult = + (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull()); + assertThat(authenticationResult.isAuthenticated()).isFalse(); + assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1); + assertThat(authenticationResult.getTokenClaims().isActive()).isFalse(); } @Test - public void authenticateWhenTokenNotFoundThenAuthenticatedButTokenNotActive() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); - OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - "token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue()); - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider - .authenticate(authentication); - assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.isTokenActive()).isFalse(); - } - - @Test - public void authenticateWhenInvalidatedTokenThenAuthenticatedButTokenNotActive() { + public void authenticateWhenTokenInvalidatedThenNotActive() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); OAuth2AccessToken accessToken = authorization.getAccessToken().getToken(); authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, accessToken); when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) .thenReturn(authorization); - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); - OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue()); - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider - .authenticate(authentication); + OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, null, null); + OAuth2TokenIntrospectionAuthenticationToken authenticationResult = + (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull()); assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.isTokenActive()).isFalse(); + assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1); + assertThat(authenticationResult.getTokenClaims().isActive()).isFalse(); } @Test - public void authenticateWhenValidAccessTokenThenActiveWithScopes() { + public void authenticateWhenTokenExpiredThenNotActive() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Instant issuedAt = Instant.now().minus(Duration.ofHours(1)); + Instant expiresAt = Instant.now().minus(Duration.ofMinutes(1)); OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300), - new HashSet<>(Arrays.asList("scope1", "Scope2"))); - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) - .accessToken(accessToken).build(); + OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken).build(); when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) .thenReturn(authorization); - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); + OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue()); + accessToken.getTokenValue(), clientPrincipal, null, null); + OAuth2TokenIntrospectionAuthenticationToken authenticationResult = + (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication); - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider - .authenticate(authentication); + verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull()); assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.isTokenActive()).isTrue(); - assertThat(authenticationResult.getClaims()).containsEntry(ACTIVE, true) - .containsEntry(IAT, accessToken.getIssuedAt()).containsEntry(EXP, accessToken.getExpiresAt()) - .containsEntry(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER).containsEntry(CLIENT_ID, "client-1") - .containsEntry(USERNAME, "principal").containsKey(SCOPE) - .containsAllEntriesOf(authorization.getAccessToken().getClaims()); + assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1); + assertThat(authenticationResult.getTokenClaims().isActive()).isFalse(); + } + @Test + public void authenticateWhenTokenBeforeUseThenNotActive() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Instant issuedAt = Instant.now(); + Instant notBefore = issuedAt.plus(Duration.ofMinutes(5)); + Instant expiresAt = issuedAt.plus(Duration.ofHours(1)); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt); + Map accessTokenClaims = Collections.singletonMap(JwtClaimNames.NBF, notBefore); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, accessToken, accessTokenClaims) + .build(); + when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) + .thenReturn(authorization); + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); + + OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, null, null); + OAuth2TokenIntrospectionAuthenticationToken authenticationResult = + (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull()); + assertThat(authenticationResult.isAuthenticated()).isTrue(); + assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1); + assertThat(authenticationResult.getTokenClaims().isActive()).isFalse(); + } + + @Test + public void authenticateWhenValidAccessTokenThenActive() { + RegisteredClient authorizedClient = TestRegisteredClients.registeredClient().build(); + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(Duration.ofHours(1)); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt, + new HashSet<>(Arrays.asList("scope1", "scope2"))); + JwtClaimsSet jwtClaims = TestJwtClaimsSets.jwtClaimsSet().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(authorizedClient, accessToken, jwtClaims.getClaims()) + .build(); + when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) + .thenReturn(authorization); + when(this.registeredClientRepository.findById(eq(authorizedClient.getId()))).thenReturn(authorizedClient); + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + TestRegisteredClients.registeredClient2().build()); + + OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, null, null); + OAuth2TokenIntrospectionAuthenticationToken authenticationResult = + (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull()); + verify(this.registeredClientRepository).findById(eq(authorizedClient.getId())); + assertThat(authenticationResult.isAuthenticated()).isTrue(); + OAuth2TokenIntrospection tokenClaims = authenticationResult.getTokenClaims(); + assertThat(tokenClaims.isActive()).isTrue(); + assertThat(tokenClaims.getClientId()).isEqualTo(authorizedClient.getClientId()); + assertThat(tokenClaims.getIssuedAt()).isEqualTo(accessToken.getIssuedAt()); + assertThat(tokenClaims.getExpiresAt()).isEqualTo(accessToken.getExpiresAt()); + assertThat(tokenClaims.getScope()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes()); + assertThat(tokenClaims.getTokenType()).isEqualTo(accessToken.getTokenType().getValue()); + assertThat(tokenClaims.getNotBefore()).isEqualTo(jwtClaims.getNotBefore()); + assertThat(tokenClaims.getSubject()).isEqualTo(jwtClaims.getSubject()); + assertThat(tokenClaims.getAudience()).containsExactlyInAnyOrderElementsOf(jwtClaims.getAudience()); + assertThat(tokenClaims.getIssuer()).isEqualTo(jwtClaims.getIssuer()); + assertThat(tokenClaims.getId()).isEqualTo(jwtClaims.getId()); } @Test public void authenticateWhenValidRefreshTokenThenActive() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + RegisteredClient authorizedClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build(); OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken(); when(this.authorizationService.findByToken(eq(refreshToken.getTokenValue()), isNull())) .thenReturn(authorization); + when(this.registeredClientRepository.findById(eq(authorizedClient.getId()))).thenReturn(authorizedClient); + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + TestRegisteredClients.registeredClient2().build()); - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - refreshToken.getTokenValue(), clientPrincipal, OAuth2TokenType.REFRESH_TOKEN.getValue()); + refreshToken.getTokenValue(), clientPrincipal, null, null); + OAuth2TokenIntrospectionAuthenticationToken authenticationResult = + (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication); - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider - .authenticate(authentication); + verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull()); + verify(this.registeredClientRepository).findById(eq(authorizedClient.getId())); assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.isTokenActive()).isTrue(); - assertThat(authenticationResult.getClaims()).containsEntry(ACTIVE, true) - .containsEntry(IAT, refreshToken.getIssuedAt()).containsEntry(EXP, refreshToken.getExpiresAt()) - .containsEntry(USERNAME, "principal").containsEntry(CLIENT_ID, "client-1").hasSize(5); + OAuth2TokenIntrospection tokenClaims = authenticationResult.getTokenClaims(); + assertThat(tokenClaims.getClaims()).hasSize(4); + assertThat(tokenClaims.isActive()).isTrue(); + assertThat(tokenClaims.getClientId()).isEqualTo(authorizedClient.getClientId()); + assertThat(tokenClaims.getIssuedAt()).isEqualTo(refreshToken.getIssuedAt()); + assertThat(tokenClaims.getExpiresAt()).isEqualTo(refreshToken.getExpiresAt()); } - @Test - public void authenticateWhenExpiredTokenThenAuthenticatedButTokenNotActive() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - Instant expiresAt = Instant.now().minus(ofHours(1)); - Instant issuedAt = expiresAt.minus(ofHours(1)); - OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt); - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken) - .build(); - when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) - .thenReturn(authorization); - - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); - OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue()); - - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider - .authenticate(authentication); - assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.isTokenActive()).isFalse(); - assertThat(authenticationResult.getClaims()).isNull(); - } - - @Test - public void authenticateWhenInvalidNotBeforeClaimThenAuthenticatedButTokenNotActive() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(ofHours(1)); - OAuth2AccessToken accessToken = new OAuth2AccessToken( - OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt); - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) - .token( - accessToken, - (metadata) -> metadata.put( - OAuth2Authorization.Token.CLAIMS_METADATA_NAME, - Collections.singletonMap(JwtClaimNames.NBF, now().plus(ofHours(1))))) - .build(); - when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) - .thenReturn(authorization); - - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); - OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue()); - - OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider - .authenticate(authentication); - assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.isTokenActive()).isFalse(); - assertThat(authenticationResult.getClaims()).isNull(); - } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationTokenTests.java index 7488904e..b0a2ebc4 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationTokenTests.java @@ -15,84 +15,91 @@ */ package org.springframework.security.oauth2.server.authorization.authentication; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Collections; +import java.util.Map; import org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; -import java.util.HashMap; -import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link OAuth2TokenIntrospectionAuthenticationToken}. * * @author Gerardo Roza + * @author Joe Grandja */ public class OAuth2TokenIntrospectionAuthenticationTokenTests { - private String tokenValue = "tokenValue"; + private String token = "token"; private OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( TestRegisteredClients.registeredClient().build()); - private String tokenTypeHint = OAuth2TokenType.ACCESS_TOKEN.getValue(); - private Map claims = new HashMap<>(); + private OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder(true).build(); @Test - public void constructorWhenTokenValueNullThenThrowIllegalArgumentException() { - assertThatThrownBy( - () -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.clientPrincipal, this.tokenTypeHint)) - .isInstanceOf(IllegalArgumentException.class).hasMessage("token cannot be empty"); + public void constructorWhenTokenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.clientPrincipal, null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("token cannot be empty"); } @Test public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() { - assertThatThrownBy( - () -> new OAuth2TokenIntrospectionAuthenticationToken(this.tokenValue, null, this.tokenTypeHint)) - .isInstanceOf(IllegalArgumentException.class).hasMessage("clientPrincipal cannot be null"); + assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(this.token, null, null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("clientPrincipal cannot be null"); } @Test - public void constructorWhenTokenAndClientPrincipalNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.claims)) - .isInstanceOf(IllegalArgumentException.class).hasMessage("clientPrincipal cannot be null"); + public void constructorWhenAuthenticatedAndTokenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.clientPrincipal, this.tokenClaims)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("token cannot be empty"); } @Test - public void constructorWhenTokenValueProvidedThenCreated() { - OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - this.tokenValue, this.clientPrincipal, this.tokenTypeHint); - assertThat(authentication.getTokenValue()).isEqualTo(this.tokenValue); - assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal); - assertThat(authentication.getTokenTypeHint()).isEqualTo(this.tokenTypeHint); - assertThat(authentication.getClaims()).isNull(); - assertThat(authentication.isTokenActive()).isFalse(); - assertThat(authentication.getCredentials().toString()).isEmpty(); - assertThat(authentication.isAuthenticated()).isFalse(); + public void constructorWhenAuthenticatedAndClientPrincipalNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(this.token, null, this.tokenClaims)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("clientPrincipal cannot be null"); + } + + @Test + public void constructorWhenAuthenticatedAndTokenClaimsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(this.token, this.clientPrincipal, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tokenClaims cannot be null"); } @Test public void constructorWhenTokenProvidedThenCreated() { + Map additionalParameters = Collections.singletonMap("custom-param", "custom-value"); OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - this.clientPrincipal, this.claims); - assertThat(authentication.getTokenValue()).isNull(); + this.token, this.clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue(), additionalParameters); + assertThat(authentication.getToken()).isEqualTo(this.token); assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal); - assertThat(authentication.getClaims()).isEqualTo(this.claims); - assertThat(authentication.isTokenActive()).isTrue(); - assertThat(authentication.getTokenTypeHint()).isNull(); assertThat(authentication.getCredentials().toString()).isEmpty(); - assertThat(authentication.isAuthenticated()).isTrue(); + assertThat(authentication.getTokenTypeHint()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN.getValue()); + assertThat(authentication.getAdditionalParameters()).containsExactlyInAnyOrderEntriesOf(additionalParameters); + assertThat(authentication.getTokenClaims()).isNotNull(); + assertThat(authentication.getTokenClaims().isActive()).isFalse(); + assertThat(authentication.isAuthenticated()).isFalse(); } @Test - public void constructorWhenNullTokenProvidedThenCreatedAsTokenNotActive() { + public void constructorWhenTokenClaimsProvidedThenCreated() { OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken( - this.clientPrincipal, null); - assertThat(authentication.getTokenValue()).isNull(); + this.token, this.clientPrincipal, this.tokenClaims); + assertThat(authentication.getToken()).isEqualTo(this.token); assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal); - assertThat(authentication.getClaims()).isNull(); - assertThat(authentication.isTokenActive()).isFalse(); - assertThat(authentication.getTokenTypeHint()).isNull(); assertThat(authentication.getCredentials().toString()).isEmpty(); + assertThat(authentication.getTokenTypeHint()).isNull(); + assertThat(authentication.getAdditionalParameters()).isEmpty(); + assertThat(authentication.getTokenClaims()).isEqualTo(this.tokenClaims); assertThat(authentication.isAuthenticated()).isTrue(); } + } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java index 97fcf02a..1c9ae660 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java @@ -36,6 +36,7 @@ public class ProviderSettingsTests { assertThat(providerSettings.tokenEndpoint()).isEqualTo("/oauth2/token"); assertThat(providerSettings.jwkSetEndpoint()).isEqualTo("/oauth2/jwks"); assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo("/oauth2/revoke"); + assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect"); } @Test @@ -44,6 +45,7 @@ public class ProviderSettingsTests { String tokenEndpoint = "/oauth2/v1/token"; String jwkSetEndpoint = "/oauth2/v1/jwks"; String tokenRevocationEndpoint = "/oauth2/v1/revoke"; + String tokenIntrospectionEndpoint = "/oauth2/v1/introspect"; String issuer = "https://example.com:9000"; ProviderSettings providerSettings = new ProviderSettings() @@ -51,13 +53,15 @@ public class ProviderSettingsTests { .authorizationEndpoint(authorizationEndpoint) .tokenEndpoint(tokenEndpoint) .jwkSetEndpoint(jwkSetEndpoint) - .tokenRevocationEndpoint(tokenRevocationEndpoint); + .tokenRevocationEndpoint(tokenRevocationEndpoint) + .tokenIntrospectionEndpoint(tokenIntrospectionEndpoint); assertThat(providerSettings.issuer()).isEqualTo(issuer); assertThat(providerSettings.authorizationEndpoint()).isEqualTo(authorizationEndpoint); assertThat(providerSettings.tokenEndpoint()).isEqualTo(tokenEndpoint); assertThat(providerSettings.jwkSetEndpoint()).isEqualTo(jwkSetEndpoint); assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint); + assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint); } @Test @@ -103,6 +107,14 @@ public class ProviderSettingsTests { .hasMessage("value cannot be null"); } + @Test + public void tokenIntrospectionEndpointWhenNullThenThrowIllegalArgumentException() { + ProviderSettings settings = new ProviderSettings(); + assertThatThrownBy(() -> settings.tokenIntrospectionEndpoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); + } + @Test public void jwksEndpointWhenNullThenThrowIllegalArgumentException() { ProviderSettings settings = new ProviderSettings(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java index 3c68d74b..a21d62d9 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java @@ -15,31 +15,20 @@ */ package org.springframework.security.oauth2.server.authorization.web; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2.TOKEN; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2.TOKEN_TYPE_HINT; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.assertj.core.api.Condition; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; + import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mock.http.client.MockClientHttpResponse; @@ -52,34 +41,38 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; -import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter; +import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; -import java.time.Duration; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; /** * Tests for {@link OAuth2TokenIntrospectionEndpointFilter}. * * @author Gerardo Roza + * @author Joe Grandja */ public class OAuth2TokenIntrospectionEndpointFilterTests { - private AuthenticationManager authenticationManager; private OAuth2TokenIntrospectionEndpointFilter filter; - private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); - private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter(); - private final Condition scopesMatchesInAnyOrder = new Condition<>( - scopes -> scopes.equals("scope1 Scope2") || scopes.equals("Scope2 scope1"), "scopes match"); - private final String tokenValue = "token.123"; + private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = + new OAuth2TokenIntrospectionHttpMessageConverter(); + private final HttpMessageConverter errorHttpResponseConverter = + new OAuth2ErrorHttpMessageConverter(); @Before public void setUp() { @@ -95,11 +88,19 @@ public class OAuth2TokenIntrospectionEndpointFilterTests { @Test public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new OAuth2TokenIntrospectionEndpointFilter(null)) - .isInstanceOf(IllegalArgumentException.class).hasMessage("authenticationManager cannot be null"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authenticationManager cannot be null"); } @Test - public void doFilterWhenNotIntrospectionRequestThenNotProcessed() throws Exception { + public void constructorWhenTokenIntrospectionEndpointUriNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2TokenIntrospectionEndpointFilter(this.authenticationManager, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tokenIntrospectionEndpointUri cannot be empty"); + } + + @Test + public void doFilterWhenNotTokenIntrospectionRequestThenNotProcessed() throws Exception { String requestUri = "/path"; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); @@ -112,7 +113,7 @@ public class OAuth2TokenIntrospectionEndpointFilterTests { } @Test - public void doFilterWhenIntrospectionRequestGetThenNotProcessed() throws Exception { + public void doFilterWhenTokenIntrospectionRequestGetThenNotProcessed() throws Exception { String requestUri = OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); @@ -125,117 +126,106 @@ public class OAuth2TokenIntrospectionEndpointFilterTests { } @Test - public void doFilterWhenIntrospectionRequestMissingTokenParamThenInvalidRequestError() throws Exception { + public void doFilterWhenTokenIntrospectionRequestMissingTokenThenInvalidRequestError() throws Exception { MockHttpServletRequest request = createTokenIntrospectionRequest( - this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue()); - request.removeParameter(TOKEN); + "token", OAuth2TokenType.ACCESS_TOKEN.getValue()); + request.removeParameter(OAuth2ParameterNames2.TOKEN); doFilterWhenTokenIntrospectionRequestInvalidParameterThenError( - TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request); + OAuth2ParameterNames2.TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request); } @Test - public void doFilterWhenTokenRequestMultipleTokenParamThenInvalidRequestError() throws Exception { + public void doFilterWhenTokenIntrospectionRequestMultipleTokenThenInvalidRequestError() throws Exception { MockHttpServletRequest request = createTokenIntrospectionRequest( - this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue()); - request.addParameter(TOKEN, "token.456"); + "token", OAuth2TokenType.ACCESS_TOKEN.getValue()); + request.addParameter(OAuth2ParameterNames2.TOKEN, "other-token"); doFilterWhenTokenIntrospectionRequestInvalidParameterThenError( - TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request); + OAuth2ParameterNames2.TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request); } @Test - public void doFilterWhenTokenRequestMultipleTokenTypeHintParamThenInvalidRequestError() throws Exception { + public void doFilterWhenTokenIntrospectionRequestMultipleTokenTypeHintThenInvalidRequestError() throws Exception { MockHttpServletRequest request = createTokenIntrospectionRequest( - this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue()); - request.addParameter(TOKEN_TYPE_HINT, OAuth2TokenType.REFRESH_TOKEN.getValue()); + "token", OAuth2TokenType.ACCESS_TOKEN.getValue()); + request.addParameter(OAuth2ParameterNames2.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue()); doFilterWhenTokenIntrospectionRequestInvalidParameterThenError( - TOKEN_TYPE_HINT, OAuth2ErrorCodes.INVALID_REQUEST, request); + OAuth2ParameterNames2.TOKEN_TYPE_HINT, OAuth2ErrorCodes.INVALID_REQUEST, request); } @Test - public void doFilterWhenIntrospectWithNullClaimsThenNotActiveTokenOkReponse() throws Exception { - MockHttpServletRequest request = createTokenIntrospectionRequest( - this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue()); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - Authentication clientPrincipal = setupSecurityContext(); - - OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken( - clientPrincipal, null); - - when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication); - - this.filter.doFilter(request, response, filterChain); - - verifyNoInteractions(filterChain); - - assertNotActiveTokenResponse(response); - } - - @Test - public void doFilterWhenIntrospectWithClaimsThenActiveTokenReponse() throws Exception { - MockHttpServletRequest request = createTokenIntrospectionRequest( - this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue()); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - Authentication clientPrincipal = setupSecurityContext(); - - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(Duration.ofHours(1)); - String clientId = "clientId"; - Map tokenIntrospectionClaims = new HashMap<>(); - tokenIntrospectionClaims.put(ACTIVE, true); - tokenIntrospectionClaims.put(CLIENT_ID, clientId); - tokenIntrospectionClaims.put(IAT, issuedAt); - tokenIntrospectionClaims.put(EXP, expiresAt); - tokenIntrospectionClaims.put(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER); - tokenIntrospectionClaims.put(SCOPE, "scope1 Scope2"); - - OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken( - clientPrincipal, tokenIntrospectionClaims); - - when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication); - - this.filter.doFilter(request, response, filterChain); - - verifyNoInteractions(filterChain); - - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = readTokenIntrospectionResponse(response); - - Map responseMap = tokenIntrospectionResponse.getClaims(); + public void doFilterWhenTokenIntrospectionRequestValidThenSuccessResponse() throws Exception { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, "token", + Instant.now(), Instant.now().plus(Duration.ofHours(1)), + new HashSet<>(Arrays.asList("scope1", "scope2"))); // @formatter:off - assertThat(responseMap).contains( - entry(ACTIVE, true), - entry(CLIENT_ID, clientId), - entry(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue()), - entry(EXP, expiresAt.getEpochSecond()), - entry(IAT, issuedAt.getEpochSecond())) - .hasEntrySatisfying(SCOPE, scopesMatchesInAnyOrder) - .hasSize(6); - // @formatter: on - } + OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder(true) + .clientId("authorized-client-id") + .username("authorizing-username") + .issuedAt(accessToken.getIssuedAt()) + .expiresAt(accessToken.getExpiresAt()) + .scopes(scopes -> scopes.addAll(accessToken.getScopes())) + .tokenType(accessToken.getTokenType().getValue()) + .notBefore(accessToken.getIssuedAt()) + .subject("authorizing-subject") + .audience("authorized-client-id") + .issuer("https://provider.com") + .id("jti") + .build(); + // @formatter:on + OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult = + new OAuth2TokenIntrospectionAuthenticationToken( + accessToken.getTokenValue(), clientPrincipal, tokenClaims); + + when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthenticationResult); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(clientPrincipal); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest request = createTokenIntrospectionRequest( + accessToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN.getValue()); + request.addParameter("custom-param-1", "custom-value-1"); + request.addParameter("custom-param-2", "custom-value-2"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + ArgumentCaptor tokenIntrospectionAuthentication = + ArgumentCaptor.forClass(OAuth2TokenIntrospectionAuthenticationToken.class); + + verifyNoInteractions(filterChain); + verify(this.authenticationManager).authenticate(tokenIntrospectionAuthentication.capture()); - private void assertNotActiveTokenResponse(MockHttpServletResponse response) throws Exception { assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); - OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = readTokenIntrospectionResponse(response); - assertThat(tokenIntrospectionResponse.getClaims()).containsEntry("active", false).hasSize(1); - } + assertThat(tokenIntrospectionAuthentication.getValue().getAdditionalParameters()) + .contains( + entry("custom-param-1", "custom-value-1"), + entry("custom-param-2", "custom-value-2")); - private OAuth2Error readError(MockHttpServletResponse response) throws Exception { - MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(), - HttpStatus.valueOf(response.getStatus())); - return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse); - } - - private OAuth2TokenIntrospectionClaims readTokenIntrospectionResponse(MockHttpServletResponse response) throws Exception { - MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(), - HttpStatus.valueOf(response.getStatus())); - return this.tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospectionClaims.class, httpResponse); + OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(response); + assertThat(tokenIntrospectionResponse.isActive()).isEqualTo(tokenClaims.isActive()); + assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(tokenClaims.getClientId()); + assertThat(tokenIntrospectionResponse.getUsername()).isEqualTo(tokenClaims.getUsername()); + assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween( + tokenClaims.getIssuedAt().minusSeconds(1), tokenClaims.getIssuedAt().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween( + tokenClaims.getExpiresAt().minusSeconds(1), tokenClaims.getExpiresAt().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getScope()).containsExactlyInAnyOrderElementsOf(tokenClaims.getScope()); + assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(tokenClaims.getTokenType()); + assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween( + tokenClaims.getNotBefore().minusSeconds(1), tokenClaims.getNotBefore().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(tokenClaims.getSubject()); + assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(tokenClaims.getAudience()); + assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(tokenClaims.getIssuer()); + assertThat(tokenIntrospectionResponse.getId()).isEqualTo(tokenClaims.getId()); } private void doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(String parameterName, String errorCode, @@ -244,8 +234,6 @@ public class OAuth2TokenIntrospectionEndpointFilterTests { MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - setupSecurityContext(); - this.filter.doFilter(request, response, filterChain); verifyNoInteractions(filterChain); @@ -256,23 +244,25 @@ public class OAuth2TokenIntrospectionEndpointFilterTests { assertThat(error.getDescription()).isEqualTo("OAuth 2.0 Token Introspection Parameter: " + parameterName); } - private static Authentication setupSecurityContext() { - RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(clientPrincipal); - SecurityContextHolder.setContext(securityContext); - return clientPrincipal; + private OAuth2Error readError(MockHttpServletResponse response) throws Exception { + MockClientHttpResponse httpResponse = new MockClientHttpResponse( + response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus())); + return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse); + } + + private OAuth2TokenIntrospection readTokenIntrospectionResponse(MockHttpServletResponse response) throws Exception { + MockClientHttpResponse httpResponse = new MockClientHttpResponse( + response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus())); + return this.tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospection.class, httpResponse); } private static MockHttpServletRequest createTokenIntrospectionRequest(String token, String tokenTypeHint) { String requestUri = OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI; MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); request.setServletPath(requestUri); - - request.addParameter(TOKEN, token); - request.addParameter(TOKEN_TYPE_HINT, tokenTypeHint); + request.addParameter(OAuth2ParameterNames2.TOKEN, token); + request.addParameter(OAuth2ParameterNames2.TOKEN_TYPE_HINT, tokenTypeHint); return request; + } } -}