Integrate Beyond Identity Passwordless Authentication into Expo
This guide provides information on how to set up Beyond Identity as a passwordless authentication provider for a React Native application that uses Expo.
This guide will cover:
- How to install the React Native SDK
- How to configure Beyond Identity as an Identity Provider
- How to create an identity and generate a passkey
- How to authenticate with a passkey
Prerequisites​
- Set up a developer account
Install the React Native SDK​
In order to use Beyond Identity functionality in your application, you will need to install the SDK. The React Native SDK provides functionality from passkey creation to passwordless authentication. A set of functions are provided to you through an Embedded
namespace.
The React Native SDK is a wrapper around our native SDKs (Android and iOS), so it has custom native code. Expo Go only directly works with libraries in the Expo SDK, so to leverage the Beyond Identity React Native SDK and other libraries outside of Expo Go, you will need to either use an expo development build or a prebuild.
Once your application is using a development build or prebuild you are ready to install the SDK:
npx expo install @beyondidentity/bi-sdk-react-native
Add the SDK config plugin to the plugins array of your app.{json,config.js,config.ts}:
{
"expo": {
"plugins": [["@beyondidentity/bi-sdk-react-native"]]
}
}
The SDK requires certain minimum native versions. Set these requirments with expo-build-properties.
npx expo install expo-build-properties
{
"expo": {
"plugins": [
["@beyondidentity/bi-sdk-react-native"],
[
"expo-build-properties",
{
"android": {
"minSdkVersion": 26
},
"ios": {
"deploymentTarget": "13.0"
}
}
]
]
}
}
Finally, rebuild your app as described in Expo's Adding custom native code guide.
Initialize the React Native SDK​
Once you've installed the SDK, initialize it so that you can call the Embedded functions. A good place to initialize this is where you register your root component. You may also add a listener to log native events with Embedded.logEventEmitter
after initializing.
import { Embedded } from '@beyondidentity/bi-sdk-react-native';
Embedded.initialize(Config.biometricAskPrompt).catch(console.error);
Embedded.logEventEmitter.addListener(
'BeyondIdentityLogger',
(message: string) => {
console.log(message);
}
);
AppRegistry.registerComponent(appName, () => App);
Set up Beyond Identity as an Identity Provider​
To set up Beyond Identity as an Identity Provider, you need to create a Realm to hold identities and configuration. Inside that realm, you'll need to create an Application that contains the authentication flow configuration. These can be configured in you admin console that was created for you when you signed up for a developer account.
Create a Realm​
From the admin console, click the realm drop-down, and click Create new realm.
Create an Application​
From the admin console, click Apps, then click Add app.
There is a lot to configure when creating an application. When creating your application make sure:
- Redirect URIs contains your application's App Scheme or Universal URL
- Configuration Type (in the Authenticator Config tab) is set to
Embedded SDK
- Invoke URL (in the Authenticator Config tab) contains your application's App Scheme or Universal URL
For help choosing options, visit the following guides:
Create an Identity and generate a Universal Passkey​
Once you have an application in the admin console you are ready to provision users in your realm's directory, generate passkeys, and handle those passkeys in your application.
Create an Identity​
Creating a user can be done either in the admin console or through an API. This guide will use the admin console. Navigate to your realm in the console and click Identities, then click Add identity.
For more information visit Workflows: User and Group Provisioning.
Generate a passkey​
Once you have an identity, you are ready to generate a passkey for this user. This step can also be done either in the admin console or through an API. This guide will use the admin console. Navigate back to Identities and select the identity you would like to bind to a passkey. Click Add a passkey, select your app and the click Proceed & send email. The user will receive an enrollment email which they can tap on to bind a passkey to their device.
Note that whichever browser or device that the user taps on the enrollment email will be the device/browser where the user can log into your Drupal site. If the user wishes to login from a different browser or device you will need to send the user another email to bind that new browser/device. Also note that private/incognito browsers act as a different browser in this case. Users can bind multiple devices and browsers.
For more information visit Workflows: Bind Passkey To User.
Bind passkey to device​
Once the user taps on the enrollment email, they will be redirected to your application. Follow Expo's deep linking guide and linking to your development build. You may register a scheme in your Expo config by adding a string under the scheme key.
{
"expo": {
"scheme": "myapp"
}
}
Links that launched your app can be observed using Linking. Intercept the link from the enrollment email. The link that is redirected to your application will take on the following form. A /bind
path will be appended to your Invoke URL (configured in your application above) as well as several other query parameters.
$invoke_url/bind?api_base_url=<api_base_url>&tenant_id=<tenant_id>&realm_id=<realm_id>&identity_id=<identity_id>&job_id=<job_id>&token=<token>
Once you receive the incoming URL, pass it into the SDK to complete the binding process. You can validate the incoming URL with isBindPasskeyUrl
. Upon success, a private key will have been created in the device's hardware trust module and the corresponding public key will have been sent to the Beyond Identity Cloud. At this point the user has a passkey enrolled on this device.
import { Embedded } from '@beyondidentity/bi-sdk-react-native';
import * as Linking from 'expo-linking';
export default function App() {
const url = decodeURI(Linking.useURL());
const isBindUrl = await Embedded.isBindPasskeyUrl(url)
if (isBindUrl) {
const bindResponse = await Embedded.bindPasskey(bindingLink);
console.log(bindResponse);
}
}
Authenticate with Passkey​
Once you have one passkey bound to a device, you can use it to authenticate. For more information visit Workflows: Authenticate with Passkey.
Craft an Authorization URL​
First you will need to craft an authorization URL. You can find the base URL in the admin console under your application, select "EXTERNAL PROTOCOL". Copy the Authorization Endpoint
and add the following additional query parameters:
https://auth-$REGION.beyondidentity.com/v1/tenants/$TENANT_ID/realms/$REALM_ID/applications/$APPLICATION_ID/authorize?
response_type=code
&client_id=$APPLICATION_CLIENT_ID
&redirect_uri=$REDIRECT_URI
&scope=openid
&state=$STATE
&code_challenge_method=256
&code_challenge=$PKCE_CODE_CHALLENGE
Check your appliction config in the admin console for your APPLICATION_CLIENT_ID
.
The REDIRECT_URI
is your application's App Scheme or Universal URL.
Note that the following query parameters includes PKCE as it is recommeded, but optional. If you send an authorization request with PKCE, you will need to store the hash of the code_challenge
so that it can be passed to the token exchange endpoint later as a code_verifier
.
You will need to set PKCE as a Client Configuration in your Application Config.
The STATE
parameter is used to mitigiate CSRF attacks. Have your application generate a random string for the STATE
value on each authentication request. You should check that this string is returned back to you to in the response.
Authenticate​
There are two ways to authenticate depending on your Application Config's Invocation Type. Invocation Type can have one of two values: Automatic or Manual.
Automatic
does a lot of the heavy lifting for you. If you initiate an OAuth2.0 request and specify the "Invoke URL" correctly, we'll get the Beyond Identity authentication URL to where it needs to be, whether this is inside of a native app or a web application.
Manual
gives you a lot more control, but you'll have to do a little extra work to wire this flow up. The possibilities include:
- Completley silent OAuth 2.0 authentication using Passkeys. No redirects needed in a web app and no web view needed in a native application.
- The flexibility to write your own intelligent routing layer using the Beyond Identity authentication URL. You may want to authenticate against passkeys in your browser on desktop, but use passkeys on your native app on mobile.
Automatic​
If "Automatic" is selected, Beyond Identity will automatically redirect to your application using the Invoke URL (the App Scheme or Univeral URL pointing to your application). To handle a web browser based authentication you can use Expo's Auth Session.
Note that the response
from useAuthRequest
hook does not need to be of type 'success'. It is sufficient if it has a url
. This is becasue the state value is stored in a JWT in the url 'request' paramater. The url will be validated through the Beyond Identity Embedded SDK.
import * as React from 'react';
import {
makeRedirectUri,
useAuthRequest,
useAutoDiscovery,
} from 'expo-auth-session';
import { Button } from 'react-native';
import { Embedded } from '@beyondidentity/bi-sdk-react-native';
export default function App() {
// Endpoint
const discovery = useAutoDiscovery(
`https://auth-${REGION}.beyondidentity.com/v1/tenants/${TENANT_ID}/realms/${REALM_ID}/applications/${APPLICATION_ID}`
);
// Request
const [request, response, promptAsync] = useAuthRequest(
{
clientId: `${CLIENT_ID}`,
scopes: ['openid'],
redirectUri: makeRedirectUri({
scheme: 'your.app',
}),
},
discovery
);
React.useEffect(() => {
const authenticate = async (url) => {
// Display UI for user to select a passwordless passkey if there are multiple.
const passkeys = await Embedded.getPasskeys();
if (await Embedded.isAuthenticateUrl(url)) {
// Pass url and a selected passkey ID into the Beyond Identity Embedded SDK authenticate function
const { redirectUrl } = await Embedded.authenticate(
url,
passkeys[0].id
);
}
};
if (response?.url) {
authenticate(url);
}
}, [response]);
return (
<Button
disabled={!request}
title="Passwordless Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Manual​
If "Manual" is selected, an authentication URL be returned to you as part of a JSON response. No redirects are needed and will not require authentication through a web service. The result is a completley silent OAuth 2.0 authentication using passkeys. Since the challenge is packaged as part of the authentication URL, following the URL will result in the same behavior as if an Invocation Type of "Automatic" were selected.
import * as React from 'react';
import { Button } from 'react-native';
import { Embedded } from '@beyondidentity/bi-sdk-react-native';
export default function App() {
async function authenticate() {
const BeyondIdentityAuthUrl = `https://auth-${REGION}.beyondidentity.com/v1/tenants/${TENANT_ID}/realms/${REALM_ID}/applications/${APPLICATION_ID}/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${URI_ENCODED_REDIRECT_URI}&scope=openid&state=${STATE}&code_challenge_method=S256&code_challenge=${PKCE_CODE_CHALLENGE}`;
let response = await fetch(BeyondIdentityAuthUrl, {
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json',
}),
});
const data = await response.json();
// Display UI for user to select a passwordless passkey if there are multiple.
const passkeys = await Embedded.getPasskeys();
if (await Embedded.isAuthenticateUrl(data.authenticate_url)) {
// Pass url and selected Passkey ID into the Beyond Identity Embedded SDK authenticate function
const { redirectUrl } = await Embedded.authenticate(
data.authenticate_url,
passkeys[0].id
);
}
}
return <Button title="Passwordless Login" onPress={authenticate} />;
}
Token Exchange​
Calling the token endpoint is the second step in the authorization flow and usually happens in your backend if your application's Client Type is Confidential
.
Parse the redirectUrl
returned when calling the function Embedded.authenticate
for a code
in the query parameters and then exchange that code for an access token.
See Workflows: Authenticate with Passkey: Token Exchange for more details.