DEV Community

Cover image for Config, Domains & Native Apps for WebAuthn rpID
vdelitz for Corbado

Posted on • Edited on • Originally published at corbado.com

Config, Domains & Native Apps for WebAuthn rpID

1. Introduction

2. What is the Relying Party ID?

3. The Two Different Passkey Integration Options for Native Apps

3.1 Passkey Integration via WebView

3.2 Native Passkey Integration

4. Configuring the Relying Party for Native Apps

4.1 Association Files on iOS: apple-app-site-association

4.2 Association Files on Android: assetlinks.json

5. Establish Trust between a Native App and Web App

5.1 iOS

5.2 Android

6. Overview for Android, iOS and Flutter Settings

7. Examples for Valid and Invalid Relying Party ID and Association Files

8. Common Relying Party ID Mistakes and how to avoid them

8.1 Changing Relying Party ID from Subdomain to Root Domain

8.2 Different Relying Party IDs for Native and Web app

8.3 Invalid and unreachable Association Files

8.4 apple-app-site-association File not yet cached by Apple CDN

8.5 Android Emulator and API Version Incompatibility

9. Recommendation

10. Conclusion

1. Introduction

Passkey authentication is quickly becoming the norm as tech giants like TikTok, GitHub, and soon-to-be WhatsApp, X (Twitter), LinkedIn, and Amazon roll them out or already have so. It’s 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.

A 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, we’ll see in detail how this domain matching process works and how, in particular, native apps are secured.

2. What is the Relying Party ID?

The Relying Party ID is essentially a domain stored within the passkey, ensuring the passkey only works if the current browser URL (the user’s 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 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:

  • The browser URL (user’s origin) matches precisely Relying Party ID OR
  • The browser URL (user's origin) is a subdomain that matches the Relying Party ID and the parent domain is registrable (e.g. "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:

Parent Domain Original Host Matching

In the following, you see the parsed PublicKeyCredentialOptions:

“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:

  1. An attacker develops a PayPal clone which is a fake website that tries to steal your PayPal credentials to log in in the name of you and send money to the attackers account. This fake PayPal website is hosted on the domain paybal.com (so it’s often not visible that it’s a different domain at first sight).
  2. You have already created a passkey in the past for the legitimate PayPal site. This passkey stored the Relying Party ID “paypal.com”.
  3. You visit the PayPal fake website at paybal.com (as you visit this site, the origin of your request is paybal.com*) and the site sends your device (the authenticator) a challenge for the Relying Party ID “paypal.com” (here it tries to trick you) and asks you to sign it with your passkey for the Relying Party ID “paypal.com”.
  4. Your device (the authenticator) checks if the origin of the request (paybal.com) and the Relying Party ID it stored in the passkey (paypal.com) match.
  5. As they obviously do not match, the authentication fails and it saves you from giving some attacker access to your PayPal account.

*In the case of a native app, the origin is the native app itself, e.g. for iOS apps .. Here, it’s 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).

3. The Two Different Integration Options for Native Apps

To implement passkeys in a native app, a developer can decide between adding them via WebView or natively. Let’s examine the benefits and disadvantages of both approaches in the following.

3.1 Passkey Integration via WebView

Passkey Integration WebView

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). A soon to be published blog post will cover more details, specifically for passkey authentication. We recommend to use SFSafariViewController/ ASWebAuthenticationSession and Chrome Custom Tabs in the context of passkeys if you do not want a native integration.

3.2 Native Passkey Integration

Native Passkey Integration

Native 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.

4. Configuring the Relying Party for Native Apps

Native 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).

4.1 Association Files on iOS: apple-app-site-association

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 ID’s /.well-known directory (https:///.well-known/apple-app-site-association).

See a live example here.

4.2 Association Files on Android: assetlinks.json

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 ID’s /.well-known directory https:///.well-known/assetlinks.json - so pretty much like on iOS).

5. Establish Trust between a Native App and Web App

5.1 iOS

The process to establish trust between an iOS app and web app looks as follows:

  1. The iOS app developer has to specify a list of domains he wants to associate with the native app. These domains are hardcoded in the iOS app’s entitlements, e.g.:
  • webcredentials:auth.corbado.com
  • webcredentials:*.corbado.com
  1. Every time the iOS app is installed or updated, iOS will download the apple-app-site-association file for each entry of the iOS app’s entitlement list.

  2. When a credential (e.g. passkey) is created inside an iOS app, the iOS app validates if the relying party server’s domain is associated with the iOS app by checking the following two aspects:

  • Is there an apple-app-site-association file for this relying party server’s domain existing on the device?
  • Is the iOS app listed in that 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.

5.2 Android

The process to establish trust between an Android app and web app looks as follows:

  1. 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.

  2. 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:

  • Is there an assetlinks.json file for this Relying Party ID at https://./well-known/assetlinks.json
  • Is the Android app correctly defined as a target.
  1. If, and only if, both questions can be answered with “yes”, a passkey can be created within the Android app.

6. Overview for Android, iOS and Flutter Settings

Here, we provide a detailed overview for the different settings that are required to properly set up passkey authentication for native apps.

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:

7. Examples for Valid and Invalid Relying Party ID and Association Files

Please see here for an overview of four examples of valid and invalid Relying Party ID and association file configurations.

8. Common Relying Party ID Mistakes and how to avoid them

8.1 Changing Relying Party ID from Subdomain to Root Domain

Mistake:

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 don’t 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.

8.2 Different Relying Party IDs for Native and Web app

Mistake:

You’re 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.

8.3 Invalid and unreachable Association Files

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 don’t 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 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.

8.4 apple-app-site-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).

8.5 Android Emulator and API Version Incompatibility

Mistake:

You start developing an Android app and want to test it on an Android emulator. For some, reason you’re getting error messages, even though you’ve 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.

9. Recommendation

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 it’s part of our approach to offering seamless passkey authentication for all technical setups. Our web components and native SDKs are prepared to be used with your root domain as relying Party ID).

Please see here for a detailed recommendation table for different scenarios.

10. Conclusion

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.

Top comments (0)