UNPKG

12.4 kBJavaScriptView Raw
1"use strict";
2// Copyright 2021 Google LLC
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15Object.defineProperty(exports, "__esModule", { value: true });
16exports.DownscopedClient = exports.EXPIRATION_TIME_OFFSET = exports.MAX_ACCESS_BOUNDARY_RULES_COUNT = void 0;
17const stream = require("stream");
18const authclient_1 = require("./authclient");
19const sts = require("./stscredentials");
20/**
21 * The required token exchange grant_type: rfc8693#section-2.1
22 */
23const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange';
24/**
25 * The requested token exchange requested_token_type: rfc8693#section-2.1
26 */
27const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
28/**
29 * The requested token exchange subject_token_type: rfc8693#section-2.1
30 */
31const STS_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
32/**
33 * The maximum number of access boundary rules a Credential Access Boundary
34 * can contain.
35 */
36exports.MAX_ACCESS_BOUNDARY_RULES_COUNT = 10;
37/**
38 * Offset to take into account network delays and server clock skews.
39 */
40exports.EXPIRATION_TIME_OFFSET = 5 * 60 * 1000;
41/**
42 * Defines a set of Google credentials that are downscoped from an existing set
43 * of Google OAuth2 credentials. This is useful to restrict the Identity and
44 * Access Management (IAM) permissions that a short-lived credential can use.
45 * The common pattern of usage is to have a token broker with elevated access
46 * generate these downscoped credentials from higher access source credentials
47 * and pass the downscoped short-lived access tokens to a token consumer via
48 * some secure authenticated channel for limited access to Google Cloud Storage
49 * resources.
50 */
51class DownscopedClient extends authclient_1.AuthClient {
52 /**
53 * Instantiates a downscoped client object using the provided source
54 * AuthClient and credential access boundary rules.
55 * To downscope permissions of a source AuthClient, a Credential Access
56 * Boundary that specifies which resources the new credential can access, as
57 * well as an upper bound on the permissions that are available on each
58 * resource, has to be defined. A downscoped client can then be instantiated
59 * using the source AuthClient and the Credential Access Boundary.
60 * @param authClient The source AuthClient to be downscoped based on the
61 * provided Credential Access Boundary rules.
62 * @param credentialAccessBoundary The Credential Access Boundary which
63 * contains a list of access boundary rules. Each rule contains information
64 * on the resource that the rule applies to, the upper bound of the
65 * permissions that are available on that resource and an optional
66 * condition to further restrict permissions.
67 * @param additionalOptions **DEPRECATED, set this in the provided `authClient`.**
68 * Optional additional behavior customization options.
69 * @param quotaProjectId **DEPRECATED, set this in the provided `authClient`.**
70 * Optional quota project id for setting up in the x-goog-user-project header.
71 */
72 constructor(authClient, credentialAccessBoundary, additionalOptions, quotaProjectId) {
73 super({ ...additionalOptions, quotaProjectId });
74 this.authClient = authClient;
75 this.credentialAccessBoundary = credentialAccessBoundary;
76 // Check 1-10 Access Boundary Rules are defined within Credential Access
77 // Boundary.
78 if (credentialAccessBoundary.accessBoundary.accessBoundaryRules.length === 0) {
79 throw new Error('At least one access boundary rule needs to be defined.');
80 }
81 else if (credentialAccessBoundary.accessBoundary.accessBoundaryRules.length >
82 exports.MAX_ACCESS_BOUNDARY_RULES_COUNT) {
83 throw new Error('The provided access boundary has more than ' +
84 `${exports.MAX_ACCESS_BOUNDARY_RULES_COUNT} access boundary rules.`);
85 }
86 // Check at least one permission should be defined in each Access Boundary
87 // Rule.
88 for (const rule of credentialAccessBoundary.accessBoundary
89 .accessBoundaryRules) {
90 if (rule.availablePermissions.length === 0) {
91 throw new Error('At least one permission should be defined in access boundary rules.');
92 }
93 }
94 this.stsCredential = new sts.StsCredentials(`https://sts.${this.universeDomain}/v1/token`);
95 this.cachedDownscopedAccessToken = null;
96 }
97 /**
98 * Provides a mechanism to inject Downscoped access tokens directly.
99 * The expiry_date field is required to facilitate determination of the token
100 * expiration which would make it easier for the token consumer to handle.
101 * @param credentials The Credentials object to set on the current client.
102 */
103 setCredentials(credentials) {
104 if (!credentials.expiry_date) {
105 throw new Error('The access token expiry_date field is missing in the provided ' +
106 'credentials.');
107 }
108 super.setCredentials(credentials);
109 this.cachedDownscopedAccessToken = credentials;
110 }
111 async getAccessToken() {
112 // If the cached access token is unavailable or expired, force refresh.
113 // The Downscoped access token will be returned in
114 // DownscopedAccessTokenResponse format.
115 if (!this.cachedDownscopedAccessToken ||
116 this.isExpired(this.cachedDownscopedAccessToken)) {
117 await this.refreshAccessTokenAsync();
118 }
119 // Return Downscoped access token in DownscopedAccessTokenResponse format.
120 return {
121 token: this.cachedDownscopedAccessToken.access_token,
122 expirationTime: this.cachedDownscopedAccessToken.expiry_date,
123 res: this.cachedDownscopedAccessToken.res,
124 };
125 }
126 /**
127 * The main authentication interface. It takes an optional url which when
128 * present is the endpoint being accessed, and returns a Promise which
129 * resolves with authorization header fields.
130 *
131 * The result has the form:
132 * { Authorization: 'Bearer <access_token_value>' }
133 */
134 async getRequestHeaders() {
135 const accessTokenResponse = await this.getAccessToken();
136 const headers = {
137 Authorization: `Bearer ${accessTokenResponse.token}`,
138 };
139 return this.addSharedMetadataHeaders(headers);
140 }
141 request(opts, callback) {
142 if (callback) {
143 this.requestAsync(opts).then(r => callback(null, r), e => {
144 return callback(e, e.response);
145 });
146 }
147 else {
148 return this.requestAsync(opts);
149 }
150 }
151 /**
152 * Authenticates the provided HTTP request, processes it and resolves with the
153 * returned response.
154 * @param opts The HTTP request options.
155 * @param reAuthRetried Whether the current attempt is a retry after a failed attempt due to an auth failure
156 * @return A promise that resolves with the successful response.
157 */
158 async requestAsync(opts, reAuthRetried = false) {
159 let response;
160 try {
161 const requestHeaders = await this.getRequestHeaders();
162 opts.headers = opts.headers || {};
163 if (requestHeaders && requestHeaders['x-goog-user-project']) {
164 opts.headers['x-goog-user-project'] =
165 requestHeaders['x-goog-user-project'];
166 }
167 if (requestHeaders && requestHeaders.Authorization) {
168 opts.headers.Authorization = requestHeaders.Authorization;
169 }
170 response = await this.transporter.request(opts);
171 }
172 catch (e) {
173 const res = e.response;
174 if (res) {
175 const statusCode = res.status;
176 // Retry the request for metadata if the following criteria are true:
177 // - We haven't already retried. It only makes sense to retry once.
178 // - The response was a 401 or a 403
179 // - The request didn't send a readableStream
180 // - forceRefreshOnFailure is true
181 const isReadableStream = res.config.data instanceof stream.Readable;
182 const isAuthErr = statusCode === 401 || statusCode === 403;
183 if (!reAuthRetried &&
184 isAuthErr &&
185 !isReadableStream &&
186 this.forceRefreshOnFailure) {
187 await this.refreshAccessTokenAsync();
188 return await this.requestAsync(opts, true);
189 }
190 }
191 throw e;
192 }
193 return response;
194 }
195 /**
196 * Forces token refresh, even if unexpired tokens are currently cached.
197 * GCP access tokens are retrieved from authclient object/source credential.
198 * Then GCP access tokens are exchanged for downscoped access tokens via the
199 * token exchange endpoint.
200 * @return A promise that resolves with the fresh downscoped access token.
201 */
202 async refreshAccessTokenAsync() {
203 var _a;
204 // Retrieve GCP access token from source credential.
205 const subjectToken = (await this.authClient.getAccessToken()).token;
206 // Construct the STS credentials options.
207 const stsCredentialsOptions = {
208 grantType: STS_GRANT_TYPE,
209 requestedTokenType: STS_REQUEST_TOKEN_TYPE,
210 subjectToken: subjectToken,
211 subjectTokenType: STS_SUBJECT_TOKEN_TYPE,
212 };
213 // Exchange the source AuthClient access token for a Downscoped access
214 // token.
215 const stsResponse = await this.stsCredential.exchangeToken(stsCredentialsOptions, undefined, this.credentialAccessBoundary);
216 /**
217 * The STS endpoint will only return the expiration time for the downscoped
218 * access token if the original access token represents a service account.
219 * The downscoped token's expiration time will always match the source
220 * credential expiration. When no expires_in is returned, we can copy the
221 * source credential's expiration time.
222 */
223 const sourceCredExpireDate = ((_a = this.authClient.credentials) === null || _a === void 0 ? void 0 : _a.expiry_date) || null;
224 const expiryDate = stsResponse.expires_in
225 ? new Date().getTime() + stsResponse.expires_in * 1000
226 : sourceCredExpireDate;
227 // Save response in cached access token.
228 this.cachedDownscopedAccessToken = {
229 access_token: stsResponse.access_token,
230 expiry_date: expiryDate,
231 res: stsResponse.res,
232 };
233 // Save credentials.
234 this.credentials = {};
235 Object.assign(this.credentials, this.cachedDownscopedAccessToken);
236 delete this.credentials.res;
237 // Trigger tokens event to notify external listeners.
238 this.emit('tokens', {
239 refresh_token: null,
240 expiry_date: this.cachedDownscopedAccessToken.expiry_date,
241 access_token: this.cachedDownscopedAccessToken.access_token,
242 token_type: 'Bearer',
243 id_token: null,
244 });
245 // Return the cached access token.
246 return this.cachedDownscopedAccessToken;
247 }
248 /**
249 * Returns whether the provided credentials are expired or not.
250 * If there is no expiry time, assumes the token is not expired or expiring.
251 * @param downscopedAccessToken The credentials to check for expiration.
252 * @return Whether the credentials are expired or not.
253 */
254 isExpired(downscopedAccessToken) {
255 const now = new Date().getTime();
256 return downscopedAccessToken.expiry_date
257 ? now >=
258 downscopedAccessToken.expiry_date - this.eagerRefreshThresholdMillis
259 : false;
260 }
261}
262exports.DownscopedClient = DownscopedClient;