UNPKG

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