Authsome
1.x
Docs/Authsome/Passkeys (WebAuthn)
Open

Reading6 min
Updated31 Jul 2026
Sourcev1/authentication/passkeys.mdx

The passkey plugin implements the WebAuthn specification (FIDO2), enabling authentication with biometrics (Face ID, Touch ID, Windows Hello) or hardware security keys (YubiKey). Passkeys are phishing-resistant and require no password.

Setup01

import (
    "github.com/xraph/authsome"
    "github.com/xraph/authsome/plugins/passkey"
)

eng, err := authsome.New(
    authsome.WithStore(store),
    authsome.WithPlugin(passkey.New(passkey.Config{
        // Relying Party ID — must match the effective domain of your origin.
        RPID: "myapp.com",

        // Relying Party display name — shown in the browser passkey UI.
        RPDisplayName: "My Application",

        // Allowed origins — must include your frontend's exact origin
        // (scheme + host + port). For production, set explicitly.
        RPOrigins: []string{
            "https://myapp.com",
            "https://www.myapp.com",
        },
    })),
)

Local development

For RPID: "localhost" (or "127.0.0.1") the plugin auto-trusts the request's Origin header on every ceremony, as long as its host matches the configured RPID. That means any localhost dev port — http://localhost:3000, http://localhost:5173, etc. — works without listing it in RPOrigins. The host check still rejects foreign origins, so this shortcut only loosens the port requirement, never the host.

For non-localhost RPIDs, behavior is strict: the browser's origin must match an entry in RPOrigins (scheme, host, and port included) exactly.

Dynamic origins setting

passkey.rp_origins ([]string) is a dynamic setting that adds extra allowed origins on top of Config.RPOrigins. Use it when origins differ between environments and you'd rather not rebuild the binary — e.g. set it from the dashboard or via your settings store for staging vs. prod.

Note

The passkey plugin uses the engine's ceremony.Store to hold in-progress registration and authentication state between the challenge and response calls. The default in-memory store is used when none is configured by the engine.

FIDO2/WebAuthn overview02

WebAuthn uses public-key cryptography:

  1. During registration, the authenticator generates a key pair. The public key is stored on the server. The private key never leaves the device.

  2. During authentication, the server sends a challenge. The authenticator signs it with the private key. The server verifies the signature with the stored public key.

Because the private key is device-bound and the challenge is domain-scoped, passkeys are immune to phishing — a fake site cannot obtain a valid authentication signature.

Registration ceremony03

The registration flow runs when an existing user wants to add a passkey to their account.

Begin registration: The server generates a challenge and public key credential creation options.

POST /v1/auth/passkey/register/begin

Request headers must include Authorization: Bearer {session_token} (user must be authenticated).

Response includes the WebAuthn PublicKeyCredentialCreationOptions JSON:

{
  "challenge": "base64url-encoded-random-challenge",
  "rp": {"id": "myapp.com", "name": "My Application"},
  "user": {"id": "base64url-user-id", "name": "[email protected]", "displayName": "Alice"},
  "pubKeyCredParams": [{"type": "public-key", "alg": -7}],
  "timeout": 60000,
  "attestation": "none",
  "excludeCredentials": [...]
}

Create passkey on device: Your JavaScript calls navigator.credentials.create(options). The browser/OS prompts the user for biometric or PIN confirmation. On success, a PublicKeyCredential is returned.

Finish registration: Submit the credential to the server.

POST /v1/auth/passkey/register/finish

{
  "credential": {
    "id": "base64url-credential-id",
    "rawId": "base64url-raw-id",
    "type": "public-key",
    "response": {
      "attestationObject": "base64url-attestation",
      "clientDataJSON": "base64url-client-data"
    }
  }
}

The engine verifies the attestation, stores the Passkey credential (ID prefix: apsk_), and returns the credential metadata.

Authentication ceremony04

The authentication flow allows users to sign in without a password.

Begin authentication: The server generates a challenge.

POST /v1/auth/passkey/authenticate/begin

{"email": "[email protected]", "app_id": "myapp"}

Response includes the WebAuthn PublicKeyCredentialRequestOptions:

{
  "challenge": "base64url-random-challenge",
  "timeout": 60000,
  "rpId": "myapp.com",
  "allowCredentials": [
    {"type": "public-key", "id": "base64url-credential-id"}
  ],
  "userVerification": "required"
}

Sign the challenge on device: Your JavaScript calls navigator.credentials.get(options). The browser prompts biometric confirmation and signs the challenge.

Finish authentication: Submit the assertion.

POST /v1/auth/passkey/authenticate/finish

{
  "credential": {
    "id": "base64url-credential-id",
    "rawId": "base64url-raw-id",
    "type": "public-key",
    "response": {
      "authenticatorData": "base64url-auth-data",
      "clientDataJSON": "base64url-client-data",
      "signature": "base64url-signature",
      "userHandle": "base64url-user-id"
    }
  }
}

The engine verifies the signature, updates the passkey's SignCount, and creates a session. Returns user + session.

Credential management05

Retrieve all passkeys for a user:

passkeys, err := engine.ListUserPasskeys(ctx, userID)

Each Passkey record:

type Passkey struct {
    ID           id.PasskeyID  `json:"id"`
    UserID       id.UserID     `json:"user_id"`
    AppID        id.AppID      `json:"app_id"`
    CredentialID []byte        `json:"credential_id"`
    PublicKey    []byte        `json:"-"`        // never serialized
    AAGUID       string        `json:"aaguid"`   // authenticator model ID
    Name         string        `json:"name"`     // user-assigned name
    SignCount     uint32        `json:"sign_count"`
    Transports   []string      `json:"transports"` // "internal", "usb", "nfc", "ble"
    BackupEligible bool        `json:"backup_eligible"`
    BackupState    bool        `json:"backup_state"`
    LastUsedAt   time.Time     `json:"last_used_at"`
    CreatedAt    time.Time     `json:"created_at"`
}

Let users rename or delete their passkeys:

// Rename a passkey
engine.UpdatePasskeyName(ctx, passkeyID, "MacBook Touch ID")

// Remove a passkey (the user must have another sign-in method)
engine.DeletePasskey(ctx, passkeyID)

API routes06

MethodPathDescription
POST/passkey/register/beginBegin passkey registration ceremony
POST/passkey/register/finishFinish passkey registration
POST/passkey/authenticate/beginBegin passkey authentication ceremony
POST/passkey/authenticate/finishFinish authentication and return session
GET/passkeyList all passkeys for the current user
PATCH/passkey/{id}Rename a passkey
DELETE/passkey/{id}Delete a passkey