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

com.trimble.id.AuthorizationCodeGrantTokenProvider Maven / Gradle / Ivy

Go to download

Trimble Identity OAuth Client library holds the client classes that are used for communicating with Trimble Identity Service

The newest version!
package com.trimble.id;

import static java.net.URLEncoder.encode;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import static com.trimble.id.AuthenticationConstants.AMPERSAND;
import static com.trimble.id.AuthenticationConstants.AUTHORIZATION_CODE_GRANT;
import static com.trimble.id.AuthenticationConstants.AUTH_CODE_REDIRECT_ERROR;
import static com.trimble.id.AuthenticationConstants.CLIENT_ID;
import static com.trimble.id.AuthenticationConstants.CLIENT_ID_PLACEHOLDER;
import static com.trimble.id.AuthenticationConstants.CODE;
import static com.trimble.id.AuthenticationConstants.CODE_CHALLENGE;
import static com.trimble.id.AuthenticationConstants.CODE_CHALLENGE_METHOD;
import static com.trimble.id.AuthenticationConstants.CODE_CHALLENGE_METHOD_PARAM;
import static com.trimble.id.AuthenticationConstants.CODE_CHALLENGE_PARAM;
import static com.trimble.id.AuthenticationConstants.CODE_VERIFIER;
import static com.trimble.id.AuthenticationConstants.EQUAL_TO;
import static com.trimble.id.AuthenticationConstants.GRANT_TYPE;
import static com.trimble.id.AuthenticationConstants.IDENTITY_PROVIDER;
import static com.trimble.id.AuthenticationConstants.OPENID;
import static com.trimble.id.AuthenticationConstants.QUERY_STRING;
import static com.trimble.id.AuthenticationConstants.QUERY_STRING_MARKER;
import static com.trimble.id.AuthenticationConstants.REDIRECT_URI;
import static com.trimble.id.AuthenticationConstants.REDIRECT_URI_PLACEHOLDER;
import static com.trimble.id.AuthenticationConstants.S256;
import static com.trimble.id.AuthenticationConstants.SCOPE_PLACEHOLDER;
import static com.trimble.id.AuthenticationConstants.SDK_VARIANT;
import static com.trimble.id.AuthenticationConstants.SDK_VERSION;
import static com.trimble.id.AuthenticationConstants.SPACE;
import static com.trimble.id.AuthenticationConstants.STATE;
import static com.trimble.id.CryptographicHelper.createCodeChallenge;
import static com.trimble.id.CryptographicHelper.generateCodeVerifier;
import static com.trimble.id.CryptographicHelper.generateState;
import com.trimble.id.analytics.AnalyticsHttpClient;

/** A token provider based on the OAuth Authorization Code grant type */
public class AuthorizationCodeGrantTokenProvider implements ITokenProvider {

	private IEndpointProvider endpointProvider;
	private String clientId;
	private String clientSecret;
	private List scopes;
	private String state;
	private String identityProvider;
	private String codeVerifier;
	private String codeChallenge;
	private URI redirectUri;
	private URI logoutRedirectUri;

	private IHttpTokenClient httpTokenClient;
	private RefreshableTokenProvider refreshableTokenProvider;

	/**
	 * Public constructor of AuthorizationCodeGrantTokenProvider class
	 * 
	 * @param endpointProvider The endpoint provider
	 * @param clientId         The client id of the application
	 * @param redirectUri      The redirect URI
	 */
	public AuthorizationCodeGrantTokenProvider(IEndpointProvider endpointProvider, String clientId, URI redirectUri) {

		this.endpointProvider = endpointProvider;
		this.clientId = clientId;
		this.scopes = new ArrayList();
		this.scopes.add(OPENID);
		this.redirectUri = redirectUri;

		this.httpTokenClient = new HttpTokenClient(endpointProvider);

		AnalyticsHttpClient.sendInitEvent(this.getClass().getSimpleName(), this.getClass().getPackage().getName(),
				SDK_VERSION, clientId);
	}

	/**
	 * Fluent extension to add client secret to the token provider
	 * 
	 * @param clientSecret The client secret of the application
	 * @return The token provider with the client secret added
	 */
	public AuthorizationCodeGrantTokenProvider withClientSecret(String clientSecret) {
		this.clientSecret = clientSecret;
		return this;
	}

	/**
	 * Fluent extension to add scopes to the token provider
	 * 
	 * @param scopes The scopes of the application
	 * @return The token provider with the scopes added
	 */
	public AuthorizationCodeGrantTokenProvider withScopes(String[] scopes) {
		this.scopes.addAll(Arrays.asList(scopes));
		return this;
	}

	/**
	 * Fluent extension to add code verifier to the token provider
	 * 
	 * @param codeVerifier The code verifier
	 * @return The token provider with the code verifier added
	 */
	public AuthorizationCodeGrantTokenProvider withCodeVerifier(String codeVerifier) {
		this.codeVerifier = codeVerifier;
		return this;
	}

	/**
	 * Fluent extension to add code challenge to the token provider
	 * 
	 * @param codeChallenge The code challenge
	 * @return The token provider with the code challenge added
	 */
	public AuthorizationCodeGrantTokenProvider withCodeChallenge(String codeChallenge) {
		this.codeChallenge = codeChallenge;
		return this;
	}

	/**
	 * Fluent extension to use PKCE flow for the token provider
	 * 
	 * @return The token provider with the PKCE flow enabled
	 */
	public AuthorizationCodeGrantTokenProvider withProofKeyForCodeExchange() {
		this.codeVerifier = generateCodeVerifier();
		this.codeChallenge = createCodeChallenge(this.codeVerifier);
		return this;
	}

	/**
	 * Fluent extension to add state to the token provider
	 * 
	 * @param state The state
	 * @return The token provider with the state added
	 */
	public AuthorizationCodeGrantTokenProvider withState(String state) {
		this.state = state;
		return this;
	}

	/**
	 * Fluent extension to add random state to the token provider
	 * 
	 * @return The token provider with random state added
	 */
	public AuthorizationCodeGrantTokenProvider withRandomState() {
		this.state = generateState();
		return this;
	}

	/**
	 * Fluent extension to add logout redirect URI to the token provider
	 * 
	 * @param logoutRedirectUri The logout redirect URI
	 * @return The token provider with the logout redirect URI added
	 */
	public AuthorizationCodeGrantTokenProvider withLogoutRedirect(URI logoutRedirectUri) {
		this.logoutRedirectUri = logoutRedirectUri;
		return this;
	}

	/**
	 * Fluent extension to add identity provider to the token provider
	 * 
	 * @param identityProvider The identity provider
	 * @return The token provider with the identity provider added
	 */
	public AuthorizationCodeGrantTokenProvider withIdentityProvider(String identityProvider) {
		this.identityProvider = identityProvider;
		return this;
	}

	/**
	 * Retrieves an access token for the authenticated user
	 * 
	 * @return A completable future that contains the access token
	 */
	@Override
	public CompletableFuture getAccessToken() throws SDKClientException {
		AnalyticsHttpClient.sendMethodEvent("getAccessToken", SDK_VARIANT, this.getClass().getPackage().getName(),
				SDK_VERSION, clientId);
		return getRefreshableTokenProvider().getAccessToken();
	}

	/**
	 * Retrieves a refresh token for the authenticated user
	 * 
	 * @return A completable future that contains the refresh token
	 */
	@Override
	public CompletableFuture getRefreshToken() throws SDKClientException {
		AnalyticsHttpClient.sendMethodEvent("getRefreshToken", SDK_VARIANT, this.getClass().getPackage().getName(),
				SDK_VERSION, clientId);
		return getRefreshableTokenProvider().getRefreshToken();
	}

	/**
	 * Retrieves an ID token for the authenticated user
	 * 
	 * @return A completable future that contains the ID token
	 */
	@Override
	public CompletableFuture getIdToken() throws SDKClientException {
		AnalyticsHttpClient.sendMethodEvent("getIdToken", SDK_VARIANT, this.getClass().getPackage().getName(),
				SDK_VERSION, clientId);
		return getRefreshableTokenProvider().getIdToken();
	}

	/**
	 * Revokes the refresh token
	 * 
	 * @return A completable future that contains the status code
	 */
	@Override
	public CompletableFuture revokeRefreshToken() throws SDKClientException {
		AnalyticsHttpClient.sendMethodEvent("revokeRefreshToken", SDK_VARIANT, this.getClass().getPackage().getName(),
				SDK_VERSION, clientId);
		return getRefreshableTokenProvider().revokeRefreshToken();
	}

	/**
	 * Create an authorization URI with the provided parameters
	 * 
	 * @return A completable future that contains the authorization URI
	 */
	public CompletableFuture createAuthorizationUri() {

		AnalyticsHttpClient.sendMethodEvent("createAuthorizationUri", SDK_VARIANT,
				this.getClass().getPackage().getName(), SDK_VERSION, clientId);

		return getEndpointProvider().getAuthorizationEndpoint().thenApply((endpoint) -> {
			try {
				String queryString = QUERY_STRING
						.replace(SCOPE_PLACEHOLDER,
								encode(String.join(SPACE, this.scopes), StandardCharsets.UTF_8.name()))
						.replace(REDIRECT_URI_PLACEHOLDER,
								encode(this.redirectUri.toString(), StandardCharsets.UTF_8.name()))
						.replace(CLIENT_ID_PLACEHOLDER,
								encode(this.clientId.toString(), StandardCharsets.UTF_8.name()));

				if (this.codeChallenge != null)
					queryString = queryString.concat(CODE_CHALLENGE_METHOD).concat(S256).concat(CODE_CHALLENGE)
							.concat(this.codeChallenge);
				if (this.state != null)
					queryString = queryString.concat(STATE).concat(encode(this.state, StandardCharsets.UTF_8.name()));
				if (this.identityProvider != null)
					queryString = queryString.concat(IDENTITY_PROVIDER)
							.concat(encode(this.identityProvider, StandardCharsets.UTF_8.name()));

				return URI.create(endpoint.toString().concat(QUERY_STRING_MARKER).concat(queryString));
			} catch (Exception e) {
				throw new SDKClientException(e, e.getCause(), e.getMessage());
			}
		});
	}

	/**
	 * Checks if the authorization was successful
	 * 
	 * @param queryString The query string
	 * @return A completable future that contains the status of the authorization
	 */
	public CompletableFuture isAuthorizationSuccessful(String queryString) throws SDKClientException {

		AnalyticsHttpClient.sendMethodEvent("isAuthorizationSuccessful", SDK_VARIANT,
				this.getClass().getPackage().getName(), SDK_VERSION, clientId);

		Map paramMap = decodeQueryString(queryString);

		if (paramMap.get(CODE) == null) {
			AnalyticsHttpClient.sendExceptionEvent("isAuthorizationSuccessful", SDK_VARIANT,
					this.getClass().getPackage().getName(), SDK_VERSION, AUTH_CODE_REDIRECT_ERROR, clientId);
			throw new SDKClientException(new IllegalArgumentException(AUTH_CODE_REDIRECT_ERROR));
		}

		// For PKCE flow, call setAccessTokenUsingAuthCodeAndPKCE() method instead of
		// setAccessTokenUsingAuthCode() method
		if (this.codeVerifier != null) {
			return setAccessTokenUsingAuthCodeAndPKCE(paramMap.get(CODE)).thenApply((isSuccessful) -> {
				return isSuccessful;
			});
		} else {
			return setAccessTokenUsingAuthCode(paramMap.get(CODE)).thenApply((isSuccessful) -> {
				return isSuccessful;
			});
		}
	}

	private Map decodeQueryString(String queryString) {
		Map paramMap = new HashMap<>();
		queryString = queryString.split(" ")[0];
		for (String param : queryString.split(AMPERSAND)) {
			String[] keyValue = param.split(EQUAL_TO);
			if (keyValue.length == 1) {
				paramMap.put(keyValue[0], null);
			} else {
				try {
					paramMap.put(keyValue[0], URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8.name()));
				} catch (Exception e) {
					throw new SDKClientException(e, e.getCause(), e.getMessage());
				}
			}
		}
		return paramMap;
	}

	private CompletableFuture setAccessTokenUsingAuthCode(String authCode) {

		List nameValuePairs = new ArrayList();
		nameValuePairs.add(new NameValuePair(GRANT_TYPE, AUTHORIZATION_CODE_GRANT));
		nameValuePairs.add(new NameValuePair(REDIRECT_URI, this.redirectUri.toString()));
		nameValuePairs.add(new NameValuePair(CLIENT_ID, this.clientId));
		nameValuePairs.add(new NameValuePair(CODE, authCode));

		if (this.codeVerifier != null) {
			nameValuePairs.add(new NameValuePair(CODE_VERIFIER, this.codeVerifier));
		}

		return getHttpTokenClient().getOauthTokens(new TokenRequest(this.clientId, this.clientSecret, nameValuePairs))
				.thenApply((tokenResponse) -> {

					if (tokenResponse.error_description != null) {
						AnalyticsHttpClient.sendExceptionEvent("setAccessTokenUsingAuthCode", SDK_VARIANT,
								this.getClass().getPackage().getName(), SDK_VERSION, tokenResponse.error_description,
								clientId);
						throw new SDKClientException(tokenResponse.error, tokenResponse.error_description);
					}

					setRefreshableTokenProvider(new RefreshableTokenProvider(this.endpointProvider, this.clientId,
							this.clientSecret, tokenResponse.accessToken,
							(Long.valueOf(new Date().getTime()) + (tokenResponse.expiresIn * 1000)),
							tokenResponse.refreshToken, tokenResponse.idToken, null));

					return true;

				});
	}

	private CompletableFuture setAccessTokenUsingAuthCodeAndPKCE(String authCode) {

		List nameValuePairs = new ArrayList();
		nameValuePairs.add(new NameValuePair(GRANT_TYPE, AUTHORIZATION_CODE_GRANT));
		nameValuePairs.add(new NameValuePair(REDIRECT_URI, this.redirectUri.toString()));
		nameValuePairs.add(new NameValuePair(CLIENT_ID, this.clientId));
		nameValuePairs.add(new NameValuePair(CODE, authCode));
		nameValuePairs.add(new NameValuePair(CODE_VERIFIER, this.codeVerifier));

		String codeVerifierForSerialRefresh = generateCodeVerifier();
		nameValuePairs.add(new NameValuePair(CODE_CHALLENGE_PARAM, createCodeChallenge(codeVerifierForSerialRefresh)));
		nameValuePairs.add(new NameValuePair(CODE_CHALLENGE_METHOD_PARAM, S256));

		return getHttpTokenClient().getOauthTokens(new TokenRequest(this.clientId, nameValuePairs))
				.thenApply((tokenResponse) -> {

					if (tokenResponse.error_description != null) {
						AnalyticsHttpClient.sendExceptionEvent("setAccessTokenUsingAuthCodeAndPKCE", SDK_VARIANT,
								this.getClass().getPackage().getName(), SDK_VERSION, tokenResponse.error_description,
								clientId);
						throw new SDKClientException(tokenResponse.error, tokenResponse.error_description);
					}

					setRefreshableTokenProvider(new RefreshableTokenProvider(this.endpointProvider, this.clientId,
							this.clientSecret, tokenResponse.accessToken,
							(Long.valueOf(new Date().getTime()) + (tokenResponse.expiresIn * 1000)),
							tokenResponse.refreshToken, tokenResponse.idToken, codeVerifierForSerialRefresh));

					return true;

				});
	}

	/**
	 * Create a logout URI with the provided parameters
	 * 
	 * @return A completable future that contains the logout URI
	 */
	public CompletableFuture createLogoutUri() {

		AnalyticsHttpClient.sendMethodEvent("createLogoutUri", SDK_VARIANT, this.getClass().getPackage().getName(),
				SDK_VERSION);

		return getEndpointProvider().getEndSessionEndpoint().thenApply((endpoint) -> {
			String idToken;
			try {
				idToken = getIdToken().get();
			} catch (InterruptedException | ExecutionException e) {
				AnalyticsHttpClient.sendExceptionEvent("createLogoutUri", SDK_VARIANT,
						this.getClass().getPackage().getName(), SDK_VERSION, e.getMessage(), clientId);
				throw new SDKClientException(e, e.getCause(), e.getMessage());
			}

			String url = endpoint + "?id_token_hint=" + idToken + "&post_logout_redirect_uri=" + logoutRedirectUri;

			if (state != null)
				url += "&state=" + state;

			return URI.create(url);
		});
	}

	/**
	 * Retrieves the endpoint provider
	 * 
	 * @return The endpoint provider
	 */
	public IEndpointProvider getEndpointProvider() {
		return endpointProvider;
	}

	/**
	 * Retrieves the http token client
	 * 
	 * @return The http token client
	 */
	public IHttpTokenClient getHttpTokenClient() {
		return httpTokenClient;
	}

	/**
	 * Retrieves the refreshable token provider
	 * 
	 * @return The refreshable token provider
	 */
	public RefreshableTokenProvider getRefreshableTokenProvider() {
		return refreshableTokenProvider;
	}

	public void setRefreshableTokenProvider(RefreshableTokenProvider refreshableTokenProvider) {
		this.refreshableTokenProvider = refreshableTokenProvider;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy