Add support for opaque access tokens

Closes gh-500
This commit is contained in:
Joe Grandja
2022-02-18 10:37:53 -05:00
parent a661e1cdb7
commit 32414451f5
30 changed files with 1591 additions and 101 deletions

View File

@@ -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<B extends HttpSecurityBui
providerSettings.getTokenIntrospectionEndpoint());
builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), FilterSecurityInterceptor.class);
NimbusJwkSetEndpointFilter jwkSetEndpointFilter =
new NimbusJwkSetEndpointFilter(
OAuth2ConfigurerUtils.getJwkSource(builder),
providerSettings.getJwkSetEndpoint());
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
JWKSource<com.nimbusds.jose.proc.SecurityContext> 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);

View File

@@ -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<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
if (jwtCustomizer != null) {
jwtGenerator.setJwtCustomizer(jwtCustomizer);
JwtGenerator jwtGenerator = getJwtGenerator(builder);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> 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 <B extends HttpSecurityBuilder<B>> 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<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
if (jwtCustomizer != null) {
jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
builder.setSharedObject(JwtGenerator.class, jwtGenerator);
}
}
return jwtGenerator;
}
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> 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<SecurityContext> 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 <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(B builder) {
OAuth2TokenCustomizer<JwtEncodingContext> 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 <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(B builder) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class);
return getOptionalBean(builder, type);
}
static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {

View File

@@ -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<String> 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);
}
}

View File

@@ -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";
}

View File

@@ -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<String, Object> claims;
private OAuth2TokenClaimsSet(Map<String, Object> claims) {
this.claims = Collections.unmodifiableMap(new HashMap<>(claims));
}
@Override
public Map<String, Object> 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<String, Object> 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<String> 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<Map<String, Object>> 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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<Jwt> {
!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) {

View File

@@ -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<OAuth2AccessToken> {
private final StringKeyGenerator accessTokenGenerator =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> 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<OAuth2TokenClaimsContext> accessTokenCustomizer) {
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
this.accessTokenCustomizer = accessTokenCustomizer;
}
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
private final Map<String, Object> claims;
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue,
Instant issuedAt, Instant expiresAt, Set<String> scopes, Map<String, Object> claims) {
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
this.claims = claims;
}
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
}
}

View File

@@ -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<Object, Object> context;
private OAuth2TokenClaimsContext(Map<Object, Object> context) {
this.context = Collections.unmodifiableMap(new HashMap<>(context));
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <V> 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<OAuth2TokenClaimsContext, Builder> {
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<OAuth2TokenClaimsSet.Builder> 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());
}
}
}

View File

@@ -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 <T> the type of the OAuth 2.0 Token
*/
@FunctionalInterface
@@ -35,6 +39,10 @@ public interface OAuth2TokenGenerator<T extends OAuth2Token> {
* 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.
*
* <p>
* 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
*/

View File

@@ -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);
}

View File

@@ -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<? extends OAuth2Token> 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<JwtEncodingContext> 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);
}

View File

@@ -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 {

View File

@@ -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<String, Object> 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<String> audience = jwtClaims.getAudience();
tokenClaims.subject(accessTokenClaims.getSubject());
List<String> 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);
}

View File

@@ -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<String, Object> 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();
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;
* <li>{@link OAuth2AuthorizationRequestMixin}</li>
* <li>{@link DurationMixin}</li>
* <li>{@link JwsAlgorithmMixin}</li>
* <li>{@link OAuth2TokenFormatMixin}</li>
* </ul>
*
* 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);
}
}

View File

@@ -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) {
}
}

View File

@@ -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);
}

View File

@@ -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<SecurityContext> jwkSource;
private static ProviderSettings providerSettings;
private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
private static final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
new OAuth2TokenIntrospectionHttpMessageConverter();
private static final HttpMessageConverter<OAuth2AccessTokenResponse> 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<OAuth2TokenClaimsContext> 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<String> 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<String, String> getTokenIntrospectionRequestParameters(AbstractOAuth2Token token,
OAuth2TokenType tokenType) {
MultiValueMap<String, String> 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<String, String> getAuthorizationCodeTokenRequestParameters(RegisteredClient registeredClient,
OAuth2Authorization authorization) {
MultiValueMap<String, String> 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<SecurityContext> jwkSource() {
return jwkSource;
ProviderSettings providerSettings() {
return providerSettings;
}
@Bean
ProviderSettings providerSettings() {
return providerSettings;
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
return accessTokenCustomizer;
}
@Bean

View File

@@ -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.<String>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");
}
}

View File

@@ -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();

View File

@@ -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<OAuth2TokenClaimsContext> 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<String> scopes = accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE);
assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes());
ArgumentCaptor<OAuth2TokenClaimsContext> 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.<Authentication>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.<Authentication>getAuthorizationGrant()).isEqualTo(tokenContext.getAuthorizationGrant());
}
}

View File

@@ -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.<Authentication>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.<OAuth2AuthorizationGrantAuthenticationToken>getAuthorizationGrant()).isEqualTo(authorizationGrant);
assertThat(context.<String>get("custom-key-1")).isEqualTo("custom-value-1");
assertThat(context.<String>get("custom-key-2")).isEqualTo("custom-value-2");
}
}

View File

@@ -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<JwtEncodingContext> jwtCustomizer;
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> 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<OAuth2Token> delegatingTokenGenerator =
new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator);
new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
this.tokenGenerator = spy(new OAuth2TokenGenerator<OAuth2Token>() {
@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);

View File

@@ -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<JwtEncodingContext> jwtCustomizer;
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> 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<Jwt>() {
this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer);
OAuth2TokenGenerator<OAuth2Token> delegatingTokenGenerator =
new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator);
this.tokenGenerator = spy(new OAuth2TokenGenerator<OAuth2Token>() {
@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<String> scope) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

View File

@@ -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<JwtEncodingContext> jwtCustomizer;
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> 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<OAuth2Token> delegatingTokenGenerator =
new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator);
new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
this.tokenGenerator = spy(new OAuth2TokenGenerator<OAuth2Token>() {
@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<String> scope) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

View File

@@ -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<String, Object> accessTokenClaims = Collections.singletonMap(JwtClaimNames.NBF, notBefore);
Map<String, Object> 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

View File

@@ -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.<String>getSetting("name1")).isEqualTo("value1");
assertThat(tokenSettings.<String>getSetting("name2")).isEqualTo("value2");
}