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

com.finbourne.horizon.extensions.auth.HttpFinbourneTokenProvider Maven / Gradle / Ivy

There is a newer version: 2.0.463
Show newest version
package com.finbourne.horizon.extensions.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.finbourne.horizon.extensions.ApiConfiguration;
import okhttp3.*;
import org.apache.commons.codec.binary.Base64;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;

/**
 * Provides {@link FinbourneToken} used for API authentication by directly querying the authentication
 * token urls on the target horizon instance. Always provides REFRESHable tokens (see
 * https://support.finbourne.com/using-a-refresh-token).
 *
 */
public class HttpFinbourneTokenProvider {

    /** Scope to ensure refresh token is enabled */
    private static final String SCOPE = "openid client groups offline_access";

    private static final MediaType FORM = MediaType.parse("application/x-www-form-urlencoded");

    /** configuration parameters to connect to horizon */
    private final ApiConfiguration apiConfiguration;

    /** client to make http calls to horizon */
    private final OkHttpClient httpClient;

    public HttpFinbourneTokenProvider(ApiConfiguration apiConfiguration, OkHttpClient httpClient) {
        this.apiConfiguration = apiConfiguration;
        this.httpClient = httpClient;
    }

    /**
     * Retrieves a {@link FinbourneToken} via an authentication call to horizon.
     *
     * Will make a complete authentication call (with username and password) if no refresh token
     * is available. Otherwise will attempt to refresh the token.
     *
     * @param refreshToken - to attempt token refresh with it is available.
     * @return an authenticated Finbourne token
     *
     * @throws FinbourneTokenException on failing to authenticate and retrieve a token
     */
    public FinbourneToken get(Optional refreshToken) throws FinbourneTokenException {
        final Request request = createAccessTokenRequest(refreshToken);
        final FinbourneToken finbourneToken = callAndMapResponseToToken(httpClient, request);
        return finbourneToken;
    }

    private FinbourneToken callAndMapResponseToToken(OkHttpClient httpClient, Request request) throws FinbourneTokenException {
        //  map json response
        Response response = null;
        try {
            response = httpClient.newCall(request).execute();
        } catch (IOException e) {
            throw new FinbourneTokenException("Authentication request call could not complete. See details:", e);
        }

        if (response.code() != 200) {
            throw new FinbourneTokenException("Authentication call to horizon failed. See response :" + response.toString());
        }

        final String content;
        final ObjectMapper mapper;
        final Map bodyValues;
        try {
            content = response.body().string();
            mapper = new ObjectMapper();
            bodyValues = mapper.readValue(content, Map.class);
        } catch (IOException e) {
            throw new FinbourneTokenException("Failed to correctly map the authentication response from horizon. See details : ", e);
        }

        if (!bodyValues.containsKey("access_token")) {
            throw new FinbourneTokenException("Response from horizon authentication is missing an access_token entry");
        }

        if (!bodyValues.containsKey("refresh_token")) {
            throw new FinbourneTokenException("Response from horizon authentication is missing an refresh_token entry");
        }

        if (!bodyValues.containsKey("expires_in")) {
            throw new FinbourneTokenException("Response from horizon authentication is missing an expires_in entry");
        }

        //  get access token, refresh token and token expiry
        final String apiToken = (String)bodyValues.get("access_token");
        final String refreshToken = (String)bodyValues.get("refresh_token");
        final int expires_in = (int)bodyValues.get("expires_in");

        FinbourneToken finbourneToken = new FinbourneToken(apiToken, refreshToken, calculateExpiryAtTime(LocalDateTime.now(), expires_in));
        return finbourneToken;
    }

    private Request createAccessTokenRequest(Optional refreshStringOpt) throws FinbourneTokenException {
        //  request body
        final String tokenRequestBody;
        try {
            if (!refreshStringOpt.isPresent()) {
                // if no refresh token is present then go through the entire authetication flow
                tokenRequestBody = String.format("grant_type=password&username=%s&password=%s&scope=%s&client_id=%s&client_secret=%s",
                        apiConfiguration.getUsername(),
                        URLEncoder.encode(apiConfiguration.getPassword(), StandardCharsets.UTF_8.toString()),
                        SCOPE,
                        apiConfiguration.getClientId(),
                        URLEncoder.encode(apiConfiguration.getClientSecret(), StandardCharsets.UTF_8.toString()));
            } else {
                // otherwise if a refresh token is present than request a refreshed access token only
                tokenRequestBody = String.format("grant_type=refresh_token&scope=%s&refresh_token=%s",
                        SCOPE,
                        refreshStringOpt.get());
            }
        } catch (UnsupportedEncodingException e) {
            throw new FinbourneTokenException("Failed to encode parameters from the API Configuration. Ensure your secrets files is properly setup.", e);
        }

        final RequestBody body = RequestBody.create(tokenRequestBody, FORM);
        final Request.Builder requestBuilder = new Request.Builder()
                .url(apiConfiguration.getTokenUrl())
                .header("Accept", "application/json")
                .post(body);

        if (refreshStringOpt.isPresent()) {
            requestBuilder.addHeader("Authorization", "Basic " +
                    Base64.encodeBase64String((apiConfiguration.getClientId() + ":" + apiConfiguration.getClientSecret()).getBytes()));
        }

        return requestBuilder.build();
    }

    public LocalDateTime calculateExpiryAtTime(LocalDateTime now, int expires_in){
        // expiration is shortened to overcome a race condition where the token is still valid when retrieved from cache but expired when
        // used in an api call
        return now.plusSeconds(expires_in - 30);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy