From 61f64e69c6648b9487085ab05b1e3b3c70a2e5bc Mon Sep 17 00:00:00 2001 From: ilkergzlkkr Date: Sat, 5 Apr 2025 15:52:52 +0300 Subject: [PATCH 1/2] webauthn --- bun.lockb | Bin 255216 -> 256800 bytes packages/openauth/package.json | 3 +- packages/openauth/src/provider/webauthn.ts | 327 +++++++++++++++++++++ packages/openauth/src/ui/webauthn.tsx | 194 ++++++++++++ packages/openauth/tsconfig.json | 1 + 5 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 packages/openauth/src/provider/webauthn.ts create mode 100644 packages/openauth/src/ui/webauthn.tsx diff --git a/bun.lockb b/bun.lockb index 7f4b865820733fdfc374b2177bd115d17dacfb6e..18e78c7b2a2272774e8ec7fcdd088713c752dcbd 100755 GIT binary patch delta 41220 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 delta 40701 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 Rd5Jp}>?(>{KiIwbe*kh67eD|2 diff --git a/packages/openauth/package.json b/packages/openauth/package.json index a5e3276b..01b8d93d 100644 --- a/packages/openauth/package.json +++ b/packages/openauth/package.json @@ -32,7 +32,8 @@ }, "peerDependencies": { "arctic": "^2.2.2", - "hono": "^4.0.0" + "hono": "^4.0.0", + "@oslojs/webauthn": "^1.0.0" }, "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", diff --git a/packages/openauth/src/provider/webauthn.ts b/packages/openauth/src/provider/webauthn.ts new file mode 100644 index 00000000..62c29910 --- /dev/null +++ b/packages/openauth/src/provider/webauthn.ts @@ -0,0 +1,327 @@ +/** + * Configures a provider that supports webauthn authentication. This is usually + * paired with the `WebAuthnUI`. + * + * ```ts + * import { WebAuthnUI } from "@openauthjs/openauth/ui/webauthn" + * import { WebAuthnProvider } from "@openauthjs/openauth/provider/webauthn" + * + * export default issuer({ + * passkey: WebAuthnProvider( + * WebAuthnUI({ + * // options returned to the browser for navigator.credentials.get() + * options: { + * userVerification: "required", + * rpId: "myapp.com", // optional, defaults to the domain of the issuer (auth.myapp.com) + * }, + * async getCredential(id) { + * const credential = await authService.getPasskeyCredential(id); + * + * if (!credential) return null; + * return { credential, claims: { userId: credential.userId } }; + * }, + * }), + * ), + * }, + * // ... + * }) + * ``` + * + * Behind the scenes, the `WebAuthnProvider` expects callbacks that implements request handlers + * that generate the UI for the following. + * + * ```ts + * WebAuthnProvider({ + * // ... + * rpId?: string; + * request: (req: Request, state: WebAuthnProviderState, form?: FormData, error?: WebAuthnProviderError) => Promise + * getCredential: (credentialId: Uint8Array) => Promise<{ credential: { id: Uint8Array; algorithm: number; publicKey: Uint8Array }; claims: Claims } | null> + * verifyAuthn?: (data) => Promise + * }) + * ``` + * + * This allows you to create your own UI. + * + * @packageDocumentation + */ + +import type { Provider } from "./provider.js" +import type { Context } from "hono" + +import { generateUnbiasedDigits, timingSafeCompare } from "../random.js" +import { getRelativeUrl } from "../util.js" +import { + decodePKIXECDSASignature, + decodeSEC1PublicKey, + p256, + verifyECDSASignature, +} from "@oslojs/crypto/ecdsa" +import { sha256 } from "@oslojs/crypto/sha2" +import { + ClientDataType, + createAssertionSignatureMessage, + parseAuthenticatorData, + parseClientDataJSON, +} from "@oslojs/webauthn" + +export type WebAuthnProviderConfig< + Claims extends Record = Record, +> = { + /** + * The request handler to generate the UI for the webauthn flow. + * + * Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + * and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) + * ojects. + * + * Also passes in the current `state` of the flow and any `error` that occurred. + * + * Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object + * in return. + */ + request: ( + req: Request, + state: WebAuthnProviderState, + form?: FormData, + error?: WebAuthnProviderError, + ) => Promise + /** + * The relying party ID to use for the webauthn flow. + */ + rpId?: string + /** + * Callback to get credential and claims for the user. + */ + getCredential: (credentialId: Uint8Array) => Promise<{ + credential: { + id: Uint8Array + algorithm: number + publicKey: Uint8Array + } + claims: Claims + } | null> + /** + * Callback to verify the credential for the user. + * + * @example + * ```ts + * { + * async verifyAuthn(data) { + * const clientData = parseClientDataJSON(data.raw.clientDataJSON); + * + * if (clientData.type !== ClientDataType.Get) { + * return { error: "rejected" } + * } + * // ... other checks + * + * // decode & verify signature + * } + * } + * ``` + */ + verifyAuthn?: (data: { + credential: { + id: Uint8Array + algorithm: number // (ES256) + publicKey: Uint8Array + } + claims: Record + raw: { + credentialId: Uint8Array + signature: Uint8Array + authenticatorData: Uint8Array + clientDataJSON: Uint8Array + } + }) => Promise +} + +/** + * The state of the webauthn flow. + * + * | State | Description | + * | ----- | ----------- | + * | `start` | The user is asked to use their credential to start the flow. | + */ +export type WebAuthnProviderState = { type: "start"; challenge: string } + +export type WebAuthnProviderError = + | { error: "invalid_challenge" } + | { error: "invalid_rp_id" } + | { error: "invalid_origin" } + | { error: "invalid_cross_origin" } + | { error: "invalid_signature" } + | { error: "invalid_client_data_type" } + | { error: "credential_not_found" } + | { error: "unresolved" } + | { error: "rejected" } + +function decodeBase64(str: string): Uint8Array { + return new Uint8Array( + atob(str) + .split("") + .map((c) => c.charCodeAt(0)), + ) +} + +export function WebAuthnProvider< + Claims extends Record = Record, +>(config: WebAuthnProviderConfig): Provider<{ claims: Claims }> { + return { + type: "code", + init(routes, ctx) { + async function transition( + c: Context, + next: WebAuthnProviderState, + fd?: FormData, + err?: WebAuthnProviderError, + ) { + await ctx.set(c, "provider", 60 * 60 * 24, next) + const resp = ctx.forward( + c, + await config.request(c.req.raw, next, fd, err), + ) + return resp + } + + async function transitionToStart( + c: Context, + fd?: FormData, + err?: WebAuthnProviderError, + ) { + const challenge = generateUnbiasedDigits(32) + return await transition(c, { type: "start", challenge }, fd, err) + } + + routes.get("/authorize", async (c) => { + return await transitionToStart(c) + }) + + routes.post("/authorize", async (c) => { + const fd = await c.req.formData() + const state = await ctx.get( + c, + "provider", + ) + + const credentialId = fd.get("credentialId")?.toString() + const signature = fd.get("signature")?.toString() + const authenticatorData = fd.get("authData")?.toString() + const clientDataJSON = fd.get("clientDataJSON")?.toString() + + if ( + state?.type !== "start" || + !credentialId || + !signature || + !authenticatorData || + !clientDataJSON + ) { + return await transitionToStart(c, fd, { error: "unresolved" }) + } + + const credId = decodeBase64(credentialId) + const sig = decodeBase64(signature) + const authData = decodeBase64(authenticatorData) + const clientDataJson = decodeBase64(clientDataJSON) + + const clientData = parseClientDataJSON(clientDataJson) + const challenge = new TextDecoder().decode(clientData.challenge) + + if (!timingSafeCompare(state.challenge, challenge)) { + return await transitionToStart(c, fd, { error: "invalid_challenge" }) + } + + const res = await config.getCredential(credId) + + if (!res) { + return await transitionToStart(c, fd, { + error: "credential_not_found", + }) + } + + const url = new URL(getRelativeUrl(c, "/")) + const origin = url.origin + const rpId = config.rpId || url.hostname + + const verifyError = config.verifyAuthn + ? await config.verifyAuthn({ + credential: res.credential, + claims: res.claims, + raw: { + credentialId: credId, + signature: sig, + authenticatorData: authData, + clientDataJSON: clientDataJson, + }, + }) + : verifyWebAuthn({ + rpId, + origin, + credentialPublicKey: res.credential.publicKey, + signature: sig, + authData, + clientDataJSON: clientDataJson, + }) + + if (verifyError) { + return await transitionToStart(c, fd, verifyError) + } + + await ctx.unset(c, "provider") + return ctx.forward(c, await ctx.success(c, res)) + }) + }, + } +} + +/** + * @internal + */ +export type WebAuthnProviderOptions< + Claims extends Record = Record, +> = WebAuthnProviderConfig + +/** + * Default implementation of the `verifyWebAuthn` function. + * This function verifies the webauthn signature and checks if the user is present and verified. + * Reference: https://webauthn.oslojs.dev/examples/authentication + */ +function verifyWebAuthn(input: { + rpId: string + origin: string + credentialPublicKey: Uint8Array + signature: Uint8Array + authData: Uint8Array + clientDataJSON: Uint8Array +}): WebAuthnProviderError | undefined { + const authenticatorData = parseAuthenticatorData(input.authData) + if (!authenticatorData.verifyRelyingPartyIdHash(input.rpId)) { + return { error: "invalid_rp_id" } + } + if (!authenticatorData.userPresent || !authenticatorData.userVerified) { + return { error: "rejected" } + } + + const clientData = parseClientDataJSON(input.clientDataJSON) + if (clientData.type !== ClientDataType.Get) { + return { error: "invalid_client_data_type" } + } + + if (clientData.origin !== input.origin) { + return { error: "invalid_origin" } + } + if (clientData.crossOrigin !== null && clientData.crossOrigin) { + return { error: "invalid_cross_origin" } + } + + // Decode DER-encoded signature + const ecdsaSignature = decodePKIXECDSASignature(input.signature) + const ecdsaPublicKey = decodeSEC1PublicKey(p256, input.credentialPublicKey) + const hash = sha256( + createAssertionSignatureMessage(input.authData, input.clientDataJSON), + ) + const valid = verifyECDSASignature(ecdsaPublicKey, hash, ecdsaSignature) + + if (!valid) { + return { error: "invalid_signature" } + } +} diff --git a/packages/openauth/src/ui/webauthn.tsx b/packages/openauth/src/ui/webauthn.tsx new file mode 100644 index 00000000..d319636d --- /dev/null +++ b/packages/openauth/src/ui/webauthn.tsx @@ -0,0 +1,194 @@ +/** @jsxImportSource hono/jsx */ + +import { Layout } from "./base.js" +import { FormAlert } from "./form.js" + +import type { WebAuthnProviderOptions } from "../provider/webauthn.js" +import { html } from "hono/html" + +const DEFAULT_COPY = { + invalid_challenge: "Invalid Challenge.", + invalid_rp_id: "Invalid RP ID.", + invalid_origin: "Invalid Origin.", + invalid_cross_origin: "Invalid Cross Origin.", + invalid_signature: "Invalid Signature.", + invalid_client_data_type: "Invalid Client Data Type.", + credential_not_found: "Credential not found.", + unresolved: "Unresolved.", + rejected: "Rejected.", + verify: "Verify", + code_info: "Please verify your identity.", +} + +export type WebAuthnUICopy = typeof DEFAULT_COPY + +export type WebAuthnUIOptions = { + /** + * The request options for navigator.credentials.get. + * + * Sent to the client via JSON. + * + * See [`Request Options`](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/parseRequestOptionsFromJSON_static) + */ + options?: Omit + /** + * Custom copy for the UI. + */ + copy?: Partial + /** + * Callback to get the webauthn credential for the user. + */ + getCredential: WebAuthnProviderOptions["getCredential"] + /** + * Callback to verify the credential for the user. + * + * @example + * ```ts + * { + * async verifyAuthn({ + * authenticatorData, + * clientDataJSON, + * credentialId, + * signature, + * }) { + * const clientData = parseClientDataJSON(clientDataJSON); + * + * if (clientData.type !== ClientDataType.Get) { + * return { error: "rejected" } + * } + * // ... other checks + * // checkout oslo/webauth + * + * const credential = await getCredentialFromDB(credentialId) + * + * // decode & verify signature + * + * return { claims: { userId: credential.userId } } + * } + * } + * ``` + */ + verifyAuthn?: WebAuthnProviderOptions["verifyAuthn"] +} + +function stringToBase64Url(from: string): string { + return btoa(from).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") +} + +export function WebAuthnUI(props: WebAuthnUIOptions): WebAuthnProviderOptions { + const copy = { + ...DEFAULT_COPY, + ...props.copy, + } + + return { + rpId: props.options?.rpId, + getCredential: props.getCredential, + verifyAuthn: props.verifyAuthn, + request: async (req, state, _form, error): Promise => { + const options = JSON.stringify({ + ...(props.options ?? {}), + challenge: stringToBase64Url(state.challenge), + }) + + const jsx = ( + +
+ {error && } + + + + + + + + + + + +

{copy.code_info}

+
+ ) + + return new Response(jsx.toString(), { + headers: { + "Content-Type": "text/html", + }, + }) + }, + } +} + +// in order to trigger the passkey flow, we need to use onclick in