Integrate Authentication into React Native
This guide shows how to create a simple React application and secure it with authentication powered by Ory. You can use this guide with both Ory Network and self-hosted Ory software.
This guide is perfect for you if:
- You have set up a React Native development environment.
- You want to build an app using React Native.
- You want to enable your users to sign up and sign in to your application.
You can find the code of the sample application here. The application is also available to download from the Apple App Store.
Clone the sample app repository
Start with cloning the repository with the sample application. Run:
# Clone using SSH
git clone git@github.com:ory/kratos-selfservice-ui-react-native.git
# Clone using HTTPS
git clone https://github.com/ory/kratos-selfservice-ui-react-native.git
Prepare environment
To run the sample application, you must set up a React Native development environment and install mobile device emulators:
-
Install Expo CLI.
-
Install Xcode and Xcode Command Line Tools to run the application in an emulated iOS environment.
noteXcode and its iOS emulation are available only on macOS.
-
Install the Android Studio Emulator to run the application in an emulated Android environment.
Run the application locally
Follow these steps to run the app locally in an emulated iOS environment:
-
Enter the directory and install all dependencies:
# Change directory
cd kratos-selfservice-ui-react-native
# Install dependencies
npm i -
Run
npm run ios
to start the iOS simulator and run the application.tipYou can also use these commands:
npm start
opens a dashboard where you can choose to run the app in iOS or Android simulated environments.npm run android
runs the app in the Android environment.
-
When the simulator starts, the Expo Go application opens on the simulated iOS device. If this doesn't happen, open the app manually.
-
In the Expo Go application, click the + icon in the top-right corner.
-
Enter the project URL provided by Metro:
npm run ios
>@ory/expo-login-registration-template@v0.5.4-alpha.1.pre.5 ios
>expo start --ios
Starting project at /Users/ory/Desktop/kratos-selfservice-ui-react-native
Developer tools running on http://localhost:19002
Starting Metro Bundler
# This is the exact URL you must provide in the simulator. Starts with 'exp://'.
Opening exp://192.168.1.144:19000 on iPhone 13 mini
By default, the application uses the "Playground" project, which is a public demo environment available to all Ory users. When using the application this way, make sure to anonymize any data you send to Ory!
Connect to your project
Instead of using the public playground project, you can connect the application directly to your project and its admin APIs. Follow these steps:
-
Go to Project settings → Overview in the Ory Console, and copy the URL from the API Endpoints field.
-
Open the
app.config.js
file to configure the application. -
Find the
KRATOS_URL
variable and replace the playground project URL with the SDK URL you copied from your project.app.config.jsexport default (parent = {}) => {
// We gracefully destruct these parameters to avoid "undefined" errors:
const { config = {} } = parent
const { env = {} } = process || {}
const {
// This is the URL of your deployment. In our case we use the Ory Demo
// environment
KRATOS_URL = "https://playground.projects.oryapis.com",
// We use sentry.io for error tracing. This helps us identify errors
// in the distributed packages. You can remove this.
SENTRY_DSN = "https://8be94c41dbe34ce1b244935c68165eab@o481709.ingest.sentry.io/5530799",
} = env
return {
...config,
extra: {
kratosUrl: KRATOS_URL,
sentryDsn: SENTRY_DSN,
},
}
}
Understanding the Implementation
With the application live, running, and connected to your project, let's have a closer look at the code to understand the implementation.
React navigation with authentication session
The entry point for the app is App.tsx
. The
component loads fonts, sets up the views, but most importantly, it defines the structure of the application - including the
navigation:
// ...
export default function App() {
const [robotoLoaded] = useFontsRoboto({ Roboto_400Regular })
const [rubikLoaded] = useFontsRubik({
Rubik_300Light,
Rubik_400Regular,
Rubik_500Medium,
})
const hydratedTheme = {
...theme,
regularFont300: rubikLoaded ? "Rubik_300Light" : "Arial",
regularFont400: rubikLoaded ? "Rubik_400Regular" : "Arial",
regularFont500: rubikLoaded ? "Rubik_500Medium" : "Arial",
codeFont400: robotoLoaded ? "Roboto_400Regular" : "Arial",
platform: "react-native",
}
return (
<ThemeProvider theme={hydratedTheme}>
<NativeThemeProvider theme={hydratedTheme}>
<SafeAreaProvider>
<SafeAreaView
edges={["top", "left", "right"]}
style={{
flex: 1,
backgroundColor: theme.grey5,
}}
>
<ProjectProvider>
<AuthProvider>
<ErrorBoundary>
<Navigation />
<ForkMe />
</ErrorBoundary>
</AuthProvider>
</ProjectProvider>
</SafeAreaView>
</SafeAreaProvider>
</NativeThemeProvider>
</ThemeProvider>
)
}
The AuthProvider
component
The <AuthProvider>
component is the main point of the integration as it adds authentication and login context to the React
Native component tree:
// ...
export default function AuthContextProvider({ children }: AuthContextProps) {
const { sdk } = useContext(ProjectContext)
const [sessionContext, setSessionContext] = useState<
SessionContext | undefined
>(undefined)
// Fetches the authentication session.
useEffect(() => {
getAuthenticatedSession().then(syncSession)
}, [])
const syncSession = async (auth: { session_token?: string } | null) => {
if (!auth?.session_token) {
return setAuth(null)
}
try {
const { data: session } = await sdk
// whoami() returns the session belonging to the session_token:
.toSession({ xSessionToken: auth.session_token })
// This means that the session is still valid! The user is logged in.
//
// Here you could print the user's email using e.g.:
//
// console.log(session.identity.traits.email)
setSessionContext({ session, session_token: auth.session_token })
} catch (err: any) {
if (err.response?.status === 401) {
// The user is no longer logged in (hence 401)
// console.log('Session is not authenticated:', err)
} else {
// A network or some other error occurred
console.error(err)
}
// Remove the session / log the user out.
setSessionContext(null)
}
}
const setAuth = (session: SessionContext) => {
if (!session) {
return killAuthenticatedSession().then(() => setSessionContext(session))
}
setAuthenticatedSession(session).then(() => syncSession(session))
}
if (sessionContext === undefined) {
return null
}
return (
<AuthContext.Provider
value={{
// The session information
session: sessionContext?.session,
sessionToken: sessionContext?.session_token,
// Is true when the user has a session
isAuthenticated: Boolean(sessionContext?.session_token),
// Fetches the session from the server
syncSession: () => getAuthenticatedSession().then(syncSession),
// Allows to override the session
setSession: setAuth,
// Is true if we have fetched the session.
didFetch: true,
}}
>
{children}
</AuthContext.Provider>
)
}
Helper methods
The helper methods in
src/helpers/auth.tsx
are simple
wrappers around the Expo SecureStore. To make them work in the web
environment, @react-native-community/async-storage
is used as a
fallback:
// ...
// getAuthenticatedSession returns a promise with the session of the authenticated user, if the
// user is authenticated or null is the user is not authenticated.
//
// If an error (e.g. network error) occurs, the promise rejects with an error.
export const getAuthenticatedSession = (): Promise<SessionContext> => {
const parse = (sessionRaw: string | null): SessionContext => {
if (!sessionRaw) {
return null
}
// sessionRaw is a JSON String that needs to be parsed.
return JSON.parse(sessionRaw)
}
let p = AsyncStore.getItem(userSessionName)
if (Platform.OS !== "web") {
// We can use SecureStore if not on web instead!
p = SecureStore.getItemAsync(userSessionName)
}
return p.then(parse)
}
// Sets the session.
export const setAuthenticatedSession = (
session: SessionContext,
): Promise<void> => {
if (!session) {
return killAuthenticatedSession()
}
if (Platform.OS === "web") {
// SecureStore is not available on the web platform. We need to use AsyncStore
// instead.
return AsyncStore.setItem(userSessionName, JSON.stringify(session))
}
return (
SecureStore
// The SecureStore only supports strings so we encode the session.
.setItemAsync(userSessionName, JSON.stringify(session))
)
}
// Removes the session from the store.
export const killAuthenticatedSession = () => {
if (Platform.OS === "web") {
// SecureStore is not available on the web platform. We need to use AsyncStore
// instead.
return AsyncStore.removeItem(userSessionName)
}
return SecureStore.deleteItemAsync(userSessionName)
}
That's all it takes to make the magic happen! Everything else is handled by the Ory Session Token.
Navigation
With this setup in place, the application can store and refresh the user session. Additionally, this allows the app to verify if the user session is still active in the navigation and shows the dashboard or login/registration screens accordingly:
// ...
export default () => {
// import { AuthContext } from './AuthProvider'
const { isAuthenticated } = useContext(AuthContext)
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS == "ios" ? "padding" : "height"}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<NavigationContainer linking={linking}>
<Stack.Navigator
screenOptions={{
headerShown: isAuthenticated,
}}
>
<Stack.Screen name="Home" component={Home} options={options} />
<Stack.Screen
name="Settings"
component={Settings}
options={options}
/>
<Stack.Screen name="Registration" component={Registration} />
<Stack.Screen name="Login" component={Login} initialParams={{}} />
<Stack.Screen name="Verification" component={Verification} />
<Stack.Screen name="Callback" component={Callback} />
<Stack.Screen name="Recovery" component={Recovery} />
</Stack.Navigator>
</NavigationContainer>
</TouchableWithoutFeedback>
<View data-testid={"flash-message"}>
<FlashMessage position="top" floating />
</View>
</KeyboardAvoidingView>
)
}
React Native authentication screens
To avoid writing a form renderer for every component - including styling - the app uses form rendering abstracted into separate
own components, which you can find in
src/components/Form
.
Home component
The Home component receives the user's Session and displays all relevant information.
// ...
const Home = ({ navigation }: Props) => {
const { isAuthenticated, session, sessionToken } = useContext(AuthContext)
useEffect(() => {
if (!isAuthenticated || !session) {
navigation.navigate("Login", {})
}
}, [isAuthenticated, sessionToken])
if (!isAuthenticated || !session) {
return null
}
const traits = session.identity?.traits
// Use the first name, the email, or the ID as the name
const first = traits.name?.first || traits.email || session.identity?.id
return (
<Layout>
<StyledCard>
<StyledText style={{ marginBottom: 14 }} variant="h1">
Welcome back, {first}!
</StyledText>
<StyledText variant="lead">
Hello, nice to have you! You signed up with this data:
</StyledText>
<CodeBox>{JSON.stringify(traits || "{}", null, 2)}</CodeBox>
<StyledText variant="lead">
You are signed in using an Ory Session Token:
</StyledText>
<CodeBox testID="session-token">{sessionToken}</CodeBox>
<StyledText variant="lead">
This app makes REST requests to Ory Identities' Public API to validate
and decode the Ory Session payload:
</StyledText>
<CodeBox testID="session-content">
{JSON.stringify(session || "{}", null, 2)}
</CodeBox>
</StyledCard>
</Layout>
)
}
export default Home
User settings component
The User Settings component performs a User Settings API Flow.
// ...
const Settings = ({ navigation, route }: Props) => {
const { sdk } = useContext(ProjectContext)
const { isAuthenticated, sessionToken, setSession, syncSession } =
useContext(AuthContext)
const [flow, setFlow] = useState<SettingsFlow | undefined>(undefined)
useEffect(() => {
if (!sessionToken || !isAuthenticated) {
navigation.navigate("Login", {})
return
}
if (route?.params?.flowId) {
fetchFlow(sdk, sessionToken, route.params.flowId)
.then(setFlow)
.catch(logSDKError)
} else {
initializeFlow(sdk, sessionToken).then(setFlow).catch(logSDKError)
}
}, [sdk, sessionToken])
if (!flow || !sessionToken) {
return null
}
const onSuccess = (result: SettingsFlow) => {
if (result.continue_with) {
for (const c of result.continue_with) {
switch (c.action) {
case "show_verification_ui": {
console.log("got a verification flow, navigating to it", c)
navigation.navigate("Verification", {
flowId: c.flow.id,
})
break
}
}
}
}
if (result.state === SettingsFlowState.Success) {
syncSession().then(() => {
showMessage({
message: "Your changes have been saved",
type: "success",
})
})
}
setFlow(result)
}
const onSubmit = (payload: UpdateSettingsFlowBody) =>
sdk
.updateSettingsFlow({
flow: flow.id,
xSessionToken: sessionToken,
updateSettingsFlowBody: payload,
})
.then(({ data }) => {
onSuccess(data)
})
.catch(
handleFormSubmitError(
undefined,
setFlow,
() => initializeFlow(sdk, sessionToken).then,
() => setSession(null),
async () => {},
),
)
return (
<Layout>
<StyledCard testID={"settings-password"}>
<CardTitle>
<StyledText variant={"h2"}>Change password</StyledText>
</CardTitle>
<SelfServiceFlow flow={flow} only="password" onSubmit={onSubmit} />
</StyledCard>
<StyledCard testID={"settings-profile"}>
<CardTitle>
<StyledText variant={"h2"}>Profile settings</StyledText>
</CardTitle>
<SelfServiceFlow flow={flow} only="profile" onSubmit={onSubmit} />
</StyledCard>
{flow?.ui.nodes.find(({ group }) => group === "totp") ? (
<StyledCard testID={"settings-totp"}>
<CardTitle>
<StyledText variant={"h2"}>2FA authenticator</StyledText>
</CardTitle>
<SelfServiceFlow flow={flow} only="totp" onSubmit={onSubmit} />
</StyledCard>
) : null}
{flow?.ui.nodes.find(({ group }) => group === "lookup_secret") ? (
<StyledCard testID={"settings-lookup"}>
<CardTitle>
<StyledText variant={"h2"}>Backup recovery codes</StyledText>
</CardTitle>
<SelfServiceFlow
flow={flow}
only="lookup_secret"
onSubmit={onSubmit}
/>
</StyledCard>
) : null}
</Layout>
)
}
export default Settings