com.sap.cloud.sdk.cloudplatform.connectivity.TokenRequest Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of connectivity-scp-cf Show documentation
Show all versions of connectivity-scp-cf Show documentation
Implementation of the Cloud platform abstraction for general-purpose connectivity
on the SAP Cloud Platform (Cloud Foundry).
/*
* Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved.
*/
package com.sap.cloud.sdk.cloudplatform.connectivity;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.Payload;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.json.JsonSanitizer;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.cloudplatform.security.AuthToken;
import com.sap.cloud.sdk.cloudplatform.security.AuthTokenAccessor;
import com.sap.cloud.sdk.cloudplatform.security.BasicAuthHeaderEncoder;
import com.sap.cloud.sdk.cloudplatform.security.BasicCredentials;
import com.sap.cloud.sdk.cloudplatform.security.exception.TokenRequestDeniedException;
import com.sap.cloud.sdk.cloudplatform.security.exception.TokenRequestFailedException;
import lombok.Getter;
class TokenRequest
{
private static final Logger logger = CloudLoggerFactory.getSanitizedLogger(TokenRequest.class);
private static final String AUTHORIZATION_HEADER = HttpHeaders.AUTHORIZATION;
private static final String BASIC_PREFIX = "Basic ";
private static final String BEARER_PREFIX = "Bearer ";
private static final String JWT_ISS = "iss";
private static final String JWT_SCOPE = "scope";
private static final String UAA_USER_SCOPE = "uaa.user";
static final int EXPIRY_SECONDS_TO_SUBTRACT = 10;
private final SubdomainReplacer subdomainReplacer;
TokenRequest( final SubdomainReplacer subdomainReplacer )
{
this.subdomainReplacer = subdomainReplacer;
}
enum GrantType
{
CLIENT_CREDENTIALS("client_credentials"),
USER_TOKEN("user_token"),
REFRESH_TOKEN("refresh_token");
@Getter
private final String identifier;
GrantType( @Nonnull final String identifier )
{
this.identifier = identifier;
}
@Override
public String toString()
{
return identifier;
}
}
private JsonObject executeTokenRequest( final HttpPost tokenRequest )
throws TokenRequestFailedException,
TokenRequestDeniedException
{
if( logger.isDebugEnabled() ) {
logger.debug("Executing token request '" + tokenRequest.getURI() + "' with client credentials grant.");
}
final String responseBody;
try {
final HttpResponse response = HttpClientAccessor.getHttpClient().execute(tokenRequest);
final StatusLine statusLine = response.getStatusLine();
final int statusCode = statusLine.getStatusCode();
switch( statusCode ) {
case HttpStatus.SC_OK: {
@Nullable
final Header contentType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);
final String expectedContentType = ContentType.APPLICATION_JSON.getMimeType();
if( contentType == null || !contentType.getValue().startsWith(expectedContentType) ) {
throw new TokenRequestFailedException(
"Failed to get access token: "
+ HttpHeaders.CONTENT_TYPE
+ " is not '"
+ expectedContentType
+ "'. "
+ XsuaaService.ERROR_BIND_XSUAA_SERVICE);
}
responseBody = HttpEntityUtil.getResponseBody(response);
break;
}
case HttpStatus.SC_UNAUTHORIZED:
case HttpStatus.SC_FORBIDDEN:
throw new TokenRequestDeniedException(
"Unable to get access token: "
+ "XSUAA service denied request with HTTP status "
+ statusCode
+ " ("
+ statusLine.getReasonPhrase()
+ "). "
+ XsuaaService.ERROR_BIND_XSUAA_SERVICE
+ " Note that this error may also occur if you are using a service plan "
+ "that is not suitable for your scenario. "
+ "If you are building a SaaS application on Cloud Foundry, "
+ "select service plan 'application' when creating your XSUAA instance. "
+ "If you are building a reuse service that should be consumed by other applications, "
+ "select service plan 'broker'.");
default:
throw new TokenRequestFailedException(
"Failed to get access token: "
+ "XSUAA service returned HTTP status "
+ statusCode
+ " ("
+ statusLine.getReasonPhrase()
+ "). "
+ XsuaaService.ERROR_BIND_XSUAA_SERVICE);
}
}
catch( final IOException e ) {
throw new TokenRequestFailedException(e);
}
if( responseBody == null ) {
throw new TokenRequestFailedException("Failed to get access token: no body returned in response.");
}
return parseResponseBody(responseBody);
}
private HttpPost newTokenRequest(
final URI xsuaaUri,
final String authorizationHeader,
final GrantType grantType,
@Nullable final String queryParams,
final boolean useProviderTenant )
throws TokenRequestFailedException
{
URI uri;
try {
final String xsuaaUriString = xsuaaUri.toString();
uri = new URI(xsuaaUriString.endsWith("/") ? xsuaaUriString : xsuaaUriString + "/");
}
catch( final URISyntaxException e ) {
throw new TokenRequestFailedException(e);
}
final Optional authToken = AuthTokenAccessor.getCurrentToken();
if( authToken.isPresent() && !useProviderTenant ) {
uri = subdomainReplacer.replaceSubdomain(getIssuerUrl(authToken.get().getJwt()), uri);
}
final URI requestUri;
try {
requestUri =
new URI(uri + "oauth/token?grant_type=" + grantType + (queryParams != null ? "&" + queryParams : ""));
}
catch( final URISyntaxException e ) {
throw new TokenRequestFailedException(e);
}
final HttpPost tokenRequest = new HttpPost(requestUri);
tokenRequest.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());
tokenRequest.setHeader(AUTHORIZATION_HEADER, authorizationHeader);
return tokenRequest;
}
private String getIssuerUrl( final DecodedJWT jwt )
{
return Optional.of(jwt.getClaim(JWT_ISS).asString()).orElseThrow(
() -> new TokenRequestFailedException(
"Failed to determine current subdomain: unable to find required field '"
+ JWT_ISS
+ "' in JWT bearer."));
}
private JsonObject parseResponseBody( final String responseBody )
throws TokenRequestFailedException
{
final JsonElement responseElement;
try {
responseElement = new JsonParser().parse(JsonSanitizer.sanitize(responseBody));
}
catch( final JsonParseException e ) {
throw new TokenRequestFailedException(e);
}
if( !responseElement.isJsonObject() ) {
throw new TokenRequestFailedException(
"Failed to parse response body: not a valid JSON object. " + XsuaaService.ERROR_BIND_XSUAA_SERVICE);
}
return responseElement.getAsJsonObject();
}
private AccessToken parseAccessToken( final JsonObject responseObject )
throws TokenRequestFailedException
{
@Nullable
final JsonElement accessTokenElement = responseObject.get("access_token");
@Nullable
final JsonElement expiresInElement = responseObject.get("expires_in");
if( accessTokenElement == null || !accessTokenElement.isJsonPrimitive() ) {
throw new TokenRequestFailedException(
"No valid access token found in response body. " + XsuaaService.ERROR_BIND_XSUAA_SERVICE);
}
if( expiresInElement == null || !expiresInElement.isJsonPrimitive() ) {
throw new TokenRequestFailedException(
"No valid token expiry found in response body. " + XsuaaService.ERROR_BIND_XSUAA_SERVICE);
}
final String accessToken = accessTokenElement.getAsString();
final ZonedDateTime expiry;
try {
final int seconds = Integer.parseInt(expiresInElement.getAsString());
final int secondsToSubtract = Math.min(EXPIRY_SECONDS_TO_SUBTRACT, seconds);
// subtract some seconds from expiry to avoid sending tokens just as they expire
expiry = ZonedDateTime.now().plusSeconds(seconds).minusSeconds(secondsToSubtract);
}
catch( final NumberFormatException e ) {
throw new TokenRequestFailedException(
"Failed to parse expiry of access token. " + XsuaaService.ERROR_BIND_XSUAA_SERVICE,
e);
}
return new AccessToken(accessToken, expiry);
}
private DecodedJWT getCurrentJwt()
throws TokenRequestFailedException
{
final Optional authToken = AuthTokenAccessor.getCurrentToken();
if( authToken.isPresent() ) {
return authToken.get().getJwt();
} else {
throw new TokenRequestFailedException(
"Failed to get access token: "
+ "no valid JWT bearer found in '"
+ AUTHORIZATION_HEADER
+ "' header of request.");
}
}
private boolean hasScopeUaaUser( final Payload jwt )
throws TokenRequestFailedException
{
@Nullable
final List scopeNames;
try {
scopeNames = jwt.getClaim(JWT_SCOPE).asList(String.class);
}
catch( final JWTDecodeException e ) {
throw new TokenRequestFailedException("Failed to get access token: failed to read scopes.", e);
}
if( scopeNames == null ) {
throw new TokenRequestFailedException("Failed to get access token: no scopes available in JWT bearer.");
}
boolean hasScope = false;
for( final String scopeName : scopeNames ) {
if( UAA_USER_SCOPE.equals(scopeName) ) {
hasScope = true;
break;
}
}
return hasScope;
}
AccessToken requestTokenWithUserTokenGrant(
final URI xsuaaUri,
final BasicCredentials clientCredentials,
final boolean useProviderTenant )
throws TokenRequestFailedException,
TokenRequestDeniedException
{
final DecodedJWT jwt = getCurrentJwt();
if( !hasScopeUaaUser(jwt) ) {
throw new TokenRequestDeniedException(
"Unable to get access token: "
+ "user does not have scope '"
+ UAA_USER_SCOPE
+ "'. "
+ "This is mandatory for the user token flow. "
+ "Please make sure to that this scope is assigned to the user.");
}
final String authorizationBearer = getCurrentJwt().getToken();
final HttpPost refreshTokenRequest =
newTokenRequest(
xsuaaUri,
BEARER_PREFIX + authorizationBearer,
GrantType.USER_TOKEN,
"response_type=token",
useProviderTenant);
refreshTokenRequest.setEntity(
new StringEntity("client_id=" + clientCredentials.getUsername(), ContentType.APPLICATION_FORM_URLENCODED));
final JsonObject refreshTokenResponse = executeTokenRequest(refreshTokenRequest);
@Nullable
final JsonElement refreshTokenElement = refreshTokenResponse.get("refresh_token");
if( refreshTokenElement == null || !refreshTokenElement.isJsonPrimitive() ) {
throw new TokenRequestFailedException(
"Failed to get access token: "
+ "no valid refresh token found in response of user token flow. "
+ XsuaaService.ERROR_BIND_XSUAA_SERVICE);
}
final HttpPost accessTokenRequest =
newTokenRequest(
xsuaaUri,
BASIC_PREFIX + BasicAuthHeaderEncoder.encodeUserPasswordBase64(clientCredentials),
GrantType.REFRESH_TOKEN,
null,
useProviderTenant);
accessTokenRequest.setEntity(
new StringEntity(
"refresh_token=" + refreshTokenElement.getAsString(),
ContentType.APPLICATION_FORM_URLENCODED));
return parseAccessToken(executeTokenRequest(accessTokenRequest));
}
AccessToken requestTokenWithClientCredentialsGrant(
final URI xsuaaUri,
final BasicCredentials clientCredentials,
final boolean useProviderTenant )
throws TokenRequestFailedException,
TokenRequestDeniedException
{
final HttpPost accessTokenRequest =
newTokenRequest(
xsuaaUri,
BASIC_PREFIX + BasicAuthHeaderEncoder.encodeUserPasswordBase64(clientCredentials),
GrantType.CLIENT_CREDENTIALS,
null,
useProviderTenant);
return parseAccessToken(executeTokenRequest(accessTokenRequest));
}
}