Passlock

repository·master·Indexed 41 days ago

https://github.com/passlock-dev/passlock

A framework-agnostic framework for implementing production-ready passkey (WebAuthn) authentication. It includes @passlock/server for backend authorization and registration tokens, @passlock/browser for client-side passkey ceremonies, and a CLI for instance initialization. Features include mailbox verification for one-time codes (OTC), session management, and progressive enhancement for users unable to use passkeys.

Tokens
23K
Snippets
73
Records
105
Agent score
52%

What's inside Passlock

  1. Authentication and Passkey features in the SvelteKit example

    master

    The SvelteKit example demonstrates several advanced authentication patterns:

    Authentication Patterns

    • Progressive Enhancement: Provides email-based one-time codes for users unable to use passkeys.
    • Mailbox Verification: Ensures email ownership via verification messages.
    • Session Management: Uses Lucia for session handling.
    • Re-authentication: Requires users to re-verify for sensitive operations (e.g., account changes).
    • Account Management: Allows updating usernames/emails and synchronizing passkeys.

    Passkey Patterns

    • Two-step Login Flow: Prompts for email first, then drops into passkey authentication, pre-selecting the credential for the identifier.
    • Autofill: Uses browser autofill for progressive enhancement.
    • Passkey Management: UI for creating and deleting account passkeys.
    • Local Device Management: Syncs passkey deletion or account changes with the user's local device/passkey manager.
    • Local Re-authentication: Uses device-level verification (e.g., FaceID) for sensitive operations.
  2. Key features of @passlock/browser

    master

    The @passlock/browser package provides several capabilities for implementing passkey authentication:

    • Framework Agnostic: Works with Astro, SvelteKit, Angular, and other TypeScript-based frameworks.
    • Domain Migration: Supports backend-authorized RP IDs with WebAuthn /.well-known/webauthn support for moving between domains.
    • Zero Config: Operates with sensible defaults out of the box.
    • Credential Management: Allows programmatic management of passkeys on end-user devices.
    • Advanced WebAuthn Support: Supports user verification, autofill, and roaming authenticators.
  3. How Passlock works: The authentication lifecycle

    master

    Passlock follows a backend-authorized pattern to handle WebAuthn complexity while keeping you in control of users and sessions. The general flow is:

    1. Authorization: Your backend authorizes a ceremony (registration or authentication) and receives a one-time token from Passlock.
    2. Browser Ceremony: Your backend sends this token to the frontend. The frontend uses the @passlock/browser SDK to perform the WebAuthn ceremony, resulting in a code or an id_token (JWT).
    3. Exchange/Verification: Your frontend sends the code or id_token back to your backend. Your backend then exchanges the code via the @passlock/server SDK or verifies the JWT to complete the process.
  4. Preferred API usage patterns for Passlock

    master

    When integrating Passlock, the recommended approach is to use the safe, class-based API rather than standalone helper functions. This ensures proper context management via a Passlock instance.

    Browser Integration

    Use the @passlock/browser package. The constructor requires a tenancyId.

    Server Integration

    Use the @passlock/server package. The constructor requires both a tenancyId and an apiKey.

    When to use standalone functions

    You should only use standalone functions (e.g., registerPasskey) if:

    • You specifically require a functional programming style.
    • You are concerned about tree-shaking and bundle size.
    • The specific documentation or use case you are following requires them.
    // Browser usage
    import { Passlock } from "@passlock/browser";
    const passlock = new Passlock({ tenancyId });
    
    // Server usage
    import { Passlock } from "@passlock/server";
    const passlock = new Passlock({ tenancyId, apiKey });
  5. Security best practices and safety defaults

    master

    To ensure secure implementation of Passlock, follow these safety guidelines:

    • Credential Security: Treat apiKey as a backend-only secret. Never expose it in client-side code.
    • Code Exchange: Always exchange code values (issued by the browser) on a trusted backend.
    • Account Lookup: When registration is prepared by the backend, use the prepared userId for local account lookups.
    • Passkey Management: Store the authenticatorId to facilitate passkey management, deletion, audit logging, and preventing duplicate entries.
    • Credential Constraints: When using allowCredentials or excludeCredentials, use only Passlock-issued passkey IDs.
    • Error Handling: Avoid recommending unsafe API variants unless the user explicitly requires throwing APIs and the documentation supports that specific choice.
  6. Handle errors using tagged unions

    master

    Instead of using try/catch blocks which result in unknown error types, this project uses discriminated unions with a _tag property. This allows for type-safe error handling and exhaustive checking of error states.

    When consuming functions that return these unions, check the _tag property to determine if the operation was a 'success' or a specific error type (e.g., '@error/NotFound').

    const user = getUser();
    
    if (user._tag === 'success') {
    	console.log(`welcome ${user.name}`);
    } else if (user._tag === '@error/NotFound') {
    	console.log(`user with email ${result.email} not found`);
    } else if (user._tag === '@error/AccountSuspended') {
    	console.log('account suspended');
    }
  7. Use processExpiresAt for local flow expiration

    master

    While Passlock manages the expiration of the one-time code itself, the application uses a custom processExpiresAt value inside the MailboxChallengeMetadata.

    This allows the application to expire the entire local flow independently of the Passlock code. This is used to:

    • Expire signup details even if the Passlock code is still valid.
    • Reject stale pending flows (like login or email change) consistently after a page reload.
    • Provide consistent validation logic when reading or verifying challenges.
  8. How Passlock browser and server components interact

    master

    Passkey operations require coordination between the browser (which manages the private key on the device) and the server (which manages the public key).

    When building an application with Passlock, follow this architectural pattern:

    1. UI Layer: A +page.svelte template manages UI state (loading indicators, error messages).
    2. Client Logic: Use utilities in $lib/client to orchestrate operations.
    3. Device Interaction: The client-side helper calls functions from @passlock/browser to interact with the user's device.
    4. Backend Communication: The client-side helper makes fetch requests to a SvelteKit +server.ts endpoint.
    5. Server Logic: The +server.ts endpoint delegates business logic to utilities in $lib/server.
    6. Server Interaction: The server-side helper invokes functions from @passlock/server to manage public keys and authentication state.
  9. Implement session management and re-authentication

    master

    Session Management

    Sessions are managed using cookie-based sessions.

    • Sessions are stored in the database.
    • An associated token is stored in an HTTP only cookie.
    • Sessions are validated globally in +hooks.server.ts.

    Re-authentication for sensitive operations

    To protect sensitive operations (like changing an account email), you can implement a time-based re-authentication check.

    1. Check the passkeyAuthenticatedAt property on the user's session.
    2. Compare this timestamp against your required threshold (e.g., within the last N minutes).
    3. If the timestamp is missing or expired, prompt the user to re-authenticate with their passkey.
  10. Register a passkey

    master

    Passkey registration is a three-step process involving the backend, the browser, and an exchange step.

    1. Authorize registration (Backend)

    Use @passlock/server to generate a registration token. You must specify the rpId (Relying Party ID) for each ceremony.

    2. Browser ceremony (Frontend)

    Use @passlock/browser to perform the registration using the token received from your backend.

    3. Exchange code (Backend)

    Exchange the code returned by the browser to obtain the user and passkey details.

    // 1. Backend: Authorize registration
    import { Passlock } from "@passlock/server";
    const passlock = new Passlock({ tenancyId: "myTenancyId", apiKey: "myApiKey" });
    const result = await passlock.authorizePasskeyRegistration({
      rpId: "example.com",
      rpName: "Example App",
      userId: "user_123",
      username: "jdoe@gmail.com",
      displayName: "Jane Doe",
    });
    // Send result.value.registrationToken to frontend
    
    // 2. Frontend: Browser ceremony
    import { Passlock } from "@passlock/browser";
    const passlockBrowser = new Passlock({ tenancyId: "myTenancyId" });
    const regResult = await passlockBrowser.registerPasskey({ registrationToken });
    // Send regResult.value.code or regResult.value.id_token to backend
    
    // 3. Backend: Exchange code
    const exchangeResult = await passlock.exchangeCode({ code: regResult.value.code });
    console.log(exchangeResult.value.userId, exchangeResult.value.authenticatorId);
  11. Implement flow-specific challenge logic

    master

    Different authentication flows require different metadata and consumption logic:

    Signup (signupChallenge.ts)

    • Metadata: Stores givenName, familyName, and processExpiresAt.
    • Consumption: consumeSignupChallenge(...) verifies the challenge, ensures the email is still unused, creates the local user, and returns a SessionUser.

    Login (loginChallenge.ts)

    • Metadata: Stores only processExpiresAt.
    • Consumption: consumeLoginChallenge(...) verifies the challenge and maps it back to an existing local account.

    Email Change (emailChange.ts)

    • Metadata: Uses base metadata and binds the challenge to a Passlock userId to ensure ownership.
    • Consumption: consumeEmailChallenge(...) verifies the challenge, checks ownership, checks for duplicate email conflicts, updates the local account, and returns the updated user and the previous email address.