> ## Documentation Index
> Fetch the complete documentation index at: https://docs.authsignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cross-device QR code sign-in

> Sign a user in on a kiosk after the user scans a QR code with a phone.

This guide tells you how to authenticate a user on a device with limited input capabilities. The user scans a QR code and completes an authentication challenge on their mobile phone.

The flow has four steps:

1. The kiosk shows a QR code. No user is signed in. This is an **anonymous challenge**.
2. The user scans the QR code with a phone.
3. The user identifies themselves and completes a challenge.
4. The shared device receives the result and signs the user in.

> [!NOTE]
>
> This guide is not about [QR code app
> verification](/authentication-methods/app-verification/qr-code.md). In that method, an enrolled
> mobile app scans the code and approves the request with a private key.

## Terms

| Term | Meaning |
| - | - |
| Kiosk | The shared device that shows the QR code. This can be a kiosk, a TV, or a desktop browser. |
| Phone | The personal device that scans the QR code. |
| Challenge ID | The identifier of the anonymous challenge. Put this value in the QR code. |
| Device code | A secret that the kiosk stores. The kiosk sends it with each poll request. Do not put it in the QR code. |
| Action code | The name of the action, for example `signIn`. The kiosk and the phone must use the same action code. |

## Sequence diagram

```mermaid theme={null}
sequenceDiagram
    participant K as Kiosk
    participant B as Your backend
    participant P as Phone browser
    participant A as Authsignal

    K->>A: 1. Create an anonymous challenge
    A->>K: challengeId + deviceCode
    K->>K: 2. Show a QR code for your sign-in page
    P->>K: 3. User scans the code
    P->>B: 4. Open /sign-in?challengeId=...
    Note right of P: User identifies themselves
    B->>A: 5. Track the action with the userId + challengeId
    A->>B: Pre-built UI URL
    P->>A: 6. User completes the challenge
    K->>A: 7. Poll with challengeId + deviceCode
    A->>K: 8. Verified result + accessToken
    K->>B: 9. Send the accessToken
    B->>A: 10. Validate the token
    B->>K: 11. Authenticated
```

## Before you start

You need these items:

* A tenant with an active authentication method.
* An action configuration. The action must challenge the user.
* The tenant ID. The kiosk uses the tenant ID as a publishable key.
* The tenant secret. Your backend uses the tenant secret.
* A web page that you control. The phone opens this page. The page reads the `challengeId` query parameter.

> [!WARNING]
>
> The kiosk uses only the tenant ID. Never put the tenant secret on the kiosk.

## Step 1 - Create the challenge on the kiosk

The kiosk creates an anonymous challenge. This endpoint needs no QR code authentication method configuration.

```bash theme={null}
curl -X POST https://api.authsignal.com/v1/client/challenge \
  -H "Authorization: Basic $(echo -n '<tenant id>' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"action": "signIn"}'
```

The response has two values:

```json theme={null}
{
  "challengeId": "d2c42aff-863b-4b00-9e1f-ff2d408cb4e5",
  "deviceCode": "e4242d4a-6286-443d-b29e-0951fe5dbfdf"
}
```

**challengeId** `string`

The identifier of the anonymous challenge. Put this value in the QR code.

**deviceCode** `string`

The secret that identifies the kiosk. Store this value on the kiosk. The kiosk sends it with each
poll request.

The challenge expires after the token duration of your tenant. The default duration is 10 minutes. Create a new challenge before the old challenge expires to show a fresh code.

## Step 2 - Show the QR code

Build the contents of the QR code. Put your own sign-in URL in the code. Add the challenge ID as a query parameter.

```
https://kiosk.example.com/sign-in?challengeId=d2c42aff-863b-4b00-9e1f-ff2d408cb4e5
```

Use any QR code library to draw the image.

![A kiosk screen with the heading Scan to sign in above a QR code](https://mintcdn.com/authsignal-23/GF2w7la1nqiZ76um/images/docs/advanced-usage/cross-device-qr-kiosk-code.png?fit=max\&auto=format\&n=GF2w7la1nqiZ76um\&q=85\&s=53256cd34c642d9037cd324a00b1c2ae)

The kiosk shows the QR code while it waits for a user

> [!WARNING]
>
> Do not put the device code in the QR code. The device code is the secret that shows that the poll
> comes from the kiosk.

## Step 3 - Complete the challenge on the phone

The phone opens your sign-in page. Your page reads the `challengeId` parameter, identifies the user, then sends the user to the Authsignal pre-built UI.

**1. Identify the user**

Ask the user for an identifier. This example asks for a user ID. You can also ask for an email
address or a phone number and then read the user ID from your own user record.

![A phone screen with the heading Verify it's you above an identifier field and a Continue button](https://mintcdn.com/authsignal-23/GF2w7la1nqiZ76um/images/docs/advanced-usage/cross-device-qr-phone-identify.png?fit=max\&auto=format\&n=GF2w7la1nqiZ76um\&q=85\&s=0680a58c8699273527c32b96efb44624)

The page on the phone collects an identifier before it tracks the action

**2. Track the action from your backend**

Send the challenge ID with the request.

```ts theme={null}
const result = await authsignal.track({
  userId,
  action: "signIn", // Must be the action code from step 1
  challengeId, // From the QR code
  redirectUrl: "https://kiosk.example.com/api/finish",
});

redirect(result.url); // The Authsignal pre-built UI
```

> [!WARNING]
>
> The action code in the `track` request must be the same as the action code in step 1. If the
> action codes are different, the request fails with `404 not_found` and the message
> `Challenge not found.`

**3. Let the user complete the challenge**

The pre-built UI challenges the user based on the methods they have enrolled and methods that are configured on the action.

**4. Validate the token on your backend**

The pre-built UI sends the user to your `redirectUrl`. The URL carries a token.

```ts theme={null}
const { isValid, userId } = await authsignal.validateChallenge({
  action: "signIn", // Must be the action code from step 1
  token,
});
```

![A phone screen with the heading All done and the text You're signed in on the kiosk](https://mintcdn.com/authsignal-23/GF2w7la1nqiZ76um/images/docs/advanced-usage/cross-device-qr-phone-success.png?fit=max\&auto=format\&n=GF2w7la1nqiZ76um\&q=85\&s=39f006069a63e384d71c41c899c732a6)

The phone confirms the result after the user completes the challenge

The kiosk receives the same result from its own poll. [Step 4](#step-4-poll-the-result-on-the-kiosk) tells you how.

## Step 4 - Poll the result on the kiosk

The kiosk polls the anonymous challenge with the challenge ID and the device code from step 1.

```bash theme={null}
curl -X POST https://api.authsignal.com/v1/client/verify \
  -H "Authorization: Basic $(echo -n '<tenant id>' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"challengeId": "...", "deviceCode": "..."}'
```

The response gives the state of the flow.

| State | `isClaimed` | `isConsumed` | `isVerified` | `accessToken` |
| - | - | - | - | - |
| No user has an identity on the challenge | `false` | `false` | `false` | absent |
| The user has an identity but no result yet | `true` | `false` | `false` | absent |
| The user completed the challenge successfully | `true` | `true` | `true` | present |
| The user completed the challenge unsuccessfully | `true` | `true` | `false` | absent |

Poll about one time each second while the challenge is active. Stop the poll when `isConsumed` is `true`. Stop the poll if the challenge expires.

![A kiosk screen with the heading Verifying on your phone above an obscured QR code and a spinner](https://mintcdn.com/authsignal-23/GF2w7la1nqiZ76um/images/docs/advanced-usage/cross-device-qr-kiosk-verifying.png?fit=max\&auto=format\&n=GF2w7la1nqiZ76um\&q=85\&s=a4ed839586a30a82a0d9a1da87076825)

The kiosk hides the code after a user claims the challenge

When the response gives `isVerified`, send the `accessToken` to your backend. Validate the token before you make the kiosk session.

```ts theme={null}
const result = await authsignal.validateChallenge({
  action: "signIn", // Must be the action code from step 1
  token: accessToken,
});

if (result.isValid) {
  // Make the kiosk session for result.userId
}
```

![A kiosk screen with the heading You're in and the text Signed in as demo-1e247861](https://mintcdn.com/authsignal-23/GF2w7la1nqiZ76um/images/docs/advanced-usage/cross-device-qr-kiosk-done.png?fit=max\&auto=format\&n=GF2w7la1nqiZ76um\&q=85\&s=ec3052803ec4fbd40b9de2a775ef4a58)

The kiosk signs the user in after it validates the access token

> [!WARNING]
>
> The final result is single-use. Authsignal invalidates the anonymous challenge when it gives the
> consumed result. A later poll with the same challenge ID and device code fails.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.