1 | |
2 |
|
3 | const b64url = (bytes: Uint8Array) => |
4 | btoa(String.fromCharCode(...bytes)) |
5 | .replace(/\+/g, "-") |
6 | .replace(/\//g, "_") |
7 | .replace(/=+$/, "") |
8 |
|
9 | |
10 | * Get Access Token (Service Account) |
11 | * Mint a one-hour Admin SDK access token from a service account with domain-wide delegation, impersonating a Workspace admin. Returns `{ token, expires_in }`, which can be passed as the Google Workspace resource of any other script in a flow. The scopes must be authorized for the service account's client ID in Admin console > Security > API controls > Domain-wide delegation. |
12 | */ |
13 | export async function main( |
14 | service_account: RT.GcpServiceAccount, |
15 | admin_email: string, |
16 | scopes: string[] = [ |
17 | "https://www.googleapis.com/auth/admin.directory.user", |
18 | "https://www.googleapis.com/auth/admin.directory.group", |
19 | "https://www.googleapis.com/auth/admin.directory.orgunit", |
20 | ], |
21 | ) { |
22 | const tokenUri = service_account.token_uri || "https://oauth2.googleapis.com/token" |
23 | const iat = Math.floor(Date.now() / 1000) |
24 | const enc = new TextEncoder() |
25 | const unsigned = [ |
26 | { alg: "RS256", typ: "JWT" }, |
27 | { |
28 | iss: service_account.client_email, |
29 | sub: admin_email, |
30 | scope: scopes.join(" "), |
31 | aud: tokenUri, |
32 | iat, |
33 | exp: iat + 3600, |
34 | }, |
35 | ] |
36 | .map((part) => b64url(enc.encode(JSON.stringify(part)))) |
37 | .join(".") |
38 |
|
39 | const pem = service_account.private_key |
40 | .replace(/\\n/g, "\n") |
41 | .replace(/-----[A-Z ]+-----/g, "") |
42 | .replace(/\s/g, "") |
43 | const key = await crypto.subtle.importKey( |
44 | "pkcs8", |
45 | Uint8Array.from(atob(pem), (c) => c.charCodeAt(0)), |
46 | { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, |
47 | false, |
48 | ["sign"], |
49 | ) |
50 | const signature = new Uint8Array( |
51 | await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, enc.encode(unsigned)), |
52 | ) |
53 |
|
54 | const response = await fetch(tokenUri, { |
55 | method: "POST", |
56 | headers: { |
57 | "Content-Type": "application/x-www-form-urlencoded", |
58 | Accept: "application/json", |
59 | }, |
60 | body: new URLSearchParams({ |
61 | grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", |
62 | assertion: `${unsigned}.${b64url(signature)}`, |
63 | }), |
64 | }) |
65 |
|
66 | if (!response.ok) { |
67 | throw new Error(`${response.status} ${await response.text()}`) |
68 | } |
69 |
|
70 | const { access_token, expires_in } = (await response.json()) as { |
71 | access_token: string |
72 | expires_in: number |
73 | } |
74 | return { token: access_token, expires_in } |
75 | } |
76 |
|