-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add options for using cookies and support for new API keys (#381)
- Loading branch information
1 parent
ff3d4a4
commit ed6abf1
Showing
33 changed files
with
3,601 additions
and
1,358 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
Large diffs are not rendered by default.
Oops, something went wrong.
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,110 @@ | ||
import * as crypto from 'crypto'; | ||
import {KeyObject} from 'crypto'; | ||
|
||
export class ApiKey { | ||
private static readonly IDENTIFIER_PATTERN = /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i; | ||
|
||
private static readonly PRIVATE_KEY_PATTERN = /^[a-f0-9]+$/i; | ||
|
||
private readonly identifier: string; | ||
|
||
private readonly privateKey?: KeyObject; | ||
|
||
private constructor(identifier: string, privateKey?: KeyObject) { | ||
this.identifier = identifier; | ||
this.privateKey = privateKey; | ||
} | ||
|
||
public static from(apiKey: string | ApiKey): ApiKey { | ||
if (apiKey instanceof ApiKey) { | ||
return apiKey; | ||
} | ||
|
||
return ApiKey.parse(apiKey); | ||
} | ||
|
||
public static parse(apiKey: string): ApiKey { | ||
const parts = apiKey.split(':'); | ||
|
||
if (parts.length > 2) { | ||
throw new Error('Invalid API key format.'); | ||
} | ||
|
||
return ApiKey.of(parts[0], parts[1]); | ||
} | ||
|
||
public static of(identifier: string, privateKey?: string): ApiKey { | ||
if (!ApiKey.IDENTIFIER_PATTERN.test(identifier)) { | ||
throw new Error('The API key identifier must be a UUID.'); | ||
} | ||
|
||
if (privateKey === undefined) { | ||
return new ApiKey(identifier); | ||
} | ||
|
||
if (!ApiKey.PRIVATE_KEY_PATTERN.test(privateKey)) { | ||
throw new Error('The API key private key must be a hexadecimal string.'); | ||
} | ||
|
||
try { | ||
return new ApiKey(identifier, crypto.createPrivateKey({ | ||
key: Buffer.from(privateKey, 'hex'), | ||
format: 'der', | ||
type: 'pkcs8', | ||
})); | ||
} catch { | ||
throw new Error('Invalid private key.'); | ||
} | ||
} | ||
|
||
public getIdentifier(): string { | ||
return this.identifier; | ||
} | ||
|
||
public async getIdentifierHash(): Promise<string> { | ||
const identifierBytes = Buffer.from(this.identifier.replace(/-/g, ''), 'hex'); | ||
const rawHash = await crypto.subtle.digest('SHA-256', identifierBytes); | ||
|
||
return Buffer.from(rawHash).toString('hex'); | ||
} | ||
|
||
public hasPrivateKey(): boolean { | ||
return this.privateKey !== undefined; | ||
} | ||
|
||
public getPrivateKey(): string|null { | ||
return this.privateKey === undefined | ||
? null | ||
: this.privateKey | ||
.export({format: 'der', type: 'pkcs8'}) | ||
.toString('hex'); | ||
} | ||
|
||
public sign(blob: Buffer): Promise<Buffer> { | ||
const {privateKey} = this; | ||
|
||
if (privateKey === undefined) { | ||
return Promise.reject(new Error('The API key does not have a private key.')); | ||
} | ||
|
||
return new Promise((resolve, reject) => { | ||
crypto.sign(null, blob, privateKey, (error, signature) => { | ||
if (error == null) { | ||
resolve(signature); | ||
} else { | ||
reject(error); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
public export(): string { | ||
const privateKey = this.getPrivateKey(); | ||
|
||
return this.identifier + (privateKey === null ? '' : `:${privateKey}`); | ||
} | ||
|
||
public toString(): string { | ||
return '[redacted]'; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
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,77 @@ | ||
import {Cache} from './cache'; | ||
|
||
export type CookieCacheConfiguration = { | ||
name: string, | ||
secure?: boolean, | ||
maxAge?: number, | ||
domain?: string, | ||
path?: string, | ||
sameSite?: 'strict' | 'lax' | 'none', | ||
}; | ||
|
||
export class CookieCache implements Cache { | ||
private readonly config: CookieCacheConfiguration; | ||
|
||
public constructor(config: CookieCacheConfiguration) { | ||
this.config = config; | ||
} | ||
|
||
public get(): string | null { | ||
const entries = document.cookie.split(';'); | ||
|
||
for (const entry of entries) { | ||
const [name, value] = entry.split('='); | ||
|
||
if (CookieCache.decode(name).trim() === this.config.name) { | ||
return CookieCache.decode(value.trim()); | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public put(value: string): void { | ||
document.cookie = CookieCache.serializeCookie(value, this.config); | ||
} | ||
|
||
public clear(): void { | ||
document.cookie = CookieCache.serializeCookie('', { | ||
...this.config, | ||
maxAge: 0, | ||
}); | ||
} | ||
|
||
private static serializeCookie(value: string, config: CookieCacheConfiguration): string { | ||
const cookie = [`${CookieCache.encode(config.name)}=${CookieCache.encode(value)}`]; | ||
|
||
if (config.maxAge !== undefined) { | ||
cookie.push(`Max-Age=${config.maxAge}`); | ||
} | ||
|
||
if (config.domain !== undefined) { | ||
cookie.push(`Domain=${CookieCache.encode(config.domain)}`); | ||
} | ||
|
||
if (config.path !== undefined) { | ||
cookie.push(`Path=${CookieCache.encode(config.path)}`); | ||
} | ||
|
||
if (config.secure === true) { | ||
cookie.push('Secure'); | ||
} | ||
|
||
if (config.sameSite !== undefined) { | ||
cookie.push(`SameSite=${({strict: 'Strict', lax: 'Lax', none: 'None'})[config.sameSite]}`); | ||
} | ||
|
||
return cookie.join('; '); | ||
} | ||
|
||
private static encode(value: string): string { | ||
return value.replace(/[,; ]+/g, encodeURIComponent); | ||
} | ||
|
||
private static decode(value: string): string { | ||
return decodeURIComponent(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
Oops, something went wrong.