-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Move Google Cloud specific stuff to
gcp
namespace (#2)
- Loading branch information
Showing
12 changed files
with
267 additions
and
270 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/* SPDX-FileCopyrightText: 2020-present Kriasoft */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import QuickLRU from "quick-lru"; | ||
import { Credentials, getCredentials } from "./credentials.js"; | ||
import { importKey, sign } from "./crypto.js"; | ||
|
||
type AuthTokenOptions<T extends "accessToken" | "idToken"> = | ||
T extends "accessToken" | ||
? { | ||
credentials: Credentials | string; | ||
scope: string[] | string; | ||
audience?: never; | ||
} | ||
: { | ||
credentials: Credentials | string; | ||
audience: string; | ||
scope?: never; | ||
}; | ||
|
||
type AccessToken = { | ||
accessToken: string; | ||
type: string; | ||
scope: string; | ||
expires: number; | ||
}; | ||
|
||
type IdToken = { | ||
idToken: string; | ||
audience: string; | ||
expires: number; | ||
}; | ||
|
||
type AuthError = { | ||
error: string; | ||
error_description: string; | ||
}; | ||
|
||
const cache = new QuickLRU<symbol, any>({ | ||
maxSize: 100, | ||
maxAge: 3600000 - 10000, | ||
}); | ||
|
||
/** | ||
* Retrieves an authentication token from OAuth 2.0 authorization server. | ||
* | ||
* @example | ||
* const token = await getAuthToken({ | ||
* credentials: env.GOOGLE_CLOUD_CREDENTIALS, | ||
* scope: "https://www.googleapis.com/auth/cloud-platform" | ||
* ); | ||
* const headers = { Authorization: `Bearer ${token.accessToken}` }; | ||
* const res = await fetch(url, { headers }); | ||
*/ | ||
async function getAuthToken< | ||
T extends AuthTokenOptions<"accessToken"> | AuthTokenOptions<"idToken"> | ||
>( | ||
options: T | ||
): Promise<T extends AuthTokenOptions<"accessToken"> ? AccessToken : IdToken> { | ||
// Normalize input arguments | ||
const credentials = getCredentials(options.credentials); | ||
const scope = | ||
"scope" in options && Array.isArray(options.scope) | ||
? options.scope.sort().join(" ") | ||
: (options.scope as string | undefined) ?? options.audience; | ||
|
||
// Attempt to retrieve the token from the cache | ||
const keyId = credentials?.private_key_id ?? credentials.client_email; | ||
const cacheKey = Symbol.for(`${keyId}:${scope}`); | ||
let token = cache.get(cacheKey); | ||
|
||
if (!token) { | ||
token = fetchAuthToken(credentials, scope); | ||
cache.set(cacheKey, token); | ||
} | ||
|
||
return token; | ||
} | ||
|
||
export async function fetchAuthToken( | ||
credentials: Credentials, | ||
scope: string | undefined | ||
): Promise<AccessToken | IdToken> { | ||
// JWT token header: {"alg":"RS256","typ":"JWT"} | ||
const header = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9`; | ||
|
||
// JWT token attributes | ||
const iss = credentials.client_email; | ||
const aud = credentials.token_uri; | ||
const iat = Math.floor(Date.now() / 1000); | ||
const exp = iat + 3600; // 1 hour max | ||
|
||
// JWT token payload | ||
const payload = self | ||
.btoa(JSON.stringify({ iss, aud, scope, exp, iat })) | ||
.replace(/=/g, "") | ||
.replace(/\+/g, "-") | ||
.replace(/\//g, "_"); | ||
|
||
// JWT token signature | ||
const signingKey = await importKey(credentials.private_key, ["sign"]); | ||
const signature = await sign(signingKey, `${header}.${payload}`); | ||
|
||
// OAuth 2.0 authorization request | ||
const body = new FormData(); | ||
body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); | ||
body.append("assertion", `${header}.${payload}.${signature}`); | ||
const res = await fetch(credentials.token_uri, { method: "POST", body }); | ||
|
||
if (!res.ok) { | ||
const data = await res.json<AuthError>(); | ||
throw new Error(data.error_description ?? data.error); | ||
} | ||
|
||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ | ||
const data = await res.json<any>(); | ||
return data.access_token | ||
? ({ | ||
accessToken: data.access_token.replace(/\.+$/, ""), | ||
type: data.token_type, | ||
scope, | ||
expires: exp, | ||
} as AccessToken) | ||
: ({ | ||
idToken: data.id_token?.replace(/\.+$/, ""), | ||
audience: scope, | ||
expires: exp, | ||
} as IdToken); | ||
} | ||
|
||
export { type AccessToken, type IdToken, getAuthToken }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* SPDX-FileCopyrightText: 2022-present Kriasoft */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import QuickLRU from "quick-lru"; | ||
|
||
const cache = new QuickLRU<symbol, Credentials>({ maxSize: 100 }); | ||
|
||
/** | ||
* Service account key for Google Cloud Platform (GCP) | ||
* https://cloud.google.com/iam/docs/creating-managing-service-account-keys | ||
*/ | ||
export type Credentials = { | ||
type: string; | ||
project_id: string; | ||
private_key_id: string; | ||
private_key: string; | ||
client_id: string; | ||
client_email: string; | ||
auth_uri: string; | ||
token_uri: string; | ||
auth_provider_x509_cert_url: string; | ||
client_x509_cert_url: string; | ||
}; | ||
|
||
export function getCredentials(value: Credentials | string): Credentials { | ||
if (typeof value === "string") { | ||
const cacheKey = Symbol.for(value); | ||
let credentials = cache.get(cacheKey); | ||
|
||
if (!credentials) { | ||
credentials = JSON.parse(value) as Credentials; | ||
cache.set(cacheKey, credentials); | ||
} | ||
|
||
return credentials; | ||
} | ||
|
||
return value; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/* SPDX-FileCopyrightText: 2022-present Kriasoft */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import { base64, base64url } from "rfc4648"; | ||
|
||
type KeyUsage = | ||
| "encrypt" | ||
| "decrypt" | ||
| "sign" | ||
| "verify" | ||
| "deriveKey" | ||
| "deriveBits" | ||
| "wrapKey" | ||
| "unwrapKey"; | ||
|
||
/** | ||
* Returns a `CryptoKey` object that you can use in the `Web Crypto API`. | ||
* https://developer.mozilla.org/docs/Web/API/SubtleCrypto | ||
* | ||
* @example | ||
* const signingKey = await importKey( | ||
* env.GOOGLE_CLOUD_CREDENTIALS.private_key, | ||
* ["sign"], | ||
* ); | ||
*/ | ||
function importKey(keyData: string, keyUsages: KeyUsage[]): Promise<CryptoKey> { | ||
return crypto.subtle.importKey( | ||
"pkcs8", | ||
base64.parse( | ||
keyData | ||
.replace("-----BEGIN PRIVATE KEY-----", "") | ||
.replace("-----END PRIVATE KEY-----", "") | ||
.replace(/\n/g, "") | ||
), | ||
{ | ||
name: "RSASSA-PKCS1-V1_5", | ||
hash: { name: "SHA-256" }, | ||
}, | ||
false, | ||
keyUsages | ||
); | ||
} | ||
|
||
/** | ||
* Generates a digital signature. | ||
*/ | ||
async function sign(key: CryptoKey, data: string): Promise<string> { | ||
const input = new TextEncoder().encode(data); | ||
const output = await self.crypto.subtle.sign(key.algorithm, key, input); | ||
return base64url.stringify(new Uint8Array(output), { pad: false }); | ||
} | ||
|
||
export { sign, importKey, KeyUsage }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/* SPDX-FileCopyrightText: 2022-present Kriasoft */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
export { AccessToken, getAuthToken, IdToken } from "./auth.js"; | ||
export { Credentials, getCredentials } from "./credentials.js"; | ||
export { importKey, KeyUsage, sign } from "./crypto.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"name": "web-auth-library", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"packageManager": "[email protected]", | ||
"description": "Authentication library for the browser environment using Web Crypto API", | ||
"license": "MIT", | ||
|
@@ -35,26 +35,15 @@ | |
], | ||
"type": "module", | ||
"exports": { | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.js" | ||
}, | ||
"./auth": { | ||
"types": "./dist/auth.d.ts", | ||
"import": "./dist/auth.js" | ||
}, | ||
"./credentials": { | ||
"types": "./dist/credentials.d.ts", | ||
"import": "./dist/credentials.js" | ||
}, | ||
"./crypto": { | ||
"types": "./dist/crypto.d.ts", | ||
"import": "./dist/crypto.js" | ||
} | ||
"./gcp": "./dist/gcp/index.js", | ||
"./gcp/auth": "./dist/gcp/auth.js", | ||
"./gcp/credentials": "./dist/gcp/credentials.js", | ||
"./gcp/crypto": "./dist/gcp/crypto.js", | ||
"./package.json": "./package.json" | ||
}, | ||
"dependencies": { | ||
"quick-lru": "^6.1.1", | ||
"rfc4648": "^1.5.1" | ||
"rfc4648": "^1.5.2" | ||
}, | ||
"devDependencies": { | ||
"@cloudflare/workers-types": "^3.11.0", | ||
|
Oops, something went wrong.