Skip to content

Commit

Permalink
initial implementation of token exchange (only access tokens)
Browse files Browse the repository at this point in the history
  • Loading branch information
andifalk committed Sep 11, 2021
1 parent 3fa4017 commit 952c424
Show file tree
Hide file tree
Showing 9 changed files with 529 additions and 21 deletions.
10 changes: 10 additions & 0 deletions src/main/java/com/example/authorizationserver/DataInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ private void createClients() {
false,
AccessTokenFormat.OPAQUE,
Set.of(GrantType.AUTHORIZATION_CODE),
Collections.singleton(
"http://localhost:8080/demo-client/login/oauth2/code/demo"),
Collections.singleton("*")),
new RegisteredClient(
UUID.randomUUID(),
"token-exchange",
passwordEncoder.encode("demo"),
true,
AccessTokenFormat.JWT,
Set.of(GrantType.TOKEN_EXCHANGE),
Collections.singleton(
"http://localhost:8080/demo-client/login/oauth2/code/demo"),
Collections.singleton("*")))
Expand Down
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,18 @@ public class TokenEndpoint {
private final PasswordTokenEndpointService passwordTokenEndpointService;
private final RefreshTokenEndpointService refreshTokenEndpointService;
private final AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService;
private final TokenExchangeEndpointService tokenExchangeEndpointService;

public TokenEndpoint(
ClientCredentialsTokenEndpointService clientCredentialsTokenEndpointService,
PasswordTokenEndpointService passwordTokenEndpointService,
RefreshTokenEndpointService refreshTokenEndpointService,
AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService) {
AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService, TokenExchangeEndpointService tokenExchangeEndpointService) {
this.clientCredentialsTokenEndpointService = clientCredentialsTokenEndpointService;
this.passwordTokenEndpointService = passwordTokenEndpointService;
this.refreshTokenEndpointService = refreshTokenEndpointService;
this.authorizationCodeTokenEndpointService = authorizationCodeTokenEndpointService;
this.tokenExchangeEndpointService = tokenExchangeEndpointService;
}

@PostMapping
Expand All @@ -63,8 +65,7 @@ public ResponseEntity<TokenResponse> getToken(
return refreshTokenEndpointService.getTokenResponseForRefreshToken(
authorizationHeader, tokenRequest);
} else if (tokenRequest.getGrant_type().equalsIgnoreCase(GrantType.TOKEN_EXCHANGE.getGrant())) {
LOG.warn("Requested grant type for 'Token Exchange' is not yet supported");
return ResponseEntity.badRequest().body(new TokenResponse("unsupported_grant_type"));
return tokenExchangeEndpointService.getTokenResponseForTokenExchange(authorizationHeader, tokenRequest);
} else {
LOG.warn("Requested grant type [{}] is unsupported", tokenRequest.getGrant_type());
return ResponseEntity.badRequest().body(new TokenResponse("unsupported_grant_type"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,13 @@ static ResponseEntity<TokenResponse> reportInvalidClientError() {
static ResponseEntity<TokenResponse> reportInvalidGrantError() {
return ResponseEntity.badRequest().body(new TokenResponse("invalid_grant"));
}

static ResponseEntity<TokenResponse> reportInvalidRequestError() {
return ResponseEntity.badRequest().body(new TokenResponse("invalid_request"));
}

static ResponseEntity<TokenResponse> reportInvalidTargetError() {
return ResponseEntity.badRequest().body(new TokenResponse("invalid_target"));
}

}
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();
}
}
}
Loading

0 comments on commit 952c424

Please sign in to comment.