1
0
Fork 0
mirror of synced 2026-06-05 12:28:19 +00:00
auth/src/client/service_account_key_json.ts
Seth Vargo 7c4e01fd00
Make auth universe-aware (#352)
This adds support for making the action "universe" aware, so it will be
usable for TPC and GDCH.
2023-11-28 21:59:39 -05:00

147 lines
4.3 KiB
TypeScript

// Copyright 2023 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.
import { createSign } from 'crypto';
import {
errorMessage,
isServiceAccountKey,
parseCredential,
ServiceAccountKey,
toBase64,
writeSecureFile,
} from '@google-github-actions/actions-utils';
import { AuthClient, Client } from './client';
import { Logger } from '../logger';
/**
* ServiceAccountKeyClientParameters is used as input to the
* ServiceAccountKeyClient.
*/
export interface ServiceAccountKeyClientParameters {
readonly logger: Logger;
readonly universe: string;
readonly serviceAccountKey: string;
}
/**
* ServiceAccountKeyClient is an authentication client that expects a Service
* Account Key JSON file.
*/
export class ServiceAccountKeyClient extends Client implements AuthClient {
readonly #serviceAccountKey: ServiceAccountKey;
readonly #audience: string;
constructor(opts: ServiceAccountKeyClientParameters) {
super({
logger: opts.logger,
universe: opts.universe,
child: `ServiceAccountKeyClient`,
});
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
if (!isServiceAccountKey(serviceAccountKey)) {
throw new Error(`Provided credential is not a valid Google Service Account Key JSON`);
}
this.#serviceAccountKey = serviceAccountKey;
this._logger.debug(`Parsed service account key`, serviceAccountKey.client_email);
this.#audience = new URL(this._endpoints.iamcredentials).origin + `/`;
this._logger.debug(`Computed audience`, this.#audience);
}
/**
* getToken generates a self-signed JWT that, by default, is capable of
* calling the iamcredentials API to mint OAuth 2.0 Access Tokens and ID
* Tokens. However, users can theoretically override the audience value and
* use the JWT to call other endpoints without calling iamcredentials.
*/
async getToken(): Promise<string> {
const logger = this._logger.withNamespace('getToken');
const now = Math.floor(new Date().getTime() / 1000);
const claims = {
iss: this.#serviceAccountKey.client_email,
sub: this.#serviceAccountKey.client_email,
aud: this.#audience,
iat: now,
exp: now + 3599,
};
logger.debug(`Built jwt`, {
claims: claims,
});
try {
return await this.signJWT(claims);
} catch (err) {
const msg = errorMessage(err);
throw new Error(
`Failed to sign auth token using ${this.#serviceAccountKey.client_email}: ${msg}`,
);
}
}
/**
* signJWT signs a JWT using the Service Account's private key.
*/
async signJWT(claims: any): Promise<string> {
const logger = this._logger.withNamespace('signJWT');
const header = {
alg: `RS256`,
typ: `JWT`,
kid: this.#serviceAccountKey.private_key_id,
};
const message = toBase64(JSON.stringify(header)) + `.` + toBase64(JSON.stringify(claims));
logger.debug(`Built jwt`, {
header: header,
claims: claims,
message: message,
});
try {
const signer = createSign(`RSA-SHA256`);
signer.write(message);
signer.end();
const signature = signer.sign(this.#serviceAccountKey.private_key);
return message + '.' + toBase64(signature);
} catch (err) {
const msg = errorMessage(err);
throw new Error(
`Failed to sign jwt using private key for ${this.#serviceAccountKey.client_email}: ${msg}`,
);
}
}
/**
* createCredentialsFile writes the Service Account Key JSON back to disk at
* the specified outputPath.
*/
async createCredentialsFile(outputPath: string): Promise<string> {
const logger = this._logger.withNamespace('createCredentialsFile');
logger.debug(`Creating credentials`, {
outputPath: outputPath,
});
return await writeSecureFile(outputPath, JSON.stringify(this.#serviceAccountKey));
}
}