Skip to content

Commit 7ef544f

Browse files
committed
Update Token Exchange
* updates based on review feedback * tests for Token Exchange Issue spring-projectsgh-60
1 parent a32eaaa commit 7ef544f

14 files changed

+1305
-112
lines changed

Diff for: oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
4444
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
4545
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
46-
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ActorAuthenticationToken;
47-
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken;
46+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
47+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken;
4848
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
4949
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
5050
import org.springframework.security.web.authentication.WebAuthenticationDetails;
@@ -111,9 +111,9 @@ private void registerHints(RuntimeHints hints) {
111111
TypeReference.of(OidcIdToken.class),
112112
TypeReference.of(AbstractOAuth2Token.class),
113113
TypeReference.of(OidcUserInfo.class),
114-
TypeReference.of(OAuth2ActorAuthenticationToken.class),
114+
TypeReference.of(OAuth2TokenExchangeActor.class),
115115
TypeReference.of(OAuth2AuthorizationRequest.class),
116-
TypeReference.of(OAuth2CompositeAuthenticationToken.class),
116+
TypeReference.of(OAuth2TokenExchangeCompositeAuthenticationToken.class),
117117
TypeReference.of(AuthorizationGrantType.class),
118118
TypeReference.of(OAuth2AuthorizationResponseType.class),
119119
TypeReference.of(OAuth2TokenFormat.class)
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
package org.springframework.security.oauth2.server.authorization.authentication;
1818

19-
import java.io.Serializable;
2019
import java.util.Collections;
20+
import java.util.Map;
21+
import java.util.Objects;
2122

22-
import org.springframework.security.authentication.AbstractAuthenticationToken;
2323
import org.springframework.security.core.Authentication;
24+
import org.springframework.security.oauth2.core.ClaimAccessor;
25+
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
2426
import org.springframework.util.Assert;
2527

2628
/**
@@ -29,25 +31,41 @@
2931
*
3032
* @author Steve Riesenberg
3133
* @since 1.3
32-
* @see OAuth2CompositeAuthenticationToken
34+
* @see OAuth2TokenExchangeCompositeAuthenticationToken
3335
*/
34-
public class OAuth2ActorAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
36+
public class OAuth2TokenExchangeActor implements ClaimAccessor {
3537

36-
private final String name;
38+
private final Map<String, Object> claims;
3739

38-
public OAuth2ActorAuthenticationToken(String name) {
39-
super(Collections.emptyList());
40-
Assert.hasText(name, "name cannot be empty");
41-
this.name = name;
40+
public OAuth2TokenExchangeActor(Map<String, Object> claims) {
41+
Assert.notNull(claims, "claims cannot be null");
42+
this.claims = Collections.unmodifiableMap(claims);
4243
}
4344

4445
@Override
45-
public Object getPrincipal() {
46-
return this.name;
46+
public Map<String, Object> getClaims() {
47+
return this.claims;
48+
}
49+
50+
public String getIssuer() {
51+
return getClaimAsString(OAuth2TokenClaimNames.ISS);
52+
}
53+
54+
public String getSubject() {
55+
return getClaimAsString(OAuth2TokenClaimNames.SUB);
4756
}
4857

4958
@Override
50-
public Object getCredentials() {
51-
return null;
59+
public boolean equals(Object obj) {
60+
if (!(obj instanceof OAuth2TokenExchangeActor other)) {
61+
return false;
62+
}
63+
return Objects.equals(this.claims, other.claims);
5264
}
65+
66+
@Override
67+
public int hashCode() {
68+
return Objects.hash(this.claims);
69+
}
70+
5371
}

Diff for: oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java

+31-20
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.LinkedList;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.Objects;
2526
import java.util.Set;
2627

2728
import org.apache.commons.logging.Log;
@@ -38,7 +39,6 @@
3839
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
3940
import org.springframework.security.oauth2.core.OAuth2Token;
4041
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
41-
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
4242
import org.springframework.security.oauth2.jwt.Jwt;
4343
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
4444
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -47,6 +47,7 @@
4747
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
4848
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
4949
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
50+
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
5051
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
5152
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
5253
import org.springframework.util.Assert;
@@ -153,11 +154,11 @@ public Authentication authenticate(Authentication authentication) throws Authent
153154
// As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.4,
154155
// The may_act claim makes a statement that one party is authorized to
155156
// become the actor and act on behalf of another party.
156-
String authorizedActorSubject = null;
157+
Map<String, Object> authorizedActorClaims = null;
157158
if (subjectToken.getClaims() != null &&
158159
subjectToken.getClaims().containsKey(MAY_ACT) &&
159160
subjectToken.getClaims().get(MAY_ACT) instanceof Map<?, ?> mayAct) {
160-
authorizedActorSubject = (String) mayAct.get(StandardClaimNames.SUB);
161+
authorizedActorClaims = (Map<String, Object>) mayAct;
161162
}
162163

163164
OAuth2Authorization actorAuthorization = null;
@@ -188,12 +189,11 @@ public Authentication authenticate(Authentication authentication) throws Authent
188189
//throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
189190
}
190191

191-
if (StringUtils.hasText(authorizedActorSubject) &&
192-
!authorizedActorSubject.equals(actorAuthorization.getPrincipalName())) {
193-
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
192+
if (authorizedActorClaims != null) {
193+
validateClaims(authorizedActorClaims, actorToken.getClaims(), OAuth2TokenClaimNames.ISS,
194+
OAuth2TokenClaimNames.SUB);
194195
}
195-
} else if (StringUtils.hasText(authorizedActorSubject) &&
196-
!authorizedActorSubject.equals(clientPrincipal.getName())) {
196+
} else if (authorizedActorClaims != null) {
197197
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
198198
}
199199

@@ -284,26 +284,37 @@ private static Set<String> validateRequestedScopes(RegisteredClient registeredCl
284284
return new LinkedHashSet<>(requestedScopes);
285285
}
286286

287+
private static void validateClaims(Map<String, Object> expectedClaims, Map<String, Object> actualClaims, String... claimNames) {
288+
if (actualClaims == null) {
289+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
290+
}
291+
292+
for (String claimName : claimNames) {
293+
if (!Objects.equals(expectedClaims.get(claimName), actualClaims.get(claimName))) {
294+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
295+
}
296+
}
297+
}
298+
287299
private static Authentication getPrincipal(OAuth2Authorization subjectAuthorization, OAuth2Authorization actorAuthorization) {
288300
Authentication subjectPrincipal = subjectAuthorization.getAttribute(Principal.class.getName());
289-
290-
List<Authentication> actorPrincipals = new LinkedList<>();
291-
if (actorAuthorization != null) {
292-
actorPrincipals.add(new OAuth2ActorAuthenticationToken(actorAuthorization.getPrincipalName()));
301+
if (actorAuthorization == null) {
302+
return subjectPrincipal;
293303
}
294304

295-
if (subjectPrincipal instanceof OAuth2CompositeAuthenticationToken compositeAuthenticationToken) {
296-
// As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.1,
297-
// the act claim can be used to represent a chain of delegation,
298-
// so we unwrap the original subject and any previous actor(s).
305+
// Capture claims for current actor's access token
306+
OAuth2TokenExchangeActor currentActor = new OAuth2TokenExchangeActor(
307+
actorAuthorization.getAccessToken().getClaims());
308+
List<OAuth2TokenExchangeActor> actorPrincipals = new LinkedList<>();
309+
actorPrincipals.add(currentActor);
310+
311+
// Add chain of delegation for previous actor(s) if any
312+
if (subjectPrincipal instanceof OAuth2TokenExchangeCompositeAuthenticationToken compositeAuthenticationToken) {
299313
subjectPrincipal = compositeAuthenticationToken.getSubject();
300314
actorPrincipals.addAll(compositeAuthenticationToken.getActors());
301-
// TODO: Should we allow delegation-to-impersonation where previous
302-
// actors exist but no actor_token exists on this request?
303315
}
304316

305-
return CollectionUtils.isEmpty(actorPrincipals) ? subjectPrincipal :
306-
new OAuth2CompositeAuthenticationToken(subjectPrincipal, actorPrincipals);
317+
return new OAuth2TokenExchangeCompositeAuthenticationToken(subjectPrincipal, actorPrincipals);
307318
}
308319

309320
@Override

Diff for: oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java

+45-44
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import java.util.Collections;
1919
import java.util.HashSet;
20-
import java.util.List;
20+
import java.util.LinkedHashSet;
2121
import java.util.Map;
2222
import java.util.Set;
2323

@@ -36,10 +36,6 @@
3636
*/
3737
public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
3838

39-
private final List<String> resources;
40-
41-
private final List<String> audiences;
42-
4339
private final String requestedTokenType;
4440

4541
private final String subjectToken;
@@ -50,70 +46,47 @@ public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationG
5046

5147
private final String actorTokenType;
5248

49+
private final Set<String> resources;
50+
51+
private final Set<String> audiences;
52+
5353
private final Set<String> scopes;
5454

5555
/**
5656
* Constructs an {@code OAuth2TokenExchangeAuthenticationToken} using the provided parameters.
5757
*
58-
* @param resources a list of resource URIs
59-
* @param audiences a list audience values
60-
* @param scopes the requested scope(s)
6158
* @param requestedTokenType the requested token type
6259
* @param subjectToken the subject token
6360
* @param subjectTokenType the subject token type
61+
* @param clientPrincipal the authenticated client principal
6462
* @param actorToken the actor token
6563
* @param actorTokenType the actor token type
66-
* @param clientPrincipal the authenticated client principal
64+
* @param resources the requested resource URI(s)
65+
* @param audiences the requested audience value(s)
66+
* @param scopes the requested scope(s)
6767
* @param additionalParameters the additional parameters
6868
*/
69-
public OAuth2TokenExchangeAuthenticationToken(List<String> resources, List<String> audiences,
70-
@Nullable Set<String> scopes, @Nullable String requestedTokenType, String subjectToken,
71-
String subjectTokenType, @Nullable String actorToken, @Nullable String actorTokenType,
72-
Authentication clientPrincipal, @Nullable Map<String, Object> additionalParameters) {
69+
public OAuth2TokenExchangeAuthenticationToken(String requestedTokenType, String subjectToken,
70+
String subjectTokenType, Authentication clientPrincipal, @Nullable String actorToken,
71+
@Nullable String actorTokenType, @Nullable Set<String> resources, @Nullable Set<String> audiences,
72+
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
7373
super(AuthorizationGrantType.TOKEN_EXCHANGE, clientPrincipal, additionalParameters);
74-
Assert.notNull(resources, "resources cannot be null");
75-
Assert.notNull(audiences, "audiences cannot be null");
7674
Assert.hasText(requestedTokenType, "requestedTokenType cannot be empty");
7775
Assert.hasText(subjectToken, "subjectToken cannot be empty");
7876
Assert.hasText(subjectTokenType, "subjectTokenType cannot be empty");
79-
this.resources = resources;
80-
this.audiences = audiences;
8177
this.requestedTokenType = requestedTokenType;
8278
this.subjectToken = subjectToken;
8379
this.subjectTokenType = subjectTokenType;
8480
this.actorToken = actorToken;
8581
this.actorTokenType = actorTokenType;
82+
this.resources = Collections.unmodifiableSet(
83+
resources != null ? new LinkedHashSet<>(resources) : Collections.emptySet());
84+
this.audiences = Collections.unmodifiableSet(
85+
audiences != null ? new LinkedHashSet<>(audiences) : Collections.emptySet());
8686
this.scopes = Collections.unmodifiableSet(
8787
scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
8888
}
8989

90-
/**
91-
* Returns the list of resource URIs.
92-
*
93-
* @return the list of resource URIs
94-
*/
95-
public List<String> getResources() {
96-
return this.resources;
97-
}
98-
99-
/**
100-
* Returns the list of audience values.
101-
*
102-
* @return the list of audience values
103-
*/
104-
public List<String> getAudiences() {
105-
return this.audiences;
106-
}
107-
108-
/**
109-
* Returns the requested scope(s).
110-
*
111-
* @return the requested scope(s), or an empty {@code Set} if not available
112-
*/
113-
public Set<String> getScopes() {
114-
return this.scopes;
115-
}
116-
11790
/**
11891
* Returns the requested token type.
11992
*
@@ -158,4 +131,32 @@ public String getActorToken() {
158131
public String getActorTokenType() {
159132
return this.actorTokenType;
160133
}
134+
135+
/**
136+
* Returns the requested resource URI(s).
137+
*
138+
* @return the requested resource URI(s), or an empty {@code Set} if not available
139+
*/
140+
public Set<String> getResources() {
141+
return this.resources;
142+
}
143+
144+
/**
145+
* Returns the requested audience value(s).
146+
*
147+
* @return the requested audience value(s), or an empty {@code Set} if not available
148+
*/
149+
public Set<String> getAudiences() {
150+
return this.audiences;
151+
}
152+
153+
/**
154+
* Returns the requested scope(s).
155+
*
156+
* @return the requested scope(s), or an empty {@code Set} if not available
157+
*/
158+
public Set<String> getScopes() {
159+
return this.scopes;
160+
}
161+
161162
}
+20-5
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
package org.springframework.security.oauth2.server.authorization.authentication;
1818

19-
import java.io.Serializable;
2019
import java.util.ArrayList;
2120
import java.util.Collections;
2221
import java.util.List;
22+
import java.util.Objects;
2323

2424
import org.springframework.security.authentication.AbstractAuthenticationToken;
2525
import org.springframework.security.core.Authentication;
@@ -33,13 +33,13 @@
3333
* @since 1.3
3434
* @see OAuth2TokenExchangeAuthenticationToken
3535
*/
36-
public class OAuth2CompositeAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
36+
public class OAuth2TokenExchangeCompositeAuthenticationToken extends AbstractAuthenticationToken {
3737

3838
private final Authentication subject;
3939

40-
private final List<Authentication> actors;
40+
private final List<OAuth2TokenExchangeActor> actors;
4141

42-
public OAuth2CompositeAuthenticationToken(Authentication subject, List<Authentication> actors) {
42+
public OAuth2TokenExchangeCompositeAuthenticationToken(Authentication subject, List<OAuth2TokenExchangeActor> actors) {
4343
super(subject != null ? subject.getAuthorities() : null);
4444
Assert.notNull(subject, "subject cannot be null");
4545
Assert.notNull(actors, "actors cannot be null");
@@ -63,7 +63,22 @@ public Authentication getSubject() {
6363
return this.subject;
6464
}
6565

66-
public List<Authentication> getActors() {
66+
public List<OAuth2TokenExchangeActor> getActors() {
6767
return this.actors;
6868
}
69+
70+
@Override
71+
public boolean equals(Object obj) {
72+
if (!(obj instanceof OAuth2TokenExchangeCompositeAuthenticationToken other)) {
73+
return false;
74+
}
75+
return super.equals(obj) && Objects.equals(this.subject, other.subject) &&
76+
Objects.equals(this.actors, other.actors);
77+
}
78+
79+
@Override
80+
public int hashCode() {
81+
return Objects.hash(super.hashCode(), this.subject, this.actors);
82+
}
83+
6984
}

0 commit comments

Comments
 (0)