16 changed files with 1129 additions and 12 deletions
@ -0,0 +1,53 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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