Skip to content

Return the raw attestation object from passkeys register - #612

Open
nidal-augment wants to merge 3 commits into
stytchauth:mainfrom
augmentcorp:nidal/expose-attestation-object
Open

Return the raw attestation object from passkeys register#612
nidal-augment wants to merge 3 commits into
stytchauth:mainfrom
augmentcorp:nidal/expose-attestation-object

Conversation

@nidal-augment

Copy link
Copy Markdown
Contributor

Motivation

Relying parties increasingly want to show users where each passkey is stored — "Apple Passwords", "1Password", "Google Password Manager" — in account-management UIs. Without that, users with multiple passkeys are guessing which is which, and the generic names that result are an industry-wide complaint.

The structured signal for this is the AAGUID: a 16-byte identifier of the credential manager, embedded in the attested credential data of the WebAuthn attestation object. The major passkey providers deliberately populate it (even with none attestation) precisely so apps can label credentials, and there's a community-maintained AAGUID → provider-name/icon mapping that GitHub and others use for exactly this.

Two properties make this a registration-time-only concern:

  1. The protocol only sends it once. Attested credential data is present in the registration ceremony's authenticator data; sign-in assertions never include it (the AT flag is unset).
  2. Stytch doesn't store it. The WebAuthn registration object exposes domain, user_agent, authenticator_type, name, verified — no AAGUID, and no way to retrieve the original attestation later.

Today the SDK receives the attestation object during StytchClient.passkeys.register, POSTs it to Stytch, and discards it — so the one moment this data exists is invisible to callers.

Changes:

  1. register now returns the raw attestation object alongside the response: (response: BasicResponse, attestationObject: Data). It's the same value the SDK already holds from credential.rawAttestationObject; nothing changes on the wire or in the ceremony.
  2. Sourcery async variants regenerated; testRegister asserts the passthrough.

Net diff is 9 lines across 3 files.

Why raw, rather than parsed?

We considered the deeper version: parse the CBOR in the SDK and expose aaguid/backup-state fields, or go further and bundle the community AAGUID list so the SDK returns provider names directly. We deliberately kept this PR to the raw bytes:

  • Returning the data takes no opinion — callers decide whether they want the AAGUID, the BE/BS backup flags, extensions, or nothing. That matches the SDK's general ethos of exposing the underlying information and letting the caller decide what to do with it.
  • Bundling the community mapping would make SDK releases the refresh cadence for third-party display data that goes stale as new providers ship — a maintenance commitment that doesn't belong in an auth SDK.
  • Parsing is a reasonable middle ground (spec-stable, no data file) — we're happy to follow up with an optional aaguid/backup-flags convenience if maintainers would take it, but it shouldn't gate the raw exposure.

Real-world validation

We (Augment) run this change in production via our fork: parse the attestation object client-side (AAGUID at authenticator-data offset 37 per WebAuthn §6.1), resolve it against the community list, and immediately rename the passkey via passkeys.update — e.g. "Apple Passwords — Jul 17, 2026" — giving users a passkey list that says where each credential lives. Verified end-to-end with real Apple Passwords registrations on device.

Note on API shape

As written this changes register's return type, which is source-breaking for existing callers. If you'd prefer a non-breaking shape (an additive overload, or a field on a response wrapper), we're glad to rework it — the essential ask is just: don't discard the attestation object.

Web parity

The JavaScript SDK's webauthn.register appears to have the same gap — the headless response carries only the registration ids, not the attestation response (callers driving navigator.credentials.create() themselves notwithstanding). For relying parties to build consistent passkey-management UIs across platforms, the JS SDK would need the equivalent change.

Checklist:

  • I have verified that this change works in the relevant demo app, or N/A — running in production in our app via our fork; verified with real Apple Passwords registrations
  • I have added or updated any tests relevant to this change, or N/A
  • I have updated any relevant README files for this change, or N/A — N/A

🤖 Generated with Claude Code

The registration ceremony's attestation object contains attested
credential data (notably the AAGUID identifying the passkey provider)
that exists only at registration time and is not stored by Stytch.
The SDK already holds it; return it alongside the response so callers
can parse what they need instead of it being discarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nidal-augment
nidal-augment requested a review from a team as a code owner July 17, 2026 15:35
@nidal-augment

Copy link
Copy Markdown
Contributor Author

For anyone wanting to consume this: the community-maintained AAGUID → provider mapping lives at passkeydeveloper/passkey-authenticator-aaguids — specifically aaguid.json, which maps each provider's AAGUID to a display name plus light/dark SVG icons (e.g. fbfc3007-154e-4ecc-8c0b-6e020557d7bd → "Apple Passwords", bada5566-a7aa-401f-bd96-45619a55120d → "1Password").

The extraction itself is small: CBOR-decode the attestation object ({fmt, attStmt, authData}), then in authData read the 16 bytes at offset 37 when the AT flag (bit 6 of the flags byte at offset 32) is set. An all-zero AAGUID means the authenticator declined to identify itself.

And to restate the parity point from the description: the JS SDK would need the same change (if it isn't already exposed there) so web apps can build the same passkey-naming experience — happy to file that as an issue on the JS repo if useful.

nidal-augment added a commit to augmentcorp/stytch-ios that referenced this pull request Jul 17, 2026
Mirrors the upstream proposal (stytchauth#612): the SDK
returns the ceremony's attestation object verbatim and takes no opinion
on interpretation. AAGUID/backup-flag parsing moves to the consuming
app, shrinking this fork's delta to what upstream should carry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nidal-augment

Copy link
Copy Markdown
Contributor Author

@ZhangYiJiang the initial test failure here was a flaky test.

@ZhangYiJiang

Copy link
Copy Markdown
Contributor

Okay I just looked through this and I think it would be better if it is an additive change, since I think if we're making the same change on the web side we'll definitely have to extend the object, not return a tuple, so it would be good for these to mirror each other. Let me know if that's a less common pattern in Swift, and sorry if this is a bit bikeshed-y

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants