/******************************************************************************
 *
 * Project:  Common Portability Library
 * Purpose:  Google OAuth2 Authentication Services
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *           Even Rouault, even.rouault at spatialys.com
 ******************************************************************************
 * Copyright (c) 2013, Frank Warmerdam
 * Copyright (c) 2017, Even Rouault
 * Copyright (c) 2017, Planet Labs
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_http.h"
#include "cpl_port.h"
#include "cpl_sha256.h"

#include <cstring>

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_json.h"
#include "cpl_string.h"

/* ==================================================================== */
/*      Values related to OAuth2 authorization to use fusion            */
/*      tables.  Many of these values are related to the                */
/*      gdalautotest@gmail.com account for GDAL managed by Even         */
/*      Rouault and Frank Warmerdam.  Some information about OAuth2     */
/*      as managed by that account can be found at the following url    */
/*      when logged in as gdalautotest@gmail.com:                       */
/*                                                                      */
/*      https://code.google.com/apis/console/#project:265656308688:access*/
/*                                                                      */
/*      Applications wanting to use their own client id and secret      */
/*      can set the following configuration options:                    */
/*       - GOA2_CLIENT_ID                                               */
/*       - GOA2_CLIENT_SECRET                                           */
/* ==================================================================== */
#define GDAL_CLIENT_ID "265656308688.apps.googleusercontent.com"
#define GDAL_CLIENT_SECRET "0IbTUDOYzaL6vnIdWTuQnvLz"

#define GOOGLE_AUTH_URL "https://accounts.google.com/o/oauth2"

/************************************************************************/
/*                      GOA2GetAuthorizationURL()                       */
/************************************************************************/

/**
 * Return authorization url for a given scope.
 *
 * Returns the URL that a user should visit, and use for authentication
 * in order to get an "auth token" indicating their willingness to use a
 * service.
 *
 * Note that when the user visits this url they will be asked to login
 * (using a google/gmail/etc) account, and to authorize use of the
 * requested scope for the application "GDAL/OGR".  Once they have done
 * so, they will be presented with a lengthy string they should "enter
 * into their application".  This is the "auth token" to be passed to
 * GOA2GetRefreshToken().  The "auth token" can only be used once.
 *
 * This function should never fail.
 *
 * @param pszScope the service being requested, not yet URL encoded, such as
 * "https://www.googleapis.com/auth/fusiontables".
 *
 * @return the URL to visit - should be freed with CPLFree().
 */

char *GOA2GetAuthorizationURL(const char *pszScope)

{
    CPLString osScope;
    osScope.Seize(CPLEscapeString(pszScope, -1, CPLES_URL));

    CPLString osURL;
    osURL.Printf("%s/auth?scope=%s&redirect_uri=urn:ietf:wg:oauth:2.0:oob&"
                 "response_type=code&client_id=%s",
                 GOOGLE_AUTH_URL, osScope.c_str(),
                 CPLGetConfigOption("GOA2_CLIENT_ID", GDAL_CLIENT_ID));
    return CPLStrdup(osURL);
}

/************************************************************************/
/*                        GOA2GetRefreshToken()                         */
/************************************************************************/

/**
 * Turn Auth Token into a Refresh Token.
 *
 * A one time "auth token" provided by the user is turned into a
 * reusable "refresh token" using a google oauth2 web service.
 *
 * A CPLError will be reported if the translation fails for some reason.
 * Common reasons include the auth token already having been used before,
 * it not being appropriate for the passed scope and configured client api
 * or http connection problems.  NULL is returned on error.
 *
 * @param pszAuthToken the authorization token from the user.
 * @param pszScope the scope for which it is valid.
 *
 * @return refresh token, to be freed with CPLFree(), null on failure.
 */

char CPL_DLL *GOA2GetRefreshToken(const char *pszAuthToken,
                                  const char *pszScope)

{
    /* -------------------------------------------------------------------- */
    /*      Prepare request.                                                */
    /* -------------------------------------------------------------------- */
    CPLString osItem;
    CPLStringList oOptions;

    oOptions.AddString(
        "HEADERS=Content-Type: application/x-www-form-urlencoded");

    osItem.Printf("POSTFIELDS="
                  "code=%s"
                  "&client_id=%s"
                  "&client_secret=%s"
                  "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"
                  "&grant_type=authorization_code",
                  pszAuthToken,
                  CPLGetConfigOption("GOA2_CLIENT_ID", GDAL_CLIENT_ID),
                  CPLGetConfigOption("GOA2_CLIENT_SECRET", GDAL_CLIENT_SECRET));
    oOptions.AddString(osItem);

    /* -------------------------------------------------------------------- */
    /*      Submit request by HTTP.                                         */
    /* -------------------------------------------------------------------- */
    CPLHTTPResult *psResult = CPLHTTPFetch(
        CPLGetConfigOption("GOA2_AUTH_URL_TOKEN", GOOGLE_AUTH_URL "/token"),
        oOptions);

    if (psResult == nullptr)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      One common mistake is to try and reuse the auth token.          */
    /*      After the first use it will return invalid_grant.               */
    /* -------------------------------------------------------------------- */
    if (psResult->pabyData != nullptr &&
        strstr(reinterpret_cast<char *>(psResult->pabyData), "invalid_grant") !=
            nullptr)
    {
        CPLHTTPDestroyResult(psResult);
        if (pszScope == nullptr)
        {
            CPLError(
                CE_Failure, CPLE_AppDefined,
                "Attempt to use a OAuth2 authorization code multiple times. "
                "Use GOA2GetAuthorizationURL(scope) with a valid scope to "
                "request a fresh authorization token.");
        }
        else
        {
            CPLString osURL;
            osURL.Seize(GOA2GetAuthorizationURL(pszScope));
            CPLError(
                CE_Failure, CPLE_AppDefined,
                "Attempt to use a OAuth2 authorization code multiple times. "
                "Request a fresh authorization token at %s.",
                osURL.c_str());
        }
        return nullptr;
    }

    if (psResult->pabyData == nullptr || psResult->pszErrBuf != nullptr)
    {
        if (psResult->pszErrBuf != nullptr)
            CPLDebug("GOA2", "%s", psResult->pszErrBuf);
        if (psResult->pabyData != nullptr)
            CPLDebug("GOA2", "%s", psResult->pabyData);

        CPLError(CE_Failure, CPLE_AppDefined,
                 "Fetching OAuth2 access code from auth code failed.");
        CPLHTTPDestroyResult(psResult);
        return nullptr;
    }

    CPLDebug("GOA2", "Access Token Response:\n%s",
             reinterpret_cast<char *>(psResult->pabyData));

    /* -------------------------------------------------------------------- */
    /*      This response is in JSON and will look something like:          */
    /* -------------------------------------------------------------------- */
    /*
    {
     "access_token" :
    "ya29.AHES6ZToqkIJkat5rIqMixR1b8PlWBACNO8OYbqqV-YF1Q13E2Kzjw", "token_type"
    : "Bearer", "expires_in" : 3600, "refresh_token" :
    "1/eF88pciwq9Tp_rHEhuiIv9AS44Ufe4GOymGawTVPGYo"
    }
    */
    CPLStringList oResponse =
        CPLParseKeyValueJson(reinterpret_cast<char *>(psResult->pabyData));
    CPLHTTPDestroyResult(psResult);

    CPLString osAccessToken = oResponse.FetchNameValueDef("access_token", "");
    CPLString osRefreshToken = oResponse.FetchNameValueDef("refresh_token", "");
    CPLDebug("GOA2", "Access Token : '%s'", osAccessToken.c_str());
    CPLDebug("GOA2", "Refresh Token : '%s'", osRefreshToken.c_str());

    if (osRefreshToken.empty())
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unable to identify a refresh token in the OAuth2 response.");
        return nullptr;
    }
    else
    {
        // Currently we discard the access token and just return the
        // refresh token.
        return CPLStrdup(osRefreshToken);
    }
}

/************************************************************************/
/*                       GOA2ProcessResponse()                          */
/************************************************************************/

static char **GOA2ProcessResponse(CPLHTTPResult *psResult)
{

    if (psResult == nullptr)
        return nullptr;

    if (psResult->pabyData == nullptr || psResult->pszErrBuf != nullptr)
    {
        if (psResult->pszErrBuf != nullptr)
            CPLDebug("GOA2", "%s", psResult->pszErrBuf);
        if (psResult->pabyData != nullptr)
            CPLDebug("GOA2", "%s", psResult->pabyData);

        CPLError(CE_Failure, CPLE_AppDefined,
                 "Fetching OAuth2 access code from auth code failed.");
        CPLHTTPDestroyResult(psResult);
        return nullptr;
    }

    CPLDebug("GOA2", "Refresh Token Response:\n%s",
             reinterpret_cast<char *>(psResult->pabyData));

    /* -------------------------------------------------------------------- */
    /*      This response is in JSON and will look something like:          */
    /* -------------------------------------------------------------------- */
    /*
    {
    "access_token":"1/fFBGRNJru1FQd44AzqT3Zg",
    "expires_in":3920,
    "token_type":"Bearer"
    }
    */
    CPLStringList oResponse =
        CPLParseKeyValueJson(reinterpret_cast<char *>(psResult->pabyData));
    CPLHTTPDestroyResult(psResult);

    CPLString osAccessToken = oResponse.FetchNameValueDef("access_token", "");

    CPLDebug("GOA2", "Access Token : '%s'", osAccessToken.c_str());

    if (osAccessToken.empty())
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unable to identify an access token in the OAuth2 response.");
        return nullptr;
    }

    return oResponse.StealList();
}

/************************************************************************/
/*                       GOA2GetAccessTokenEx()                         */
/************************************************************************/

static char **GOA2GetAccessTokenEx(const char *pszRefreshToken,
                                   const char *pszClientId,
                                   const char *pszClientSecret,
                                   CSLConstList /*papszOptions*/)
{

    /* -------------------------------------------------------------------- */
    /*      Prepare request.                                                */
    /* -------------------------------------------------------------------- */
    CPLString osItem;
    CPLStringList oOptions;

    oOptions.AddString(
        "HEADERS=Content-Type: application/x-www-form-urlencoded");

    osItem.Printf(
        "POSTFIELDS="
        "refresh_token=%s"
        "&client_id=%s"
        "&client_secret=%s"
        "&grant_type=refresh_token",
        pszRefreshToken,
        (pszClientId && !EQUAL(pszClientId, ""))
            ? pszClientId
            : CPLGetConfigOption("GOA2_CLIENT_ID", GDAL_CLIENT_ID),
        (pszClientSecret && !EQUAL(pszClientSecret, ""))
            ? pszClientSecret
            : CPLGetConfigOption("GOA2_CLIENT_SECRET", GDAL_CLIENT_SECRET));
    oOptions.AddString(osItem);

    /* -------------------------------------------------------------------- */
    /*      Submit request by HTTP.                                         */
    /* -------------------------------------------------------------------- */
    CPLHTTPResult *psResult = CPLHTTPFetch(
        CPLGetConfigOption("GOA2_AUTH_URL_TOKEN", GOOGLE_AUTH_URL "/token"),
        oOptions);

    return GOA2ProcessResponse(psResult);
}

/************************************************************************/
/*                         GOA2GetAccessToken()                         */
/************************************************************************/

/**
 * Fetch access token using refresh token.
 *
 * The permanent refresh token is used to fetch a temporary (usually one
 * hour) access token using Google OAuth2 web services.
 *
 * A CPLError will be reported if the request fails for some reason.
 * Common reasons include the refresh token having been revoked by the
 * user or http connection problems.
 *
 * @param pszRefreshToken the refresh token from GOA2GetRefreshToken().
 * @param pszScope the scope for which it is valid. Currently unused
 *
 * @return access token, to be freed with CPLFree(), null on failure.
 */

char *GOA2GetAccessToken(const char *pszRefreshToken,
                         CPL_UNUSED const char *pszScope)
{
    char **papszRet =
        GOA2GetAccessTokenEx(pszRefreshToken, nullptr, nullptr, nullptr);
    const char *pszAccessToken = CSLFetchNameValue(papszRet, "access_token");
    char *pszRet = pszAccessToken ? CPLStrdup(pszAccessToken) : nullptr;
    CSLDestroy(papszRet);
    return pszRet;
}

/************************************************************************/
/*               GOA2GetAccessTokenFromCloudEngineVM()                  */
/************************************************************************/

/**
 * Fetch access token using Cloud Engine internal REST API
 *
 * The default service accounts bound to the current Google Cloud Engine VM
 * is used for OAuth2 authentication
 *
 * A CPLError will be reported if the request fails for some reason.
 * Common reasons include the refresh token having been revoked by the
 * user or http connection problems.
 *
 * @param papszOptions NULL terminated list of options. None currently
 *
 * @return a list of key=value pairs, including a access_token and expires_in
 */

char **GOA2GetAccessTokenFromCloudEngineVM(CSLConstList papszOptions)
{
    CPLStringList oOptions;

    oOptions.AddString("HEADERS=Metadata-Flavor: Google");

    /* -------------------------------------------------------------------- */
    /*      Submit request by HTTP.                                         */
    /* -------------------------------------------------------------------- */
    const char *pszURL = CSLFetchNameValueDef(
        papszOptions, "URL",
        CPLGetConfigOption("CPL_GCE_CREDENTIALS_URL",
                           "http://metadata.google.internal/computeMetadata/v1/"
                           "instance/service-accounts/default/token"));
    CPLHTTPResult *psResult = CPLHTTPFetch(pszURL, oOptions);

    return GOA2ProcessResponse(psResult);
}

/************************************************************************/
/*                 GOA2GetAccessTokenFromServiceAccount()               */
/************************************************************************/

/**
 * Fetch access token using Service Account OAuth2
 *
 * See https://developers.google.com/identity/protocols/OAuth2ServiceAccount
 *
 * A CPLError will be reported if the request fails for some reason.
 *
 * @param pszPrivateKey Private key as a RSA private key
 * @param pszClientEmail Client email
 * @param pszScope the service being requested
 * @param papszAdditionalClaims additional claims, or NULL
 * @param papszOptions NULL terminated list of options. None currently
 *
 * @return a list of key=value pairs, including a access_token and expires_in
 */

char **GOA2GetAccessTokenFromServiceAccount(const char *pszPrivateKey,
                                            const char *pszClientEmail,
                                            const char *pszScope,
                                            CSLConstList papszAdditionalClaims,
                                            CSLConstList papszOptions)
{
    CPL_IGNORE_RET_VAL(papszOptions);

    /** See
     * https://developers.google.com/identity/protocols/OAuth2ServiceAccount and
     * https://jwt.io/ */

    // JWT header '{"alg":"RS256","typ":"JWT"}' encoded in Base64
    const char *pszB64JWTHeader = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9";
    const char *pszAud = CPLGetConfigOption(
        "GO2A_AUD", "https://www.googleapis.com/oauth2/v4/token");

    CPLString osClaim;
    osClaim = "{\"iss\": \"";
    osClaim += pszClientEmail;
    osClaim += "\", \"scope\": \"";
    osClaim += pszScope;
    osClaim += "\", \"aud\": \"";
    osClaim += pszAud;
    osClaim += "\", \"iat\": ";
    GIntBig now = static_cast<GIntBig>(time(nullptr));
    const char *pszNow = CPLGetConfigOption("GOA2_NOW", nullptr);
    if (pszNow)
        now = CPLAtoGIntBig(pszNow);
    osClaim += CPLSPrintf(CPL_FRMT_GIB, now);
    osClaim += ", \"exp\": ";
    osClaim += CPLSPrintf(
        CPL_FRMT_GIB,
        now + atoi(CPLGetConfigOption("GOA2_EXPIRATION_DELAY", "3600")));
    for (CSLConstList papszIter = papszAdditionalClaims;
         papszIter && *papszIter; ++papszIter)
    {
        char *pszKey = nullptr;
        const char *pszValue = CPLParseNameValue(*papszIter, &pszKey);
        if (pszKey && pszValue)
        {
            osClaim += ", \"";
            osClaim += pszKey;
            osClaim += "\": ";
            osClaim += pszValue;
            CPLFree(pszKey);
        }
    }
    osClaim += "}";
#ifdef DEBUG_VERBOSE
    CPLDebug("GOA2", "%s", osClaim.c_str());
#endif

    char *pszB64Claim =
        CPLBase64Encode(static_cast<int>(osClaim.size()),
                        reinterpret_cast<const GByte *>(osClaim.c_str()));
    // Build string to sign
    CPLString osToSign(CPLString(pszB64JWTHeader) + "." + pszB64Claim);
    CPLFree(pszB64Claim);

    unsigned int nSignatureLen = 0;
    // Sign request
    GByte *pabySignature =
        CPL_RSA_SHA256_Sign(pszPrivateKey, osToSign.c_str(),
                            static_cast<int>(osToSign.size()), &nSignatureLen);
    if (pabySignature == nullptr)
    {
        return nullptr;
    }
    char *pszB64Signature = CPLBase64Encode(nSignatureLen, pabySignature);
    CPLFree(pabySignature);
    // Build signed request
    CPLString osRequest(osToSign + "." + pszB64Signature);
    CPLFree(pszB64Signature);

    // Issue request
    CPLString osPostData("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%"
                         "3Ajwt-bearer&assertion=");
    char *pszAssertionEncoded = CPLEscapeString(osRequest, -1, CPLES_URL);
    CPLString osAssertionEncoded(pszAssertionEncoded);
    CPLFree(pszAssertionEncoded);
    // Required in addition to URL escaping otherwise is considered to be space
    osAssertionEncoded.replaceAll("+", "%2B");
    osPostData += osAssertionEncoded;

    char **papszHTTPOptions = nullptr;
    papszHTTPOptions =
        CSLSetNameValue(papszHTTPOptions, "POSTFIELDS", osPostData);
    CPLHTTPResult *psResult = CPLHTTPFetch(pszAud, papszHTTPOptions);
    CSLDestroy(papszHTTPOptions);

    return GOA2ProcessResponse(psResult);
}

/************************************************************************/
/*                              GOA2Manager()                           */
/************************************************************************/

/** Constructor */
GOA2Manager::GOA2Manager() = default;

/************************************************************************/
/*                         SetAuthFromGCE()                             */
/************************************************************************/

/** Specifies that the authentication will be done using the local
 * credentials of the current Google Compute Engine VM
 *
 * This queries
 * http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
 *
 * @param papszOptions NULL terminated list of options.
 * @return true in case of success (no network access is done at this stage)
 */
bool GOA2Manager::SetAuthFromGCE(CSLConstList papszOptions)
{
    m_eMethod = GCE;
    m_aosOptions = papszOptions;
    return true;
}

/************************************************************************/
/*                       SetAuthFromRefreshToken()                      */
/************************************************************************/

/** Specifies that the authentication will be done using the OAuth2 client
 * id method.
 *
 * See http://code.google.com/apis/accounts/docs/OAuth2.html
 *
 * @param pszRefreshToken refresh token. Must be non NULL.
 * @param pszClientId client id (may be NULL, in which case the GOA2_CLIENT_ID
 *                    configuration option is used)
 * @param pszClientSecret client secret (may be NULL, in which case the
 *                        GOA2_CLIENT_SECRET configuration option is used)
 * @param papszOptions NULL terminated list of options, or NULL.
 * @return true in case of success (no network access is done at this stage)
 */
bool GOA2Manager::SetAuthFromRefreshToken(const char *pszRefreshToken,
                                          const char *pszClientId,
                                          const char *pszClientSecret,
                                          CSLConstList papszOptions)
{
    if (pszRefreshToken == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Refresh token should be set");
        return false;
    }
    m_eMethod = ACCESS_TOKEN_FROM_REFRESH;
    m_osRefreshToken = pszRefreshToken;
    m_osClientId = pszClientId ? pszClientId : "";
    m_osClientSecret = pszClientSecret ? pszClientSecret : "";
    m_aosOptions = papszOptions;
    return true;
}

/************************************************************************/
/*                      SetAuthFromServiceAccount()                     */
/************************************************************************/

/** Specifies that the authentication will be done using the OAuth2 service
 * account method.
 *
 * See https://developers.google.com/identity/protocols/OAuth2ServiceAccount
 *
 * @param pszPrivateKey RSA private key. Must be non NULL.
 * @param pszClientEmail client email. Must be non NULL.
 * @param pszScope authorization scope. Must be non NULL.
 * @param papszAdditionalClaims NULL terminate list of additional claims, or
 * NULL.
 * @param papszOptions NULL terminated list of options, or NULL.
 * @return true in case of success (no network access is done at this stage)
 */
bool GOA2Manager::SetAuthFromServiceAccount(const char *pszPrivateKey,
                                            const char *pszClientEmail,
                                            const char *pszScope,
                                            CSLConstList papszAdditionalClaims,
                                            CSLConstList papszOptions)
{
    if (pszPrivateKey == nullptr || EQUAL(pszPrivateKey, ""))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Private key should be set");
        return false;
    }
    if (pszClientEmail == nullptr || EQUAL(pszClientEmail, ""))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Client email should be set");
        return false;
    }
    if (pszScope == nullptr || EQUAL(pszScope, ""))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Scope should be set");
        return false;
    }
    m_eMethod = SERVICE_ACCOUNT;
    m_osPrivateKey = pszPrivateKey;
    m_osClientEmail = pszClientEmail;
    m_osScope = pszScope;
    m_aosAdditionalClaims = papszAdditionalClaims;
    m_aosOptions = papszOptions;
    return true;
}

/************************************************************************/
/*                             GetBearer()                              */
/************************************************************************/

/** Return the access token.
 *
 * This is the value to append to a "Authorization: Bearer " HTTP header.
 *
 * A network request is issued only if no access token has been yet queried,
 * or if its expiration delay has been reached.
 *
 * @return the access token, or NULL in case of error.
 */
const char *GOA2Manager::GetBearer() const
{
    time_t nCurTime = time(nullptr);
    if (nCurTime < m_nExpirationTime - 5)
        return m_osCurrentBearer.c_str();

    char **papszRet = nullptr;
    if (m_eMethod == GCE)
    {
        papszRet = GOA2GetAccessTokenFromCloudEngineVM(m_aosOptions.List());
    }
    else if (m_eMethod == ACCESS_TOKEN_FROM_REFRESH)
    {
        papszRet =
            GOA2GetAccessTokenEx(m_osRefreshToken.c_str(), m_osClientId.c_str(),
                                 m_osClientSecret.c_str(), m_aosOptions.List());
    }
    else if (m_eMethod == SERVICE_ACCOUNT)
    {
        papszRet = GOA2GetAccessTokenFromServiceAccount(
            m_osPrivateKey, m_osClientEmail, m_osScope,
            m_aosAdditionalClaims.List(), m_aosOptions.List());
    }

    m_nExpirationTime = 0;
    m_osCurrentBearer.clear();
    const char *pszAccessToken = CSLFetchNameValue(papszRet, "access_token");
    if (pszAccessToken == nullptr)
    {
        CSLDestroy(papszRet);
        return nullptr;
    }
    const char *pszExpires = CSLFetchNameValue(papszRet, "expires_in");
    if (pszExpires)
    {
        m_nExpirationTime = nCurTime + atoi(pszExpires);
    }
    m_osCurrentBearer = pszAccessToken;
    CSLDestroy(papszRet);
    return m_osCurrentBearer.c_str();
}
