diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java index 67e1517f..ad0b7d49 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java @@ -43,8 +43,8 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ActorAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; import org.springframework.security.web.authentication.WebAuthenticationDetails; @@ -111,9 +111,9 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis TypeReference.of(OidcIdToken.class), TypeReference.of(AbstractOAuth2Token.class), TypeReference.of(OidcUserInfo.class), - TypeReference.of(OAuth2ActorAuthenticationToken.class), + TypeReference.of(OAuth2TokenExchangeActor.class), TypeReference.of(OAuth2AuthorizationRequest.class), - TypeReference.of(OAuth2CompositeAuthenticationToken.class), + TypeReference.of(OAuth2TokenExchangeCompositeAuthenticationToken.class), TypeReference.of(AuthorizationGrantType.class), TypeReference.of(OAuth2AuthorizationResponseType.class), TypeReference.of(OAuth2TokenFormat.class) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ActorAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ActorAuthenticationToken.java deleted file mode 100644 index 5ba186eb..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ActorAuthenticationToken.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020-2024 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.authentication; - -import java.io.Serializable; -import java.util.Collections; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; - -/** - * An {@link Authentication} implementation used for the OAuth 2.0 Token Exchange Grant - * to represent an actor in a composite token (e.g. the "delegation" use case). - * - * @author Steve Riesenberg - * @since 1.3 - * @see OAuth2CompositeAuthenticationToken - */ -public class OAuth2ActorAuthenticationToken extends AbstractAuthenticationToken implements Serializable { - - private final String name; - - public OAuth2ActorAuthenticationToken(String name) { - super(Collections.emptyList()); - Assert.hasText(name, "name cannot be empty"); - this.name = name; - } - - @Override - public Object getPrincipal() { - return this.name; - } - - @Override - public Object getCredentials() { - return null; - } -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java index 97bc402f..1ebd2363 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java @@ -17,12 +17,16 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; +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.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; /** * Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s. @@ -74,4 +78,24 @@ final class OAuth2AuthenticationProviderUtils { return authorizationBuilder.build(); } + + static OAuth2AccessToken accessToken(OAuth2Authorization.Builder builder, T token, + OAuth2TokenContext accessTokenContext) { + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + token.getTokenValue(), token.getIssuedAt(), token.getExpiresAt(), + accessTokenContext.getAuthorizedScopes()); + OAuth2TokenFormat accessTokenFormat = accessTokenContext.getRegisteredClient().getTokenSettings() + .getAccessTokenFormat(); + builder.token(accessToken, (metadata) -> { + if (token instanceof ClaimAccessor claimAccessor) { + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claimAccessor.getClaims()); + } + metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false); + metadata.put(OAuth2TokenFormat.class.getName(), accessTokenFormat.getValue()); + }); + + return accessToken; + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index 800acf6f..c3402fdf 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -37,7 +37,6 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; 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; @@ -204,15 +203,8 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth this.logger.trace("Generated access token"); } - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), - generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - if (generatedAccessToken instanceof ClaimAccessor) { - authorizationBuilder.token(accessToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); - } else { - authorizationBuilder.accessToken(accessToken); - } + OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder, + generatedAccessToken, tokenContext); // ----- Refresh token ----- OAuth2RefreshToken refreshToken = null; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java index d05cfafe..f84bf73e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java @@ -27,7 +27,6 @@ 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; @@ -137,22 +136,15 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth this.logger.trace("Generated access token"); } - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), - generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - // @formatter:off OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(clientPrincipal.getName()) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizedScopes(authorizedScopes); // @formatter:on - if (generatedAccessToken instanceof ClaimAccessor) { - authorizationBuilder.token(accessToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); - } else { - authorizationBuilder.accessToken(accessToken); - } + + OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder, + generatedAccessToken, tokenContext); OAuth2Authorization authorization = authorizationBuilder.build(); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java index 5e08ea54..15cc0e69 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java @@ -25,7 +25,6 @@ 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.OAuth2DeviceCode; @@ -213,15 +212,8 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat this.logger.trace("Generated access token"); } - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), - generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - if (generatedAccessToken instanceof ClaimAccessor) { - authorizationBuilder.token(accessToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); - } else { - authorizationBuilder.accessToken(accessToken); - } + OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder, + generatedAccessToken, tokenContext); // ----- Refresh token ----- OAuth2RefreshToken refreshToken = null; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java index f1355155..bd78367f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java @@ -29,7 +29,6 @@ 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; @@ -181,17 +180,8 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic this.logger.trace("Generated access token"); } - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), - generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - if (generatedAccessToken instanceof ClaimAccessor) { - authorizationBuilder.token(accessToken, (metadata) -> { - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()); - metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false); - }); - } else { - authorizationBuilder.accessToken(accessToken); - } + OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder, + generatedAccessToken, tokenContext); // ----- Refresh token ----- OAuth2RefreshToken currentRefreshToken = refreshToken.getToken(); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeActor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeActor.java new file mode 100644 index 00000000..3e868c6c --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeActor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020-2024 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.authentication; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames; +import org.springframework.util.Assert; + +/** + * A {@link ClaimAccessor} used for the OAuth 2.0 Token Exchange Grant to represent an actor in a + * {@link OAuth2TokenExchangeCompositeAuthenticationToken} (e.g. the "delegation" use case). + * + * @author Steve Riesenberg + * @since 1.3 + * @see OAuth2TokenExchangeCompositeAuthenticationToken + */ +public final class OAuth2TokenExchangeActor implements ClaimAccessor { + + private final Map claims; + + public OAuth2TokenExchangeActor(Map claims) { + Assert.notNull(claims, "claims cannot be null"); + this.claims = Collections.unmodifiableMap(claims); + } + + @Override + public Map getClaims() { + return this.claims; + } + + public String getIssuer() { + return getClaimAsString(OAuth2TokenClaimNames.ISS); + } + + public String getSubject() { + return getClaimAsString(OAuth2TokenClaimNames.SUB); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OAuth2TokenExchangeActor other)) { + return false; + } + return Objects.equals(this.claims, other.claims); + } + + @Override + public int hashCode() { + return Objects.hash(this.claims); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java index 4dd8133f..59cbdf2a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java @@ -22,6 +22,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.apache.commons.logging.Log; @@ -31,15 +32,12 @@ 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; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.oidc.StandardClaimNames; -import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; @@ -47,6 +45,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.util.Assert; @@ -73,6 +72,8 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt"; + private static final String ACCESS_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:access_token"; + private static final String MAY_ACT = "may_act"; private final Log logger = LogFactory.getLog(getClass()); @@ -136,11 +137,8 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); } - if (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getSubjectTokenType()) && - !Jwt.class.isAssignableFrom(subjectToken.getToken().getClass())) { - // TODO: Need a way to validate subject_token_type, since access tokens - // are always stored as OAuth2AccessToken instead of Jwt. - //throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); + if (!isValidTokenType(tokenExchangeAuthentication.getSubjectTokenType(), subjectToken)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } if (subjectAuthorization.getAttribute(Principal.class.getName()) == null) { @@ -153,11 +151,11 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti // As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.4, // The may_act claim makes a statement that one party is authorized to // become the actor and act on behalf of another party. - String authorizedActorSubject = null; + Map authorizedActorClaims = null; if (subjectToken.getClaims() != null && subjectToken.getClaims().containsKey(MAY_ACT) && subjectToken.getClaims().get(MAY_ACT) instanceof Map mayAct) { - authorizedActorSubject = (String) mayAct.get(StandardClaimNames.SUB); + authorizedActorClaims = (Map) mayAct; } OAuth2Authorization actorAuthorization = null; @@ -181,19 +179,15 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); } - if (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getActorTokenType()) && - !Jwt.class.isAssignableFrom(actorToken.getToken().getClass())) { - // TODO: Need a way to validate actor_token_type, since access tokens - // are always stored as OAuth2AccessToken instead of Jwt. - //throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); + if (!isValidTokenType(tokenExchangeAuthentication.getActorTokenType(), actorToken)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } - if (StringUtils.hasText(authorizedActorSubject) && - !authorizedActorSubject.equals(actorAuthorization.getPrincipalName())) { - throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); + if (authorizedActorClaims != null) { + validateClaims(authorizedActorClaims, actorToken.getClaims(), OAuth2TokenClaimNames.ISS, + OAuth2TokenClaimNames.SUB); } - } else if (StringUtils.hasText(authorizedActorSubject) && - !authorizedActorSubject.equals(clientPrincipal.getName())) { + } else if (authorizedActorClaims != null) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); } @@ -235,10 +229,6 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti this.logger.trace("Generated access token"); } - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), - generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - // @formatter:off OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(subjectAuthorization.getPrincipalName()) @@ -247,14 +237,8 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti .attribute(Principal.class.getName(), principal); // @formatter:on - if (generatedAccessToken instanceof ClaimAccessor) { - authorizationBuilder.token(accessToken, (metadata) -> { - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()); - metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false); - }); - } else { - authorizationBuilder.accessToken(accessToken); - } + OAuth2AccessToken accessToken = OAuth2AuthenticationProviderUtils.accessToken(authorizationBuilder, + generatedAccessToken, tokenContext); OAuth2Authorization authorization = authorizationBuilder.build(); this.authorizationService.save(authorization); @@ -274,6 +258,13 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti registeredClient, clientPrincipal, accessToken, null, additionalParameters); } + private static boolean isValidTokenType(String tokenType, OAuth2Authorization.Token token) { + String tokenFormat = token.getMetadata(OAuth2TokenFormat.class.getName()); + return ACCESS_TOKEN_TYPE_VALUE.equals(tokenType) || + JWT_TOKEN_TYPE_VALUE.equals(tokenType) && + OAuth2TokenFormat.SELF_CONTAINED.getValue().equals(tokenFormat); + } + private static Set validateRequestedScopes(RegisteredClient registeredClient, Set requestedScopes) { for (String requestedScope : requestedScopes) { if (!registeredClient.getScopes().contains(requestedScope)) { @@ -284,26 +275,40 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti return new LinkedHashSet<>(requestedScopes); } + private static void validateClaims(Map expectedClaims, Map actualClaims, String... claimNames) { + if (actualClaims == null) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); + } + + for (String claimName : claimNames) { + if (!Objects.equals(expectedClaims.get(claimName), actualClaims.get(claimName))) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); + } + } + } + private static Authentication getPrincipal(OAuth2Authorization subjectAuthorization, OAuth2Authorization actorAuthorization) { Authentication subjectPrincipal = subjectAuthorization.getAttribute(Principal.class.getName()); - - List actorPrincipals = new LinkedList<>(); - if (actorAuthorization != null) { - actorPrincipals.add(new OAuth2ActorAuthenticationToken(actorAuthorization.getPrincipalName())); + if (actorAuthorization == null) { + if (subjectPrincipal instanceof OAuth2TokenExchangeCompositeAuthenticationToken compositeAuthenticationToken) { + return compositeAuthenticationToken.getSubject(); + } + return subjectPrincipal; } - if (subjectPrincipal instanceof OAuth2CompositeAuthenticationToken compositeAuthenticationToken) { - // As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.1, - // the act claim can be used to represent a chain of delegation, - // so we unwrap the original subject and any previous actor(s). + // Capture claims for current actor's access token + OAuth2TokenExchangeActor currentActor = new OAuth2TokenExchangeActor( + actorAuthorization.getAccessToken().getClaims()); + List actorPrincipals = new LinkedList<>(); + actorPrincipals.add(currentActor); + + // Add chain of delegation for previous actor(s) if any + if (subjectPrincipal instanceof OAuth2TokenExchangeCompositeAuthenticationToken compositeAuthenticationToken) { subjectPrincipal = compositeAuthenticationToken.getSubject(); actorPrincipals.addAll(compositeAuthenticationToken.getActors()); - // TODO: Should we allow delegation-to-impersonation where previous - // actors exist but no actor_token exists on this request? } - return CollectionUtils.isEmpty(actorPrincipals) ? subjectPrincipal : - new OAuth2CompositeAuthenticationToken(subjectPrincipal, actorPrincipals); + return new OAuth2TokenExchangeCompositeAuthenticationToken(subjectPrincipal, actorPrincipals); } @Override diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java index 06083a9d..f1615777 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java @@ -17,7 +17,7 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.util.Collections; import java.util.HashSet; -import java.util.List; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -36,10 +36,6 @@ import org.springframework.util.Assert; */ public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { - private final List resources; - - private final List audiences; - private final String requestedTokenType; private final String subjectToken; @@ -50,70 +46,47 @@ public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationG private final String actorTokenType; + private final Set resources; + + private final Set audiences; + private final Set scopes; /** * Constructs an {@code OAuth2TokenExchangeAuthenticationToken} using the provided parameters. * - * @param resources a list of resource URIs - * @param audiences a list audience values - * @param scopes the requested scope(s) * @param requestedTokenType the requested token type * @param subjectToken the subject token * @param subjectTokenType the subject token type + * @param clientPrincipal the authenticated client principal * @param actorToken the actor token * @param actorTokenType the actor token type - * @param clientPrincipal the authenticated client principal + * @param resources the requested resource URI(s) + * @param audiences the requested audience value(s) + * @param scopes the requested scope(s) * @param additionalParameters the additional parameters */ - public OAuth2TokenExchangeAuthenticationToken(List resources, List audiences, - @Nullable Set scopes, @Nullable String requestedTokenType, String subjectToken, - String subjectTokenType, @Nullable String actorToken, @Nullable String actorTokenType, - Authentication clientPrincipal, @Nullable Map additionalParameters) { + public OAuth2TokenExchangeAuthenticationToken(String requestedTokenType, String subjectToken, + String subjectTokenType, Authentication clientPrincipal, @Nullable String actorToken, + @Nullable String actorTokenType, @Nullable Set resources, @Nullable Set audiences, + @Nullable Set scopes, @Nullable Map additionalParameters) { super(AuthorizationGrantType.TOKEN_EXCHANGE, clientPrincipal, additionalParameters); - Assert.notNull(resources, "resources cannot be null"); - Assert.notNull(audiences, "audiences cannot be null"); Assert.hasText(requestedTokenType, "requestedTokenType cannot be empty"); Assert.hasText(subjectToken, "subjectToken cannot be empty"); Assert.hasText(subjectTokenType, "subjectTokenType cannot be empty"); - this.resources = resources; - this.audiences = audiences; this.requestedTokenType = requestedTokenType; this.subjectToken = subjectToken; this.subjectTokenType = subjectTokenType; this.actorToken = actorToken; this.actorTokenType = actorTokenType; + this.resources = Collections.unmodifiableSet( + resources != null ? new LinkedHashSet<>(resources) : Collections.emptySet()); + this.audiences = Collections.unmodifiableSet( + audiences != null ? new LinkedHashSet<>(audiences) : Collections.emptySet()); this.scopes = Collections.unmodifiableSet( scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); } - /** - * Returns the list of resource URIs. - * - * @return the list of resource URIs - */ - public List getResources() { - return this.resources; - } - - /** - * Returns the list of audience values. - * - * @return the list of audience values - */ - public List getAudiences() { - return this.audiences; - } - - /** - * Returns the requested scope(s). - * - * @return the requested scope(s), or an empty {@code Set} if not available - */ - public Set getScopes() { - return this.scopes; - } - /** * Returns the requested token type. * @@ -158,4 +131,32 @@ public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationG public String getActorTokenType() { return this.actorTokenType; } + + /** + * Returns the requested resource URI(s). + * + * @return the requested resource URI(s), or an empty {@code Set} if not available + */ + public Set getResources() { + return this.resources; + } + + /** + * Returns the requested audience value(s). + * + * @return the requested audience value(s), or an empty {@code Set} if not available + */ + public Set getAudiences() { + return this.audiences; + } + + /** + * Returns the requested scope(s). + * + * @return the requested scope(s), or an empty {@code Set} if not available + */ + public Set getScopes() { + return this.scopes; + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2CompositeAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeCompositeAuthenticationToken.java similarity index 71% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2CompositeAuthenticationToken.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeCompositeAuthenticationToken.java index 3fb8b572..458db85a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2CompositeAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeCompositeAuthenticationToken.java @@ -16,10 +16,10 @@ package org.springframework.security.oauth2.server.authorization.authentication; -import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.Authentication; @@ -33,13 +33,13 @@ import org.springframework.util.Assert; * @since 1.3 * @see OAuth2TokenExchangeAuthenticationToken */ -public class OAuth2CompositeAuthenticationToken extends AbstractAuthenticationToken implements Serializable { +public class OAuth2TokenExchangeCompositeAuthenticationToken extends AbstractAuthenticationToken { private final Authentication subject; - private final List actors; + private final List actors; - public OAuth2CompositeAuthenticationToken(Authentication subject, List actors) { + public OAuth2TokenExchangeCompositeAuthenticationToken(Authentication subject, List actors) { super(subject != null ? subject.getAuthorities() : null); Assert.notNull(subject, "subject cannot be null"); Assert.notNull(actors, "actors cannot be null"); @@ -63,7 +63,22 @@ public class OAuth2CompositeAuthenticationToken extends AbstractAuthenticationTo return this.subject; } - public List getActors() { + public List getActors() { return this.actors; } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OAuth2TokenExchangeCompositeAuthenticationToken other)) { + return false; + } + return super.equals(obj) && Objects.equals(this.subject, other.subject) && + Objects.equals(this.actors, other.actors); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), this.subject, this.actors); + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DelegatingOAuth2TokenCustomizer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DelegatingOAuth2TokenCustomizer.java deleted file mode 100644 index aba4a2fd..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DelegatingOAuth2TokenCustomizer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020-2024 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.config.annotation.web.configurers; - -import java.util.Collections; -import java.util.List; - -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; -import org.springframework.util.Assert; - -/** - * @author Steve Riesenberg - * @since 1.3 - */ -final class DelegatingOAuth2TokenCustomizer implements OAuth2TokenCustomizer { - - private final List> tokenCustomizers; - - DelegatingOAuth2TokenCustomizer(List> tokenCustomizers) { - Assert.notEmpty(tokenCustomizers, "tokenCustomizers cannot be empty"); - this.tokenCustomizers = Collections.unmodifiableList(tokenCustomizers); - } - - @Override - public void customize(T context) { - for (OAuth2TokenCustomizer tokenCustomizer : this.tokenCustomizers) { - tokenCustomizer.customize(context); - } - } - -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java index 3fd9c202..2ff97378 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java @@ -15,8 +15,6 @@ */ package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import com.nimbusds.jose.jwk.source.JWKSource; @@ -172,39 +170,13 @@ final class OAuth2ConfigurerUtils { } private static OAuth2TokenCustomizer getJwtCustomizer(HttpSecurity httpSecurity) { - OAuth2TokenCustomizer defaultTokenCustomizer = OAuth2TokenExchangeTokenCustomizers.jwt(); ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class); - OAuth2TokenCustomizer userTokenCustomizer = getOptionalBean(httpSecurity, type); - - OAuth2TokenCustomizer tokenCustomizer; - if (userTokenCustomizer != null) { - List> tokenCustomizers = new ArrayList<>(); - tokenCustomizers.add(defaultTokenCustomizer); - tokenCustomizers.add(userTokenCustomizer); - tokenCustomizer = new DelegatingOAuth2TokenCustomizer<>(tokenCustomizers); - } else { - tokenCustomizer = defaultTokenCustomizer; - } - - return tokenCustomizer; + return getOptionalBean(httpSecurity, type); } private static OAuth2TokenCustomizer getAccessTokenCustomizer(HttpSecurity httpSecurity) { - OAuth2TokenCustomizer defaultTokenCustomizer = OAuth2TokenExchangeTokenCustomizers.accessToken(); ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class); - OAuth2TokenCustomizer userTokenCustomizer = getOptionalBean(httpSecurity, type); - - OAuth2TokenCustomizer tokenCustomizer; - if (userTokenCustomizer != null) { - List> tokenCustomizers = new ArrayList<>(); - tokenCustomizers.add(defaultTokenCustomizer); - tokenCustomizers.add(userTokenCustomizer); - tokenCustomizer = new DelegatingOAuth2TokenCustomizer<>(tokenCustomizers); - } else { - tokenCustomizer = defaultTokenCustomizer; - } - - return tokenCustomizer; + return getOptionalBean(httpSecurity, type); } static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenExchangeTokenCustomizers.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenExchangeTokenCustomizers.java deleted file mode 100644 index 39957414..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenExchangeTokenCustomizers.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020-2024 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.config.annotation.web.configurers; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; -import org.springframework.util.CollectionUtils; - -/** - * @author Steve Riesenberg - * @since 1.3 - */ -final class OAuth2TokenExchangeTokenCustomizers { - - private OAuth2TokenExchangeTokenCustomizers() { - } - - static OAuth2TokenCustomizer jwt() { - return (context) -> context.getClaims().claims((claims) -> customize(context, claims)); - } - - static OAuth2TokenCustomizer accessToken() { - return (context) -> context.getClaims().claims((claims) -> customize(context, claims)); - } - - private static void customize(OAuth2TokenContext context, Map claims) { - if (!AuthorizationGrantType.TOKEN_EXCHANGE.equals(context.getAuthorizationGrantType())) { - return; - } - - if (context.getAuthorizationGrant() instanceof OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication) { - // Customize the token claims when audience is present in the request - List audience = tokenExchangeAuthentication.getAudiences(); - if (!CollectionUtils.isEmpty(audience)) { - claims.put(OAuth2TokenClaimNames.AUD, audience); - } - } - - // As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.1, - // we handle a composite principal with an actor by adding an "act" - // claim with a "sub" claim of the actor. - // - // If more than one actor is present, we create a chain of delegation by - // nesting "act" claims. - if (context.getPrincipal() instanceof OAuth2CompositeAuthenticationToken compositeAuthenticationToken) { - Map currentClaims = claims; - for (Authentication actorPrincipal : compositeAuthenticationToken.getActors()) { - Map actClaim = new HashMap<>(); - actClaim.put("sub", actorPrincipal.getName()); - currentClaims.put("act", Collections.unmodifiableMap(actClaim)); - currentClaims = actClaim; - } - } - } - -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java index 55c039df..bcd84d67 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java @@ -27,8 +27,8 @@ import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ActorAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; /** @@ -39,7 +39,7 @@ import org.springframework.security.oauth2.server.authorization.settings.OAuth2T *
  • {@link UnmodifiableMapMixin}
  • *
  • {@link HashSetMixin}
  • *
  • {@link OAuth2AuthorizationRequestMixin}
  • - *
  • {@link OAuth2CompositeAuthenticationTokenMixin}
  • + *
  • {@link OAuth2TokenExchangeCompositeAuthenticationTokenMixin}
  • *
  • {@link DurationMixin}
  • *
  • {@link JwsAlgorithmMixin}
  • *
  • {@link OAuth2TokenFormatMixin}
  • @@ -80,9 +80,10 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule { UnmodifiableMapMixin.class); context.setMixInAnnotations(HashSet.class, HashSetMixin.class); context.setMixInAnnotations(LinkedHashSet.class, HashSetMixin.class); - context.setMixInAnnotations(OAuth2ActorAuthenticationToken.class, OAuth2ActorAuthenticationTokenMixin.class); + context.setMixInAnnotations(OAuth2TokenExchangeActor.class, OAuth2TokenExchangeActorMixin.class); context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class); - context.setMixInAnnotations(OAuth2CompositeAuthenticationToken.class, OAuth2CompositeAuthenticationTokenMixin.class); + context.setMixInAnnotations(OAuth2TokenExchangeCompositeAuthenticationToken.class, + OAuth2TokenExchangeCompositeAuthenticationTokenMixin.class); context.setMixInAnnotations(Duration.class, DurationMixin.class); context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class); context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2ActorAuthenticationTokenMixin.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenExchangeActorMixin.java similarity index 85% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2ActorAuthenticationTokenMixin.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenExchangeActorMixin.java index 12d63f8f..5d928b8b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2ActorAuthenticationTokenMixin.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenExchangeActorMixin.java @@ -16,29 +16,31 @@ package org.springframework.security.oauth2.server.authorization.jackson2; +import java.util.Map; + import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ActorAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; /** - * This mixin class is used to serialize/deserialize {@link OAuth2ActorAuthenticationToken}. + * This mixin class is used to serialize/deserialize {@link OAuth2TokenExchangeActor}. * * @author Steve Riesenberg * @since 1.3 - * @see OAuth2ActorAuthenticationToken + * @see OAuth2TokenExchangeActor */ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE) @JsonIgnoreProperties(ignoreUnknown = true) -abstract class OAuth2ActorAuthenticationTokenMixin { +abstract class OAuth2TokenExchangeActorMixin { @JsonCreator - OAuth2ActorAuthenticationTokenMixin(@JsonProperty("name") String name) { + OAuth2TokenExchangeActorMixin(@JsonProperty("claims") Map claims) { } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2CompositeAuthenticationTokenMixin.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenExchangeCompositeAuthenticationTokenMixin.java similarity index 82% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2CompositeAuthenticationTokenMixin.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenExchangeCompositeAuthenticationTokenMixin.java index 34e3cce3..82ef287f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2CompositeAuthenticationTokenMixin.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenExchangeCompositeAuthenticationTokenMixin.java @@ -25,23 +25,23 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; /** - * This mixin class is used to serialize/deserialize {@link OAuth2CompositeAuthenticationToken}. + * This mixin class is used to serialize/deserialize {@link OAuth2TokenExchangeCompositeAuthenticationToken}. * * @author Steve Riesenberg * @since 1.3 - * @see OAuth2CompositeAuthenticationToken + * @see OAuth2TokenExchangeCompositeAuthenticationToken */ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE) @JsonIgnoreProperties(ignoreUnknown = true) -abstract class OAuth2CompositeAuthenticationTokenMixin { +abstract class OAuth2TokenExchangeCompositeAuthenticationTokenMixin { @JsonCreator - OAuth2CompositeAuthenticationTokenMixin(@JsonProperty("subject") Authentication subject, + OAuth2TokenExchangeCompositeAuthenticationTokenMixin(@JsonProperty("subject") Authentication subject, @JsonProperty("actors") List actors) { } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcAuthenticationProviderUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcAuthenticationProviderUtils.java index cd8cac4d..9a02ef7e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcAuthenticationProviderUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcAuthenticationProviderUtils.java @@ -16,10 +16,14 @@ package org.springframework.security.oauth2.server.authorization.oidc.authentication; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; /** * Utility methods for the OpenID Connect 1.0 {@link AuthenticationProvider}'s. @@ -60,4 +64,24 @@ final class OidcAuthenticationProviderUtils { return authorizationBuilder.build(); } + + static OAuth2AccessToken accessToken(OAuth2Authorization.Builder builder, T token, + OAuth2TokenContext accessTokenContext) { + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + token.getTokenValue(), token.getIssuedAt(), token.getExpiresAt(), + accessTokenContext.getAuthorizedScopes()); + OAuth2TokenFormat accessTokenFormat = accessTokenContext.getRegisteredClient().getTokenSettings() + .getAccessTokenFormat(); + builder.token(accessToken, (metadata) -> { + if (token instanceof ClaimAccessor claimAccessor) { + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claimAccessor.getClaims()); + } + metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false); + metadata.put(OAuth2TokenFormat.class.getName(), accessTokenFormat.getValue()); + }); + + return accessToken; + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java index 92611f8f..c125a8e0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java @@ -34,7 +34,6 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; 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; @@ -285,22 +284,14 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe this.logger.trace("Generated registration access token"); } - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - registrationAccessToken.getTokenValue(), registrationAccessToken.getIssuedAt(), - registrationAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - // @formatter:off OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(registeredClient.getClientId()) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizedScopes(authorizedScopes); // @formatter:on - if (registrationAccessToken instanceof ClaimAccessor) { - authorizationBuilder.token(accessToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) registrationAccessToken).getClaims())); - } else { - authorizationBuilder.accessToken(accessToken); - } + + OidcAuthenticationProviderUtils.accessToken(authorizationBuilder, registrationAccessToken, tokenContext); OAuth2Authorization authorization = authorizationBuilder.build(); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java index 80e8cd93..704b1022 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/DefaultOAuth2TokenClaimsConsumer.java @@ -18,7 +18,9 @@ package org.springframework.security.oauth2.server.authorization.token; import java.security.MessageDigest; import java.security.cert.X509Certificate; import java.util.Base64; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; @@ -28,6 +30,8 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; /** * @author Joe Grandja @@ -64,6 +68,20 @@ final class DefaultOAuth2TokenClaimsConsumer implements Consumer currentClaims = claims; + for (OAuth2TokenExchangeActor actor : compositeAuthenticationToken.getActors()) { + Map actorClaims = actor.getClaims(); + Map actClaim = new LinkedHashMap<>(); + actClaim.put(OAuth2TokenClaimNames.ISS, actorClaims.get(OAuth2TokenClaimNames.ISS)); + actClaim.put(OAuth2TokenClaimNames.SUB, actorClaims.get(OAuth2TokenClaimNames.SUB)); + currentClaims.put("act", Collections.unmodifiableMap(actClaim)); + currentClaims = actClaim; + } + } } private static String computeSHA256Thumbprint(X509Certificate x509Certificate) throws Exception { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenExchangeAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenExchangeAuthenticationConverter.java index 6fe1b4a7..d99d3031 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenExchangeAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenExchangeAuthenticationConverter.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -194,8 +195,9 @@ public final class OAuth2TokenExchangeAuthenticationConverter implements Authent } }); - return new OAuth2TokenExchangeAuthenticationToken(resources, audiences, requestedScopes, requestedTokenType, - subjectToken, subjectTokenType, actorToken, actorTokenType, clientPrincipal, additionalParameters); + return new OAuth2TokenExchangeAuthenticationToken(requestedTokenType, subjectToken, subjectTokenType, + clientPrincipal, actorToken, actorTokenType, new LinkedHashSet<>(resources), + new LinkedHashSet<>(audiences), requestedScopes, additionalParameters); } private static void validateTokenType(String parameterName, String tokenTypeValue) { @@ -221,4 +223,5 @@ public final class OAuth2TokenExchangeAuthenticationConverter implements Authent return false; } } + }