This blog post explains WebAuthn Relying Party ID for passkey authentication. It outlines the right configuration, domains matching & native app configurations.
Vincent
Created: September 21, 2023
Updated: October 2, 2024
Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.
Passkey authentication is quickly becoming the norm as tech giants like TikTok, GitHub, and WhatsApp, X (Twitter), LinkedIn, and Amazon roll them out or already have so. Its evident that the tech world is recognizing the importance of simple and secure authentication.
Beyond the seamless user experience of authenticating with Face ID, Touch ID or Windows Hello, passkeys offer unparalleled security compared to traditional authentication methods like passwords. One of the standout features is the 100% phishing-resistance.
Want to try passkeys yourself? Check our Passkeys Demo.
Try PasskeysA phishing attack is an attack where the victim is tricked to provide credentials to a fake site that mimics to be the original site. For instance, imagine receiving an email from what seems to be your bank, asking you to log in. You click the link, and the website looks legitimate, so you enter your credentials and the attacker can use them to log into the original site. This is becoming an ever-increasing problem in the digital era and even major companies like Okta (an authentication provider!) or Retool have fallen victim to spear-phishing attacks (a special form of phishing where single victims are particularly targeted speared with very personal phishing tricks), emphasizing the need for robust security measures.
Contrary, if you use a passkey, and the site is a fake, your authentication will fail. This is because passkeys are bound to the domains they were created for via the Relying Party ID.
You cannot login with a passkey on any other site it is technically impossible it is a 100% phishing protection for the user.
This mechanism is baked into WebAuthn, the passkey- underlying web standard for passwordless authentication. WebAuthn is built on two core ceremonies: the registration and authentication ceremony.
During the registration (sign-up) ceremony a new passkey is created for an online service (the Relying Party) via a web or native app. In this process, the domain (Relying Party ID) for which the passkey is created is stored in the passkey.
This allows the authentication (login) ceremony to check if the online service (the Relying Party), that the web or native app belongs to, matches with the Relying Party ID stored in the passkey.
In the following, well see in detail how this domain matching process works and how, in particular, native apps are secured.
The Relying Party ID is essentially a domain stored within the passkey, ensuring
the passkey only works if the current browser URL (the users origin that is
automatically sent on every request) matches it (see the native app approach
below). It's a crucial component of the
WebAuthn specification, which you can delve into
here. The Relying Party ID can be the
root domain (e.g. corbado.com
) or a subdomain (auth.corbado.com
). You cannot
store the root domain as Relying Party ID it if it is on the public suffix
list (find the list and libraries for public suffix detection
here). Changing the Relying Party ID for an
online service will break existing passkeys (the only exception: the new
Relying Party ID is a subdomain of the old Relying Party ID).
During the authentication process, the Relying Party ID is checked against the browser URL (user's origin) to ensure they match. Matching in this sense means that either:
com
or any domain on the Public Suffix List don't work)Here is a detailed outline which originalHost(second column) is allowed to access its parent domain:
In the following, you see the parsed PublicKeyCredentialCreationOptions:
rp
stands for Relying Party.
One of the major benefits of the Relying Party ID is the prevention of phishing attacks. Imagine the following scenario:
paypal.com
(here it tries to trick you) and asks you to sign it with your passkey for the Relying Party ID paypal.com.paypal.com
) and the Relying Party ID it stored in the passkey (paypal.com
) match.In the case of a native app, the origin is the native app itself, e.g. for iOS apps <App Identifier Prefix>.<BundleIdentifier>
. Here, its checked if there is an association between your locally installed native app and the server that needs to provide the corresponding association file (see below).
Become part of our Passkeys Community for updates and support.
JoinTo implement passkeys in a native app, a developer can decide between adding them via WebView or natively. Lets examine the benefits and disadvantages of both approaches in the following.
Using a WebView* to integrate passkeys means embedding a web browser within the native app to handle the authentication process. This approach essentially displays a web page inside the app, making it easier to reuse web- based authentication flows without having to rewrite them for the native platform. However, there are some drawbacks. WebViews might not support all passkey features, and there's a potential risk of "man-in-the-middle" attacks if not implemented securely. Additionally, the user experience might not be as smooth as with native integrations, and there can be challenges in maintaining consistent behavior across different devices and OS versions.
*There are multiple types of WebViews : On iOS (WKWebView, SFSafariViewController or SFAuthenticationSession / ASWebAuthenticationSession for OAuth/OpenID Connect based authentication flows) and Android (WebView, CCT-Chrome Custom Tabs). See this blog post for details. We recommend to use SFSafariViewController/ ASWebAuthenticationSession and Chrome Custom Tabs in the context of passkeys if you do not want a native integration.
Want to find out how many people can use passkeys?
View Adoption DataNative integration involves building the passkey functionality directly into the iOS or Android app using platform-specific APIs and libraries. This method offers a more seamless user experience, as there's no need to transition between the native app and a WebView. It also allows for better performance and a more consistent look and feel. From a security standpoint, the native integration can offer enhanced protection against certain types of attacks, especially when combined with platform-specific security features. However, the implementation effort can be higher, as developers need to write and maintain separate code for each platform (Android and iOS). Additionally, staying updated with the latest passkey / WebAuthn specifications might require more frequent app updates.
In the following, we focus on the native passkey integration.
Are your users passkey-ready?
Test Passkey-ReadinessNative apps (e.g. iOS or Android apps) present a challenge compared to web apps. Unlike web apps, there's no browser URL to match against the Relying Party ID. Nevertheless, to ensure the same level of security, domains are connected to native apps via association files , so that trust between a domain and a native app is established.
This is particularly important if a passkey was created on a web app and should be used for the same Relying Party ID on a native app (and vice versa).
iOS uses the apple-app-site-association file. This file contains various entitlements, but for WebAuthn and passkeys, the webcredentials entitlement is important.
In webcredentials.apps, you need to store your Application Identifier Prefix
(e.g. 9RF9KY77B2
) and your Bundle Identifier (e.g. com.corbado.passkeys
).
For iOS native apps to work, the apple-app-site-association file must be
stored under the Relying Party IDs /.well-known
directory (https://<Relying Party ID>/.well-known/apple-app-site-association
).
See a live example here.
Android uses the assetlinks.json
file, which, like its iOS counterpart,
requires particular configurations for WebAuthn and passkeys.
You need to have the relation values
delegate_permission/common.handle_all_urls
and
delegate_permission/common.get_login_creds
set. Besides, you need to add
your package name and the SHA-256 fingerprint of your signing certificate.
To allow the sharing of a passkey between a native app and a web app, you need to add two entries. One for the namespace web and one for the namespace android_app.
See a live example here.
For Android apps to work, the assetlinks.json file must be stored under the
Relying Party IDs /.well-known
directory https://<Relying Party ID>/.well- known/assetlinks.json
- so pretty much like on iOS).
Check out this blog post to see a sample implementation that shares passkeys between a native Android / iOS app and a web app.
Want to experiment with passkey flows? Try our Passkeys Debugger.
Try for FreeThe process to establish trust between an iOS app and web app looks as follows:
Every time the iOS app is installed or updated, iOS will download the apple-app-site-association file for each entry of the iOS apps entitlement list.
When a credential (e.g. passkey) is created inside an iOS app, the iOS app validates if the relying party servers domain is associated with the iOS app by checking the following two aspects:
apple-app-site-association
file for this relying party servers domain existing on the device?apple-app-site-association
file?If, and only if, both questions can be answered with yes, a passkey can be created within the iOS app.
Ben Gould
Head of Engineering
I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.
10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.
Join Passkeys CommunityThe process to establish trust between an Android app and web app looks as follows:
The Android app developer has to specify a list of domains he wants to
associate with the Android app. These domains are stored as targets with the
namespace web in the assetlinks.json file. To declare that Android apps
and web apps share credentials,
delegate_permission/common.get_login_creds
needs to be specified. Find
details here.
If a passkey is created inside the Android app, the Android app validates
if the Relying Party ID is associated with the Android app by checking the
assetlinks.json
:
assetlinks.json
file for this Relying Party ID at https://<Relying Party ID>./well-known/assetlinks.json
Subscribe to our Passkeys Substack for the latest news, insights and strategies.
SubscribeIn the following, we provide a detailed overview for the different settings that are required to properly set up passkey authentication for native apps.
Feature | iOS | Android |
---|---|---|
Official implementation guidance for native passkey authentication | Apple Developer | Google Developer |
Allows sharing passkeys with web apps | Yes, via apple-app-site-association file | Yes, via assetlinks.json |
Location of association file | https://acme.com/.well-known/apple-app-site-association | https://acme.com/.well-known/assetlinks.json |
File cached | Yes (since iOS 14), initial sync may take up to 24h | Yes (by Play Services) |
By-pass possible | Yes, with alternate mode section | No |
Testable with | apple-app-site-association test | assetlinks.json test |
App identifier with sample | <Application Identifier Prefix>.<Bundle Identifier> , e.g. T84QZS65DQ.com.facebook.Messenger | SHA256 fingerprint,e.g. E3:F9:E1:E0:CF:99:D0:E5:6A:05:5B:A6: 5E:24:1B:33:99: 7F:CE:A5:24:32: 6B:0C:DD:6E:C1:32: 7E:D0:FD:C1 |
Where to find app identifier | The Team Application Identifier Prefix can be found in the developer account on developer.apple.com and the Bundle Identifier is the exact name from within the XCode project | When already uploaded: Google Play Console > Release management > App signing Local: keytool -list -v -keystore <keystore path> -alias <key alias> -storepass <store password> -keypass <key password> |
Name of section that links app to web | applinks (not required for passkeys) | delegate_permission/common.handle_all_urls (required for passkeys) |
Name of section that allows credential sharing between app / web | webcredentials | delegate_permission/common.get_login_creds |
Registry of all association files | Enable and add associated domain in XCode development environment (setting of property to generate entitlements file) | When using the Credential Manager API, the registry of the assetlinks.json in manifest is not required for passkeys (for passwords it is though). When not using the Credential Manager API, you need to list the hostnames with <data> -entry in AndroidManifest.xml in the specific activity (part of Android-App source Code). The <intent-filter android:autoVerify="true"> needs to be autoVerify=true. |
For Flutter, the respective rule of Android or iOS applies. The only Flutter- specific setting is the registry of association files, where you should add:
As we experienced ourselves, working with Relying Party IDs, different levels of (sub-)domains and CNAMEs can be quite a challenging task, we present four distinct examples and explain why and how they work.
Note, that the CNAME table row is not required for passkey authentication and just a result of our research that we wanted to add.
Relying Party ID | corbado.com |
---|---|
Entitlements (iOS only) | webcredentials:corbado.com |
Location apple-app-site-association file | https://corbado.com/.well-known/apple-app-site-association |
Location assetlinks.json file | https://corbado.com/.well-known/assetlinks.json |
CNAME | n/a |
In this example, the apple-app-site-association
/ assetlinks.json
file for
corbado.com can be downloaded without any issues when the native app is
installed / updated, because the file is on the same location as the Relying
Party ID.
A passkey for the Relying Party ID can be created.
Relying Party ID | auth.corbado.com |
---|---|
Entitlements (iOS only) | webcredentials:auth.corbado.com |
Location apple-app-site-association file | https://pro-123.passkeys.eu/.well-known/apple-app-site-association |
Location assetlinks.json file | https://pro-123.passkeys.eu/.well-known/assetlinks.json |
CNAME | auth.corbado.com => pro-123.passkeys.eu |
In this example, the apple-app-site-association
/ assetlinks.json
file for
auth.corbado.com can be downloaded without any issues when the native app is
installed / updated, because the file is on the location as the Relying Party
ID, as the CNAME points from the Relying Party ID to the stored location.
A passkey for the Relying Party ID can be created.
Relying Party ID | corbado.com |
---|---|
Entitlements (iOS only) | webcredentials:corbado.com; webcredentials:auth.corbado.com |
Location apple-app-site-association file | https://pro-123.passkeys.eu/.well-known/apple-app-site-association |
Location assetlinks.json file | https://pro-123.passkeys.eu/.well-known/assetlinks.json |
CNAME | auth.corbado.com => pro-123.passkeys.eu |
In this example, the apple-app-site-association
/ assetlinks.json
file for
auth.corbado.com can be downloaded without any issues when the native app is
installed / updated, because the file is, due to the CNAME, on the location,
where auth.corbado.com
expects it.
BUT: The apple-site-association-file
/ assetlinks.json
for corbado.com cannot
be downloaded, when the native app is installed / updated, because the file is
not at https://corbado.com/.well-known/apple-app-site-association
/
https://corbado.com/.well-known/assetlinks.json
, where it is expected and no
CNAME is pointing to it.
A passkey for the Relying Party ID cannot be created.
Relying Party ID | auth.corbado.com |
---|---|
Entitlements (iOS only) | webcredentials:*.corbado.com |
Location apple-app-site-association file | https://corbado.com/.well-known/apple-app-site-association |
Location assetlinks.json file | https://corbado.com/.well-known/assetlinks.json |
CNAME | n/a |
In this example, the apple-app-site-association
file for corbado.com can be
downloaded without any issues, when the native app is installed / updated,
because the file is where its expected and the webcredentials entitlement
(*.corbado.com
) matches the Relying Party ID (auth.corbado.com
). Note that
this example does not work for Android, as Android does not work with
something like (wildcard) entitlements. In general, this way of defining
Relying Party IDs is not recommended.
A passkey for the Relying Party ID can be created.
Become part of our Passkeys Community for updates and support.
JoinMistake:
You started developing and defined a subdomain (e.g. login.acme.com
) as your
Relying Party ID. First users created a passkey for this Relying Party ID.
Then, you notice that you also need these passkeys for authentication on
another subdomain (e.g. app.acme.com
). As the origin of a user and the Relying
Party ID for the new subdomain dont match, the user cannot sign-in with the
passkey. Changing the Relying Party ID in your WebAuthn settings to acme.com
would invalidate all existing passkeys, as the new origin and the Relying
Party ID stored in the existing passkeys do not match.
Solution:
Double-check initially when defining your Relying Party ID as this is more or less final. If you are unsure and want to be future-ready, meaning that other subdomains in the future might need the passkey for authentication, we recommend to use the root domain (e.g. acme.com) as Relying party ID unless it is on the Public Suffix List.
Mistake:
Youre developing a native and web app simultaneously. To speed things up, you use two different WebAuthn servers (with different Relying Party IDs for the native and web app). As your users create the first passkeys, the respective Relying Party ID is stored in the passkey. Allowing cross-device / cross-platform login with the same passkey, e.g. with a passkey created in a web app and trying to sign-in in a native app, is not possible anymore. Merging the two WebAuthn server will abandon the passkeys which were registered with the old WebAuthn server (old Relying Party ID) and your users cannot login with these passkeys.
Solution:
If you have a multiple applications (e.g. a web app and a native app), always have only one WebAuthn server and define only one Relying Party ID for all your apps. Linking between these app can be done via the steps described above.
Mistake:
You start developing your application, configured the association files and deployed them to your server. For some reason, you are still getting error messages and dont find the root cause.
Solution:
A potential cause for the error message might be a malformed or unreachable association file. Before deploying any association file to a server, we highly recommend to check the validity and reachability (often these files might be behind a robust VPN or firewall that prevents the proper access for the crawlers of Apple and Google) of an association file via the tools provided for iOS and Android.
association File Not Yet Cached by Apple CDN
Mistake:
You deployed your apple-app-site-association file to your server and want to immediately start creating a passkey on your test device. For some reason, you cannot create the passkey and get error messages.
Solution:
The reason behind these error messages is that iOS devices download the apple- app-site-association
file to validate the Relying Party. To do so, iOS devices do not send a direct request to your server but
use a CDN instead. Both the device and the CDN cache the apple-app-site- association
file after it has been successfully retrieved. Due to this caching
functionality, new changes in your apple-app-site-association
file are not
directly reflected in your app. It can take up to 24 hours until the CDN
cached the apple-app-site-association
file. To circumvent this restriction
during development, you can append ?mode=developer
to the Relying
Party ID and disable caching completely (e.g. the Relying Party ID would be
acme.com?mode=developer
).
Mistake:
You start developing an Android app and want to test it on an Android emulator. For some, reason youre getting error messages, even though youve setup the Android emulator properly and other apps seem to work smoothly on it.
Solution:
Android versions, Play Store support and API versions play a major role when testing a passkey application. Besides, you need to be logged into a Google account. Please refer to our troubleshooting section for details.
Our overall recommendation is to choose your Relying Party ID carefully based on your application landscape and requirements. Below, we have collected the most common use cases, but our general recommendation is that you should aim to choose your root domain as your Relying Party ID and configure your authentication this way. With Corbado, we've also already pre-configured it this ways for you (as its part of our approach to offering seamless passkey authentication for all technical setups. Our UI components and SDKs are prepared to be used with your root domain as relying Party ID).
Case | Recommendation |
---|---|
A) You have one root domain: Example: acme.com All applications and authentication runs on this root domain or subdomains of it | ✔️ Choose the root domain as your Relying Party ID as this won’t cause any problems for web or native apps. |
B) You have multiple root domains: Example: kayak.com, kayak.co.uk, kayak.de You serve your users from different international top-level domains. Kayak.com for USA and kayak.co.uk for the UK or you have completely different root domains that should allow the same users to login with the same passkeys. | ❌ On your web apps you cannot share the passkeys. There is no solution to sharing passkeys in the web. You would need to at least choose a common authentication root domain. ✔️ You can connect your native apps to any number of root domains as long as you have control over the root association files. ❌ In case you want to later migrate to another root domain to host your website, you will not be able to use your already created passkeys, because you will have to rebrand and change the domain (Relying Party ID). |
C) You do NOT have a root domain yet, you are running on backend only or on a public subdomain. There are some cases where this might happen: 1. You work on a freely available subdomain, where the root domain is not under your control (the root domain is listed in the https://publicsuffix.org/) for example CDN URLs 2. You work on a native app. | ❌ On public subdomains, you cannot control the association files on the root level of the root domain Therefore, passkeys will not work natively. ⚠️ The only way to fix this for some services is to change to a paid plan, where you can define a CNAME or get a custom root domain for yourself. |
In the following, we provide a very specific decision tree that should help you determine the right Relying Party ID and how should handle / host association files when using Corbado as your passkey solution.
The first decision is if you are in a development or production environment. The next level of decision you have to make is based on your application landscape: do you only have a native app or a native and web app.
For the development environment, we assume that you want to quickly start developing and testing. The Relying Party ID can be changed later if you want to go live.
pro-XXX.frontendapi.cloud.corbado.io
(default value)It's not easily possible to test both the web app and native app at the same time
Option 1:
Either you set Relying Party ID = pro-XXX.frontendapi.cloud.corbado.io
(native app
works) OR set Relying Party ID = localhost
(web app works)
Option 2:
The only solution for native and web app to work at the same time is to use a local reverse proxy (it's a rather hacky solution):
acme-dev.com
acme-dev.com
=> pro-XXX.frontendapi.cloud.corbado.io
/etc/hosts
entry localhost acme-dev.com
acme-dev.com
=> localhost:3000
(as an example)In the production environment, you have to decide if you are fine with a
subdomain as Relying Party ID (e.g. auth.acme.com
) or if you want a root
domain as Relying Party ID (e.g. acme.com
)
auth.acme.com
auth.acme.com
=> pro-XXX.frontendapi.cloud.corbado.io
auth.acme.com
auth.acme.com
=> pro-XXX.frontendapi.cloud.corbado.io
(also needed for cookies to work if you use Corbado's session management)acme.com
acme.com/.well-known/<association file>
acme.com
acme.com/.well-known/<association file>
auth.acme.com
=> pro-XXX.frontendapi.cloud.corbado.io
to make cookies work (this CNAME is not needed for the Relying Party ID solely for session management)The Relying Party ID is a cornerstone of WebAuthn and passkey-based authentication and helps to prevent 100% of phishing attacks. Properly configuring it, understanding domain matching intricacies, and ensuring correct deployment for native apps is crucial. This blog post showed you how to set them up correctly and how to handle different mistakes. For further insights into setting up passkeys for native app, we recommend to read about passkeys in Flutter.
If you have further questions or need assistance, don't hesitate to reach out.
Table of Contents
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.
We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour
Start for free