16 changed files with 1129 additions and 12 deletions
@ -0,0 +1,53 @@ |
|||||||
|
/* |
||||||
|
* 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; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
/* |
||||||
|
* 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.ArrayList; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
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 the principal in a composite token (e.g. the "delegation" use case). |
||||||
|
* |
||||||
|
* @author Steve Riesenberg |
||||||
|
* @since 1.3 |
||||||
|
* @see OAuth2TokenExchangeAuthenticationToken |
||||||
|
*/ |
||||||
|
public class OAuth2CompositeAuthenticationToken extends AbstractAuthenticationToken implements Serializable { |
||||||
|
|
||||||
|
private final Authentication subject; |
||||||
|
|
||||||
|
private final List<Authentication> actors; |
||||||
|
|
||||||
|
public OAuth2CompositeAuthenticationToken(Authentication subject, List<Authentication> actors) { |
||||||
|
super(subject != null ? subject.getAuthorities() : null); |
||||||
|
Assert.notNull(subject, "subject cannot be null"); |
||||||
|
Assert.notNull(actors, "actors cannot be null"); |
||||||
|
this.subject = subject; |
||||||
|
this.actors = Collections.unmodifiableList(new ArrayList<>(actors)); |
||||||
|
setDetails(subject.getDetails()); |
||||||
|
setAuthenticated(subject.isAuthenticated()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object getPrincipal() { |
||||||
|
return this.subject.getPrincipal(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object getCredentials() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public Authentication getSubject() { |
||||||
|
return this.subject; |
||||||
|
} |
||||||
|
|
||||||
|
public List<Authentication> getActors() { |
||||||
|
return this.actors; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,314 @@ |
|||||||
|
/* |
||||||
|
* 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.security.Principal; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.LinkedList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
|
||||||
|
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.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; |
||||||
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; |
||||||
|
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.OAuth2TokenContext; |
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.CollectionUtils; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; |
||||||
|
|
||||||
|
/** |
||||||
|
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Token Exchange Grant. |
||||||
|
* |
||||||
|
* @author Steve Riesenberg |
||||||
|
* @since 1.3 |
||||||
|
* @see OAuth2TokenExchangeAuthenticationToken |
||||||
|
* @see OAuth2AccessTokenAuthenticationToken |
||||||
|
* @see OAuth2AuthorizationService |
||||||
|
* @see OAuth2TokenGenerator |
||||||
|
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8693#section-1">Section 1 Introduction</a> |
||||||
|
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8693#section-2.1">Section 2.1 Request</a> |
||||||
|
*/ |
||||||
|
public final class OAuth2TokenExchangeAuthenticationProvider implements AuthenticationProvider { |
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; |
||||||
|
|
||||||
|
private static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType( |
||||||
|
"urn:ietf:params:oauth:grant-type:token-exchange"); |
||||||
|
|
||||||
|
private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt"; |
||||||
|
|
||||||
|
private static final String MAY_ACT = "may_act"; |
||||||
|
|
||||||
|
private static final String ISSUED_TOKEN_TYPE = "issued_token_type"; |
||||||
|
|
||||||
|
private final Log logger = LogFactory.getLog(getClass()); |
||||||
|
|
||||||
|
private final OAuth2AuthorizationService authorizationService; |
||||||
|
|
||||||
|
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs an {@code OAuth2TokenExchangeAuthenticationProvider} using the provided parameters. |
||||||
|
* |
||||||
|
* @param authorizationService the authorization service |
||||||
|
* @param tokenGenerator the token generator |
||||||
|
*/ |
||||||
|
public OAuth2TokenExchangeAuthenticationProvider(OAuth2AuthorizationService authorizationService, |
||||||
|
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) { |
||||||
|
Assert.notNull(authorizationService, "authorizationService cannot be null"); |
||||||
|
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); |
||||||
|
this.authorizationService = authorizationService; |
||||||
|
this.tokenGenerator = tokenGenerator; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||||
|
OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication = |
||||||
|
(OAuth2TokenExchangeAuthenticationToken) authentication; |
||||||
|
|
||||||
|
OAuth2ClientAuthenticationToken clientPrincipal = |
||||||
|
getAuthenticatedClientElseThrowInvalidClient(tokenExchangeAuthentication); |
||||||
|
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); |
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled()) { |
||||||
|
this.logger.trace("Retrieved registered client"); |
||||||
|
} |
||||||
|
|
||||||
|
if (!registeredClient.getAuthorizationGrantTypes().contains(TOKEN_EXCHANGE)) { |
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); |
||||||
|
} |
||||||
|
|
||||||
|
if (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getRequestedTokenType()) && |
||||||
|
!OAuth2TokenFormat.SELF_CONTAINED.equals(registeredClient.getTokenSettings().getAccessTokenFormat())) { |
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled()) { |
||||||
|
this.logger.trace("Retrieved authorization with subject token"); |
||||||
|
} |
||||||
|
|
||||||
|
OAuth2Authorization.Token<OAuth2Token> subjectToken = subjectAuthorization.getToken( |
||||||
|
tokenExchangeAuthentication.getSubjectToken()); |
||||||
|
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 (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getSubjectTokenType()) && |
||||||
|
!Jwt.class.isAssignableFrom(subjectToken.getToken().getClass())) { |
||||||
|
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.
|
||||||
|
String authorizedActorSubject = null; |
||||||
|
if (subjectToken.getClaims() != null && |
||||||
|
subjectToken.getClaims().containsKey(MAY_ACT) && |
||||||
|
subjectToken.getClaims().get(MAY_ACT) instanceof Map<?, ?> mayAct) { |
||||||
|
authorizedActorSubject = (String) mayAct.get(StandardClaimNames.SUB); |
||||||
|
} |
||||||
|
|
||||||
|
OAuth2Authorization actorAuthorization = null; |
||||||
|
if (StringUtils.hasText(tokenExchangeAuthentication.getActorToken())) { |
||||||
|
actorAuthorization = this.authorizationService.findByToken( |
||||||
|
tokenExchangeAuthentication.getActorToken(), OAuth2TokenType.ACCESS_TOKEN); |
||||||
|
if (actorAuthorization == null) { |
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled()) { |
||||||
|
this.logger.trace("Retrieved authorization with actor token"); |
||||||
|
} |
||||||
|
|
||||||
|
OAuth2Authorization.Token<OAuth2Token> actorToken = actorAuthorization.getToken( |
||||||
|
tokenExchangeAuthentication.getActorToken()); |
||||||
|
if (!actorToken.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 (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getActorTokenType()) && |
||||||
|
!Jwt.class.isAssignableFrom(actorToken.getToken().getClass())) { |
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); |
||||||
|
} |
||||||
|
|
||||||
|
if (StringUtils.hasText(authorizedActorSubject) && |
||||||
|
!authorizedActorSubject.equals(actorAuthorization.getPrincipalName())) { |
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); |
||||||
|
} |
||||||
|
} else if (StringUtils.hasText(authorizedActorSubject) && |
||||||
|
!authorizedActorSubject.equals(clientPrincipal.getName())) { |
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); |
||||||
|
} |
||||||
|
|
||||||
|
Set<String> authorizedScopes = Collections.emptySet(); |
||||||
|
if (!CollectionUtils.isEmpty(tokenExchangeAuthentication.getScopes())) { |
||||||
|
authorizedScopes = validateRequestedScopes(registeredClient, tokenExchangeAuthentication.getScopes()); |
||||||
|
} else if (!CollectionUtils.isEmpty(subjectAuthorization.getAuthorizedScopes())) { |
||||||
|
authorizedScopes = validateRequestedScopes(registeredClient, subjectAuthorization.getAuthorizedScopes()); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled()) { |
||||||
|
this.logger.trace("Validated token request parameters"); |
||||||
|
} |
||||||
|
|
||||||
|
Authentication principal = getPrincipal(subjectAuthorization, actorAuthorization); |
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() |
||||||
|
.registeredClient(registeredClient) |
||||||
|
.authorization(subjectAuthorization) |
||||||
|
.principal(principal) |
||||||
|
.authorizationServerContext(AuthorizationServerContextHolder.getContext()) |
||||||
|
.authorizedScopes(authorizedScopes) |
||||||
|
.tokenType(OAuth2TokenType.ACCESS_TOKEN) |
||||||
|
.authorizationGrantType(TOKEN_EXCHANGE) |
||||||
|
.authorizationGrant(tokenExchangeAuthentication); |
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
// ----- Access token -----
|
||||||
|
OAuth2TokenContext tokenContext = tokenContextBuilder.build(); |
||||||
|
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); |
||||||
|
if (generatedAccessToken == null) { |
||||||
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, |
||||||
|
"The token generator failed to generate the access token.", ERROR_URI); |
||||||
|
throw new OAuth2AuthenticationException(error); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled()) { |
||||||
|
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()) |
||||||
|
.authorizationGrantType(TOKEN_EXCHANGE) |
||||||
|
.authorizedScopes(authorizedScopes) |
||||||
|
.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); |
||||||
|
} |
||||||
|
|
||||||
|
OAuth2Authorization authorization = authorizationBuilder.build(); |
||||||
|
this.authorizationService.save(authorization); |
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled()) { |
||||||
|
this.logger.trace("Saved authorization"); |
||||||
|
} |
||||||
|
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>(); |
||||||
|
additionalParameters.put(ISSUED_TOKEN_TYPE, tokenExchangeAuthentication.getRequestedTokenType()); |
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled()) { |
||||||
|
this.logger.trace("Authenticated token request"); |
||||||
|
} |
||||||
|
|
||||||
|
return new OAuth2AccessTokenAuthenticationToken( |
||||||
|
registeredClient, clientPrincipal, accessToken, null, additionalParameters); |
||||||
|
} |
||||||
|
|
||||||
|
private static Set<String> validateRequestedScopes(RegisteredClient registeredClient, Set<String> requestedScopes) { |
||||||
|
for (String requestedScope : requestedScopes) { |
||||||
|
if (!registeredClient.getScopes().contains(requestedScope)) { |
||||||
|
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return new LinkedHashSet<>(requestedScopes); |
||||||
|
} |
||||||
|
|
||||||
|
private static Authentication getPrincipal(OAuth2Authorization subjectAuthorization, OAuth2Authorization actorAuthorization) { |
||||||
|
Authentication subjectPrincipal = subjectAuthorization.getAttribute(Principal.class.getName()); |
||||||
|
|
||||||
|
List<Authentication> actorPrincipals = new LinkedList<>(); |
||||||
|
if (actorAuthorization != null) { |
||||||
|
actorPrincipals.add(new OAuth2ActorAuthenticationToken(actorAuthorization.getPrincipalName())); |
||||||
|
} |
||||||
|
|
||||||
|
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).
|
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supports(Class<?> authentication) { |
||||||
|
return OAuth2TokenExchangeAuthenticationToken.class.isAssignableFrom(authentication); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,164 @@ |
|||||||
|
/* |
||||||
|
* 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.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* An {@link Authentication} implementation used for the OAuth 2.0 Token Exchange Grant. |
||||||
|
* |
||||||
|
* @author Steve Riesenberg |
||||||
|
* @since 1.3 |
||||||
|
* @see OAuth2AuthorizationGrantAuthenticationToken |
||||||
|
* @see OAuth2TokenExchangeAuthenticationProvider |
||||||
|
*/ |
||||||
|
public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { |
||||||
|
|
||||||
|
private static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType( |
||||||
|
"urn:ietf:params:oauth:grant-type:token-exchange"); |
||||||
|
|
||||||
|
private final List<String> resources; |
||||||
|
|
||||||
|
private final List<String> audiences; |
||||||
|
|
||||||
|
private final String requestedTokenType; |
||||||
|
|
||||||
|
private final String subjectToken; |
||||||
|
|
||||||
|
private final String subjectTokenType; |
||||||
|
|
||||||
|
private final String actorToken; |
||||||
|
|
||||||
|
private final String actorTokenType; |
||||||
|
|
||||||
|
private final Set<String> 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 actorToken the actor token |
||||||
|
* @param actorTokenType the actor token type |
||||||
|
* @param clientPrincipal the authenticated client principal |
||||||
|
* @param additionalParameters the additional parameters |
||||||
|
*/ |
||||||
|
public OAuth2TokenExchangeAuthenticationToken(List<String> resources, List<String> audiences, |
||||||
|
@Nullable Set<String> scopes, @Nullable String requestedTokenType, String subjectToken, |
||||||
|
String subjectTokenType, @Nullable String actorToken, @Nullable String actorTokenType, |
||||||
|
Authentication clientPrincipal, @Nullable Map<String, Object> additionalParameters) { |
||||||
|
super(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.scopes = Collections.unmodifiableSet( |
||||||
|
scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the list of resource URIs. |
||||||
|
* |
||||||
|
* @return the list of resource URIs |
||||||
|
*/ |
||||||
|
public List<String> getResources() { |
||||||
|
return this.resources; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the list of audience values. |
||||||
|
* |
||||||
|
* @return the list of audience values |
||||||
|
*/ |
||||||
|
public List<String> getAudiences() { |
||||||
|
return this.audiences; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the requested scope(s). |
||||||
|
* |
||||||
|
* @return the requested scope(s), or an empty {@code Set} if not available |
||||||
|
*/ |
||||||
|
public Set<String> getScopes() { |
||||||
|
return this.scopes; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the requested token type. |
||||||
|
* |
||||||
|
* @return the requested token type |
||||||
|
*/ |
||||||
|
public String getRequestedTokenType() { |
||||||
|
return this.requestedTokenType; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the subject token. |
||||||
|
* |
||||||
|
* @return the subject token |
||||||
|
*/ |
||||||
|
public String getSubjectToken() { |
||||||
|
return this.subjectToken; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the subject token type. |
||||||
|
* |
||||||
|
* @return the subject token type |
||||||
|
*/ |
||||||
|
public String getSubjectTokenType() { |
||||||
|
return this.subjectTokenType; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the actor token. |
||||||
|
* |
||||||
|
* @return the actor token |
||||||
|
*/ |
||||||
|
public String getActorToken() { |
||||||
|
return this.actorToken; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the actor token type. |
||||||
|
* |
||||||
|
* @return the actor token type |
||||||
|
*/ |
||||||
|
public String getActorTokenType() { |
||||||
|
return this.actorTokenType; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
/* |
||||||
|
* 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<T extends OAuth2TokenContext> implements OAuth2TokenCustomizer<T> { |
||||||
|
|
||||||
|
private final List<OAuth2TokenCustomizer<T>> tokenCustomizers; |
||||||
|
|
||||||
|
DelegatingOAuth2TokenCustomizer(List<OAuth2TokenCustomizer<T>> tokenCustomizers) { |
||||||
|
Assert.notEmpty(tokenCustomizers, "tokenCustomizers cannot be empty"); |
||||||
|
this.tokenCustomizers = Collections.unmodifiableList(tokenCustomizers); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void customize(T context) { |
||||||
|
for (OAuth2TokenCustomizer<T> tokenCustomizer : this.tokenCustomizers) { |
||||||
|
tokenCustomizer.customize(context); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,85 @@ |
|||||||
|
/* |
||||||
|
* 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 static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType( |
||||||
|
"urn:ietf:params:oauth:grant-type:token-exchange"); |
||||||
|
|
||||||
|
private OAuth2TokenExchangeTokenCustomizers() { |
||||||
|
} |
||||||
|
|
||||||
|
static OAuth2TokenCustomizer<JwtEncodingContext> jwt() { |
||||||
|
return (context) -> context.getClaims().claims((claims) -> customize(context, claims)); |
||||||
|
} |
||||||
|
|
||||||
|
static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessToken() { |
||||||
|
return (context) -> context.getClaims().claims((claims) -> customize(context, claims)); |
||||||
|
} |
||||||
|
|
||||||
|
private static void customize(OAuth2TokenContext context, Map<String, Object> claims) { |
||||||
|
if (!TOKEN_EXCHANGE.equals(context.getAuthorizationGrantType())) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (context.getAuthorizationGrant() instanceof OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication) { |
||||||
|
// Customize the token claims when audience is present in the request
|
||||||
|
List<String> 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<String, Object> currentClaims = claims; |
||||||
|
for (Authentication actorPrincipal : compositeAuthenticationToken.getActors()) { |
||||||
|
Map<String, Object> actClaim = new HashMap<>(); |
||||||
|
actClaim.put("sub", actorPrincipal.getName()); |
||||||
|
currentClaims.put("act", Collections.unmodifiableMap(actClaim)); |
||||||
|
currentClaims = actClaim; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
/* |
||||||
|
* 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.jackson2; |
||||||
|
|
||||||
|
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; |
||||||
|
|
||||||
|
/** |
||||||
|
* This mixin class is used to serialize/deserialize {@link OAuth2ActorAuthenticationToken}. |
||||||
|
* |
||||||
|
* @author Steve Riesenberg |
||||||
|
* @since 1.3 |
||||||
|
* @see OAuth2ActorAuthenticationToken |
||||||
|
*/ |
||||||
|
@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 { |
||||||
|
|
||||||
|
@JsonCreator |
||||||
|
OAuth2ActorAuthenticationTokenMixin(@JsonProperty("name") String name) { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
/* |
||||||
|
* 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.jackson2; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
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.core.Authentication; |
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken; |
||||||
|
|
||||||
|
/** |
||||||
|
* This mixin class is used to serialize/deserialize {@link OAuth2CompositeAuthenticationToken}. |
||||||
|
* |
||||||
|
* @author Steve Riesenberg |
||||||
|
* @since 1.3 |
||||||
|
* @see OAuth2CompositeAuthenticationToken |
||||||
|
*/ |
||||||
|
@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 { |
||||||
|
|
||||||
|
@JsonCreator |
||||||
|
OAuth2CompositeAuthenticationTokenMixin(@JsonProperty("subject") Authentication subject, |
||||||
|
@JsonProperty("actors") List<Authentication> actors) { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,241 @@ |
|||||||
|
/* |
||||||
|
* 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.web.authentication; |
||||||
|
|
||||||
|
import java.net.URI; |
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.core.context.SecurityContextHolder; |
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
||||||
|
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.endpoint.OAuth2ParameterNames; |
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationToken; |
||||||
|
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; |
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter; |
||||||
|
import org.springframework.util.CollectionUtils; |
||||||
|
import org.springframework.util.MultiValueMap; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0 Token Exchange Grant |
||||||
|
* and then converts it to an {@link OAuth2TokenExchangeAuthenticationToken} used for authenticating the authorization grant. |
||||||
|
* |
||||||
|
* @author Steve Riesenberg |
||||||
|
* @since 1.3 |
||||||
|
* @see AuthenticationConverter |
||||||
|
* @see OAuth2TokenExchangeAuthenticationToken |
||||||
|
* @see OAuth2TokenEndpointFilter |
||||||
|
*/ |
||||||
|
public final class OAuth2TokenExchangeAuthenticationConverter implements AuthenticationConverter { |
||||||
|
|
||||||
|
private static final String TOKEN_TYPE_IDENTIFIERS_URI = "https://datatracker.ietf.org/doc/html/rfc8693#section-3"; |
||||||
|
|
||||||
|
private static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType( |
||||||
|
"urn:ietf:params:oauth:grant-type:token-exchange"); |
||||||
|
|
||||||
|
private static final String AUDIENCE = "audience"; |
||||||
|
|
||||||
|
private static final String RESOURCE = "resource"; |
||||||
|
|
||||||
|
private static final String REQUESTED_TOKEN_TYPE = "requested_token_type"; |
||||||
|
|
||||||
|
private static final String SUBJECT_TOKEN = "subject_token"; |
||||||
|
|
||||||
|
private static final String SUBJECT_TOKEN_TYPE = "subject_token_type"; |
||||||
|
|
||||||
|
private static final String ACTOR_TOKEN = "actor_token"; |
||||||
|
|
||||||
|
private static final String ACTOR_TOKEN_TYPE = "actor_token_type"; |
||||||
|
|
||||||
|
private static final String ACCESS_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:access_token"; |
||||||
|
|
||||||
|
private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt"; |
||||||
|
|
||||||
|
private static final Set<String> SUPPORTED_TOKEN_TYPES = Set.of(ACCESS_TOKEN_TYPE_VALUE, JWT_TOKEN_TYPE_VALUE); |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Authentication convert(HttpServletRequest request) { |
||||||
|
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request); |
||||||
|
|
||||||
|
// grant_type (REQUIRED)
|
||||||
|
String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE); |
||||||
|
if (!TOKEN_EXCHANGE.getValue().equals(grantType)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); |
||||||
|
|
||||||
|
// resource (OPTIONAL)
|
||||||
|
List<String> resources = parameters.getOrDefault(RESOURCE, Collections.emptyList()); |
||||||
|
if (!CollectionUtils.isEmpty(resources)) { |
||||||
|
for (String resource : resources) { |
||||||
|
if (!isValidUri(resource)) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
RESOURCE, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// audience (OPTIONAL)
|
||||||
|
List<String> audiences = parameters.getOrDefault(AUDIENCE, Collections.emptyList()); |
||||||
|
|
||||||
|
// scope (OPTIONAL)
|
||||||
|
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); |
||||||
|
if (StringUtils.hasText(scope) && |
||||||
|
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
OAuth2ParameterNames.SCOPE, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} |
||||||
|
|
||||||
|
Set<String> requestedScopes = null; |
||||||
|
if (StringUtils.hasText(scope)) { |
||||||
|
requestedScopes = new HashSet<>( |
||||||
|
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); |
||||||
|
} |
||||||
|
|
||||||
|
// requested_token_type (OPTIONAL)
|
||||||
|
String requestedTokenType = parameters.getFirst(REQUESTED_TOKEN_TYPE); |
||||||
|
if (StringUtils.hasText(requestedTokenType)) { |
||||||
|
if (parameters.get(REQUESTED_TOKEN_TYPE).size() != 1) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
REQUESTED_TOKEN_TYPE, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} |
||||||
|
|
||||||
|
validateTokenType(REQUESTED_TOKEN_TYPE, requestedTokenType); |
||||||
|
} else { |
||||||
|
requestedTokenType = ACCESS_TOKEN_TYPE_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
// subject_token (REQUIRED)
|
||||||
|
String subjectToken = parameters.getFirst(SUBJECT_TOKEN); |
||||||
|
if (!StringUtils.hasText(subjectToken) || |
||||||
|
parameters.get(SUBJECT_TOKEN).size() != 1) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
SUBJECT_TOKEN, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} |
||||||
|
|
||||||
|
// subject_token_type (REQUIRED)
|
||||||
|
String subjectTokenType = parameters.getFirst(SUBJECT_TOKEN_TYPE); |
||||||
|
if (!StringUtils.hasText(subjectTokenType) || |
||||||
|
parameters.get(SUBJECT_TOKEN_TYPE).size() != 1) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
SUBJECT_TOKEN_TYPE, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} else { |
||||||
|
validateTokenType(SUBJECT_TOKEN_TYPE, subjectTokenType); |
||||||
|
} |
||||||
|
|
||||||
|
// actor_token (OPTIONAL, REQUIRED if actor_token_type is provided)
|
||||||
|
String actorToken = parameters.getFirst(ACTOR_TOKEN); |
||||||
|
if (StringUtils.hasText(actorToken) && |
||||||
|
parameters.get(ACTOR_TOKEN).size() != 1) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
ACTOR_TOKEN, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} |
||||||
|
|
||||||
|
// actor_token_type (OPTIONAL, REQUIRED if actor_token is provided)
|
||||||
|
String actorTokenType = parameters.getFirst(ACTOR_TOKEN_TYPE); |
||||||
|
if (StringUtils.hasText(actorTokenType)) { |
||||||
|
if (parameters.get(ACTOR_TOKEN_TYPE).size() != 1) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
ACTOR_TOKEN_TYPE, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} |
||||||
|
|
||||||
|
validateTokenType(ACTOR_TOKEN_TYPE, actorTokenType); |
||||||
|
} |
||||||
|
|
||||||
|
if (!StringUtils.hasText(actorToken) && StringUtils.hasText(actorTokenType)) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
ACTOR_TOKEN, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} else if (StringUtils.hasText(actorToken) && !StringUtils.hasText(actorTokenType)) { |
||||||
|
OAuth2EndpointUtils.throwError( |
||||||
|
OAuth2ErrorCodes.INVALID_REQUEST, |
||||||
|
ACTOR_TOKEN_TYPE, |
||||||
|
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); |
||||||
|
} |
||||||
|
|
||||||
|
Map<String, Object> additionalParameters = new HashMap<>(); |
||||||
|
parameters.forEach((key, value) -> { |
||||||
|
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && |
||||||
|
!key.equals(RESOURCE) && |
||||||
|
!key.equals(AUDIENCE) && |
||||||
|
!key.equals(REQUESTED_TOKEN_TYPE) && |
||||||
|
!key.equals(SUBJECT_TOKEN) && |
||||||
|
!key.equals(SUBJECT_TOKEN_TYPE) && |
||||||
|
!key.equals(ACTOR_TOKEN) && |
||||||
|
!key.equals(ACTOR_TOKEN_TYPE) && |
||||||
|
!key.equals(OAuth2ParameterNames.SCOPE)) { |
||||||
|
additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0])); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return new OAuth2TokenExchangeAuthenticationToken(resources, audiences, requestedScopes, requestedTokenType, |
||||||
|
subjectToken, subjectTokenType, actorToken, actorTokenType, clientPrincipal, additionalParameters); |
||||||
|
} |
||||||
|
|
||||||
|
private static void validateTokenType(String parameterName, String tokenTypeValue) { |
||||||
|
if (!SUPPORTED_TOKEN_TYPES.contains(tokenTypeValue)) { |
||||||
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.UNSUPPORTED_TOKEN_TYPE, |
||||||
|
String.format("OAuth 2.0 Token Exchange parameter: %s", parameterName), TOKEN_TYPE_IDENTIFIERS_URI); |
||||||
|
// @formatter:off
|
||||||
|
String message = String.format( |
||||||
|
"OAuth 2.0 Token Exchange parameter: %s - " + |
||||||
|
"The provided value is not supported by this authorization server. " + |
||||||
|
"Supported values are %s and %s.", |
||||||
|
parameterName, ACCESS_TOKEN_TYPE_VALUE, JWT_TOKEN_TYPE_VALUE); |
||||||
|
// @formatter:on
|
||||||
|
throw new OAuth2AuthenticationException(error, message); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean isValidUri(String uri) { |
||||||
|
try { |
||||||
|
URI validUri = new URI(uri); |
||||||
|
return validUri.isAbsolute() && validUri.getFragment() == null; |
||||||
|
} catch (URISyntaxException ex) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue