Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeSubjectTokenResolver;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
Expand Down Expand Up @@ -271,6 +272,11 @@ private static List<AuthenticationProvider> createDefaultAuthenticationProviders

OAuth2TokenExchangeAuthenticationProvider tokenExchangeAuthenticationProvider = new OAuth2TokenExchangeAuthenticationProvider(
authorizationService, tokenGenerator);
OAuth2TokenExchangeSubjectTokenResolver subjectTokenResolver = OAuth2ConfigurerUtils
.getOptionalBean(httpSecurity, OAuth2TokenExchangeSubjectTokenResolver.class);
if (subjectTokenResolver != null) {
tokenExchangeAuthenticationProvider.setSubjectTokenResolver(subjectTokenResolver);
}
authenticationProviders.add(tokenExchangeAuthenticationProvider);

return authenticationProviders;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,24 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti

private static final String MAY_ACT = "may_act";

/**
* The attribute name for the subject token claims stored in the
* {@link OAuth2Authorization}. These claims are available to
* {@link org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer}
* implementations for mapping external ID token claims to the generated access token.
* @since 7.0
*/
public static final String SUBJECT_TOKEN_CLAIMS_ATTRIBUTE = OAuth2TokenExchangeSubjectTokenContext.class.getName()
+ ".CLAIMS";

private final Log logger = LogFactory.getLog(getClass());

private final OAuth2AuthorizationService authorizationService;

private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;

private @Nullable OAuth2TokenExchangeSubjectTokenResolver subjectTokenResolver;

/**
* Constructs an {@code OAuth2TokenExchangeAuthenticationProvider} using the provided
* parameters.
Expand All @@ -100,6 +112,18 @@ public OAuth2TokenExchangeAuthenticationProvider(OAuth2AuthorizationService auth
this.tokenGenerator = tokenGenerator;
}

/**
* Sets the {@link OAuth2TokenExchangeSubjectTokenResolver} used for resolving
* externally-issued subject tokens (e.g., OIDC ID tokens from trusted identity
* providers).
* @param subjectTokenResolver the subject token resolver
* @since 7.0
*/
public void setSubjectTokenResolver(OAuth2TokenExchangeSubjectTokenResolver subjectTokenResolver) {
Assert.notNull(subjectTokenResolver, "subjectTokenResolver cannot be null");
this.subjectTokenResolver = subjectTokenResolver;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication = (OAuth2TokenExchangeAuthenticationToken) authentication;
Expand All @@ -123,45 +147,73 @@ public Authentication authenticate(Authentication authentication) throws Authent
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}

OAuth2Authorization subjectAuthorization = this.authorizationService
.findByToken(tokenExchangeAuthentication.getSubjectToken(), OAuth2TokenType.ACCESS_TOKEN);
if (subjectAuthorization == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
// Try to resolve the subject token using the configured resolver (e.g., for
// externally-issued ID tokens)
OAuth2TokenExchangeSubjectTokenContext subjectTokenContext = null;
if (this.subjectTokenResolver != null) {
subjectTokenContext = this.subjectTokenResolver.resolve(tokenExchangeAuthentication.getSubjectToken(),
tokenExchangeAuthentication.getSubjectTokenType(), registeredClient);
}

if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with subject token");
}
OAuth2Authorization subjectAuthorization;
Map<String, Object> authorizedActorClaims = null;

OAuth2Authorization.Token<OAuth2Token> subjectToken = subjectAuthorization
.getToken(tokenExchangeAuthentication.getSubjectToken());
Assert.notNull(subjectToken, "subjectToken cannot be null");
if (!subjectToken.isActive()) {
// As per https://tools.ietf.org/html/rfc6749#section-5.2
// invalid_grant: The provided authorization grant (e.g., authorization code,
// resource owner credentials) or refresh token is invalid, expired, revoked
// [...].
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (subjectTokenContext != null) {
// Build an OAuth2Authorization from the resolved external subject token
// @formatter:off
subjectAuthorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(subjectTokenContext.getPrincipalName())
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
.attribute(Principal.class.getName(), subjectTokenContext.getPrincipal())
.attribute(SUBJECT_TOKEN_CLAIMS_ATTRIBUTE, subjectTokenContext.getClaims())
.build();
// @formatter:on

if (!isValidTokenType(tokenExchangeAuthentication.getSubjectTokenType(), subjectToken)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Resolved external subject token");
}
}
else {
subjectAuthorization = this.authorizationService.findByToken(tokenExchangeAuthentication.getSubjectToken(),
OAuth2TokenType.ACCESS_TOKEN);
if (subjectAuthorization == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}

if (subjectAuthorization.getAttribute(Principal.class.getName()) == null) {
// As per https://datatracker.ietf.org/doc/html/rfc8693#section-1.1,
// we require a principal to be available via the subject_token for
// impersonation or delegation use cases.
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with subject token");
}

// 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.
Map<String, Object> authorizedActorClaims = null;
if (subjectToken.getClaims() != null && subjectToken.getClaims().containsKey(MAY_ACT)
&& subjectToken.getClaims().get(MAY_ACT) instanceof Map<?, ?> mayAct) {
authorizedActorClaims = (Map<String, Object>) mayAct;
OAuth2Authorization.Token<OAuth2Token> subjectToken = subjectAuthorization
.getToken(tokenExchangeAuthentication.getSubjectToken());
Assert.notNull(subjectToken, "subjectToken cannot be null");
if (!subjectToken.isActive()) {
// As per https://tools.ietf.org/html/rfc6749#section-5.2
// invalid_grant: The provided authorization grant (e.g., authorization
// code,
// resource owner credentials) or refresh token is invalid, expired,
// revoked [...].
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}

if (!isValidTokenType(tokenExchangeAuthentication.getSubjectTokenType(), subjectToken)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}

if (subjectAuthorization.getAttribute(Principal.class.getName()) == null) {
// As per https://datatracker.ietf.org/doc/html/rfc8693#section-1.1,
// we require a principal to be available via the subject_token for
// impersonation or delegation use cases.
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}

// 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.
if (subjectToken.getClaims() != null && subjectToken.getClaims().containsKey(MAY_ACT)
&& subjectToken.getClaims().get(MAY_ACT) instanceof Map<?, ?> mayAct) {
authorizedActorClaims = (Map<String, Object>) mayAct;
}
}

OAuth2Authorization actorAuthorization = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2004-present 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.Set;

import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;

/**
* The context returned by an {@link OAuth2TokenExchangeSubjectTokenResolver} containing
* the resolved principal and claims from the subject token.
*
* @author Bapuji Koraganti
* @since 7.0
* @see OAuth2TokenExchangeSubjectTokenResolver
*/
public final class OAuth2TokenExchangeSubjectTokenContext {

private final Authentication principal;

private final String principalName;

private final Map<String, Object> claims;

private final Set<String> scopes;

/**
* Constructs an {@code OAuth2TokenExchangeSubjectTokenContext} using the provided
* parameters.
* @param principal the authenticated principal resolved from the subject token
* @param principalName the principal name (e.g., the {@code sub} claim)
* @param claims the claims extracted from the subject token
* @param scopes the scopes associated with the subject token
*/
public OAuth2TokenExchangeSubjectTokenContext(Authentication principal, String principalName,
Map<String, Object> claims, Set<String> scopes) {
Assert.notNull(principal, "principal cannot be null");
Assert.hasText(principalName, "principalName cannot be empty");
Assert.notNull(claims, "claims cannot be null");
Assert.notNull(scopes, "scopes cannot be null");
this.principal = principal;
this.principalName = principalName;
this.claims = Collections.unmodifiableMap(claims);
this.scopes = Collections.unmodifiableSet(scopes);
}

/**
* Returns the authenticated principal resolved from the subject token.
* @return the authenticated principal
*/
public Authentication getPrincipal() {
return this.principal;
}

/**
* Returns the principal name (e.g., the {@code sub} claim from an ID token).
* @return the principal name
*/
public String getPrincipalName() {
return this.principalName;
}

/**
* Returns the claims extracted from the subject token.
* @return the claims
*/
public Map<String, Object> getClaims() {
return this.claims;
}

/**
* Returns the scopes associated with the subject token.
* @return the scopes
*/
public Set<String> getScopes() {
return this.scopes;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2004-present 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 org.jspecify.annotations.Nullable;

import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;

/**
* A strategy for resolving an externally-issued subject token into an
* {@link OAuth2TokenExchangeSubjectTokenContext} during the OAuth 2.0 Token Exchange
* Grant.
*
* <p>
* Implementations of this interface are responsible for validating and decoding the
* subject token (e.g., an externally-issued ID token) and constructing the authorization
* and principal context needed for token exchange.
*
* <p>
* <b>NOTE:</b> When this resolver returns a non-{@code null} context, the
* {@link OAuth2TokenExchangeAuthenticationProvider} constructs a synthetic
* {@link org.springframework.security.oauth2.server.authorization.OAuth2Authorization}
* that contains only the principal name and principal attribute. This synthetic
* authorization does not contain an access token or other token metadata. Token
* generators or customizers that inspect the authorization's tokens should account for
* this.
*
* @author Bapuji Koraganti
* @since 7.0
* @see OAuth2TokenExchangeAuthenticationProvider
* @see OAuth2TokenExchangeSubjectTokenContext
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc8693#section-2.1">Section 2.1 Request</a>
*/
@FunctionalInterface
public interface OAuth2TokenExchangeSubjectTokenResolver {

/**
* Resolves the subject token into an {@link OAuth2TokenExchangeSubjectTokenContext}.
* Returns {@code null} if this resolver cannot handle the given token type.
* @param subjectToken the subject token value
* @param subjectTokenType the token type identifier (e.g.,
* {@code urn:ietf:params:oauth:token-type:id_token})
* @param registeredClient the registered client performing the token exchange
* @return the resolved subject token context, or {@code null} if not supported
*/
@Nullable OAuth2TokenExchangeSubjectTokenContext resolve(String subjectToken, String subjectTokenType,
RegisteredClient registeredClient);

}
Loading