Skip to content

Commit 952c424

Browse files
committed
initial implementation of token exchange (only access tokens)
1 parent 3fa4017 commit 952c424

9 files changed

Lines changed: 529 additions & 21 deletions

File tree

src/main/java/com/example/authorizationserver/DataInitializer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@ private void createClients() {
219219
false,
220220
AccessTokenFormat.OPAQUE,
221221
Set.of(GrantType.AUTHORIZATION_CODE),
222+
Collections.singleton(
223+
"http://localhost:8080/demo-client/login/oauth2/code/demo"),
224+
Collections.singleton("*")),
225+
new RegisteredClient(
226+
UUID.randomUUID(),
227+
"token-exchange",
228+
passwordEncoder.encode("demo"),
229+
true,
230+
AccessTokenFormat.JWT,
231+
Set.of(GrantType.TOKEN_EXCHANGE),
222232
Collections.singleton(
223233
"http://localhost:8080/demo-client/login/oauth2/code/demo"),
224234
Collections.singleton("*")))
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.example.authorizationserver.oauth.common;
2+
3+
public enum TokenType {
4+
5+
// Indicates that the token is an OAuth 2.0 access token
6+
ACCESS_TOKEN("urn:ietf:params:oauth:token-type:access_token"),
7+
8+
// Indicates that the token is an OAuth 2.0 refresh token.
9+
REFRESH_TOKEN("urn:ietf:params:oauth:token-type:refresh_token"),
10+
11+
// Indicates that the token is an ID Token as defined in OpenID.Core.
12+
ID_TOKEN("urn:ietf:params:oauth:token-type:id_token"),
13+
14+
// Indicates that the token is a base64url-encoded SAML 1.1 assertion.
15+
SAML11_TOKEN("urn:ietf:params:oauth:token-type:saml1"),
16+
17+
// Indicates that the token is a base64url-encoded SAML 2.0 assertion.
18+
SAML2_TOKEN("urn:ietf:params:oauth:token-type:saml2"),
19+
20+
// Indicated that the token is a JSON web token.
21+
JWT_TOKEN("urn:ietf:params:oauth:token-type:jwt");
22+
23+
private final String identifier;
24+
25+
TokenType(String identifier) {
26+
this.identifier = identifier;
27+
}
28+
29+
public String getIdentifier() {
30+
return identifier;
31+
}
32+
33+
public static TokenType getTokenTypeForIdentifier(String identifier) {
34+
if (ACCESS_TOKEN.getIdentifier().equals(identifier)) {
35+
return ACCESS_TOKEN;
36+
} else if (REFRESH_TOKEN.getIdentifier().equals(identifier)) {
37+
return REFRESH_TOKEN;
38+
} else if (ID_TOKEN.getIdentifier().equals(identifier)) {
39+
return ID_TOKEN;
40+
} else if (SAML11_TOKEN.getIdentifier().equals(identifier)) {
41+
return SAML11_TOKEN;
42+
} else if (SAML2_TOKEN.getIdentifier().equals(identifier)) {
43+
return SAML2_TOKEN;
44+
} else if (JWT_TOKEN.getIdentifier().equals(identifier)) {
45+
return JWT_TOKEN;
46+
} else {
47+
throw new IllegalArgumentException("Invalid token type " + identifier);
48+
}
49+
}
50+
}

src/main/java/com/example/authorizationserver/oauth/endpoint/token/TokenEndpoint.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,18 @@ public class TokenEndpoint {
2929
private final PasswordTokenEndpointService passwordTokenEndpointService;
3030
private final RefreshTokenEndpointService refreshTokenEndpointService;
3131
private final AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService;
32+
private final TokenExchangeEndpointService tokenExchangeEndpointService;
3233

3334
public TokenEndpoint(
3435
ClientCredentialsTokenEndpointService clientCredentialsTokenEndpointService,
3536
PasswordTokenEndpointService passwordTokenEndpointService,
3637
RefreshTokenEndpointService refreshTokenEndpointService,
37-
AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService) {
38+
AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService, TokenExchangeEndpointService tokenExchangeEndpointService) {
3839
this.clientCredentialsTokenEndpointService = clientCredentialsTokenEndpointService;
3940
this.passwordTokenEndpointService = passwordTokenEndpointService;
4041
this.refreshTokenEndpointService = refreshTokenEndpointService;
4142
this.authorizationCodeTokenEndpointService = authorizationCodeTokenEndpointService;
43+
this.tokenExchangeEndpointService = tokenExchangeEndpointService;
4244
}
4345

4446
@PostMapping
@@ -63,8 +65,7 @@ public ResponseEntity<TokenResponse> getToken(
6365
return refreshTokenEndpointService.getTokenResponseForRefreshToken(
6466
authorizationHeader, tokenRequest);
6567
} else if (tokenRequest.getGrant_type().equalsIgnoreCase(GrantType.TOKEN_EXCHANGE.getGrant())) {
66-
LOG.warn("Requested grant type for 'Token Exchange' is not yet supported");
67-
return ResponseEntity.badRequest().body(new TokenResponse("unsupported_grant_type"));
68+
return tokenExchangeEndpointService.getTokenResponseForTokenExchange(authorizationHeader, tokenRequest);
6869
} else {
6970
LOG.warn("Requested grant type [{}] is unsupported", tokenRequest.getGrant_type());
7071
return ResponseEntity.badRequest().body(new TokenResponse("unsupported_grant_type"));

src/main/java/com/example/authorizationserver/oauth/endpoint/token/TokenEndpointHelper.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,13 @@ static ResponseEntity<TokenResponse> reportInvalidClientError() {
3737
static ResponseEntity<TokenResponse> reportInvalidGrantError() {
3838
return ResponseEntity.badRequest().body(new TokenResponse("invalid_grant"));
3939
}
40+
41+
static ResponseEntity<TokenResponse> reportInvalidRequestError() {
42+
return ResponseEntity.badRequest().body(new TokenResponse("invalid_request"));
43+
}
44+
45+
static ResponseEntity<TokenResponse> reportInvalidTargetError() {
46+
return ResponseEntity.badRequest().body(new TokenResponse("invalid_target"));
47+
}
48+
4049
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package com.example.authorizationserver.oauth.endpoint.token;
2+
3+
import com.example.authorizationserver.config.AuthorizationServerConfigurationProperties;
4+
import com.example.authorizationserver.oauth.client.model.AccessTokenFormat;
5+
import com.example.authorizationserver.oauth.client.model.RegisteredClient;
6+
import com.example.authorizationserver.oauth.common.ClientCredentials;
7+
import com.example.authorizationserver.oauth.common.GrantType;
8+
import com.example.authorizationserver.oauth.common.TokenType;
9+
import com.example.authorizationserver.oauth.endpoint.token.resource.TokenRequest;
10+
import com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse;
11+
import com.example.authorizationserver.scim.model.ScimUserEntity;
12+
import com.example.authorizationserver.scim.service.ScimService;
13+
import com.example.authorizationserver.security.client.RegisteredClientAuthenticationService;
14+
import com.example.authorizationserver.token.jwt.JsonWebTokenService;
15+
import com.example.authorizationserver.token.store.TokenService;
16+
import com.example.authorizationserver.token.store.model.JsonWebToken;
17+
import com.nimbusds.jose.JOSEException;
18+
import com.nimbusds.jwt.JWTClaimsSet;
19+
import org.apache.commons.lang3.StringUtils;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.http.ResponseEntity;
23+
import org.springframework.security.core.AuthenticationException;
24+
import org.springframework.stereotype.Service;
25+
26+
import java.text.ParseException;
27+
import java.time.Duration;
28+
import java.util.*;
29+
30+
import static com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse.BEARER_TOKEN_TYPE;
31+
32+
@Service
33+
public class TokenExchangeEndpointService {
34+
private static final Logger LOG = LoggerFactory.getLogger(TokenExchangeEndpointService.class);
35+
36+
private final TokenService tokenService;
37+
private final ScimService scimService;
38+
private final AuthorizationServerConfigurationProperties authorizationServerProperties;
39+
private final RegisteredClientAuthenticationService registeredClientAuthenticationService;
40+
private final JsonWebTokenService jsonWebTokenService;
41+
42+
public TokenExchangeEndpointService(
43+
TokenService tokenService,
44+
ScimService scimService, AuthorizationServerConfigurationProperties authorizationServerProperties,
45+
RegisteredClientAuthenticationService registeredClientAuthenticationService, JsonWebTokenService jsonWebTokenService) {
46+
this.tokenService = tokenService;
47+
this.scimService = scimService;
48+
this.authorizationServerProperties = authorizationServerProperties;
49+
this.registeredClientAuthenticationService = registeredClientAuthenticationService;
50+
this.jsonWebTokenService = jsonWebTokenService;
51+
}
52+
53+
/**
54+
* ------------------------- Exchanging a Token
55+
*
56+
* <p>The client makes a token exchange request to the token endpoint with an extension grant type using the HTTP POST method.
57+
* The following parameters are included in the HTTP request entity-body using the application/x-www-form-urlencoded format per
58+
* Appendix B with a character encoding of UTF-8 in the HTTP request entity-body:
59+
*
60+
* <p>grant_type REQUIRED. Value MUST be set to "urn:ietf:params:oauth:grant-type:token-exchange".
61+
* refresh_token REQUIRED. The refresh token issued to the client. scope OPTIONAL. The scope of the access request as
62+
* described by Section 3.3. The requested scope MUST NOT include any scope not originally granted
63+
* by the resource owner, and if omitted is treated as equal to the scope originally granted by
64+
* the resource owner.
65+
*/
66+
public ResponseEntity<TokenResponse> getTokenResponseForTokenExchange(
67+
String authorizationHeader, TokenRequest tokenRequest) {
68+
69+
LOG.debug("Exchange token for given token with [{}]", tokenRequest);
70+
71+
ClientCredentials clientCredentials =
72+
TokenEndpointHelper.retrieveClientCredentials(authorizationHeader, tokenRequest);
73+
74+
if (clientCredentials == null) {
75+
return TokenEndpointHelper.reportInvalidClientError();
76+
}
77+
78+
Duration accessTokenLifetime = authorizationServerProperties.getAccessToken().getLifetime();
79+
Duration refreshTokenLifetime = authorizationServerProperties.getRefreshToken().getLifetime();
80+
81+
RegisteredClient registeredClient;
82+
83+
try {
84+
registeredClient =
85+
registeredClientAuthenticationService.authenticate(
86+
clientCredentials.getClientId(), clientCredentials.getClientSecret());
87+
88+
} catch (AuthenticationException ex) {
89+
return TokenEndpointHelper.reportInvalidClientError();
90+
}
91+
92+
if (registeredClient.getGrantTypes().contains(GrantType.TOKEN_EXCHANGE)) {
93+
TokenType tokenType = TokenType.ACCESS_TOKEN;
94+
if (tokenRequest.getRequested_token_type() != null) {
95+
try {
96+
tokenType = TokenType.getTokenTypeForIdentifier(tokenRequest.getRequested_token_type());
97+
} catch (IllegalArgumentException ex) {
98+
LOG.warn("Token exchange is not valid for requested token type [{}]", tokenRequest.getRequested_token_type());
99+
return TokenEndpointHelper.reportInvalidRequestError();
100+
}
101+
}
102+
if (!TokenType.ACCESS_TOKEN.equals(tokenType)) {
103+
LOG.warn("Token exchange is not valid for requested token type [{}]", tokenRequest.getRequested_token_type());
104+
return TokenEndpointHelper.reportInvalidRequestError();
105+
}
106+
JsonWebToken jsonWebToken = tokenService.findJsonWebToken(tokenRequest.getSubject_token());
107+
if (jsonWebToken != null && jsonWebToken.isAccessToken()) {
108+
Set<String> scopes = new HashSet<>();
109+
if (StringUtils.isNotBlank(tokenRequest.getScope())) {
110+
scopes = new HashSet<>(Arrays.asList(tokenRequest.getScope().split(" ")));
111+
}
112+
113+
try {
114+
JWTClaimsSet jwtClaimsSet =
115+
jsonWebTokenService.parseAndValidateToken(jsonWebToken.getValue());
116+
String subject = jwtClaimsSet.getSubject();
117+
if (TokenService.ANONYMOUS_TOKEN.equals(subject)) {
118+
119+
LOG.info(
120+
"Creating anonymous token response for token exchange with client [{}]",
121+
clientCredentials.getClientId());
122+
123+
return ResponseEntity.ok(
124+
new TokenResponse(
125+
AccessTokenFormat.JWT.equals(registeredClient.getAccessTokenFormat())
126+
? tokenService
127+
.createAnonymousJwtAccessToken(
128+
clientCredentials.getClientId(), scopes, accessTokenLifetime)
129+
.getValue()
130+
: tokenService
131+
.createAnonymousOpaqueAccessToken(
132+
clientCredentials.getClientId(), scopes, accessTokenLifetime)
133+
.getValue(),
134+
tokenService
135+
.createAnonymousRefreshToken(
136+
clientCredentials.getClientId(), scopes, refreshTokenLifetime)
137+
.getValue(),
138+
accessTokenLifetime.toSeconds(),
139+
null,
140+
BEARER_TOKEN_TYPE, TokenType.ACCESS_TOKEN.getIdentifier(), tokenRequest.getScope()));
141+
} else {
142+
Optional<ScimUserEntity> authenticatedUser =
143+
scimService.findUserByIdentifier(UUID.fromString(subject));
144+
if (authenticatedUser.isPresent()) {
145+
146+
LOG.info(
147+
"Creating personalized token response for token exchange with client [{}]",
148+
clientCredentials.getClientId());
149+
150+
return ResponseEntity.ok(
151+
new TokenResponse(
152+
AccessTokenFormat.JWT.equals(registeredClient.getAccessTokenFormat())
153+
? tokenService
154+
.createPersonalizedJwtAccessToken(
155+
authenticatedUser.get(),
156+
clientCredentials.getClientId(),
157+
null,
158+
scopes,
159+
accessTokenLifetime)
160+
.getValue()
161+
: tokenService
162+
.createPersonalizedOpaqueAccessToken(
163+
authenticatedUser.get(),
164+
clientCredentials.getClientId(),
165+
scopes,
166+
accessTokenLifetime)
167+
.getValue(),
168+
tokenService
169+
.createPersonalizedRefreshToken(
170+
clientCredentials.getClientId(),
171+
authenticatedUser.get(),
172+
scopes,
173+
refreshTokenLifetime)
174+
.getValue(),
175+
accessTokenLifetime.toSeconds(),
176+
null,
177+
BEARER_TOKEN_TYPE, TokenType.ACCESS_TOKEN.getIdentifier(), tokenRequest.getScope()));
178+
}
179+
}
180+
tokenService.remove(jsonWebToken);
181+
} catch (ParseException | JOSEException e) {
182+
return TokenEndpointHelper.reportInvalidRequestError();
183+
}
184+
}
185+
return TokenEndpointHelper.reportInvalidClientError();
186+
} else {
187+
return TokenEndpointHelper.reportUnauthorizedClientError();
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)