UNPKG

@atproto/oauth-client

Version:

OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).

116 lines (99 loc) 3.52 kB
import { Key } from '@atproto/jwk' import { base64url } from 'multiformats/bases/base64' import { requestLocalLock } from './lock.js' import { RuntimeImplementation, RuntimeLock } from './runtime-implementation.js' export class Runtime { readonly hasImplementationLock: boolean readonly usingLock: RuntimeLock constructor(protected implementation: RuntimeImplementation) { const { requestLock } = implementation this.hasImplementationLock = requestLock != null this.usingLock = requestLock?.bind(implementation) || // Falling back to a local lock requestLocalLock } public async generateKey(algs: string[]): Promise<Key> { const algsSorted = Array.from(algs).sort(compareAlgos) return this.implementation.createKey(algsSorted) } public async sha256(text: string): Promise<string> { const bytes = new TextEncoder().encode(text) const digest = await this.implementation.digest(bytes, { name: 'sha256' }) return base64url.baseEncode(digest) } public async generateNonce(length = 16): Promise<string> { const bytes = await this.implementation.getRandomValues(length) return base64url.baseEncode(bytes) } public async generatePKCE(byteLength?: number) { const verifier = await this.generateVerifier(byteLength) return { verifier, challenge: await this.sha256(verifier), method: 'S256' as const, } } public async calculateJwkThumbprint(jwk) { const components = extractJktComponents(jwk) const data = JSON.stringify(components) return this.sha256(data) } /** * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1} * @note It is RECOMMENDED that the output of a suitable random number generator * be used to create a 32-octet sequence. The octet sequence is then * base64url-encoded to produce a 43-octet URL safe string to use as the code * verifier. */ protected async generateVerifier(byteLength = 32) { if (byteLength < 32 || byteLength > 96) { throw new TypeError('Invalid code_verifier length') } const bytes = await this.implementation.getRandomValues(byteLength) return base64url.baseEncode(bytes) } } function extractJktComponents(jwk) { const get = (field) => { const value = jwk[field] if (typeof value !== 'string' || !value) { throw new TypeError(`"${field}" Parameter missing or invalid`) } return value } switch (jwk.kty) { case 'EC': return { crv: get('crv'), kty: get('kty'), x: get('x'), y: get('y') } case 'OKP': return { crv: get('crv'), kty: get('kty'), x: get('x') } case 'RSA': return { e: get('e'), kty: get('kty'), n: get('n') } case 'oct': return { k: get('k'), kty: get('kty') } default: throw new TypeError('"kty" (Key Type) Parameter missing or unsupported') } } /** * 256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other (in original order) */ function compareAlgos(a: string, b: string): number { if (a === 'ES256K') return -1 if (b === 'ES256K') return 1 for (const prefix of ['ES', 'PS', 'RS']) { if (a.startsWith(prefix)) { if (b.startsWith(prefix)) { const aLen = parseInt(a.slice(2, 5)) const bLen = parseInt(b.slice(2, 5)) // Prefer shorter key lengths return aLen - bLen } return -1 } else if (b.startsWith(prefix)) { return 1 } } // Don't know how to compare, keep original order return 0 }