# How TREK Implements Passkey and WebAuthn Passwordless Authentication

> Discover how TREK implements passkey and WebAuthn passwordless authentication using a three-layer architecture for secure, token-based login.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: how-to-guide
- Published: 2026-07-09

---

**TREK implements WebAuthn passwordless authentication through a three-layer architecture consisting of feature-toggle guards, a dedicated passkey service for challenge management, and HTTP endpoints that mint standard session tokens upon successful credential verification.**

TREK is an open-source authentication platform that supports modern passwordless login via WebAuthn. This article examines how the repository handles passkey registration and authentication ceremonies, from challenge generation to session token issuance.

## Architecture Overview

TREK’s WebAuthn implementation is organized into distinct layers that separate configuration, business logic, and HTTP transport concerns.

### Feature Toggle and RP Resolution

Before any WebAuthn ceremony executes, the `PasskeyEnabledGuard` located in [`server/src/nest/auth/passkey-enabled.guard.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/auth/passkey-enabled.guard.ts) validates that the administrator has enabled the **Passkey** feature flag. The guard also resolves the **Relying Party (RP) ID** through [`server/src/services/webauthnConfig.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/webauthnConfig.ts), which derives the identifier from the `APP_URL` environment variable or an explicit `WEBAUTHN_RPID` setting. If either the toggle is disabled or the RP domain cannot be determined, the guard returns a **404 Not Found** response before any authentication logic executes.

### Challenge Management

All WebAuthn challenges are ephemeral and stored in the `webauthn_challenges` database table. Each challenge has a **five-minute time-to-live (TTL)** and is **single-use** only. The implementation uses a `DELETE … RETURNING` SQL pattern to atomically consume challenges, ensuring that replay attacks using captured challenge data are impossible.

## The Passkey Service Layer

The core WebAuthn logic resides in [`server/src/services/passkeyService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/passkeyService.ts), which orchestrates both registration and authentication ceremonies using the `@simplewebauthn/server` library.

### Registration Flow

The registration process involves two distinct phases:

- **`passkeyRegisterOptions`**: Generates cryptographically random registration options by calling `generateRegistrationOptions`. This method creates a challenge that is persisted in the `webauthn_challenges` table with the issuing user’s ID.
- **`passkeyRegisterVerify`**: Validates the client’s attestation response using `verifyRegistrationResponse`. Upon successful verification, the credential’s public key, initial counter, supported transports, device type, and backup eligibility are persisted in the `webauthn_credentials` table.

### Login Flow

Authentication follows a similar two-phase pattern:

- **`passkeyLoginOptions`**: Creates a discoverable-credential authentication challenge via `generateAuthenticationOptions`. This allows the authenticator to select the appropriate credential without the user providing a username first.
- **`passkeyLoginVerify`**: Processes the assertion using `verifyAuthenticationResponse`. The service performs critical counter verification against the stored value in `webauthn_credentials` to detect potential credential cloning. If the counter is valid, the service updates the stored counter and mints a session token by calling `generateToken`, creating the same JWT-based session cookie used for password and OIDC authentication.

## HTTP Controller and Security Controls

The `PasskeyController` in [`server/src/nest/auth/passkey.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/auth/passkey.controller.ts) exposes the WebAuthn functionality through RESTful endpoints.

### Endpoint Structure

- **`POST /api/auth/passkey/register/options`**: Initiates registration. Requires existing authentication and password re-verification to prevent account takeover.
- **`POST /api/auth/passkey/register/verify`**: Completes registration and stores the new credential.
- **`POST /api/auth/passkey/login/options`**: Public endpoint returning authentication options for discoverable credentials.
- **`POST /api/auth/passkey/login/verify`**: Validates the assertion and establishes the session.
- **`GET /api/auth/passkey/credentials`**: Lists the user’s registered passkeys. Users can also rename or delete their own credentials through this resource.

### Rate Limiting and Audit

Every endpoint is protected by `RateLimitService` to prevent brute-force enumeration of credential IDs. Additionally, `writeAudit` logs all registration and authentication attempts with metadata including IP address and credential ID. To prevent timing side-channel attacks, the login verification endpoint pads response times to a minimum of **350 milliseconds** (`LOGIN_MIN_LATENCY_MS`), matching the latency of password hash verification.

## Client Integration Examples

### Registration Ceremony

The following TypeScript demonstrates how a client completes passkey registration:

```typescript
// 1️⃣ Get registration options (authenticated)
await fetch('/api/auth/passkey/register/options', {
  method: 'POST',
  credentials: 'include',
  body: JSON.stringify({ password: 'myCurrentPassword' })
}).then(r => r.json()).then(async data => {
  // 2️⃣ Call the browser API
  const credential = await navigator.credentials.create({
    publicKey: data.options
  });

  // 3️⃣ Send attestation back to the server
  await fetch('/api/auth/passkey/register/verify', {
    method: 'POST',
    credentials: 'include',
    body: JSON.stringify({ attestationResponse: credential, name: 'My Phone' })
  });
});

```

### Login Ceremony

For passwordless login using a discoverable credential:

```typescript
// 1️⃣ Get login options (no auth)
const opts = await fetch('/api/auth/passkey/login/options')
  .then(r => r.json()).then(r => r.options);

// 2️⃣ Ask the authenticator to sign the challenge
const assertion = await navigator.credentials.get({ publicKey: opts });

// 3️⃣ Verify on the server and obtain a session token
const resp = await fetch('/api/auth/passkey/login/verify', {
  method: 'POST',
  body: JSON.stringify({ assertionResponse: assertion })
}).then(r => r.json());

if (resp.token) {
  // The server set the same auth cookie as a password login
  console.log('Logged in as', resp.user);
}

```

## Summary

- **TREK** implements WebAuthn passwordless authentication using `@simplewebauthn/server` for cryptographic operations.
- The `PasskeyEnabledGuard` ensures all passkey routes are disabled when the feature flag is off or the RP ID is unresolvable.
- Challenges are single-use, time-bound records stored in `webauthn_challenges` and consumed atomically to prevent replay attacks.
- The [`passkeyService.ts`](https://github.com/mauriceboe/TREK/blob/main/passkeyService.ts) file handles registration via `passkeyRegisterOptions` and `passkeyRegisterVerify`, and authentication via `passkeyLoginOptions` and `passkeyLoginVerify`.
- Successful WebAuthn verification mints standard JWT session tokens through `generateToken`, making passkeys interchangeable with password and OIDC flows.
- The `PasskeyController` exposes REST endpoints with rate limiting, audit logging, and constant-time response padding to mitigate timing attacks.

## Frequently Asked Questions

### What WebAuthn library does TREK use?

TREK relies on **`@simplewebauthn/server`** for all server-side WebAuthn operations. The library provides the `generateRegistrationOptions`, `verifyRegistrationResponse`, `generateAuthenticationOptions`, and `verifyAuthenticationResponse` functions used in [`server/src/services/passkeyService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/passkeyService.ts).

### How does TREK prevent replay attacks during WebAuthn ceremonies?

The system stores challenges in the `webauthn_challenges` table with a strict five-minute expiration. When a challenge is consumed during verification, the code executes a `DELETE … RETURNING` query that atomically removes the record. If the challenge was already used or expired, the verification fails, rendering replayed request data invalid.

### Can users manage multiple passkeys in TREK?

Yes. The `GET /api/auth/passkey/credentials` endpoint allows authenticated users to list all credentials stored in the `webauthn_credentials` table associated with their account. Users can rename specific passkeys for identification or delete compromised devices without affecting other registered authenticators.

### Is passkey login in TREK resistant to phishing?

Yes. The implementation leverages WebAuthn’s origin-binding properties, verifying that the `origin` in the authentication response matches the configured RP ID in [`server/src/services/webauthnConfig.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/webauthnConfig.ts). Additionally, the use of discoverable credentials and the requirement for genuine user presence during the ceremony ensure that authentication cannot be phished from a malicious domain.