Passkey Integration Guide

Add phishing-resistant, biometric passkeys to your RiskOS™ workflows through the Digital Intelligence Web SDK, using WebAuthn registration and assertion ceremonies.

Overview

The Socure Passkey Service provides passkey lifecycle and ceremony execution. You own your user records; the service has no knowledge of them and stores no user profile data. The service executes the WebAuthn registration and assertion ceremonies and stores the resulting credentials, keyed by the identifiers you supply (account, relying party, and your user identifier).

The service attests proof of possession: for the identifiers you supply, it confirms whether the end user proved possession of a matching registered passkey. It does not make an authentication or access decision. You decide whether a successful possession proof authenticates the user for your system.

You integrate through the Socure Digital Intelligence Web SDK and your existing RiskOS™ workflows, in two phases:

  • Registration (first visit) creates a passkey and stores the credential.
  • Assertion (return visit) proves possession of an existing credential. Because the credential itself identifies the user, the return flow can also tell you which of your users it is, so you can resolve them without re-collecting identity.

The following sections detail both phases. Passkeys give your returning users a phishing-resistant, biometric credential.

Redemption comes in two forms, and you choose based on what you already know:

  • Verify — You supply a customerUserId and Socure confirms the passkey belongs to that user. Used to confirm a registration you just initiated.
  • Usernameless Verify — You supply no user, and Socure returns the passkeyUserId the passkey belongs to. Used on return, when the passkey is what tells you who the user is.

Key terms

  • relyingPartyId (rpId) — Your own domain, the relying party the passkey is scoped to (for example, yourdomain.com). You provide it to the SDK on each registerPasskey/assertPasskey call, and to your RiskOS™ workflows. It must be a domain you control, chosen so that every origin that runs a ceremony is that domain or a subdomain of it, and it must be consistent across registration and assertion. It is never a Socure domain. The service never infers it from anything else, so it is required on every call.

    Supply it as a bare domain: yourdomain.com, not https://yourdomain.com/. A value carrying a scheme, port, path, or trailing dot is rejected, as is a public suffix such as com, co.uk, or github.io, since an rpId there would scope your credentials across every site under it. A subdomain is a valid choice: app.yourdomain.com scopes credentials to that host and anything beneath it, so pick the parent when your flows span sibling subdomains. Surrounding whitespace and letter case are normalized, so YourDomain.com and yourdomain.com are the same relying party. Supply an internationalized domain in its punycode form, since that is the form the browser reports.

    The SDK parameter is named relyingPartyId; the field is named rpId in the RiskOS™ workflow contracts and anywhere the underlying WebAuthn options are visible, matching the WebAuthn specification's rp.id, and it is sent as data.custom.rp_id on the RiskOS™ evaluation request. All name the same value.

  • customerUserId — Your own identifier for the user, from your system. You provide it when requesting a passkeyUserToken (registration), and again when redeeming a passkeyResultId in Verify mode, so RiskOS™ can tie the passkey result back to the right user. In Usernameless Verify mode you do not supply it; Socure returns the passkeyUserId instead. It is sent as data.custom.customer_user_id on the RiskOS™ evaluation request. Socure treats it as opaque and does not interpret it.

  • passkeyUserId — Socure's opaque, stable identifier for the user, scoped to your relying party. It is returned when you register (in the Passkey Registration response) and, in Usernameless Verify mode, when you redeem an assertion result. It carries no personal data, and Socure holds no reverse mapping from it back to your customerUserId: you store passkeyUserId against your own user record at registration and use it to recognize the user on return. It is stable across a user's devices and re-registrations.

  • sessionToken — The Digital Intelligence token from the DI Web SDK (getSessionToken()). It links the passkey activity to the active DI session and is passed to your RiskOS™ workflow, as with other DI modules today.

  • passkeyUserToken — A single-use, short-lived JWT (approximately 5-minute TTL) returned by the RiskOS™ registration workflow on completion. It binds an upcoming registration to a specific user and relying party. You pass it straight into registerPasskey(). Single-use is enforced: one token drives at most one registration ceremony, and it is spent the moment the ceremony begins, whether or not the ceremony succeeds. It is not a per-user credential but a per-registration one, so registering a second device means running the registration workflow again for a second token (see Register more than one device).

  • Registration — The first-visit phase: RiskOS™ risk-assesses the user, a passkey is created on their device, and the credential is stored on the Socure backend. This workflow includes a Passkey Registration step.

  • Assertion — The return-visit phase: the SDK runs the passkey ceremony in which the user proves possession of an existing credential, and RiskOS™ redeems the result. This workflow includes a Passkey Usernameless Verify step, which returns the passkeyUserId the credential belongs to.

  • passkeyResultId — A single-use, short-lived (approximately 5-minute TTL) reference the SDK returns once a ceremony reaches the Socure backend, whether or not the passkey verified. You redeem it against RiskOS™ to learn the outcome, sending it as data.custom.passkey_result_id on the RiskOS™ evaluation request. Redemption consumes the reference and is scoped by the account and relyingPartyId: if no matching result exists (expired, already redeemed, never existed, or a relyingPartyId that does not match the ceremony) redemption returns 404. Otherwise it returns an outcome of verified or failed. In Verify mode the customerUserId is checked as part of that result; in Usernameless Verify mode the response carries the passkeyUserId the credential belongs to instead (see the phase steps that follow). A ceremony that never reaches the backend, for example one the user cancels or that finds no passkey, produces no passkeyResultId at all.


How it fits together

You integrate with two surfaces only:

  1. The RiskOS™ workflows — called as you do today. RiskOS™ manages the integration with the Passkey service.
  2. The DI Web SDK — which invokes the passkey registration and assertion ceremonies in the browser through registerPasskey() and assertPasskey(), and returns a passkeyResultId.

The SDK orchestrates each ceremony: it calls the service and invokes the browser's WebAuthn APIs. Challenge generation, attestation and assertion validation, and credential storage all happen on the Socure backend, not in the SDK.


Prerequisites

  • The DI Web SDK integrated into your application, with access to the active Digital Intelligence sessionToken (getSessionToken()), as you do for other DI modules today.

  • Your relying party ID (your domain) to pass to the SDK and to your RiskOS™ workflows.

  • Your relying party registered with Socure before your first registration. Socure provisions your relyingPartyId together with the exact list of origins your app is served from (your web origins, and your app signing identities for native). Registration cannot be requested for a relying party that has not been provisioned, so this is a setup step, not something the first call creates.

    Two rules govern that list, and the first one constrains how you pick your relyingPartyId. Every web origin must be the relyingPartyId itself or a subdomain of it. https://app.yourdomain.com and https://www.yourdomain.com can both sit under yourdomain.com, but an origin on a different registrable domain cannot be added to that relying party at all. If your flows run on two unrelated domains, that is two relying parties, provisioned separately, with separate credentials: a passkey registered under one is invisible to the other. Browsers enforce the same relationship locally, so this is less a Socure restriction than the shape WebAuthn already has.

    Origins are matched exactly, by scheme and host, plus port when it is not the default. Every origin that runs a ceremony must be on the list, so https://app.yourdomain.com does not cover https://www.yourdomain.com. Origins must be https, must carry no path, query, or fragment, and must include a non-default port exactly as the browser reports it, as in https://app.yourdomain.com:8443. Adding an origin later is a Socure-side change, not something your app can do at call time.

  • Your Socure account enabled for passkeys. The passkey endpoints reject accounts that have not been enabled, so confirm enablement with your Socure contact before your first workflow call. Usernameless Verify (identifying a returning user from their passkey alone) is a separate capability; if your return flow relies on it, confirm it is enabled for your account as well.

  • Your RiskOS™ registration and assertion workflows with the passkey steps enabled. The registration workflow includes Passkey Registration and Passkey Verify steps; the assertion workflow includes a Passkey Usernameless Verify step.


Phase 1 — Registration

Registration maps to your registration workflow: RiskOS™ risk-assesses the user, and on completion the workflow issues a passkeyUserToken.

sequenceDiagram
    participant App as Your app
    participant SDK as DI Web SDK
    participant RiskOS as RiskOS™ workflow
    participant Backend as Socure Passkey Service
    App->>RiskOS: Run registration workflow (phone, sessionToken, customerUserId, rpId, PII)
    RiskOS->>RiskOS: Risk checks (phone risk, SNA/OTP step-up, DI, allow/deny list)
    RiskOS-->>App: passkeyUserToken (single-use, ~5 min) + passkeyUserId
    App->>App: Store passkeyUserId against your user record
    App->>SDK: registerPasskey(passkeyUserToken, relyingPartyId, ...)
    SDK->>Backend: Start registration ceremony
    Backend-->>SDK: WebAuthn options + challenge
    SDK-->>App: PasskeyResult (passkeyResultId)
    App->>RiskOS: Verify registration (passkeyResultId, customerUserId)
    RiskOS-->>App: outcome (verified | failed)

Step 1 — Run the RiskOS™ registration workflow

Call your RiskOS™ registration workflow as usual, passing the phone number, the DI sessionToken, the customerUserId, your relyingPartyId, and any additional PII. RiskOS™ runs its risk checks, such as phone risk, SNA/OTP step-up, Digital Intelligence, and allow/deny list.

On completion, the Passkey Registration step returns a single-use passkeyUserToken (a short-lived JWT, approximately 5-minute TTL) in the RiskOS™ response. This token binds the upcoming registration to the correct user and relying party.

The same response also returns the user's passkeyUserId. Store it against your user record now: on a later return visit, Passkey Usernameless Verify hands you this same passkeyUserId, and it's how you recognize the user without re-collecting identity (see Phase 2).

The relyingPartyId is validated at this point. A malformed value, or one that has not been provisioned for your account, fails here rather than later in the ceremony, so a configuration mistake surfaces on that workflow call.

The passkey enrichment requires two data.custom fields on this call. Other data fields depend on the rest of your workflow (the phone, sessionToken, and PII noted above); passkey itself requires only these.

{
  "data": {
    "custom": {
      "rp_id": "{{rpId}}",
      "customer_user_id": "{{customerUserId}}"
    }
  }
}
FieldTypeRequiredDescription
data.custom.rp_idStringRequiredRelying party ID as a bare domain; the same value as the SDK's relyingPartyId. Validated on this call.
data.custom.customer_user_idStringRequiredYour identifier for the user. Socure treats it as opaque.

Step 2 — Create the passkey through the SDK

SigmaDeviceManager.registerPasskey(
  passkeyUserToken: string,
  relyingPartyId: string,
  username?: string,
  displayName?: string
): Promise<PasskeyResult>
  • passkeyUserToken — the token returned by RiskOS™ in Step 1.
  • relyingPartyId — your domain, the relying party the passkey is scoped to. Must match the relyingPartyId used to start the registration workflow.
  • username (optional) — sets the WebAuthn user.name, the account identifier the browser prompt and the platform's credential picker display for this passkey.
  • displayName (optional) — sets the WebAuthn user.displayName, a human-friendly name the prompt renders. Distinct from username and shown differently by the browser.

The SDK applies username and displayName in the browser, so the values never leave the device: Socure does not receive, store, or verify them, and they are not part of the ceremony result. Supplying them is still worth doing. When you omit them, the prompt falls back to the display name provisioned for your relying party, which is identical for every user and every credential, so nothing in the prompt tells one passkey from another.

Because you supply the relyingPartyId in two places, the service cross-checks the value passed here against the one bound into the passkeyUserToken and rejects the call if they disagree. This happens before any passkey is created, so a typo or a www. divergence between your workflow and your app fails immediately instead of storing a credential your assertion calls would never find.

The SDK, working with the Socure backend, prompts the user to create the passkey and returns a PasskeyResult with the passkeyResultId. Challenge generation, attestation validation, and credential storage all happen on the Socure backend, not in the SDK.

📘

Retrying:

The preceding cross-checks run before the passkeyUserToken is spent, so you can correct a mismatched relyingPartyId or account and retry the call with the same token. Once the ceremony begins, the token is spent, so any failure past that point needs a new passkeyUserToken from a fresh registration workflow call. Retrying registerPasskey() with a spent token is rejected exactly as an expired one is, so treat "invalid or expired token" as "request a new one," not as "retry."

Step 3 — Verify registration with RiskOS™

Submit the passkeyResultId to RiskOS™ through the Passkey Verify step to confirm the credential was stored and belongs to the customerUserId you registered. It returns an outcome of verified or failed, so a registration whose ceremony did not validate comes back as failed rather than as an error, and no credential was stored in that case. The response also echoes the passkeyUserId, which matches the one returned when the token was minted. The passkeyResultId is single-use, with an approximately 5-minute TTL; submit it promptly, as an expired or already-redeemed ID returns 404.

Send the passkeyResultId in the data.custom block, along with the same rp_id and customer_user_id used to start registration:

{
  "data": {
    "custom": {
      "rp_id": "{{rpId}}",
      "customer_user_id": "{{customerUserId}}",
      "passkey_result_id": "{{passkeyResultId}}"
    }
  }
}
FieldTypeRequiredDescription
data.custom.rp_idStringRequiredThe relying party the passkey was registered under.
data.custom.customer_user_idStringRequiredThe user the credential belongs to.
data.custom.passkey_result_idStringRequiredThe passkeyResultId from the registration ceremony. Single-use, ~5-min TTL.

Register more than one device

A passkeyUserToken is single-use, but the user's identity is not. Requesting a token for a customerUserId that has already registered resolves to the same underlying user, so each additional device is one more registration workflow call, one more registerPasskey(), and one more stored credential. Later registrations do not replace earlier ones, and the service imposes no limit on how many a user holds.

If you can, supply a username. The credential picker labels each passkey with the user.name it was created with, so a user who holds passkeys for more than one of your accounts sees identical entries unless you give them something to tell apart.


Phase 2 — Assertion

Assertion maps to your assertion workflow.

sequenceDiagram
    participant App as Your app
    participant SDK as DI Web SDK
    participant RiskOS as RiskOS™ workflow
    participant Backend as Socure Passkey Service
    App->>SDK: assertPasskey(relyingPartyId)
    SDK->>Backend: Start assertion ceremony
    Backend-->>SDK: WebAuthn options + challenge
    SDK-->>App: PasskeyResult (passkeyResultId)
    App->>RiskOS: Run assertion workflow (passkeyResultId)
    RiskOS->>Backend: Usernameless verify against account, rpId
    Backend-->>RiskOS: outcome (verified | failed) + passkeyUserId
    RiskOS-->>App: outcome + passkeyUserId
    App->>App: Map passkeyUserId to your user

Step 1 — Run the passkey assertion through the SDK

SigmaDeviceManager.assertPasskey(relyingPartyId: string): Promise<PasskeyResult>
  • relyingPartyId — your domain, the relying party to assert against. Must match the relyingPartyId the passkey was registered with. There is no token on this call, so this value alone identifies the relying party; passing a different one finds none of the credentials registered under the original.

The SDK, working with the Socure backend, prompts the user to select their passkey and returns a PasskeyResult with a single-use passkeyResultId. Challenge generation and assertion validation happen on the Socure backend, not in the SDK.

Step 2 — Usernameless Verify with RiskOS™

Submit the passkeyResultId to your RiskOS™ assertion workflow through the Passkey Usernameless Verify step. You do not supply a customerUserId: assertPasskey() takes no user identifier, the user selects the passkey, and the credential itself identifies them. RiskOS™ redeems the result against the Passkey service and, when the ceremony validated, returns the passkeyUserId the credential belongs to. Map that passkeyUserId to the user record you stored it against at registration, then continue your flow for that user without re-collecting identity.

Do not send customer_user_id on a usernameless verify call; it is rejected. If you already have the user in hand and want a yes/no confirmation instead of discovery, use the Verify step instead (as in registration Step 3).

Send the passkeyResultId in the data.custom block, with the rp_id the passkey was registered under:

{
  "data": {
    "custom": {
      "rp_id": "{{rpId}}",
      "passkey_result_id": "{{passkeyResultId}}"
    }
  }
}
FieldTypeRequiredDescription
data.custom.rp_idStringRequiredThe relying party the passkey was registered under.
data.custom.passkey_result_idStringRequiredThe passkeyResultId from the assertion ceremony. Single-use, ~5-min TTL.

Step 3 — Handle the decision

  • If the result verified, RiskOS™ returns the passkeyUserId. Map it to your user record and let that user proceed. If you do not recognize the passkeyUserId (no stored mapping), treat the user as unrecognized and fall back to your full verification or registration flow.
  • If the user cannot prove possession, RiskOS™ drops into phone entry and step-up before a final decision, as drawn in the RiskOS™ workflow.

When you redeem a passkeyResultId through Passkey Usernameless Verify, RiskOS™ returns the result. The response includes the passkeyResultId, the relyingPartyId (rpId), the completion timestamp (completedAt), an outcome field, and a passkeyUserId. The outcome is one of:

  • verified — The ceremony validated. passkeyUserId is populated with the identifier the credential belongs to; resolve it to your user.
  • failed — The ceremony did not validate, for example the credential was rejected or the app's origin is not on your provisioned list. passkeyUserId is not returned. Treat it as "not proven."

passkeyUserId is present only on a verified outcome; on failed it is absent.

Two conditions arrive outside the outcome field; handle both as failures:

  • No passkeyResultId — The ceremony never reached the backend: the user canceled, or no passkey was available, so the SDK call itself failed and there is nothing to redeem.
  • 404 on redemption — The passkeyResultId is past its approximately 5-minute TTL, was already redeemed, never existed, or the relyingPartyId does not match the ceremony.
📘

Note:

expired is reserved in the redemption contract but is not emitted today; expiry surfaces as a 404. Do not branch on it.


Troubleshooting

SymptomCause and resolution
registerPasskey() is rejected before any browser prompt appearsThe relyingPartyId passed to the SDK does not match the one bound into the passkeyUserToken, or the relying party is not provisioned for your account. Both are configuration mistakes, and both leave the token unspent, so correct the value and retry with the same token.
The browser prompt appears but the SDK call failsThe relyingPartyId is not a registrable suffix of the origin serving your app. The browser enforces that relationship locally, so the ceremony is rejected before it reaches Socure and no passkeyResultId is produced. An origin that is a valid suffix but simply not on your provisioned allow-list is different: that ceremony completes and redeems to a failed outcome, not an SDK failure.
The ceremony fails on one of your domains but works on anotherThat origin is probably not under the relyingPartyId it is running against. An origin must be the relying party or a subdomain of it, so a second registrable domain needs its own relying party and its own registrations.
Registration succeeded but assertion finds no passkeyThe relyingPartyId at assertion differs from the one used at registration, a www. divergence being the usual cause. Credentials are scoped to the exact relying party.
Redemption returns 404The passkeyResultId is older than approximately 5 minutes, was already redeemed, never existed, or the relyingPartyId does not match the one the ceremony ran under.
Redemption returns a failed outcomeThe ceremony did not validate: the credential was rejected, or the app's origin is not on your provisioned list. In Verify mode, failed also covers a valid credential that belongs to a different customerUserId than the one you submitted; if you expected verified there, confirm you submitted the same customerUserId and relyingPartyId the passkey was registered under.
Usernameless Verify returns verified but a passkeyUserId you do not recognizeThe user registered under a passkeyUserId you never stored, or your mapping is missing. Confirm you persist the passkeyUserId from the Passkey Registration response at enrollment. Treat an unknown handle as an unrecognized user and fall back to full verification.
A second device cannot registerConfirm you requested a fresh passkeyUserToken; the previous one is spent.

Related


Did this page help you?