# Auth · Credentials

> Passkeys and security keys — challenges, registration options and sign-in options.

Public-key credentials — passkeys and security keys. These routes issue the options a browser's WebAuthn call needs
and resolve a credential back to the account that owns it.

## The shape of a flow

Registering a passkey is `POST …/pubkey/attest` for creation options, then
`navigator.credentials.create()`, then
[`POST /api/auth/identity/:id/credential`](/docs/auth-identity) to store what came back.

Signing in is `POST …/pubkey/assert` for request options, then
`navigator.credentials.get()`, then [`POST /api/auth/signin`](/docs/auth) with
`method: "credential"`.

## Usernameless sign-in

`…/pubkey/assert` takes an identifier only when you have one. Omit it and the allow-list stays open,
which is what a bare "sign in with a passkey" button needs — the authenticator picks the credential, and
`GET /api/auth/credential/:id/identity` turns it into an account.

The relying party id is derived from the request, so a preview deployment works without configuration.

## List credentials

`GET /api/auth/credential`

Guard: admin

Every public-key credential registered on the deployment — passkeys and security keys — paged by cursor.

### Parameters

- `cursor` _string_ — Continue from a previous page. Query parameter.
- `limit` _number_ — Page size. Query parameter.

### Returns

The page of credentials, and a cursor when there is more.

### Request (cURL)

```
curl https://example.com/api/auth/credential \
  -H "authorization: Bearer $TOKEN"
```

### Response

```json
{
  "credentials": [
    { "id": "AXcv…", "identity": "01J2788XY98FDTA410C8S716CD" }
  ],
  "cursor": null
}
```

## Read a credential

`GET /api/auth/credential/:id`

Guard: open

The stored public-key credential, by the id the authenticator returned when it was created.

### Parameters

- `id` _string_ **Required** — The credential id. Path parameter.

### Returns

The credential, or `404`.

### Request (cURL)

```
curl https://example.com/api/auth/credential/AXcv1234
```

### Response

```json
{
  "id": "AXcv1234",
  "publicKey": "pQECAyYgASFYIH…",
  "counter": 3
}
```

## Check whether a credential exists

`HEAD /api/auth/credential/:id`

Guard: open

The same lookup with no body.

### Parameters

- `id` _string_ **Required** — Path parameter.

### Returns

`200` when it exists, `404` when not.

### Request (cURL)

```
curl -I https://example.com/api/auth/credential/AXcv1234
```

### Response

```json
{}
```

## Look up the identity behind a credential

`GET /api/auth/credential/:id/identity`

Guard: open

Which account a passkey belongs to. This is what makes a usernameless sign-in possible: the authenticator
hands back a credential id and nothing else, and this turns it into an identity.

### Parameters

- `id` _string_ **Required** — The credential id. Path parameter.

### Returns

`{ identity }`, or `404` when the credential is unknown.

### Request (cURL)

```
curl https://example.com/api/auth/credential/AXcv1234/identity
```

### Response

```json
{
  "identity": {
    "id": "01J2788XY98FDTA410C8S716CD",
    "kind": "member",
    "displayName": "Ada Lovelace"
  }
}
```

## Check whether a credential has an owner

`HEAD /api/auth/credential/:id/identity`

Guard: open

The same lookup with no body.

### Parameters

- `id` _string_ **Required** — Path parameter.

### Returns

`200` when the credential resolves, `404` when not.

### Request (cURL)

```
curl -I https://example.com/api/auth/credential/AXcv1234/identity
```

### Response

```json
{}
```

## Issue a challenge

`GET /api/auth/credential/challenge`

Guard: open

A fresh challenge string for an authenticator to sign — an id and a random salt joined by a colon.

It answers `text/plain`, not JSON. Worth knowing before wiring a client that assumes every
endpoint here returns an object.

### Returns

The challenge, as plain text.

### Request (cURL)

```
curl https://example.com/api/auth/credential/challenge
```

### Response

```json
"01J2788XY98FDTA410C8S716CD:9f2c41ba7d0e4a1f8b3c5d6e7f809a1b"
```

## Check the challenge endpoint

`HEAD /api/auth/credential/challenge`

Guard: open

The same call with no body.

### Returns

`200`.

### Request (cURL)

```
curl -I https://example.com/api/auth/credential/challenge
```

### Response

```json
{}
```

## Verify a signed challenge

`POST /api/auth/credential/challenge`

Guard: open

`Not implemented.` Answers `501` with the signed challenge echoed back. Assertion is verified through `POST /api/auth/credential/pubkey/assert` instead.

### Parameters

- `body` _string_ — The signed challenge, as plain text.

### Returns

`501`.

### Request (cURL)

```
curl -X POST https://example.com/api/auth/credential/challenge \
  -H "content-type: text/plain" \
  --data "01J2788XY98FDTA410C8S716CD:9f2c…"
```

### Response

```json
{
  "signedChallenge": "01J2788XY98FDTA410C8S716CD:9f2c…"
}
```

## Begin registering a passkey

`POST /api/auth/credential/pubkey/attest`

Guard: open

Returns the `PublicKeyCredentialCreationOptions` to hand to
`navigator.credentials.create()` — relying party, user handle, and a one-time challenge the
worker remembers until the attestation comes back.

The relying party id is derived from the request, so this works on a preview deployment without
configuration. If a session is present the credential is attached to that identity; otherwise a fresh id is
minted for the account being created.

### Parameters

- `name` _string_ **Required** — The account name shown by the authenticator — usually the email or username.
- `displayName` _string_ **Required** — The human name shown alongside it.

### Returns

Creation options, ready to pass to the WebAuthn API.

### Request (cURL)

```
curl -X POST https://example.com/api/auth/credential/pubkey/attest \
  -H "content-type: application/json" \
  -d '{ "name": "ada@example.com", "displayName": "Ada Lovelace" }'
```

### Request (JavaScript)

```
const options = await fetch('/api/auth/credential/pubkey/attest', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'ada@example.com', displayName: 'Ada Lovelace' })
}).then((response) => response.json())

const credential = await navigator.credentials.create({ publicKey: options })
```

### Response

```json
{
  "rp": { "name": "Moddable", "id": "example.com" },
  "user": {
    "id": "01J2788XY98FDTA410C8S716CD",
    "name": "ada@example.com",
    "displayName": "Ada Lovelace"
  },
  "challenge": "9f2c41ba7d0e4a1f8b3c5d6e7f809a1b",
  "pubKeyCredParams": [{ "type": "public-key", "alg": -7 }]
}
```

## Begin signing in with a passkey

`POST /api/auth/credential/pubkey/assert`

Guard: open

Returns the `PublicKeyCredentialRequestOptions` to hand to
`navigator.credentials.get()` — a one-time challenge, and the credentials allowed to answer it.

Naming an identifier narrows the allow-list to that account's credentials. Omitting it leaves the list open,
which is what a "sign in with a passkey" button with no username field wants.

### Parameters

- `kind` _enum_ — Which identifier the body carries, when narrowing to one account.
  - One of: `email`, `phone`, `username`, `identity`
- `email | phone | username | identity` _string_ — The identifier named by `kind`.

### Returns

Request options, ready to pass to the WebAuthn API.

### Request (cURL)

```
curl -X POST https://example.com/api/auth/credential/pubkey/assert \
  -H "content-type: application/json" \
  -d '{ "kind": "email", "email": "ada@example.com" }'
```

### Response

```json
{
  "rpId": "example.com",
  "challenge": "9f2c41ba7d0e4a1f8b3c5d6e7f809a1b",
  "allowCredentials": [{ "type": "public-key", "id": "AXcv1234" }],
  "userVerification": "preferred"
}
```
