-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial implementation of token exchange (only access tokens)
- Loading branch information
Showing
9 changed files
with
529 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/main/java/com/example/authorizationserver/oauth/common/TokenType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package com.example.authorizationserver.oauth.common; | ||
|
||
public enum TokenType { | ||
|
||
// Indicates that the token is an OAuth 2.0 access token | ||
ACCESS_TOKEN("urn:ietf:params:oauth:token-type:access_token"), | ||
|
||
// Indicates that the token is an OAuth 2.0 refresh token. | ||
REFRESH_TOKEN("urn:ietf:params:oauth:token-type:refresh_token"), | ||
|
||
// Indicates that the token is an ID Token as defined in OpenID.Core. | ||
ID_TOKEN("urn:ietf:params:oauth:token-type:id_token"), | ||
|
||
// Indicates that the token is a base64url-encoded SAML 1.1 assertion. | ||
SAML11_TOKEN("urn:ietf:params:oauth:token-type:saml1"), | ||
|
||
// Indicates that the token is a base64url-encoded SAML 2.0 assertion. | ||
SAML2_TOKEN("urn:ietf:params:oauth:token-type:saml2"), | ||
|
||
// Indicated that the token is a JSON web token. | ||
JWT_TOKEN("urn:ietf:params:oauth:token-type:jwt"); | ||
|
||
private final String identifier; | ||
|
||
TokenType(String identifier) { | ||
this.identifier = identifier; | ||
} | ||
|
||
public String getIdentifier() { | ||
return identifier; | ||
} | ||
|
||
public static TokenType getTokenTypeForIdentifier(String identifier) { | ||
if (ACCESS_TOKEN.getIdentifier().equals(identifier)) { | ||
return ACCESS_TOKEN; | ||
} else if (REFRESH_TOKEN.getIdentifier().equals(identifier)) { | ||
return REFRESH_TOKEN; | ||
} else if (ID_TOKEN.getIdentifier().equals(identifier)) { | ||
return ID_TOKEN; | ||
} else if (SAML11_TOKEN.getIdentifier().equals(identifier)) { | ||
return SAML11_TOKEN; | ||
} else if (SAML2_TOKEN.getIdentifier().equals(identifier)) { | ||
return SAML2_TOKEN; | ||
} else if (JWT_TOKEN.getIdentifier().equals(identifier)) { | ||
return JWT_TOKEN; | ||
} else { | ||
throw new IllegalArgumentException("Invalid token type " + identifier); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
190 changes: 190 additions & 0 deletions
190
...va/com/example/authorizationserver/oauth/endpoint/token/TokenExchangeEndpointService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
package com.example.authorizationserver.oauth.endpoint.token; | ||
|
||
import com.example.authorizationserver.config.AuthorizationServerConfigurationProperties; | ||
import com.example.authorizationserver.oauth.client.model.AccessTokenFormat; | ||
import com.example.authorizationserver.oauth.client.model.RegisteredClient; | ||
import com.example.authorizationserver.oauth.common.ClientCredentials; | ||
import com.example.authorizationserver.oauth.common.GrantType; | ||
import com.example.authorizationserver.oauth.common.TokenType; | ||
import com.example.authorizationserver.oauth.endpoint.token.resource.TokenRequest; | ||
import com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse; | ||
import com.example.authorizationserver.scim.model.ScimUserEntity; | ||
import com.example.authorizationserver.scim.service.ScimService; | ||
import com.example.authorizationserver.security.client.RegisteredClientAuthenticationService; | ||
import com.example.authorizationserver.token.jwt.JsonWebTokenService; | ||
import com.example.authorizationserver.token.store.TokenService; | ||
import com.example.authorizationserver.token.store.model.JsonWebToken; | ||
import com.nimbusds.jose.JOSEException; | ||
import com.nimbusds.jwt.JWTClaimsSet; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.text.ParseException; | ||
import java.time.Duration; | ||
import java.util.*; | ||
|
||
import static com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse.BEARER_TOKEN_TYPE; | ||
|
||
@Service | ||
public class TokenExchangeEndpointService { | ||
private static final Logger LOG = LoggerFactory.getLogger(TokenExchangeEndpointService.class); | ||
|
||
private final TokenService tokenService; | ||
private final ScimService scimService; | ||
private final AuthorizationServerConfigurationProperties authorizationServerProperties; | ||
private final RegisteredClientAuthenticationService registeredClientAuthenticationService; | ||
private final JsonWebTokenService jsonWebTokenService; | ||
|
||
public TokenExchangeEndpointService( | ||
TokenService tokenService, | ||
ScimService scimService, AuthorizationServerConfigurationProperties authorizationServerProperties, | ||
RegisteredClientAuthenticationService registeredClientAuthenticationService, JsonWebTokenService jsonWebTokenService) { | ||
this.tokenService = tokenService; | ||
this.scimService = scimService; | ||
this.authorizationServerProperties = authorizationServerProperties; | ||
this.registeredClientAuthenticationService = registeredClientAuthenticationService; | ||
this.jsonWebTokenService = jsonWebTokenService; | ||
} | ||
|
||
/** | ||
* ------------------------- Exchanging a Token | ||
* | ||
* <p>The client makes a token exchange request to the token endpoint with an extension grant type using the HTTP POST method. | ||
* The following parameters are included in the HTTP request entity-body using the application/x-www-form-urlencoded format per | ||
* Appendix B with a character encoding of UTF-8 in the HTTP request entity-body: | ||
* | ||
* <p>grant_type REQUIRED. Value MUST be set to "urn:ietf:params:oauth:grant-type:token-exchange". | ||
* refresh_token REQUIRED. The refresh token issued to the client. scope OPTIONAL. The scope of the access request as | ||
* described by Section 3.3. The requested scope MUST NOT include any scope not originally granted | ||
* by the resource owner, and if omitted is treated as equal to the scope originally granted by | ||
* the resource owner. | ||
*/ | ||
public ResponseEntity<TokenResponse> getTokenResponseForTokenExchange( | ||
String authorizationHeader, TokenRequest tokenRequest) { | ||
|
||
LOG.debug("Exchange token for given token with [{}]", tokenRequest); | ||
|
||
ClientCredentials clientCredentials = | ||
TokenEndpointHelper.retrieveClientCredentials(authorizationHeader, tokenRequest); | ||
|
||
if (clientCredentials == null) { | ||
return TokenEndpointHelper.reportInvalidClientError(); | ||
} | ||
|
||
Duration accessTokenLifetime = authorizationServerProperties.getAccessToken().getLifetime(); | ||
Duration refreshTokenLifetime = authorizationServerProperties.getRefreshToken().getLifetime(); | ||
|
||
RegisteredClient registeredClient; | ||
|
||
try { | ||
registeredClient = | ||
registeredClientAuthenticationService.authenticate( | ||
clientCredentials.getClientId(), clientCredentials.getClientSecret()); | ||
|
||
} catch (AuthenticationException ex) { | ||
return TokenEndpointHelper.reportInvalidClientError(); | ||
} | ||
|
||
if (registeredClient.getGrantTypes().contains(GrantType.TOKEN_EXCHANGE)) { | ||
TokenType tokenType = TokenType.ACCESS_TOKEN; | ||
if (tokenRequest.getRequested_token_type() != null) { | ||
try { | ||
tokenType = TokenType.getTokenTypeForIdentifier(tokenRequest.getRequested_token_type()); | ||
} catch (IllegalArgumentException ex) { | ||
LOG.warn("Token exchange is not valid for requested token type [{}]", tokenRequest.getRequested_token_type()); | ||
return TokenEndpointHelper.reportInvalidRequestError(); | ||
} | ||
} | ||
if (!TokenType.ACCESS_TOKEN.equals(tokenType)) { | ||
LOG.warn("Token exchange is not valid for requested token type [{}]", tokenRequest.getRequested_token_type()); | ||
return TokenEndpointHelper.reportInvalidRequestError(); | ||
} | ||
JsonWebToken jsonWebToken = tokenService.findJsonWebToken(tokenRequest.getSubject_token()); | ||
if (jsonWebToken != null && jsonWebToken.isAccessToken()) { | ||
Set<String> scopes = new HashSet<>(); | ||
if (StringUtils.isNotBlank(tokenRequest.getScope())) { | ||
scopes = new HashSet<>(Arrays.asList(tokenRequest.getScope().split(" "))); | ||
} | ||
|
||
try { | ||
JWTClaimsSet jwtClaimsSet = | ||
jsonWebTokenService.parseAndValidateToken(jsonWebToken.getValue()); | ||
String subject = jwtClaimsSet.getSubject(); | ||
if (TokenService.ANONYMOUS_TOKEN.equals(subject)) { | ||
|
||
LOG.info( | ||
"Creating anonymous token response for token exchange with client [{}]", | ||
clientCredentials.getClientId()); | ||
|
||
return ResponseEntity.ok( | ||
new TokenResponse( | ||
AccessTokenFormat.JWT.equals(registeredClient.getAccessTokenFormat()) | ||
? tokenService | ||
.createAnonymousJwtAccessToken( | ||
clientCredentials.getClientId(), scopes, accessTokenLifetime) | ||
.getValue() | ||
: tokenService | ||
.createAnonymousOpaqueAccessToken( | ||
clientCredentials.getClientId(), scopes, accessTokenLifetime) | ||
.getValue(), | ||
tokenService | ||
.createAnonymousRefreshToken( | ||
clientCredentials.getClientId(), scopes, refreshTokenLifetime) | ||
.getValue(), | ||
accessTokenLifetime.toSeconds(), | ||
null, | ||
BEARER_TOKEN_TYPE, TokenType.ACCESS_TOKEN.getIdentifier(), tokenRequest.getScope())); | ||
} else { | ||
Optional<ScimUserEntity> authenticatedUser = | ||
scimService.findUserByIdentifier(UUID.fromString(subject)); | ||
if (authenticatedUser.isPresent()) { | ||
|
||
LOG.info( | ||
"Creating personalized token response for token exchange with client [{}]", | ||
clientCredentials.getClientId()); | ||
|
||
return ResponseEntity.ok( | ||
new TokenResponse( | ||
AccessTokenFormat.JWT.equals(registeredClient.getAccessTokenFormat()) | ||
? tokenService | ||
.createPersonalizedJwtAccessToken( | ||
authenticatedUser.get(), | ||
clientCredentials.getClientId(), | ||
null, | ||
scopes, | ||
accessTokenLifetime) | ||
.getValue() | ||
: tokenService | ||
.createPersonalizedOpaqueAccessToken( | ||
authenticatedUser.get(), | ||
clientCredentials.getClientId(), | ||
scopes, | ||
accessTokenLifetime) | ||
.getValue(), | ||
tokenService | ||
.createPersonalizedRefreshToken( | ||
clientCredentials.getClientId(), | ||
authenticatedUser.get(), | ||
scopes, | ||
refreshTokenLifetime) | ||
.getValue(), | ||
accessTokenLifetime.toSeconds(), | ||
null, | ||
BEARER_TOKEN_TYPE, TokenType.ACCESS_TOKEN.getIdentifier(), tokenRequest.getScope())); | ||
} | ||
} | ||
tokenService.remove(jsonWebToken); | ||
} catch (ParseException | JOSEException e) { | ||
return TokenEndpointHelper.reportInvalidRequestError(); | ||
} | ||
} | ||
return TokenEndpointHelper.reportInvalidClientError(); | ||
} else { | ||
return TokenEndpointHelper.reportUnauthorizedClientError(); | ||
} | ||
} | ||
} |
Oops, something went wrong.