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 a7e9e697..4346d94a 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 @@ -26,6 +26,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import com.nimbusds.jose.jwk.source.JWKSource; + import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -367,11 +369,12 @@ public final class OAuth2AuthorizationServerConfigurer jwkSource = OAuth2ConfigurerUtils.getJwkSource(builder); + if (jwkSource != null) { + NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter( + jwkSource, providerSettings.getJwkSetEndpoint()); + builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class); + } OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter = new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ConfigurerUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ConfigurerUtils.java index a158587e..f0f5c6ec 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ConfigurerUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ConfigurerUtils.java @@ -34,9 +34,11 @@ import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2Au import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; @@ -93,28 +95,55 @@ final class OAuth2ConfigurerUtils { if (tokenGenerator == null) { tokenGenerator = getOptionalBean(builder, OAuth2TokenGenerator.class); if (tokenGenerator == null) { - JwtGenerator jwtGenerator = new JwtGenerator(getJwtEncoder(builder)); - OAuth2TokenCustomizer jwtCustomizer = getJwtCustomizer(builder); - if (jwtCustomizer != null) { - jwtGenerator.setJwtCustomizer(jwtCustomizer); + JwtGenerator jwtGenerator = getJwtGenerator(builder); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + OAuth2TokenCustomizer accessTokenCustomizer = getAccessTokenCustomizer(builder); + if (accessTokenCustomizer != null) { + accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer); } OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); - tokenGenerator = new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator); + if (jwtGenerator != null) { + tokenGenerator = new DelegatingOAuth2TokenGenerator( + jwtGenerator, accessTokenGenerator, refreshTokenGenerator); + } else { + tokenGenerator = new DelegatingOAuth2TokenGenerator( + accessTokenGenerator, refreshTokenGenerator); + } } builder.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator); } return tokenGenerator; } + private static > JwtGenerator getJwtGenerator(B builder) { + JwtGenerator jwtGenerator = builder.getSharedObject(JwtGenerator.class); + if (jwtGenerator == null) { + JwtEncoder jwtEncoder = getJwtEncoder(builder); + if (jwtEncoder != null) { + jwtGenerator = new JwtGenerator(jwtEncoder); + OAuth2TokenCustomizer jwtCustomizer = getJwtCustomizer(builder); + if (jwtCustomizer != null) { + jwtGenerator.setJwtCustomizer(jwtCustomizer); + } + builder.setSharedObject(JwtGenerator.class, jwtGenerator); + } + } + return jwtGenerator; + } + private static > JwtEncoder getJwtEncoder(B builder) { JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class); if (jwtEncoder == null) { jwtEncoder = getOptionalBean(builder, JwtEncoder.class); if (jwtEncoder == null) { JWKSource jwkSource = getJwkSource(builder); - jwtEncoder = new NimbusJwsEncoder(jwkSource); + if (jwkSource != null) { + jwtEncoder = new NimbusJwsEncoder(jwkSource); + } + } + if (jwtEncoder != null) { + builder.setSharedObject(JwtEncoder.class, jwtEncoder); } - builder.setSharedObject(JwtEncoder.class, jwtEncoder); } return jwtEncoder; } @@ -124,23 +153,22 @@ final class OAuth2ConfigurerUtils { JWKSource jwkSource = builder.getSharedObject(JWKSource.class); if (jwkSource == null) { ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class); - jwkSource = getBean(builder, type); - builder.setSharedObject(JWKSource.class, jwkSource); + jwkSource = getOptionalBean(builder, type); + if (jwkSource != null) { + builder.setSharedObject(JWKSource.class, jwkSource); + } } return jwkSource; } - @SuppressWarnings("unchecked") private static > OAuth2TokenCustomizer getJwtCustomizer(B builder) { - OAuth2TokenCustomizer jwtCustomizer = builder.getSharedObject(OAuth2TokenCustomizer.class); - if (jwtCustomizer == null) { - ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class); - jwtCustomizer = getOptionalBean(builder, type); - if (jwtCustomizer != null) { - builder.setSharedObject(OAuth2TokenCustomizer.class, jwtCustomizer); - } - } - return jwtCustomizer; + ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class); + return getOptionalBean(builder, type); + } + + private static > OAuth2TokenCustomizer getAccessTokenCustomizer(B builder) { + ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class); + return getOptionalBean(builder, type); } static > ProviderSettings getProviderSettings(B builder) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimAccessor.java new file mode 100644 index 00000000..ed87a0df --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimAccessor.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020-2022 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 java.net.URL; +import java.time.Instant; +import java.util.List; + +/** + * A {@link ClaimAccessor} for the "claims" that may be contained in an {@link OAuth2TokenClaimsSet}. + * + * @author Joe Grandja + * @since 0.2.3 + * @see ClaimAccessor + * @see OAuth2TokenClaimNames + * @see OAuth2TokenClaimsSet + */ +public interface OAuth2TokenClaimAccessor extends ClaimAccessor { + + /** + * Returns the Issuer {@code (iss)} claim which identifies the principal that issued the OAuth 2.0 Token. + * + * @return the Issuer identifier + */ + default URL getIssuer() { + return getClaimAsURL(OAuth2TokenClaimNames.ISS); + } + + /** + * Returns the Subject {@code (sub)} claim which identifies the principal that is the subject of the OAuth 2.0 Token. + * + * @return the Subject identifier + */ + default String getSubject() { + return getClaimAsString(OAuth2TokenClaimNames.SUB); + } + + /** + * Returns the Audience {@code (aud)} claim which identifies the recipient(s) that the OAuth 2.0 Token is intended for. + * + * @return the Audience(s) that this OAuth 2.0 Token is intended for + */ + default List getAudience() { + return getClaimAsStringList(OAuth2TokenClaimNames.AUD); + } + + /** + * Returns the Expiration time {@code (exp)} claim which identifies the expiration time on or after + * which the OAuth 2.0 Token MUST NOT be accepted for processing. + * + * @return the Expiration time on or after which the OAuth 2.0 Token MUST NOT be accepted for processing + */ + default Instant getExpiresAt() { + return getClaimAsInstant(OAuth2TokenClaimNames.EXP); + } + + /** + * Returns the Not Before {@code (nbf)} claim which identifies the time before + * which the OAuth 2.0 Token MUST NOT be accepted for processing. + * + * @return the Not Before time before which the OAuth 2.0 Token MUST NOT be accepted for processing + */ + default Instant getNotBefore() { + return getClaimAsInstant(OAuth2TokenClaimNames.NBF); + } + + /** + * Returns the Issued at {@code (iat)} claim which identifies the time at which the OAuth 2.0 Token was issued. + * + * @return the Issued at claim which identifies the time at which the OAuth 2.0 Token was issued + */ + default Instant getIssuedAt() { + return getClaimAsInstant(OAuth2TokenClaimNames.IAT); + } + + /** + * Returns the ID {@code (jti)} claim which provides a unique identifier for the OAuth 2.0 Token. + * + * @return the ID claim which provides a unique identifier for the OAuth 2.0 Token + */ + default String getId() { + return getClaimAsString(OAuth2TokenClaimNames.JTI); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimNames.java new file mode 100644 index 00000000..8d93e700 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimNames.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2022 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; + +/** + * The names of the "claims" that may be contained in an {@link OAuth2TokenClaimsSet} + * and are associated to an {@link OAuth2Token}. + * + * @author Joe Grandja + * @since 0.2.3 + * @see OAuth2TokenClaimAccessor + * @see OAuth2TokenClaimsSet + * @see OAuth2Token + */ +public interface OAuth2TokenClaimNames { + + /** + * {@code iss} - the Issuer claim identifies the principal that issued the OAuth 2.0 Token + */ + String ISS = "iss"; + + /** + * {@code sub} - the Subject claim identifies the principal that is the subject of the OAuth 2.0 Token + */ + String SUB = "sub"; + + /** + * {@code aud} - the Audience claim identifies the recipient(s) that the OAuth 2.0 Token is intended for + */ + String AUD = "aud"; + + /** + * {@code exp} - the Expiration time claim identifies the expiration time on or after + * which the OAuth 2.0 Token MUST NOT be accepted for processing + */ + String EXP = "exp"; + + /** + * {@code nbf} - the Not Before claim identifies the time before which the OAuth 2.0 Token + * MUST NOT be accepted for processing + */ + String NBF = "nbf"; + + /** + * {@code iat} - The Issued at claim identifies the time at which the OAuth 2.0 Token was issued + */ + String IAT = "iat"; + + /** + * {@code jti} - The ID claim provides a unique identifier for the OAuth 2.0 Token + */ + String JTI = "jti"; + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSet.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSet.java new file mode 100644 index 00000000..c0a3349c --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSet.java @@ -0,0 +1,187 @@ +/* + * Copyright 2020-2022 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 java.net.URL; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.security.oauth2.core.converter.ClaimConversionService; +import org.springframework.util.Assert; + +/** + * A representation of a set of claims that are associated to an {@link OAuth2Token}. + * + * @author Joe Grandja + * @since 0.2.3 + * @see OAuth2TokenClaimAccessor + * @see OAuth2TokenClaimNames + * @see OAuth2Token + */ +public final class OAuth2TokenClaimsSet implements OAuth2TokenClaimAccessor { + private final Map claims; + + private OAuth2TokenClaimsSet(Map claims) { + this.claims = Collections.unmodifiableMap(new HashMap<>(claims)); + } + + @Override + public Map getClaims() { + return this.claims; + } + + /** + * Returns a new {@link Builder}. + * + * @return the {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for {@link OAuth2TokenClaimsSet}. + */ + public static final class Builder { + private final Map claims = new HashMap<>(); + + private Builder() { + } + + /** + * Sets the issuer {@code (iss)} claim, which identifies the principal that issued the OAuth 2.0 Token. + * + * @param issuer the issuer identifier + * @return the {@link Builder} + */ + public Builder issuer(String issuer) { + return claim(OAuth2TokenClaimNames.ISS, issuer); + } + + /** + * Sets the subject {@code (sub)} claim, which identifies the principal that is the subject of the OAuth 2.0 Token. + * + * @param subject the subject identifier + * @return the {@link Builder} + */ + public Builder subject(String subject) { + return claim(OAuth2TokenClaimNames.SUB, subject); + } + + /** + * Sets the audience {@code (aud)} claim, which identifies the recipient(s) that the OAuth 2.0 Token is intended for. + * + * @param audience the audience that this OAuth 2.0 Token is intended for + * @return the {@link Builder} + */ + public Builder audience(List audience) { + return claim(OAuth2TokenClaimNames.AUD, audience); + } + + /** + * Sets the expiration time {@code (exp)} claim, which identifies the time on or after + * which the OAuth 2.0 Token MUST NOT be accepted for processing. + * + * @param expiresAt the time on or after which the OAuth 2.0 Token MUST NOT be accepted for processing + * @return the {@link Builder} + */ + public Builder expiresAt(Instant expiresAt) { + return claim(OAuth2TokenClaimNames.EXP, expiresAt); + } + + /** + * Sets the not before {@code (nbf)} claim, which identifies the time before + * which the OAuth 2.0 Token MUST NOT be accepted for processing. + * + * @param notBefore the time before which the OAuth 2.0 Token MUST NOT be accepted for processing + * @return the {@link Builder} + */ + public Builder notBefore(Instant notBefore) { + return claim(OAuth2TokenClaimNames.NBF, notBefore); + } + + /** + * Sets the issued at {@code (iat)} claim, which identifies the time at which the OAuth 2.0 Token was issued. + * + * @param issuedAt the time at which the OAuth 2.0 Token was issued + * @return the {@link Builder} + */ + public Builder issuedAt(Instant issuedAt) { + return claim(OAuth2TokenClaimNames.IAT, issuedAt); + } + + /** + * Sets the ID {@code (jti)} claim, which provides a unique identifier for the OAuth 2.0 Token. + * + * @param jti the unique identifier for the OAuth 2.0 Token + * @return the {@link Builder} + */ + public Builder id(String jti) { + return claim(OAuth2TokenClaimNames.JTI, jti); + } + + /** + * Sets the claim. + * + * @param name the claim name + * @param value the claim value + * @return the {@link Builder} + */ + 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; + } + + /** + * A {@code Consumer} to be provided access to the claims allowing the ability to add, replace, or remove. + * + * @param claimsConsumer a {@code Consumer} of the claims + */ + public Builder claims(Consumer> claimsConsumer) { + claimsConsumer.accept(this.claims); + return this; + } + + /** + * Builds a new {@link OAuth2TokenClaimsSet}. + * + * @return a {@link OAuth2TokenClaimsSet} + */ + public OAuth2TokenClaimsSet build() { + Assert.notEmpty(this.claims, "claims cannot be empty"); + + // The value of the 'iss' claim is a String or URL (StringOrURI). + // Attempt to convert to URL. + Object issuer = this.claims.get(OAuth2TokenClaimNames.ISS); + if (issuer != null) { + URL convertedValue = ClaimConversionService.getSharedInstance().convert(issuer, URL.class); + if (convertedValue != null) { + this.claims.put(OAuth2TokenClaimNames.ISS, convertedValue); + } + } + + return new OAuth2TokenClaimsSet(this.claims); + } + + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenFormat.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenFormat.java new file mode 100644 index 00000000..9d0568e5 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenFormat.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020-2022 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 java.io.Serializable; + +import org.springframework.util.Assert; + +/** + * Standard data formats for OAuth 2.0 Tokens. + * + * @author Joe Grandja + * @since 0.2.3 + */ +public final class OAuth2TokenFormat implements Serializable { + private static final long serialVersionUID = Version.SERIAL_VERSION_UID; + + /** + * Self-contained tokens use a protected, time-limited data structure that contains token metadata + * and claims of the user and/or client. JSON Web Token (JWT) is a widely used format. + */ + public static final OAuth2TokenFormat SELF_CONTAINED = new OAuth2TokenFormat("self-contained"); + + /** + * Reference (opaque) tokens are unique identifiers that serve as a reference + * to the token metadata and claims of the user and/or client, stored at the provider. + */ + public static final OAuth2TokenFormat REFERENCE = new OAuth2TokenFormat("reference"); + + private final String value; + + /** + * Constructs an {@code OAuth2TokenFormat} using the provided value. + * + * @param value the value of the token format + */ + public OAuth2TokenFormat(String value) { + Assert.hasText(value, "value cannot be empty"); + this.value = value; + } + + /** + * Returns the value of the token format. + * + * @return the value of the token format + */ + public String getValue() { + return this.value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + OAuth2TokenFormat that = (OAuth2TokenFormat) obj; + return getValue().equals(that.getValue()); + } + + @Override + public int hashCode() { + return getValue().hashCode(); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JwtGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JwtGenerator.java index b55b9e53..1c7144b7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JwtGenerator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JwtGenerator.java @@ -23,6 +23,7 @@ import java.util.function.Consumer; import org.springframework.lang.Nullable; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -75,6 +76,10 @@ public final class JwtGenerator implements OAuth2TokenGenerator { !OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue()))) { return null; } + if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && + !OAuth2TokenFormat.SELF_CONTAINED.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) { + return null; + } String issuer = null; if (context.getProviderContext() != null) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGenerator.java new file mode 100644 index 00000000..4409a1b6 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGenerator.java @@ -0,0 +1,148 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization; + +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; + +import org.springframework.lang.Nullable; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; +import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * An {@link OAuth2TokenGenerator} that generates a + * {@link OAuth2TokenFormat#REFERENCE "reference"} (opaque) {@link OAuth2AccessToken}. + * + * @author Joe Grandja + * @since 0.2.3 + * @see OAuth2TokenGenerator + * @see OAuth2AccessToken + * @see OAuth2TokenCustomizer + * @see OAuth2TokenClaimsContext + * @see OAuth2TokenClaimsSet + */ +public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator { + private final StringKeyGenerator accessTokenGenerator = + new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + private OAuth2TokenCustomizer accessTokenCustomizer; + + @Nullable + @Override + public OAuth2AccessToken generate(OAuth2TokenContext context) { + if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || + !OAuth2TokenFormat.REFERENCE.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) { + return null; + } + + String issuer = null; + if (context.getProviderContext() != null) { + issuer = context.getProviderContext().getIssuer(); + } + RegisteredClient registeredClient = context.getRegisteredClient(); + + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive()); + + // @formatter:off + OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder(); + if (StringUtils.hasText(issuer)) { + claimsBuilder.issuer(issuer); + } + claimsBuilder + .subject(context.getPrincipal().getName()) + .audience(Collections.singletonList(registeredClient.getClientId())) + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .notBefore(issuedAt) + .id(UUID.randomUUID().toString()); + if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) { + claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes()); + } + // @formatter:on + + if (this.accessTokenCustomizer != null) { + // @formatter:off + OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder) + .registeredClient(context.getRegisteredClient()) + .principal(context.getPrincipal()) + .providerContext(context.getProviderContext()) + .authorizedScopes(context.getAuthorizedScopes()) + .tokenType(context.getTokenType()) + .authorizationGrantType(context.getAuthorizationGrantType()); + if (context.getAuthorization() != null) { + accessTokenContextBuilder.authorization(context.getAuthorization()); + } + if (context.getAuthorizationGrant() != null) { + accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant()); + } + // @formatter:on + + OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build(); + this.accessTokenCustomizer.customize(accessTokenContext); + } + + OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build(); + + OAuth2AccessToken accessToken = new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER, + this.accessTokenGenerator.generateKey(), accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(), + context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims()); + + return accessToken; + } + + /** + * Sets the {@link OAuth2TokenCustomizer} that customizes the + * {@link OAuth2TokenClaimsContext.Builder#claims(Consumer) claims} for the {@link OAuth2AccessToken}. + * + * @param accessTokenCustomizer the {@link OAuth2TokenCustomizer} that customizes the claims for the {@code OAuth2AccessToken} + */ + public void setAccessTokenCustomizer(OAuth2TokenCustomizer accessTokenCustomizer) { + Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null"); + this.accessTokenCustomizer = accessTokenCustomizer; + } + + private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor { + private final Map claims; + + private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, + Instant issuedAt, Instant expiresAt, Set scopes, Map claims) { + super(tokenType, tokenValue, issuedAt, expiresAt, scopes); + this.claims = claims; + } + + @Override + public Map getClaims() { + return this.claims; + } + + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContext.java new file mode 100644 index 00000000..85dd9cf9 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContext.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet; +import org.springframework.util.Assert; + +/** + * An {@link OAuth2TokenContext} implementation that provides access + * to the {@link #getClaims() claims} of an OAuth 2.0 Token, allowing the ability to customize. + * + * @author Joe Grandja + * @since 0.2.3 + * @see OAuth2TokenContext + * @see OAuth2TokenClaimsSet.Builder + */ +public final class OAuth2TokenClaimsContext implements OAuth2TokenContext { + private final Map context; + + private OAuth2TokenClaimsContext(Map context) { + this.context = Collections.unmodifiableMap(new HashMap<>(context)); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public V get(Object key) { + return hasKey(key) ? (V) this.context.get(key) : null; + } + + @Override + public boolean hasKey(Object key) { + Assert.notNull(key, "key cannot be null"); + return this.context.containsKey(key); + } + + /** + * Returns the {@link OAuth2TokenClaimsSet.Builder claims} + * allowing the ability to add, replace, or remove. + * + * @return the {@link OAuth2TokenClaimsSet.Builder} + */ + public OAuth2TokenClaimsSet.Builder getClaims() { + return get(OAuth2TokenClaimsSet.Builder.class); + } + + /** + * Constructs a new {@link Builder} with the provided claims. + * + * @param claimsBuilder the claims to initialize the builder + * @return the {@link Builder} + */ + public static Builder with(OAuth2TokenClaimsSet.Builder claimsBuilder) { + return new Builder(claimsBuilder); + } + + /** + * A builder for {@link OAuth2TokenClaimsContext}. + */ + public static final class Builder extends AbstractBuilder { + + private Builder(OAuth2TokenClaimsSet.Builder claimsBuilder) { + Assert.notNull(claimsBuilder, "claimsBuilder cannot be null"); + put(OAuth2TokenClaimsSet.Builder.class, claimsBuilder); + } + + /** + * A {@code Consumer} of the {@link OAuth2TokenClaimsSet.Builder claims} + * allowing the ability to add, replace, or remove. + * + * @param claimsConsumer a {@code Consumer} of the {@link OAuth2TokenClaimsSet.Builder claims} + * @return the {@link Builder} for further configuration + */ + public Builder claims(Consumer claimsConsumer) { + claimsConsumer.accept(get(OAuth2TokenClaimsSet.Builder.class)); + return this; + } + + /** + * Builds a new {@link OAuth2TokenClaimsContext}. + * + * @return the {@link OAuth2TokenClaimsContext} + */ + public OAuth2TokenClaimsContext build() { + return new OAuth2TokenClaimsContext(getContext()); + } + + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenGenerator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenGenerator.java index c2bb3a20..ffb5e825 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenGenerator.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenGenerator.java @@ -16,7 +16,9 @@ package org.springframework.security.oauth2.server.authorization; import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet; /** * Implementations of this interface are responsible for generating an {@link OAuth2Token} @@ -26,6 +28,8 @@ import org.springframework.security.oauth2.core.OAuth2Token; * @since 0.2.3 * @see OAuth2Token * @see OAuth2TokenContext + * @see OAuth2TokenClaimsSet + * @see ClaimAccessor * @param the type of the OAuth 2.0 Token */ @FunctionalInterface @@ -35,6 +39,10 @@ public interface OAuth2TokenGenerator { * Generate an OAuth 2.0 Token using the attributes contained in the {@link OAuth2TokenContext}, * or return {@code null} if the {@link OAuth2TokenContext#getTokenType()} is not supported. * + *

+ * If the returned {@link OAuth2Token} has a set of claims, it should implement {@link ClaimAccessor} + * in order for it to be stored with the {@link OAuth2Authorization}. + * * @param context the context containing the OAuth 2.0 Token attributes * @return an {@link OAuth2Token} or {@code null} if the {@link OAuth2TokenContext#getTokenType()} is not supported */ diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index da38613b..31af50d4 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -27,6 +27,7 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -47,6 +48,7 @@ import org.springframework.security.oauth2.server.authorization.DefaultOAuth2Tok import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator; @@ -103,8 +105,8 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); this.authorizationService = authorizationService; this.jwtGenerator = new JwtGenerator(jwtEncoder); - this.tokenGenerator = new DelegatingOAuth2TokenGenerator( - this.jwtGenerator, new OAuth2RefreshTokenGenerator()); + this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator, + new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator()); } /** @@ -216,9 +218,9 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - if (generatedAccessToken instanceof Jwt) { + if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) generatedAccessToken).getClaims())); + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); } else { authorizationBuilder.accessToken(accessToken); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java index d19744b7..360b7fd4 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java @@ -23,6 +23,7 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -32,8 +33,10 @@ import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext; @@ -65,6 +68,9 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator tokenGenerator; + // TODO Remove after removing @Deprecated OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder) + private JwtGenerator jwtGenerator; + /** * Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters. * @@ -78,7 +84,9 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth Assert.notNull(authorizationService, "authorizationService cannot be null"); Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); this.authorizationService = authorizationService; - this.tokenGenerator = new JwtGenerator(jwtEncoder); + this.jwtGenerator = new JwtGenerator(jwtEncoder); + this.tokenGenerator = new DelegatingOAuth2TokenGenerator( + this.jwtGenerator, new OAuth2AccessTokenGenerator()); } /** @@ -107,8 +115,8 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth @Deprecated public void setJwtCustomizer(OAuth2TokenCustomizer jwtCustomizer) { Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null"); - if (this.tokenGenerator instanceof JwtGenerator) { - ((JwtGenerator) this.tokenGenerator).setJwtCustomizer(jwtCustomizer); + if (this.jwtGenerator != null) { + this.jwtGenerator.setJwtCustomizer(jwtCustomizer); } } @@ -167,9 +175,9 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes); // @formatter:on - if (generatedAccessToken instanceof Jwt) { + if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) generatedAccessToken).getClaims())); + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); } else { authorizationBuilder.accessToken(accessToken); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java index a25fc156..25b1d1cb 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java @@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -44,6 +45,7 @@ import org.springframework.security.oauth2.server.authorization.DefaultOAuth2Tok import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator; @@ -97,8 +99,8 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); this.authorizationService = authorizationService; this.jwtGenerator = new JwtGenerator(jwtEncoder); - this.tokenGenerator = new DelegatingOAuth2TokenGenerator( - this.jwtGenerator, new OAuth2RefreshTokenGenerator()); + this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator, + new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator()); } /** @@ -215,9 +217,9 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - if (generatedAccessToken instanceof Jwt) { + if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> { - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) generatedAccessToken).getClaims()); + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()); metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false); }); } else { 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 435b9925..f2e6ee41 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 @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -18,15 +18,14 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.net.URL; 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.core.OAuth2TokenClaimAccessor; 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.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; @@ -121,25 +120,23 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut tokenClaims.scopes(scopes -> scopes.addAll(accessToken.getScopes())); tokenClaims.tokenType(accessToken.getTokenType().getValue()); - Map claims = authorizedToken.getClaims(); - if (!CollectionUtils.isEmpty(claims)) { - // Assuming JWT as it's the only (currently) supported access token format - JwtClaimAccessor jwtClaims = () -> claims; + if (!CollectionUtils.isEmpty(authorizedToken.getClaims())) { + OAuth2TokenClaimAccessor accessTokenClaims = authorizedToken::getClaims; - Instant notBefore = jwtClaims.getNotBefore(); + Instant notBefore = accessTokenClaims.getNotBefore(); if (notBefore != null) { tokenClaims.notBefore(notBefore); } - tokenClaims.subject(jwtClaims.getSubject()); - List audience = jwtClaims.getAudience(); + tokenClaims.subject(accessTokenClaims.getSubject()); + List audience = accessTokenClaims.getAudience(); if (!CollectionUtils.isEmpty(audience)) { tokenClaims.audiences(audiences -> audiences.addAll(audience)); } - URL issuer = jwtClaims.getIssuer(); + URL issuer = accessTokenClaims.getIssuer(); if (issuer != null) { tokenClaims.issuer(issuer.toExternalForm()); } - String jti = jwtClaims.getId(); + String jti = accessTokenClaims.getId(); if (StringUtils.hasText(jti)) { tokenClaims.id(jti); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java index 7a475189..70ad8e7a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -40,7 +40,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.server.authorization.config.ClientSettings; +import org.springframework.security.oauth2.server.authorization.config.ConfigurationSettingNames; import org.springframework.security.oauth2.server.authorization.config.TokenSettings; import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; import org.springframework.util.Assert; @@ -241,7 +243,11 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build()); Map tokenSettingsMap = parseMap(rs.getString("token_settings")); - builder.tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build()); + TokenSettings.Builder tokenSettingsBuilder = TokenSettings.withSettings(tokenSettingsMap); + if (!tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) { + tokenSettingsBuilder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED); + } + builder.tokenSettings(tokenSettingsBuilder.build()); return builder.build(); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java index d9c0f88c..5a85175e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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,6 +16,7 @@ package org.springframework.security.oauth2.server.authorization.config; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -132,6 +133,12 @@ public final class ConfigurationSettingNames { */ public static final String ACCESS_TOKEN_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("access-token-time-to-live"); + /** + * Set the {@link OAuth2TokenFormat token format} for an access token. + * @since 0.2.3 + */ + public static final String ACCESS_TOKEN_FORMAT = TOKEN_SETTINGS_NAMESPACE.concat("access-token-format"); + /** * Set to {@code true} if refresh tokens are reused when returning the access token response, * or {@code false} if a new refresh token is issued. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java index 5d34a17d..ba82fcf0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.config; import java.time.Duration; import java.util.Map; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.util.Assert; @@ -45,6 +46,17 @@ public final class TokenSettings extends AbstractSettings { return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE); } + /** + * Returns the token format for an access token. + * The default is {@link OAuth2TokenFormat#SELF_CONTAINED}. + * + * @return the token format for an access token + * @since 0.2.3 + */ + public OAuth2TokenFormat getAccessTokenFormat() { + return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT); + } + /** * Returns {@code true} if refresh tokens are reused when returning the access token response, * or {@code false} if a new refresh token is issued. The default is {@code true}. @@ -80,6 +92,7 @@ public final class TokenSettings extends AbstractSettings { public static Builder builder() { return new Builder() .accessTokenTimeToLive(Duration.ofMinutes(5)) + .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) .reuseRefreshTokens(true) .refreshTokenTimeToLive(Duration.ofMinutes(60)) .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256); @@ -117,6 +130,18 @@ public final class TokenSettings extends AbstractSettings { return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive); } + /** + * Set the token format for an access token. + * + * @param accessTokenFormat the token format for an access token + * @return the {@link Builder} for further configuration + * @since 0.2.3 + */ + public Builder accessTokenFormat(OAuth2TokenFormat accessTokenFormat) { + Assert.notNull(accessTokenFormat, "accessTokenFormat cannot be null"); + return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT, accessTokenFormat); + } + /** * Set to {@code true} if refresh tokens are reused when returning the access token response, * or {@code false} if a new refresh token is issued. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java index c1497d14..e3e6a760 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -24,6 +24,7 @@ import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -38,6 +39,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; *

  • {@link OAuth2AuthorizationRequestMixin}
  • *
  • {@link DurationMixin}
  • *
  • {@link JwsAlgorithmMixin}
  • + *
  • {@link OAuth2TokenFormatMixin}
  • * * * If not already enabled, default typing will be automatically enabled as type info is @@ -60,6 +62,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; * @see OAuth2AuthorizationRequestMixin * @see DurationMixin * @see JwsAlgorithmMixin + * @see OAuth2TokenFormatMixin */ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule { @@ -78,6 +81,7 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule { context.setMixInAnnotations(Duration.class, DurationMixin.class); context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class); context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class); + context.setMixInAnnotations(OAuth2TokenFormat.class, OAuth2TokenFormatMixin.class); } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenFormatMixin.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenFormatMixin.java new file mode 100644 index 00000000..4da8bde3 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenFormatMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.OAuth2TokenFormat; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2TokenFormat}. + * + * @author Joe Grandja + * @since 0.2.3 + * @see OAuth2TokenFormat + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2TokenFormatMixin { + + @JsonCreator + OAuth2TokenFormatMixin(@JsonProperty("value") String value) { + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java index 4886af96..4c05a177 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java @@ -33,6 +33,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -46,7 +47,6 @@ import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimName import org.springframework.security.oauth2.core.oidc.OidcClientRegistration; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.DefaultOAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; @@ -286,9 +286,9 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes); // @formatter:on - if (registrationAccessToken instanceof Jwt) { + if (registrationAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) registrationAccessToken).getClaims())); + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) registrationAccessToken).getClaims())); } else { authorizationBuilder.accessToken(accessToken); } 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 217de6a3..2f5af429 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 @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -15,10 +15,16 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; @@ -28,10 +34,12 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.jdbc.core.JdbcOperations; @@ -48,18 +56,26 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthorizationCode; import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.OAuth2TokenIntrospection; import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; 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.JdbcOAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper; @@ -67,6 +83,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere 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.security.oauth2.server.authorization.config.TokenSettings; import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -74,7 +91,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -88,8 +106,11 @@ public class OAuth2TokenIntrospectionTests { private static EmbeddedDatabase db; private static JWKSource jwkSource; private static ProviderSettings providerSettings; - private final HttpMessageConverter tokenIntrospectionHttpResponseConverter = + private static OAuth2TokenCustomizer accessTokenCustomizer; + private static final HttpMessageConverter tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionHttpMessageConverter(); + private static final HttpMessageConverter accessTokenHttpResponseConverter = + new OAuth2AccessTokenResponseHttpMessageConverter(); @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -111,6 +132,7 @@ public class OAuth2TokenIntrospectionTests { JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK); jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); providerSettings = ProviderSettings.builder().tokenIntrospectionEndpoint("/test/introspect").build(); + accessTokenCustomizer = mock(OAuth2TokenCustomizer.class); db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(EmbeddedDatabaseType.HSQL) @@ -145,9 +167,19 @@ public class OAuth2TokenIntrospectionTests { OAuth2AccessToken accessToken = new OAuth2AccessToken( OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt, new HashSet<>(Arrays.asList("scope1", "scope2"))); - JwtClaimsSet tokenClaims = TestJwtClaimsSets.jwtClaimsSet().build(); + // @formatter:off + OAuth2TokenClaimsSet accessTokenClaims = OAuth2TokenClaimsSet.builder() + .issuer("https://provider.com") + .subject("subject") + .audience(Collections.singletonList(authorizedRegisteredClient.getClientId())) + .issuedAt(issuedAt) + .notBefore(issuedAt) + .expiresAt(expiresAt) + .id("id") + .build(); + // @formatter:on OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(authorizedRegisteredClient, accessToken, tokenClaims.getClaims()) + .authorization(authorizedRegisteredClient, accessToken, accessTokenClaims.getClaims()) .build(); this.registeredClientRepository.save(authorizedRegisteredClient); this.authorizationService.save(authorization); @@ -155,7 +187,7 @@ public class OAuth2TokenIntrospectionTests { // @formatter:off MvcResult mvcResult = this.mvc.perform(post(providerSettings.getTokenIntrospectionEndpoint()) .params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN)) - .with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret()))) + .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient))) .andExpect(status().isOk()) .andReturn(); // @formatter:on @@ -165,17 +197,17 @@ public class OAuth2TokenIntrospectionTests { assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId()); assertThat(tokenIntrospectionResponse.getUsername()).isNull(); assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween( - accessToken.getIssuedAt().minusSeconds(1), accessToken.getIssuedAt().plusSeconds(1)); + accessTokenClaims.getIssuedAt().minusSeconds(1), accessTokenClaims.getIssuedAt().plusSeconds(1)); assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween( - accessToken.getExpiresAt().minusSeconds(1), accessToken.getExpiresAt().plusSeconds(1)); + accessTokenClaims.getExpiresAt().minusSeconds(1), accessTokenClaims.getExpiresAt().plusSeconds(1)); assertThat(tokenIntrospectionResponse.getScopes()).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()); + accessTokenClaims.getNotBefore().minusSeconds(1), accessTokenClaims.getNotBefore().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(accessTokenClaims.getSubject()); + assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(accessTokenClaims.getAudience()); + assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(accessTokenClaims.getIssuer()); + assertThat(tokenIntrospectionResponse.getId()).isEqualTo(accessTokenClaims.getId()); } @Test @@ -195,7 +227,7 @@ public class OAuth2TokenIntrospectionTests { // @formatter:off MvcResult mvcResult = this.mvc.perform(post(providerSettings.getTokenIntrospectionEndpoint()) .params(getTokenIntrospectionRequestParameters(refreshToken, OAuth2TokenType.REFRESH_TOKEN)) - .with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret()))) + .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient))) .andExpect(status().isOk()) .andReturn(); // @formatter:on @@ -217,6 +249,71 @@ public class OAuth2TokenIntrospectionTests { assertThat(tokenIntrospectionResponse.getId()).isNull(); } + @Test + public void requestWhenObtainReferenceAccessTokenAndIntrospectThenActive() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // @formatter:off + TokenSettings tokenSettings = TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .build(); + RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient() + .tokenSettings(tokenSettings) + .build(); + // @formatter:on + this.registeredClientRepository.save(authorizedRegisteredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build(); + this.authorizationService.save(authorization); + + // @formatter:off + MvcResult mvcResult = this.mvc.perform(post(providerSettings.getTokenEndpoint()) + .params(getAuthorizationCodeTokenRequestParameters(authorizedRegisteredClient, authorization)) + .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(authorizedRegisteredClient))) + .andExpect(status().isOk()) + .andReturn(); + // @formatter:on + + OAuth2AccessTokenResponse accessTokenResponse = readAccessTokenResponse(mvcResult); + OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + + RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build(); + this.registeredClientRepository.save(introspectRegisteredClient); + + // @formatter:off + mvcResult = this.mvc.perform(post(providerSettings.getTokenIntrospectionEndpoint()) + .params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN)) + .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient))) + .andExpect(status().isOk()) + .andReturn(); + // @formatter:on + + OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult); + + ArgumentCaptor accessTokenClaimsContextCaptor = ArgumentCaptor.forClass(OAuth2TokenClaimsContext.class); + verify(accessTokenCustomizer).customize(accessTokenClaimsContextCaptor.capture()); + + OAuth2TokenClaimsContext accessTokenClaimsContext = accessTokenClaimsContextCaptor.getValue(); + OAuth2TokenClaimsSet accessTokenClaims = accessTokenClaimsContext.getClaims().build(); + + assertThat(tokenIntrospectionResponse.isActive()).isTrue(); + assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId()); + assertThat(tokenIntrospectionResponse.getUsername()).isNull(); + assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween( + accessTokenClaims.getIssuedAt().minusSeconds(1), accessTokenClaims.getIssuedAt().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween( + accessTokenClaims.getExpiresAt().minusSeconds(1), accessTokenClaims.getExpiresAt().plusSeconds(1)); + List scopes = new ArrayList<>(accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE)); + assertThat(tokenIntrospectionResponse.getScopes()).containsExactlyInAnyOrderElementsOf(scopes); + assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(accessToken.getTokenType().getValue()); + assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween( + accessTokenClaims.getNotBefore().minusSeconds(1), accessTokenClaims.getNotBefore().plusSeconds(1)); + assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(accessTokenClaims.getSubject()); + assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(accessTokenClaims.getAudience()); + assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(accessTokenClaims.getIssuer()); + assertThat(tokenIntrospectionResponse.getId()).isEqualTo(accessTokenClaims.getId()); + } + private static MultiValueMap getTokenIntrospectionRequestParameters(AbstractOAuth2Token token, OAuth2TokenType tokenType) { MultiValueMap parameters = new LinkedMultiValueMap<>(); @@ -225,11 +322,37 @@ public class OAuth2TokenIntrospectionTests { return parameters; } - private OAuth2TokenIntrospection readTokenIntrospectionResponse(MvcResult mvcResult) throws Exception { + private static 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); + return tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospection.class, httpResponse); + } + + private static MultiValueMap getAuthorizationCodeTokenRequestParameters(RegisteredClient registeredClient, + OAuth2Authorization authorization) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + parameters.set(OAuth2ParameterNames.CODE, authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()); + parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next()); + return parameters; + } + + private static OAuth2AccessTokenResponse readAccessTokenResponse(MvcResult mvcResult) throws Exception { + MockHttpServletResponse servletResponse = mvcResult.getResponse(); + MockClientHttpResponse httpResponse = new MockClientHttpResponse( + servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus())); + return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse); + } + + private static String getAuthorizationHeader(RegisteredClient registeredClient) throws Exception { + String clientId = registeredClient.getClientId(); + String clientSecret = registeredClient.getClientSecret(); + clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()); + clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8.name()); + String credentialsString = clientId + ":" + clientSecret; + byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8)); + return "Basic " + new String(encodedBytes, StandardCharsets.UTF_8); } @EnableWebSecurity @@ -244,6 +367,11 @@ public class OAuth2TokenIntrospectionTests { return authorizationService; } + @Bean + OAuth2AuthorizationConsentService authorizationConsentService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) { + return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository); + } + @Bean RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations, PasswordEncoder passwordEncoder) { JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcOperations); @@ -259,13 +387,13 @@ public class OAuth2TokenIntrospectionTests { } @Bean - JWKSource jwkSource() { - return jwkSource; + ProviderSettings providerSettings() { + return providerSettings; } @Bean - ProviderSettings providerSettings() { - return providerSettings; + OAuth2TokenCustomizer accessTokenCustomizer() { + return accessTokenCustomizer; } @Bean diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSetTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSetTests.java new file mode 100644 index 00000000..1c23ab95 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSetTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020-2022 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 java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link OAuth2TokenClaimsSet}. + * + * @author Joe Grandja + */ +public class OAuth2TokenClaimsSetTests { + + @Test + public void buildWhenClaimsEmptyThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> OAuth2TokenClaimsSet.builder().build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("claims cannot be empty"); + } + + @Test + public void buildWhenAllClaimsProvidedThenAllClaimsAreSet() { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); + String customClaimName = "custom-claim-name"; + String customClaimValue = "custom-claim-value"; + + // @formatter:off + OAuth2TokenClaimsSet expectedClaimsSet = OAuth2TokenClaimsSet.builder() + .issuer("https://provider.com") + .subject("subject") + .audience(Collections.singletonList("client-1")) + .issuedAt(issuedAt) + .notBefore(issuedAt) + .expiresAt(expiresAt) + .id("id") + .claims(claims -> claims.put(customClaimName, customClaimValue)) + .build(); + + OAuth2TokenClaimsSet claimsSet = OAuth2TokenClaimsSet.builder() + .issuer(expectedClaimsSet.getIssuer().toExternalForm()) + .subject(expectedClaimsSet.getSubject()) + .audience(expectedClaimsSet.getAudience()) + .issuedAt(expectedClaimsSet.getIssuedAt()) + .notBefore(expectedClaimsSet.getNotBefore()) + .expiresAt(expectedClaimsSet.getExpiresAt()) + .id(expectedClaimsSet.getId()) + .claims(claims -> claims.put(customClaimName, expectedClaimsSet.getClaim(customClaimName))) + .build(); + // @formatter:on + + assertThat(claimsSet.getIssuer()).isEqualTo(expectedClaimsSet.getIssuer()); + assertThat(claimsSet.getSubject()).isEqualTo(expectedClaimsSet.getSubject()); + assertThat(claimsSet.getAudience()).isEqualTo(expectedClaimsSet.getAudience()); + assertThat(claimsSet.getIssuedAt()).isEqualTo(expectedClaimsSet.getIssuedAt()); + assertThat(claimsSet.getNotBefore()).isEqualTo(expectedClaimsSet.getNotBefore()); + assertThat(claimsSet.getExpiresAt()).isEqualTo(expectedClaimsSet.getExpiresAt()); + assertThat(claimsSet.getId()).isEqualTo(expectedClaimsSet.getId()); + assertThat(claimsSet.getClaim(customClaimName)).isEqualTo(expectedClaimsSet.getClaim(customClaimName)); + assertThat(claimsSet.getClaims()).isEqualTo(expectedClaimsSet.getClaims()); + } + + @Test + public void claimWhenNameNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> OAuth2TokenClaimsSet.builder().claim(null, "value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("name cannot be empty"); + } + + @Test + public void claimWhenValueNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> OAuth2TokenClaimsSet.builder().claim("name", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("value cannot be null"); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JwtGeneratorTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JwtGeneratorTests.java index 5c9dd224..6719e537 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JwtGeneratorTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JwtGeneratorTests.java @@ -29,6 +29,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -45,6 +46,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.config.TokenSettings; import org.springframework.security.oauth2.server.authorization.context.ProviderContext; import static org.assertj.core.api.Assertions.assertThat; @@ -99,6 +101,24 @@ public class JwtGeneratorTests { assertThat(this.jwtGenerator.generate(tokenContext)).isNull(); } + @Test + public void generateWhenUnsupportedTokenFormatThenReturnNull() { + // @formatter:off + TokenSettings tokenSettings = TokenSettings.builder() + .accessTokenFormat(new OAuth2TokenFormat("unsupported_token_format")) + .build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .tokenSettings(tokenSettings) + .build(); + OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) + .build(); + // @formatter:on + + assertThat(this.jwtGenerator.generate(tokenContext)).isNull(); + } + @Test public void generateWhenAccessTokenTypeThenReturnJwt() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGeneratorTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGeneratorTests.java new file mode 100644 index 00000000..e61e92b5 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGeneratorTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization; + +import java.security.Principal; +import java.time.Instant; +import java.util.Collections; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2TokenClaimAccessor; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; +import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.config.TokenSettings; +import org.springframework.security.oauth2.server.authorization.context.ProviderContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link OAuth2AccessTokenGenerator}. + * + * @author Joe Grandja + */ +public class OAuth2AccessTokenGeneratorTests { + private OAuth2TokenCustomizer accessTokenCustomizer; + private OAuth2AccessTokenGenerator accessTokenGenerator; + private ProviderContext providerContext; + + @Before + public void setUp() { + this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class); + this.accessTokenGenerator = new OAuth2AccessTokenGenerator(); + this.accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer); + ProviderSettings providerSettings = ProviderSettings.builder().issuer("https://provider.com").build(); + this.providerContext = new ProviderContext(providerSettings, null); + } + + @Test + public void setAccessTokenCustomizerWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.accessTokenGenerator.setAccessTokenCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("accessTokenCustomizer cannot be null"); + } + + @Test + public void generateWhenUnsupportedTokenTypeThenReturnNull() { + // @formatter:off + TokenSettings tokenSettings = TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .tokenSettings(tokenSettings) + .build(); + OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .tokenType(new OAuth2TokenType("unsupported_token_type")) + .build(); + // @formatter:on + + assertThat(this.accessTokenGenerator.generate(tokenContext)).isNull(); + } + + @Test + public void generateWhenUnsupportedTokenFormatThenReturnNull() { + // @formatter:off + TokenSettings tokenSettings = TokenSettings.builder() + .accessTokenFormat(new OAuth2TokenFormat("unsupported_token_format")) + .build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .tokenSettings(tokenSettings) + .build(); + OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) + .build(); + // @formatter:on + + assertThat(this.accessTokenGenerator.generate(tokenContext)).isNull(); + } + + @Test + public void generateWhenReferenceAccessTokenTypeThenReturnAccessToken() { + // @formatter:off + TokenSettings tokenSettings = TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .build(); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .tokenSettings(tokenSettings) + .build(); + // @formatter:on + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + Authentication principal = authorization.getAttribute(Principal.class.getName()); + + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( + OAuth2AuthorizationRequest.class.getName()); + OAuth2AuthorizationCodeAuthenticationToken authentication = + new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri(), null); + + // @formatter:off + OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(principal) + .providerContext(this.providerContext) + .authorization(authorization) + .authorizedScopes(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME)) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrant(authentication) + .build(); + // @formatter:on + + OAuth2AccessToken accessToken = this.accessTokenGenerator.generate(tokenContext); + assertThat(accessToken).isNotNull(); + + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive()); + assertThat(accessToken.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); + assertThat(accessToken.getExpiresAt()).isBetween(expiresAt.minusSeconds(1), expiresAt.plusSeconds(1)); + assertThat(accessToken.getScopes()).isEqualTo(tokenContext.getAuthorizedScopes()); + + assertThat(accessToken).isInstanceOf(ClaimAccessor.class); + OAuth2TokenClaimAccessor accessTokenClaims = ((ClaimAccessor) accessToken)::getClaims; + assertThat(accessTokenClaims.getClaims()).isNotEmpty(); + + assertThat(accessTokenClaims.getIssuer().toExternalForm()).isEqualTo(tokenContext.getProviderContext().getIssuer()); + assertThat(accessTokenClaims.getSubject()).isEqualTo(tokenContext.getPrincipal().getName()); + assertThat(accessTokenClaims.getAudience()).isEqualTo( + Collections.singletonList(tokenContext.getRegisteredClient().getClientId())); + assertThat(accessTokenClaims.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); + assertThat(accessTokenClaims.getExpiresAt()).isBetween(expiresAt.minusSeconds(1), expiresAt.plusSeconds(1)); + assertThat(accessTokenClaims.getNotBefore()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); + assertThat(accessTokenClaims.getId()).isNotNull(); + + Set scopes = accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE); + assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes()); + + ArgumentCaptor tokenClaimsContextCaptor = ArgumentCaptor.forClass(OAuth2TokenClaimsContext.class); + verify(this.accessTokenCustomizer).customize(tokenClaimsContextCaptor.capture()); + + OAuth2TokenClaimsContext tokenClaimsContext = tokenClaimsContextCaptor.getValue(); + assertThat(tokenClaimsContext.getClaims()).isNotNull(); + assertThat(tokenClaimsContext.getRegisteredClient()).isEqualTo(tokenContext.getRegisteredClient()); + assertThat(tokenClaimsContext.getPrincipal()).isEqualTo(tokenContext.getPrincipal()); + assertThat(tokenClaimsContext.getProviderContext()).isEqualTo(tokenContext.getProviderContext()); + assertThat(tokenClaimsContext.getAuthorization()).isEqualTo(tokenContext.getAuthorization()); + assertThat(tokenClaimsContext.getAuthorizedScopes()).isEqualTo(tokenContext.getAuthorizedScopes()); + assertThat(tokenClaimsContext.getTokenType()).isEqualTo(tokenContext.getTokenType()); + assertThat(tokenClaimsContext.getAuthorizationGrantType()).isEqualTo(tokenContext.getAuthorizationGrantType()); + assertThat(tokenClaimsContext.getAuthorizationGrant()).isEqualTo(tokenContext.getAuthorizationGrant()); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContextTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContextTests.java new file mode 100644 index 00000000..1b8cfb51 --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContextTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization; + +import java.security.Principal; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet; +import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.context.ProviderContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link OAuth2TokenClaimsContext}. + * + * @author Joe Grandja + */ +public class OAuth2TokenClaimsContextTests { + + @Test + public void withWhenClaimsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> OAuth2TokenClaimsContext.with(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("claimsBuilder cannot be null"); + } + + @Test + public void buildWhenAllValuesProvidedThenAllValuesAreSet() { + String issuer = "https://provider.com"; + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); + + // @formatter:off + OAuth2TokenClaimsSet.Builder claims = OAuth2TokenClaimsSet.builder() + .issuer(issuer) + .subject("subject") + .audience(Collections.singletonList("client-1")) + .issuedAt(issuedAt) + .notBefore(issuedAt) + .expiresAt(expiresAt) + .id("id"); + // @formatter:on + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + Authentication principal = authorization.getAttribute(Principal.class.getName()); + ProviderSettings providerSettings = ProviderSettings.builder().issuer(issuer).build(); + ProviderContext providerContext = new ProviderContext(providerSettings, null); + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( + OAuth2AuthorizationRequest.class.getName()); + OAuth2AuthorizationCodeAuthenticationToken authorizationGrant = + new OAuth2AuthorizationCodeAuthenticationToken( + "code", clientPrincipal, authorizationRequest.getRedirectUri(), null); + + // @formatter:off + OAuth2TokenClaimsContext context = OAuth2TokenClaimsContext.with(claims) + .registeredClient(registeredClient) + .principal(principal) + .providerContext(providerContext) + .authorization(authorization) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrant(authorizationGrant) + .put("custom-key-1", "custom-value-1") + .context(ctx -> ctx.put("custom-key-2", "custom-value-2")) + .build(); + // @formatter:on + + assertThat(context.getClaims()).isEqualTo(claims); + assertThat(context.getRegisteredClient()).isEqualTo(registeredClient); + assertThat(context.getPrincipal()).isEqualTo(principal); + assertThat(context.getProviderContext()).isEqualTo(providerContext); + assertThat(context.getAuthorization()).isEqualTo(authorization); + assertThat(context.getTokenType()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN); + assertThat(context.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(context.getAuthorizationGrant()).isEqualTo(authorizationGrant); + assertThat(context.get("custom-key-1")).isEqualTo("custom-value-1"); + assertThat(context.get("custom-key-2")).isEqualTo("custom-value-2"); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index 527f2a5a..cc62ed0c 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -38,6 +38,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthorizationCode; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -52,9 +53,11 @@ import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator; @@ -90,6 +93,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { private OAuth2AuthorizationService authorizationService; private JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer; + private OAuth2TokenCustomizer accessTokenCustomizer; private OAuth2TokenGenerator tokenGenerator; private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider; @@ -100,9 +104,12 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { this.jwtCustomizer = mock(OAuth2TokenCustomizer.class); JwtGenerator jwtGenerator = new JwtGenerator(this.jwtEncoder); jwtGenerator.setJwtCustomizer(this.jwtCustomizer); + this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer); OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); OAuth2TokenGenerator delegatingTokenGenerator = - new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator); + new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator); this.tokenGenerator = spy(new OAuth2TokenGenerator() { @Override public OAuth2Token generate(OAuth2TokenContext context) { @@ -312,8 +319,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null); - when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt()); - doAnswer(answer -> { OAuth2TokenContext context = answer.getArgument(0); if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { @@ -686,6 +691,31 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { assertThat(accessTokenAuthentication.getRefreshToken().getTokenValue()).isEqualTo(refreshTokenGenerator.get()); } + @Test + public void authenticateWhenAccessTokenFormatReferenceThenAccessTokenGeneratorCalled() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .tokenSettings(TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .build()) + .build(); + // @formatter:on + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(AUTHORIZATION_CODE_TOKEN_TYPE))) + .thenReturn(authorization); + + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( + OAuth2AuthorizationRequest.class.getName()); + OAuth2AuthorizationCodeAuthenticationToken authentication = + new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null); + + this.authenticationProvider.authenticate(authentication); + + verify(this.accessTokenCustomizer).customize(any()); + } + private static Jwt createJwt() { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java index 98940f7c..e016c5d1 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java @@ -31,22 +31,28 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JoseHeaderNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.config.TokenSettings; import org.springframework.security.oauth2.server.authorization.context.ProviderContext; import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder; @@ -69,6 +75,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests { private OAuth2AuthorizationService authorizationService; private JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer; + private OAuth2TokenCustomizer accessTokenCustomizer; private OAuth2TokenGenerator tokenGenerator; private OAuth2ClientCredentialsAuthenticationProvider authenticationProvider; @@ -79,10 +86,15 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests { this.jwtCustomizer = mock(OAuth2TokenCustomizer.class); JwtGenerator jwtGenerator = new JwtGenerator(this.jwtEncoder); jwtGenerator.setJwtCustomizer(this.jwtCustomizer); - this.tokenGenerator = spy(new OAuth2TokenGenerator() { + this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer); + OAuth2TokenGenerator delegatingTokenGenerator = + new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator); + this.tokenGenerator = spy(new OAuth2TokenGenerator() { @Override - public Jwt generate(OAuth2TokenContext context) { - return jwtGenerator.generate(context); + public OAuth2Token generate(OAuth2TokenContext context) { + return delegatingTokenGenerator.generate(context); } }); this.authenticationProvider = new OAuth2ClientCredentialsAuthenticationProvider( @@ -275,6 +287,25 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests { assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(authorization.getAccessToken().getToken()); } + @Test + public void authenticateWhenAccessTokenFormatReferenceThenAccessTokenGeneratorCalled() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient2() + .tokenSettings(TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .build()) + .build(); + // @formatter:on + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2ClientCredentialsAuthenticationToken authentication = + new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, null, null); + + this.authenticationProvider.authenticate(authentication); + + verify(this.accessTokenCustomizer).customize(any()); + } + private static Jwt createJwt(Set scope) { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java index 3c1ea187..e3897cee 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java @@ -38,6 +38,7 @@ 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.OAuth2Token; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; @@ -50,9 +51,11 @@ import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator; @@ -88,6 +91,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { private OAuth2AuthorizationService authorizationService; private JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer; + private OAuth2TokenCustomizer accessTokenCustomizer; private OAuth2TokenGenerator tokenGenerator; private OAuth2RefreshTokenAuthenticationProvider authenticationProvider; @@ -99,9 +103,12 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { this.jwtCustomizer = mock(OAuth2TokenCustomizer.class); JwtGenerator jwtGenerator = new JwtGenerator(this.jwtEncoder); jwtGenerator.setJwtCustomizer(this.jwtCustomizer); + this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer); OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); OAuth2TokenGenerator delegatingTokenGenerator = - new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator); + new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator); this.tokenGenerator = spy(new OAuth2TokenGenerator() { @Override public OAuth2Token generate(OAuth2TokenContext context) { @@ -623,6 +630,31 @@ public class OAuth2RefreshTokenAuthenticationProviderTests { }); } + @Test + public void authenticateWhenAccessTokenFormatReferenceThenAccessTokenGeneratorCalled() { + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .tokenSettings(TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .build()) + .build(); + // @formatter:on + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + when(this.authorizationService.findByToken( + eq(authorization.getRefreshToken().getToken().getTokenValue()), + eq(OAuth2TokenType.REFRESH_TOKEN))) + .thenReturn(authorization); + + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( + registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); + OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken( + authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null); + + this.authenticationProvider.authenticate(authentication); + + verify(this.accessTokenCustomizer).customize(any()); + } + private static Jwt createJwt(Set scope) { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); 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 6bfc9716..d3c87dee 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 @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -31,10 +31,9 @@ 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.OAuth2TokenClaimNames; +import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet; 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; @@ -189,7 +188,7 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { 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); + Map accessTokenClaims = Collections.singletonMap(OAuth2TokenClaimNames.NBF, notBefore); OAuth2Authorization authorization = TestOAuth2Authorizations .authorization(registeredClient, accessToken, accessTokenClaims) .build(); @@ -217,9 +216,21 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { OAuth2AccessToken accessToken = new OAuth2AccessToken( OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt, new HashSet<>(Arrays.asList("scope1", "scope2"))); - JwtClaimsSet jwtClaims = TestJwtClaimsSets.jwtClaimsSet().build(); + + // @formatter:off + OAuth2TokenClaimsSet claimsSet = OAuth2TokenClaimsSet.builder() + .issuer("https://provider.com") + .subject("subject") + .audience(Collections.singletonList(authorizedClient.getClientId())) + .issuedAt(issuedAt) + .notBefore(issuedAt) + .expiresAt(expiresAt) + .id("id") + .build(); + // @formatter:on + OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(authorizedClient, accessToken, jwtClaims.getClaims()) + .authorization(authorizedClient, accessToken, claimsSet.getClaims()) .build(); when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())) .thenReturn(authorization); @@ -243,11 +254,11 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests { assertThat(tokenClaims.getExpiresAt()).isEqualTo(accessToken.getExpiresAt()); assertThat(tokenClaims.getScopes()).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()); + assertThat(tokenClaims.getNotBefore()).isEqualTo(claimsSet.getNotBefore()); + assertThat(tokenClaims.getSubject()).isEqualTo(claimsSet.getSubject()); + assertThat(tokenClaims.getAudience()).containsExactlyInAnyOrderElementsOf(claimsSet.getAudience()); + assertThat(tokenClaims.getIssuer()).isEqualTo(claimsSet.getIssuer()); + assertThat(tokenClaims.getId()).isEqualTo(claimsSet.getId()); } @Test diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java index f33313dd..f177977f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -19,6 +19,7 @@ import java.time.Duration; import org.junit.Test; +import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import static org.assertj.core.api.Assertions.assertThat; @@ -34,8 +35,9 @@ public class TokenSettingsTests { @Test public void buildWhenDefaultThenDefaultsAreSet() { TokenSettings tokenSettings = TokenSettings.builder().build(); - assertThat(tokenSettings.getSettings()).hasSize(4); + assertThat(tokenSettings.getSettings()).hasSize(5); assertThat(tokenSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5)); + assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.SELF_CONTAINED); assertThat(tokenSettings.isReuseRefreshTokens()).isTrue(); assertThat(tokenSettings.getRefreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60)); assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(SignatureAlgorithm.RS256); @@ -68,6 +70,22 @@ public class TokenSettingsTests { .isEqualTo("accessTokenTimeToLive must be greater than Duration.ZERO"); } + @Test + public void accessTokenFormatWhenProvidedThenSet() { + TokenSettings tokenSettings = TokenSettings.builder() + .accessTokenFormat(OAuth2TokenFormat.REFERENCE) + .build(); + assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.REFERENCE); + } + + @Test + public void accessTokenFormatWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> TokenSettings.builder().accessTokenFormat(null)) + .isInstanceOf(IllegalArgumentException.class) + .extracting(Throwable::getMessage) + .isEqualTo("accessTokenFormat cannot be null"); + } + @Test public void reuseRefreshTokensWhenFalseThenSet() { TokenSettings tokenSettings = TokenSettings.builder() @@ -118,7 +136,7 @@ public class TokenSettingsTests { .setting("name1", "value1") .settings(settings -> settings.put("name2", "value2")) .build(); - assertThat(tokenSettings.getSettings()).hasSize(6); + assertThat(tokenSettings.getSettings()).hasSize(7); assertThat(tokenSettings.getSetting("name1")).isEqualTo("value1"); assertThat(tokenSettings.getSetting("name2")).isEqualTo("value2"); }