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

com.intuit.oauth2.client.OAuth2PlatformClient Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2017 Intuit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.intuit.oauth2.client;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import com.intuit.oauth2.exception.InvalidRequestException;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.intuit.oauth2.config.OAuth2Config;
import com.intuit.oauth2.data.BearerTokenResponse;
import com.intuit.oauth2.data.PlatformResponse;
import com.intuit.oauth2.data.UserInfoResponse;
import com.intuit.oauth2.exception.ConnectionException;
import com.intuit.oauth2.exception.OAuthException;
import com.intuit.oauth2.exception.OpenIdException;
import com.intuit.oauth2.http.HttpRequestClient;
import com.intuit.oauth2.http.MethodType;
import com.intuit.oauth2.http.Request;
import com.intuit.oauth2.http.Response;
import com.intuit.oauth2.utils.LoggerImpl;
import com.intuit.oauth2.utils.MapperImpl;

/**
 * Client class for OAuth2 API's with methods to retrieve bearer token,
 * refresh token and get UserInfo
 *
 * @author dderose
 *
 */
public class OAuth2PlatformClient {


    private OAuth2Config oauth2Config;

    private static final Logger logger = LoggerImpl.getInstance();
    private static final ObjectMapper mapper  = MapperImpl.getInstance();

    public OAuth2PlatformClient(OAuth2Config oauth2Config) {
        this.oauth2Config = oauth2Config;
    }

    /** `
     * Hiding the default constructor as OAuth2PlatformClient is always required to function properly
     */
    protected OAuth2PlatformClient() {

    }


    /**
     * Method to retrieve OAuth2 access token by passing the redirectURI and authCode
     *
     * @param authCode
     * @param redirectURI
     * @return
     * @throws OAuthException
     */
    public BearerTokenResponse retrieveBearerTokens(String authCode, String redirectURI) throws OAuthException {

        logger.debug("Enter OAuth2PlatformClient::retrieveBearerTokens");

        try {
            HttpRequestClient client = new HttpRequestClient(oauth2Config.getProxyConfig());
            Request request = new Request.RequestBuilder(MethodType.POST, oauth2Config.getIntuitBearerTokenEndpoint())
                    .requiresAuthentication(true)
                    .authString(getAuthHeader())
                    .postParams(getUrlParameters(null, authCode, redirectURI))
                    .build();

            Response response = client.makeRequest(request);

            logger.debug("Response Code : "+ response.getStatusCode());
            logger.debug("intuit_tid : "+ response.getIntuit_tid());
            if (response.getStatusCode() != 200) {
                logger.debug("failed getting access token");
                logger.debug("Response content : "+ response.getContent());
                throw new OAuthException("Failed getting access token", response.getStatusCode() + "", response.getIntuit_tid(), response);
            }

            ObjectReader reader = mapper.readerFor(BearerTokenResponse.class);
            BearerTokenResponse bearerTokenResponse = reader.readValue(response.getContent());
            bearerTokenResponse.setIntuit_tid(response.getIntuit_tid());

            return bearerTokenResponse;

        } catch (OAuthException ex) {
            logger.error("OAuthException while retrieving bearer tokens: " + ex.getResponseContent());
            throw new OAuthException("Failed getting access token", ex.getStatusCode() + "", ex.getIntuit_tid(), ex.getResponse());
        }  catch (Exception ex) {
            logger.error("Exception while retrieving bearer tokens", ex);
            throw new OAuthException(ex.getMessage(), ex);
        }
    }

    /**
     * Method to renew OAuth2 tokens by passing the refreshToken
     *
     * @param refreshToken
     * @return
     * @throws OAuthException
     */
    public BearerTokenResponse refreshToken(String refreshToken) throws OAuthException {

        logger.debug("Enter OAuth2PlatformClient::refreshToken");
        try {
            HttpRequestClient client = new HttpRequestClient(oauth2Config.getProxyConfig());
            Request request = new Request.RequestBuilder(MethodType.POST, oauth2Config.getIntuitBearerTokenEndpoint())
                    .requiresAuthentication(true)
                    .authString(getAuthHeader())
                    .postParams(getUrlParameters("refresh", refreshToken, null))
                    .build();
            Response response = client.makeRequest(request);

            logger.debug("Response Code : "+ response.getStatusCode());
            logger.debug("intuit_tid : "+ response.getIntuit_tid());
            if (response.getStatusCode() != 200) {
                logger.debug("Failed to refresh token");
                logger.debug("Response content : "+ response.getContent());
                throw new OAuthException("Failed to refresh token", response.getStatusCode() + "", response.getIntuit_tid(), response);
            }

            ObjectReader reader = mapper.readerFor(BearerTokenResponse.class);
            BearerTokenResponse bearerTokenResponse = reader.readValue(response.getContent());
            bearerTokenResponse.setIntuit_tid(response.getIntuit_tid());
            return bearerTokenResponse;
        } catch (OAuthException ex) {
            logger.error("OAuthException while calling refreshToken:  " + ex.getResponseContent());
            throw new OAuthException(ex.getMessage(), ex.getStatusCode() + "", ex.getIntuit_tid(), ex.getResponse());
        } catch (Exception ex) {
            logger.error("Exception while calling refreshToken ");
            throw new OAuthException(ex.getMessage(), ex);
        }
    }


    /**
     * Method to build post parameters
     *
     * @param action
     * @param token
     * @param redirectUri
     * @return
     */
    private List getUrlParameters(String action, String token, String redirectUri) {
        List urlParameters = new ArrayList();
        if (action == "revoke") {
            urlParameters.add(new BasicNameValuePair("token", token));
        } else if (action == "refresh") {
            urlParameters.add(new BasicNameValuePair("refresh_token", token));
            urlParameters.add(new BasicNameValuePair("grant_type", "refresh_token"));
        } else {
            urlParameters.add(new BasicNameValuePair("code", token));
            urlParameters.add(new BasicNameValuePair("redirect_uri", redirectUri));
            urlParameters.add(new BasicNameValuePair("grant_type", "authorization_code"));
        }
        return urlParameters;
    }

    /**
     * Method to revoke OAuth2 tokens
     *
     * @param token
     * @return
     * @throws ConnectionException
     */
    public PlatformResponse revokeToken(String token) throws ConnectionException {

        logger.debug("Enter OAuth2PlatformClient::revokeToken");

        PlatformResponse platformResponse = new PlatformResponse();
        try {

            HttpRequestClient client = new HttpRequestClient(oauth2Config.getProxyConfig());
            Request request = new Request.RequestBuilder(MethodType.POST, oauth2Config.getIntuitRevokeTokenEndpoint())
                    .requiresAuthentication(true)
                    .authString(getAuthHeader())
                    .postParams(getUrlParameters("revoke", token, null))
                    .build();

            Response response = client.makeRequest(request);

            logger.debug("Response Code : "+ response.getStatusCode());
            logger.debug("intuit_tid: " + response.getIntuit_tid());
            if (response.getStatusCode() != 200) {
                logger.debug("failed to revoke token");

                platformResponse.setStatus("ERROR");
                platformResponse.setErrorCode(response.getStatusCode() + "");
                platformResponse.setErrorMessage("Failed to revoke token");
                return platformResponse;
            }

            platformResponse.setStatus("SUCCESS");
            platformResponse.setIntuit_tid(response.getIntuit_tid());
            return platformResponse;
        }
        catch (Exception ex) {
            logger.error("Exception while calling revokeToken ", ex);
            throw new ConnectionException(ex.getMessage(), ex);
        }
    }

    /**
     * Method to generate auth header based on client ID and Client Secret
     *
     * @return
     */
    private String getAuthHeader() {
        byte[] bytesEncoded = Base64.encodeBase64((oauth2Config.getClientId() + ":" + oauth2Config.getClientSecret()).getBytes());
        String base64ClientIdSec = new String(bytesEncoded);
        return "Basic " + base64ClientIdSec;
    }

    /**
     * Method to retrieve UserInfo data associated with the accessToken generated
     * The response depends on the Scope supplied during openId
     *
     * @param accessToken
     * @return
     * @throws OpenIdException
     */
    public UserInfoResponse getUserInfo(String accessToken) throws OpenIdException {

        logger.debug("Enter OAuth2PlatformClient::getUserInfo");

        try {
            HttpRequestClient client = new HttpRequestClient(oauth2Config.getProxyConfig());
            Request request = new Request.RequestBuilder(MethodType.GET, oauth2Config.getUserProfileEndpoint())
                    .requiresAuthentication(true)
                    .authString("Bearer " + accessToken)
                    .build();

            Response response = client.makeRequest(request);

            logger.debug("Response Code : "+ response.getStatusCode());
            logger.debug("intuit_tid: " + response.getIntuit_tid());
            if (response.getStatusCode() == 200) {
                ObjectReader reader = mapper.readerFor(UserInfoResponse.class);
                UserInfoResponse userInfoResponse = reader.readValue(response.getContent());
                userInfoResponse.setIntuit_tid(response.getIntuit_tid());
                return userInfoResponse;
            } else {
                logger.debug("failed getting user info");
                throw new OpenIdException("failed getting user info", response.getStatusCode() + "", response.getIntuit_tid(), response);
            }
        }
        catch (OpenIdException ex) {
            logger.error("OpenIdException while retrieving user info: " + ex.getResponseContent());
            throw new OpenIdException("failed getting user info", ex.getStatusCode() + "", ex.getIntuit_tid(), ex.getResponse());
        }
        catch (Exception ex) {
            logger.error("Exception while retrieving user info ", ex);
            throw new OpenIdException(ex.getMessage(), ex);
        }

    }

    /**
     * Method to validate IDToken
     *
     * @param idToken
     * @return
     * @throws OpenIdException
     */
    public boolean validateIDToken(String idToken) throws OpenIdException {

        logger.debug("Enter OAuth2PlatformClient::validateIDToken");

        String[] idTokenParts = idToken.split("\\.");

        if (idTokenParts.length < 3) {
            logger.debug("invalid idTokenParts length");
            return false;
        }

        String idTokenHeader = base64UrlDecode(idTokenParts[0]);
        String idTokenPayload = base64UrlDecode(idTokenParts[1]);
        byte[] idTokenSignature = base64UrlDecodeToBytes(idTokenParts[2]);

        JSONObject idTokenHeaderJson = new JSONObject(idTokenHeader);
        JSONObject idTokenHeaderPayload = new JSONObject(idTokenPayload);

        //Step 1 : First check if the issuer is as mentioned in "issuer" in the discovery doc
        String issuer = idTokenHeaderPayload.getString("iss");
        if(!issuer.equalsIgnoreCase(oauth2Config.getIntuitIdTokenIssuer())) {
            logger.debug("issuer value mismtach");
            return false;
        }

        //Step 2 : check if the aud field in idToken is same as application's clientId
        JSONArray jsonaud = idTokenHeaderPayload.getJSONArray("aud");
        String aud = jsonaud.getString(0);

        if(!aud.equalsIgnoreCase(oauth2Config.getClientId())) {
            logger.debug("incorrect client id");
            return false;
        }

        //Step 3 : ensure the timestamp has not elapsed
        Long expirationTimestamp = idTokenHeaderPayload.getLong("exp");
        Long currentTime = System.currentTimeMillis() / 1000;

        if((expirationTimestamp - currentTime) <= 0) {
            logger.debug("expirationTimestamp has elapsed");
            return false;
        }

        //Step 4: Verify that the ID token is properly signed by the issuer
        HashMap keyMap = getKeyMapFromJWKSUri();
        if (keyMap == null || keyMap.isEmpty()) {
            logger.debug("unable to retrive keyMap from JWKS url");
            return false;
        }

        //first get the kid from the header.
        String keyId = idTokenHeaderJson.getString("kid");
        JSONObject keyDetails = keyMap.get(keyId);

        //now get the exponent (e) and modulo (n) to form the PublicKey
        String exponent = keyDetails.getString("e");
        String modulo = keyDetails.getString("n");

        //build the public key
        PublicKey publicKey = getPublicKey(modulo, exponent);

        byte[] data = (idTokenParts[0] + "." + idTokenParts[1]).getBytes(StandardCharsets.UTF_8);

        try {
            //verify token using public key
            boolean isSignatureValid = verifyUsingPublicKey(data, idTokenSignature, publicKey);
            logger.debug("isSignatureValid: " + isSignatureValid);
            return isSignatureValid;

        } catch (GeneralSecurityException e) {
            logger.error("Exception while validating ID token ", e);
            throw new OpenIdException(e.getMessage(), e);
        }

    }

    /**
     * Build JWKS keymap
     *
     * @return
     * @throws OpenIdException
     */
    private HashMap getKeyMapFromJWKSUri() throws OpenIdException {

        logger.debug("Enter OAuth2PlatformClient::getKeyMapFromJWKSUri");

        try {

            HttpRequestClient client = new HttpRequestClient(oauth2Config.getProxyConfig());
            Request request = new Request.RequestBuilder(MethodType.GET, oauth2Config.getIntuitJwksURI())
                    .requiresAuthentication(false)
                    .build();

            Response response = client.makeRequest(request);

            logger.debug("Response Code : "+ response.getStatusCode());
            if (response.getStatusCode() != 200) {
                logger.debug("failed JWKS URI");
                throw new OpenIdException("failed JWKS URI", response.getStatusCode() + "");
            }

            return buildKeyMap(response.getContent());
        }
        catch (Exception ex) {
            logger.error("Exception while retrieving jwks ", ex);
            throw new OpenIdException(ex.getMessage(), ex);
        }
    }

    /**
     * Build public key
     *
     * @param MODULUS
     * @param EXPONENT
     * @return
     */
    private PublicKey getPublicKey(String MODULUS, String EXPONENT) {
        byte[] nb = base64UrlDecodeToBytes(MODULUS);
        byte[] eb = base64UrlDecodeToBytes(EXPONENT);
        BigInteger n = new BigInteger(1, nb);
        BigInteger e = new BigInteger(1, eb);

        RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(n, e);
        try {
            PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(rsaPublicKeySpec);
            return publicKey;
        } catch (Exception ex) {
            logger.error("Exception while getting public key ", ex);
            throw new RuntimeException("Cant create public key", ex);
        }
    }

    /**
     * Verify signature
     *
     * @param data
     * @param signature
     * @param pubKey
     * @return
     * @throws GeneralSecurityException
     */
    private boolean verifyUsingPublicKey(byte[] data, byte[] signature, PublicKey pubKey)
            throws GeneralSecurityException {
        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initVerify(pubKey);
        sig.update(data);
        return sig.verify(signature);
    }

    /**
     * @param input
     * @return
     */
    private String base64UrlDecode(String input) {
        byte[] decodedBytes = base64UrlDecodeToBytes(input);
        String result = new String(decodedBytes, StandardCharsets.UTF_8);
        return result;
    }

    /**
     * @param input
     * @return
     */
    private byte[] base64UrlDecodeToBytes(String input) {
        Base64 decoder = new Base64(-1, null, true);
        byte[] decodedBytes = decoder.decode(input);

        return decodedBytes;
    }

    /**
     * Build Map from response
     *
     * @param content
     * @return
     * @throws ConnectionException
     */
    private HashMap buildKeyMap(String content) throws ConnectionException {
        HashMap retMap = new HashMap();
        JSONObject jwksPayload = new JSONObject(content);
        JSONArray keysArray = jwksPayload.getJSONArray("keys");

        for (int i=0;i




© 2015 - 2024 Weber Informatics LLC | Privacy Policy