Learn how origin validation works in WebAuthn for native iOS and Android apps - incl. Android SHA-256 fingerprints, AASA files and server-side trust.
Amine
Created: April 24, 2025
Updated: April 30, 2025
When implementing passkeys with WebAuthn, validating the origin is a critical security step. It ensures your users authenticate only through trusted apps or websites. But here's the catch: validating origins becomes particularly challenging when dealing with native mobile apps. Unlike web apps, native apps don't naturally have a URL-based origin, making the validation process both different and potentially tricky.
This raises two fundamental questions that we'll tackle in this post:
Before we dive deeper, a quick context check: this article assumes you're already familiar with basic WebAuthn concepts — particularly the roles of the Relying Party (RP) and Relying Party ID (RPID). If you're new to these concepts or need a refresher, check out our previous blog post that covers the foundational elements of WebAuthn.
Ready? Let's dig in!
In web development, the term origin refers to the unique combination of the protocol, domain, and port from which content is served. It’s essentially a security boundary for web browsers, defining how web applications interact with each other (for a detailed definition, see MDN’s Glossary on Origin).
Example: https://example.com
Note: the protocol/domain/port origin model applies only to web apps.
For native apps, origin is set up differently—as we explain in the next sections.
In WebAuthn, the concept of origin is essential for ensuring authenticity and
security. When a passkey is created or used, the clientDataJSON
—sent from the user's
device to your server—explicitly includes the origin. This helps your server verify
that the authentication request originated from a trusted source.
Here's a simplified example of how the origin might appear within the
clientDataJSON
:
{ "type": "webauthn.create", "challenge": "rGhL28M9u...", "origin": "https://example.com", "crossOrigin": false }
To inspect and understand exactly how this data looks in real-world scenarios, consider
using the
Corbado Passkeys Debugger,
which decodes and clearly displays clientDataJSON
:
Native apps typically don't have a conventional origin like web apps (e.g.
https://www.example.com
). This absence of a standard, URL-based origin means
specialized validation measures are required on platforms such as
Android and
iOS, which we'll explore in detail in the upcoming
sections.
Since native Android apps don't have a traditional URL-based origin, Android relies on the SHA‑256 digest of the APK’s signing certificate ("fingerprint") — as the foundation for origin validation. You can obtain the fingerprint with:
keytool -list -v -keystore my-release-key.jks | grep SHA256:
For instance, a typical SHA-256 fingerprint looks like this:
8B:BF:39:60:61:89:30:A4:45:F3:D7:09:1E:7B:1B:05:0F:8A:FD:AF:24:EB:F1:EB:2E:3D:13:88:09:FC:79:59
To generate this fingerprint for your Android app, refer to platform-specific guides such as the official Flutter documentation or the Android developer documentation.
Before using this fingerprint for forming the final WebAuthn origin, you need to transform it by hex decoding the SHA-256 fingerprint and then base64 encoding the result without padding.
Here's a JavaScript snippet demonstrating this transformation:
function hexToBase64NoPadding(hexWithColons) { const hex = hexWithColons.replace(/:/g, ""); const buffer = Buffer.from(hex, "hex"); return buffer.toString("base64").replace(/=+$/, ""); }
You can adapt this logic to other languages easily (consider using ChatGPT or your preferred tool for translation).
If we apply this transformation to the example fingerprint provided earlier, we get the following base64-encoded string without padding:
i78xYGGJMKRF89cJHnsbBQ+K/a8k6/HrLj0TiAn8eVk
The origin string format for Android is as follows:
android:apk-key-hash:<base64-string-without-padding-of-fingerprint>
The resulting final origin string, based on the example fingerprint, then is:
android:apk-key-hash:i78xYGGJMKRF89cJHnsbBQ+K/a8k6/HrLj0TiAn8eVk
This is the origin that your server must trust and validate when processing WebAuthn registrations or logins from an Android app.
Unlike Android, native iOS apps leverage the RPID
directly to form the WebAuthn origin: https://
+ RPID
Since iOS apps don't have a browser-based origin, trust must be explicitly established between the app and the domain identified by the RPID through Apple's standardized mechanism known as the Apple App Site Association (AASA) file.
To validate your app’s origin on iOS, you must serve an Apple App Site Association
file from your domain. For example, if your RPID is example.com
, the AASA file must be
accessible via:
https://example.com/.well-known/apple-app-site-association
This JSON file explicitly associates your iOS app with your domain, allowing the server-side verification to recognize and trust requests originating from the native app.
If you're new to configuring this file or need additional details, refer back to our previous blog post covering Apple App Site Association in more depth.
Once a user completes a passkey registration or login using a
native app, the WebAuthn ceremony sends a payload back to
your server. One of the critical components in this payload is the clientDataJSON
object
— and this is where origin validation begins.
When your server receives a WebAuthn request (for either registration or authentication),
it extracts and parses the clientDataJSON
. Among other metadata, this JSON includes the
origin
— essentially, the identity of the app or website that initiated the request.
Your job as a relying party is to validate that this origin matches one of the origins you explicitly trust. If the origin doesn’t match, the request must be rejected to prevent spoofing or phishing attempts.
Native apps complicate things a bit because — as you've seen — Android and iOS each have their own way of representing origins:
android:apk-key-hash:<base64-string-without-padding-of-fingerprint>
for its origin (e.g.
android:apk-key-hash:i78xYGGJMKRF89cJHnsbBQ+K/a8k6/HrLj0TiAn8eVk
)https://
+ RPID
(e.g. https://example.com
)Therefore, you must configure both origins on your server — one for each platform — to support passkey flows across iOS and Android.
To assist you in setting up your server-side library (relying party), we provide guidance on configuring multiple origins for the following libraries and languages:
Go: go-webauthn
webauthnConfig := &webauthn.Config{ RPDisplayName: "Example App", RPID: "example.com", RPOrigins: []string{ "https://example.com", // iOS (RPID: example.com) "android:apk-key-hash:i78xYGGJMKRF89cJHnsbBQ+K/a8k6/HrLj0TiAn8eVk", // Android }, }
In go-webauthn
, all origins listed in RPOrigins
will be matched against the origin
sent in clientDataJSON
. Even though Android origins are not explicitly mentioned in
the docs, the library accepts them as long as they match exactly.
JavaScript/TypeScript:
SimpleWebAuthn
const expectedOrigins = [ "https://example.com", // iOS (RPID: example.com) "android:apk-key-hash:i78xYGGJMKRF89cJHnsbBQ+K/a8k6/HrLj0TiAn8eVk", // Android ]; verifyRegistrationResponse({ credential: parsedCredential, expectedChallenge: challenge, expectedOrigin: expectedOrigins, expectedRPID: "example.com", });
SimpleWebAuthn optionally supports verifying registrations from multiple origins and RP IDs! Simply pass in an array of possible origins and IDs for expectedOrigin and expectedRPID respectively.
Relevant docs:
verifyRegistrationResponse
verifyAuthenticationResponse
PHP: web-auth/webauthn-framework
To support native apps in addition to the web origin, you need to extend the
origin verification logic. As of version 4.8+, webauthn-framework
introduced more
flexible input validation to accommodate non-web origins. The maintainers have made it
possible to “hook into and whitelist origins” in custom code
(GitHub issue).
There isn’t simply a config file where you list allowed origins in webauthn-framework
(as of v4.x/5.0), but you can achieve it with a small amount of code:
$allowedOrigins = [ "https://example.com", // iOS (RPID: example.com) "android:apk-key-hash:i78xYGGJMKRF89cJHnsbBQ+K/a8k6/HrLj0TiAn8eVk", // Android ]; $publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check( $credentialId, $authenticatorAssertionResponse, $publicKeyCredentialRequestOptions, $request, $userHandle, $allowedOrigins );
Hybrid apps using WebViews require special attention:
assetlinks.json
at:https://your-domain.com/.well-known/assetlinks.json
apple-app-site-association
file at:https://your-domain.com/.well-known/apple-app-site-association
Note: WebAuthn only works in WebViews when loaded over HTTPS from a verified domain that’s linked to the app via platform trust mechanisms.
Different build environments require distinct configurations:
Origin validation is one of the most critical pieces of the WebAuthn puzzle—especially when extending passkey support to native mobile apps. As we’ve seen, both Android and iOS diverge from the browser-based model, each with their own mechanisms for establishing trust:
apk-key-hash
origin.https://
+
RPID, with trust being established via the Apple App Site Association mechanism.Understanding and correctly validating these origins on the server is essential to prevent spoofed or malicious authentication requests.
https://
origin format.android:apk-key-hash:<base64-string-without-padding-of-fingerprint>
for its origin.https://
+
RPID, with trust being established via the Apple App Site Association mechanism.go-webauthn
, SimpleWebAuthn
, or web-auth/webauthn-framework
depending on your
stack.If you're looking to go deeper:
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
Related Articles
Table of Contents