UNPKG

google-auth-library

Version:
273 lines 12.4 kB
"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