All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.sdk.cloudplatform.connectivity.TokenRequest Maven / Gradle / Ivy

Go to download

Implementation of the Cloud platform abstraction for general-purpose connectivity on the SAP Cloud Platform (Cloud Foundry).

There is a newer version: 2.28.0
Show newest version
/*
 * 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));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy