"use strict";
// Copyright 2021 Google LLC
//
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.DownscopedClient = exports.EXPIRATION_TIME_OFFSET = exports.MAX_ACCESS_BOUNDARY_RULES_COUNT = void 0;
const gaxios_1 = require("gaxios");
const stream = require("stream");
const authclient_1 = require("./authclient");
const sts = require("./stscredentials");
/**
 * The required token exchange grant_type: rfc8693#section-2.1
 */
const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange';
/**
 * The requested token exchange requested_token_type: rfc8693#section-2.1
 */
const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
/**
 * The requested token exchange subject_token_type: rfc8693#section-2.1
 */
const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
/**
 * The maximum number of access boundary rules a Credential Access Boundary
 * can contain.
 */
exports.MAX_ACCESS_BOUNDARY_RULES_COUNT = 10;
/**
 * Offset to take into account network delays and server clock skews.
 */
exports.EXPIRATION_TIME_OFFSET = 5 * 60 * 1000;
/**
 * Defines a set of Google credentials that are downscoped from an existing set
 * of Google OAuth2 credentials. This is useful to restrict the Identity and
 * Access Management (IAM) permissions that a short-lived credential can use.
 * The common pattern of usage is to have a token broker with elevated access
 * generate these downscoped credentials from higher access source credentials
 * and pass the downscoped short-lived access tokens to a token consumer via
 * some secure authenticated channel for limited access to Google Cloud Storage
 * resources.
 */
class DownscopedClient extends authclient_1.AuthClient {
    authClient;
    credentialAccessBoundary;
    cachedDownscopedAccessToken;
    stsCredential;
    /**
     * Instantiates a downscoped client object using the provided source
     * AuthClient and credential access boundary rules.
     * To downscope permissions of a source AuthClient, a Credential Access
     * Boundary that specifies which resources the new credential can access, as
     * well as an upper bound on the permissions that are available on each
     * resource, has to be defined. A downscoped client can then be instantiated
     * using the source AuthClient and the Credential Access Boundary.
     * @param options the {@link DownscopedClientOptions `DownscopedClientOptions`} to use. Passing an `AuthClient` directly is **@DEPRECATED**.
     * @param credentialAccessBoundary **@DEPRECATED**. Provide a {@link DownscopedClientOptions `DownscopedClientOptions`} object in the first parameter instead.
     */
    constructor(
    /**
     * AuthClient is for backwards-compatibility.
     */
    options, 
    /**
     * @deprecated - provide a {@link DownscopedClientOptions `DownscopedClientOptions`} object in the first parameter instead
     */
    credentialAccessBoundary = {
        accessBoundary: {
            accessBoundaryRules: [],
        },
    }) {
        super(options instanceof authclient_1.AuthClient ? {} : options);
        if (options instanceof authclient_1.AuthClient) {
            this.authClient = options;
            this.credentialAccessBoundary = credentialAccessBoundary;
        }
        else {
            this.authClient = options.authClient;
            this.credentialAccessBoundary = options.credentialAccessBoundary;
        }
        // Check 1-10 Access Boundary Rules are defined within Credential Access
        // Boundary.
        if (this.credentialAccessBoundary.accessBoundary.accessBoundaryRules
            .length === 0) {
            throw new Error('At least one access boundary rule needs to be defined.');
        }
        else if (this.credentialAccessBoundary.accessBoundary.accessBoundaryRules.length >
            exports.MAX_ACCESS_BOUNDARY_RULES_COUNT) {
            throw new Error('The provided access boundary has more than ' +
                `${exports.MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.`);
        }
        // Check at least one permission should be defined in each Access Boundary
        // Rule.
        for (const rule of this.credentialAccessBoundary.accessBoundary
            .accessBoundaryRules) {
            if (rule.availablePermissions.length === 0) {
                throw new Error('At least one permission should be defined in access boundary rules.');
            }
        }
        this.stsCredential = new sts.StsCredentials({
            tokenExchangeEndpoint: `https://sts.${this.universeDomain}/v1/token`,
        });
        this.cachedDownscopedAccessToken = null;
    }
    /**
     * Provides a mechanism to inject Downscoped access tokens directly.
     * The expiry_date field is required to facilitate determination of the token
     * expiration which would make it easier for the token consumer to handle.
     * @param credentials The Credentials object to set on the current client.
     */
    setCredentials(credentials) {
        if (!credentials.expiry_date) {
            throw new Error('The access token expiry_date field is missing in the provided ' +
                'credentials.');
        }
        super.setCredentials(credentials);
        this.cachedDownscopedAccessToken = credentials;
    }
    async getAccessToken() {
        // If the cached access token is unavailable or expired, force refresh.
        // The Downscoped access token will be returned in
        // DownscopedAccessTokenResponse format.
        if (!this.cachedDownscopedAccessToken ||
            this.isExpired(this.cachedDownscopedAccessToken)) {
            await this.refreshAccessTokenAsync();
        }
        // Return Downscoped access token in DownscopedAccessTokenResponse format.
        return {
            token: this.cachedDownscopedAccessToken.access_token,
            expirationTime: this.cachedDownscopedAccessToken.expiry_date,
            res: this.cachedDownscopedAccessToken.res,
        };
    }
    /**
     * The main authentication interface. It takes an optional url which when
     * present is the endpoint being accessed, and returns a Promise which
     * resolves with authorization header fields.
     *
     * The result has the form:
     * { authorization: 'Bearer <access_token_value>' }
     */
    async getRequestHeaders() {
        const accessTokenResponse = await this.getAccessToken();
        const headers = new Headers({
            authorization: `Bearer ${accessTokenResponse.token}`,
        });
        return this.addSharedMetadataHeaders(headers);
    }
    request(opts, callback) {
        if (callback) {
            this.requestAsync(opts).then(r => callback(null, r), e => {
                return callback(e, e.response);
            });
        }
        else {
            return this.requestAsync(opts);
        }
    }
    /**
     * Authenticates the provided HTTP request, processes it and resolves with the
     * returned response.
     * @param opts The HTTP request options.
     * @param reAuthRetried Whether the current attempt is a retry after a failed attempt due to an auth failure
     * @return A promise that resolves with the successful response.
     */
    async requestAsync(opts, reAuthRetried = false) {
        let response;
        try {
            const requestHeaders = await this.getRequestHeaders();
            opts.headers = gaxios_1.Gaxios.mergeHeaders(opts.headers);
            this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders);
            response = await this.transporter.request(opts);
        }
        catch (e) {
            const res = e.response;
            if (res) {
                const statusCode = res.status;
                // Retry the request for metadata if the following criteria are true:
                // - We haven't already retried.  It only makes sense to retry once.
                // - The response was a 401 or a 403
                // - The request didn't send a readableStream
                // - forceRefreshOnFailure is true
                const isReadableStream = res.config.data instanceof stream.Readable;
                const isAuthErr = statusCode === 401 || statusCode === 403;
                if (!reAuthRetried &&
                    isAuthErr &&
                    !isReadableStream &&
                    this.forceRefreshOnFailure) {
                    await this.refreshAccessTokenAsync();
                    return await this.requestAsync(opts, true);
                }
            }
            throw e;
        }
        return response;
    }
    /**
     * Forces token refresh, even if unexpired tokens are currently cached.
     * GCP access tokens are retrieved from authclient object/source credential.
     * Then GCP access tokens are exchanged for downscoped access tokens via the
     * token exchange endpoint.
     * @return A promise that resolves with the fresh downscoped access token.
     */
    async refreshAccessTokenAsync() {
        // Retrieve GCP access token from source credential.
        const subjectToken = (await this.authClient.getAccessToken()).token;
        // Construct the STS credentials options.
        const stsCredentialsOptions = {
            grantType: STS_GRANT_TYPE,
            requestedTokenType: STS_REQUEST_TOKEN_TYPE,
            subjectToken: subjectToken,
            subjectTokenType: STS_SUBJECT_TOKEN_TYPE,
        };
        // Exchange the source AuthClient access token for a Downscoped access
        // token.
        const stsResponse = await this.stsCredential.exchangeToken(stsCredentialsOptions, undefined, this.credentialAccessBoundary);
        /**
         * The STS endpoint will only return the expiration time for the downscoped
         * access token if the original access token represents a service account.
         * The downscoped token's expiration time will always match the source
         * credential expiration. When no expires_in is returned, we can copy the
         * source credential's expiration time.
         */
        const sourceCredExpireDate = this.authClient.credentials?.expiry_date || null;
        const expiryDate = stsResponse.expires_in
            ? new Date().getTime() + stsResponse.expires_in * 1000
            : sourceCredExpireDate;
        // Save response in cached access token.
        this.cachedDownscopedAccessToken = {
            access_token: stsResponse.access_token,
            expiry_date: expiryDate,
            res: stsResponse.res,
        };
        // Save credentials.
        this.credentials = {};
        Object.assign(this.credentials, this.cachedDownscopedAccessToken);
        delete this.credentials.res;
        // Trigger tokens event to notify external listeners.
        this.emit('tokens', {
            refresh_token: null,
            expiry_date: this.cachedDownscopedAccessToken.expiry_date,
            access_token: this.cachedDownscopedAccessToken.access_token,
            token_type: 'Bearer',
            id_token: null,
        });
        // Return the cached access token.
        return this.cachedDownscopedAccessToken;
    }
    /**
     * Returns whether the provided credentials are expired or not.
     * If there is no expiry time, assumes the token is not expired or expiring.
     * @param downscopedAccessToken The credentials to check for expiration.
     * @return Whether the credentials are expired or not.
     */
    isExpired(downscopedAccessToken) {
        const now = new Date().getTime();
        return downscopedAccessToken.expiry_date
            ? now >=
                downscopedAccessToken.expiry_date - this.eagerRefreshThresholdMillis
            : false;
    }
}
exports.DownscopedClient = DownscopedClient;
//# sourceMappingURL=downscopedclient.js.map