React Native Authentication

Add Authentication to your React Native App

Aeneas Rekkas - July 08, 2021

Follow the step-by-step guide to add authentication to your React Native application and screens for:

  • login
  • registration
  • profile management
  • update password
  • recover password
  • verify account

The examples use Ory Kratos, an open source identity and authentication REST API server written in Golang.

As the user storage / identity management we will use the open source Ory Kratos project. The React Native application shown in the screens below is available on github.com/ory/kratos-selfservice-ui-react-native as well. Both are maintained by @aeneasr and @zepatrik.

You can download the React Native example app right now from the Apple App Store and Google Play Store!

In the future, this guide will also cover Two-Factor-Authentication (2FA/MFA), "Sign in with Google", password reset, email verification, phone number and SMS verification, "Sign in with GitHub", "Sign in with Facebook" and many other flows.

Authentication for React Native using Expo

This guide assumes that you have worked with ReactJS and React Native before as we will not cover React fundamentals and focus on implementing login, registration, and so on.

To make things a bit easier, we will use expo. At a minimum, you need NodeJS and NPM installed locally. We will use the Ory Kratos React Native Login, Registration and Profile Management template:

npm install -g expo-cli
expo init login-signup-app -t @ory/expo-login-registration-template --npm

# We also want to install and initialize all required components:
cd login-signup-app
npm i
npm run expo-install
# For iOS you also need to run:
npm run pod-install

In case you just want to start exploring, simply run:

  • npm start opens a dashboard where you can open iOS, Android, or web.
  • npm run android opens the app as an android app (requires a connected android VM or device).
  • npm run ios opens the app in the iOS simulator (works only on Mac OSX).
  • npm run web opens the app as a browser app.

Expo CLI Dashboard for ORY Kratos React Native Reference Implementation

Running these commands directly will use our hosted demo environment of Ory Kratos at playground.projects.oryapis.com. Configure this in app.config.js:

export 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/api/kratos/public',

    // 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
    }
  }
}
info

Please be aware that the demo environment including its admin APIs is available to everyone. Use only anonymised data when playing around with it! If you want your own managed environment reach out to office@ory.sh or set up your own open source environment. Information on achieving that is available in part two of this article.

React Native Profile Management and Update Password Flow

This short screen capture shows using the apps' user login, user registration, dashboard, and profile management features:

User Management and Identity Management Admin API

To manage identities, use the Ory Kratos CLI. An overview of all commands can be found in the Ory Kratos documentation. Because we are using the playground in this example, we will use the Ory CLI instead as it allows to connect to Ory.

docker run oryd/ory:v0.0.57 help

To list all identities in the system, run:

# A public access token to manage the Ory playground project!
export ORY_ACCESS_TOKEN=eGWGK00ZoEZHuEpvARqxGvo1FDNkumLo
docker run \
  -e ORY_ACCESS_TOKEN=$ORY_ACCESS_TOKEN \
  oryd/ory:v0.0.57 \
  identities \
  list

ID					VERIFIED ADDRESS 1		RECOVERY ADDRESS 1		SCHEMA ID	SCHEMA URL
f9c33e56-5b43-458a-8cfa-f38a4fb98b9c	office+demo@ory.sh		office+demo@ory.sh		default		https://demo.tenants.staging.oryapis.dev/api/kratos/public/schemas/default
[...]

You can also search for the identity you just signed up using jq:

yourEmail=<the-email-you-used-for-signup>
# For example:
#   yourEmail=office+demo@ory.sh
export ORY_ACCESS_TOKEN=eGWGK00ZoEZHuEpvARqxGvo1FDNkumLo
docker run \
  -e ORY_ACCESS_TOKEN=$ORY_ACCESS_TOKEN \
  oryd/ory:v0.0.57 \
  identities \
  list \
  --format json | \
    jq '.[] | select(.traits.email == "'$yourEmail'")'

{
  "id": "f9c33e56-5b43-458a-8cfa-f38a4fb98b9c",
  "recovery_addresses": [
    {
      "id": "bd7c396c-d893-4a2b-8627-b50aa38e2569",
      "value": "office+demo@ory.sh",
      "via": "email"
    }
  ],
  "schema_id": "default",
  "schema_url": "https://demo.tenants.staging.oryapis.dev/api/kratos/public/schemas/default",
  "traits": {
    "email": "office+demo@ory.sh",
    "name": {
      "first": "Aeneas",
      "last": "Hackerman"
    }
  },
  "verifiable_addresses": [
    {
      "id": "bee9b276-b57f-41dc-8c61-82eb83c2d4fd",
      "status": "completed",
      "value": "office+demo@ory.sh",
      "verified": true,
      "verified_at": "2020-11-26T08:45:22.094Z",
      "via": "email"
    }
  ]
}

To learn more about administration of Ory Kratos' identities, head over to the Managing Users and Identities Documentation!

JSON Web Tokens versus Ory Kratos Session Tokens

Ory Kratos does not issue JSON Web Tokens but instead so-called opaque Ory Kratos Session Tokens. You can still convert those tokens to JSON Web Tokens if you want, but Ory Kratos Session Tokens are more secure because:

  1. JSON Web Tokens can not hold secrets: Unless encrypted, JSON Web Tokens can be read by everyone, including 3rd Parties. Therefore, they can not keep secrets.
  2. JSON Web Tokens can not be revoked / invalidated / logged out: Well, you can revoke them, but they will be considered valid until the "expiry" of the token is reached. Unless, of course, you have a blacklist or check with Hydra if the token was revoked, which however defeats the purpose of using JSON Web Tokens in the first place.

Run Ory Kratos Login, Registration, 2FA Server Locally in Docker

Instead of using the hosted demo environment, you can also deploy your Ory Kratos installation locally and run the React Native app against its API. To run the app against a local deployment, check out Ory Kratos locally and run the quickstart:

# You might want to cd into another directory:
#   cd ..

git clone https://github.com/ory/kratos.git
cd kratos
git checkout v0.5.4-alpha.1
docker-compose -f quickstart.yml -f quickstart-standalone.yml \
  up --build --force-recreate -d

Next you need to set up port forwarding for the Ory Kratos Docker Image you just started. We use the tool ngrok.

$ ngrok http 4433

Account                       ...
Version                       ...
Region                        ...
Web Interface                 ...
Forwarding                    ...
Forwarding                    https://04ee3e08367a.ngrok.io -> http://localhost:4433

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

Copy the HTTPS forwarding URL (example from above https://04ee3e08367a.ngrok.io) and in a new terminal, set it as an environment variable and start the app:

# Change into the directory of your react native app:
cd login-signup-app
KRATOS_URL=<the-ngrok-url> npm start

# For example:
#   KRATOS_URL=https://04ee3e08367a.ngrok.io npm start

Now your app will use the local deployment of Ory Kratos with your own database!

React Navigation with Authentication Session

The entry point for the app is App.tsx. Besides loading some fonts and setting up the views, this component includes 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 most interesting component here is the <AuthProvider>. This component adds an authentication / login context to the React Native component tree:

// ...
export default ({ children }: AuthContextProps) => {
  const { project } = useContext(ProjectContext)
  const [sessionContext, setSessionContext] = useState<
    SessionContext | undefined
  >(undefined)

  // Fetches the authentication session.
  useEffect(() => {
    getAuthenticatedSession().then(syncSession)
  }, [])

  const syncSession = (auth: SessionContext) => {
    if (!auth) {
      return setAuth(null)
    }

    // Use the session token from the auth session:
    return (
      newKratosSdk(project)
        // whoami() returns the session belonging to the session_token:
        .toSession(auth.session_token)
        .then(({ data: session }) => {
          // 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 })
          return Promise.resolve()
        })
        .catch((err: AxiosError) => {
          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>
  )
}

The helper methods in src/helpers/auth.tsx are simple wrappers around Expo's SecureStore. In order to work on the web as well, we use @react-native-community/async-storage 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 Kratos' Session Token.

We now have a place to store and refresh the user session. In addition, we have a way to see if the user session is still active in the navigation and show the dashboard or login / registration screens:

// ...
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.Navigator>
        </NavigationContainer>
      </TouchableWithoutFeedback>
      <View data-testid={'flash-message'}>
        <FlashMessage position="top" floating />
      </View>
    </KeyboardAvoidingView>
  )
}

React Native Authentication Screens

Let's take a look at the different screens: To avoid writing a form renderer for every component - including styling - we abstracted the form rendering into their own components, which you can find in src/components/Form.

There isn't anything special happening there, but you can have a look if you intend to change the layout for example.

React Native Login Component Example

The User Login component uses the Ory Kratos TypeScript SDK and the User Login API Flow.

// ...
const Login = ({ navigation, route }: Props) => {
  const { project } = useContext(ProjectContext)
  const { setSession, session, sessionToken } = useContext(AuthContext)
  const [flow, setFlow] = useState<SelfServiceLoginFlow | undefined>(undefined)

  const initializeFlow = () =>
    newKratosSdk(project)
      .initializeSelfServiceLoginFlowWithoutBrowser(
        route.params.refresh,
        route.params.aal,
        sessionToken
      )
      .then((response) => {
        const { data: flow } = response
        // The flow was initialized successfully, let's set the form data:
        setFlow(flow)
      })
      .catch(console.error)

  // When the component is mounted, we initialize a new use login flow:
  useFocusEffect(
    React.useCallback(() => {
      initializeFlow()

      return () => {
        setFlow(undefined)
      }
    }, [project])
  )

  // This will update the login flow with the user provided input:
  const onSubmit = (payload: SubmitSelfServiceLoginFlowBody) =>
    flow
      ? newKratosSdk(project)
          .submitSelfServiceLoginFlow(flow.id, sessionToken, payload)
          .then(({ data }) => Promise.resolve(data as SessionContext))
          // Looks like everything worked and we have a session!
          .then((session) => {
            setSession(session)
            setTimeout(() => {
              navigation.navigate('Home')
            }, 100)
          })
          .catch(handleFormSubmitError(setFlow, initializeFlow))
      : Promise.resolve()

  return (
    <AuthLayout>
      <StyledCard>
        <AuthSubTitle>Sign in to your account</AuthSubTitle>
        <SelfServiceFlow flow={flow} onSubmit={onSubmit} />
      </StyledCard>

      <NavigationCard
        testID="nav-signup"
        description="Need an account?"
        cta="Sign up!"
        onPress={() => navigation.navigate('Registration')}
      />

      <ProjectPicker />
    </AuthLayout>
  )
}

export default Login

React Native Registration Component Example

The User Registration component performs a User Registration API Flow.

// ...
const Registration = ({ navigation }: Props) => {
  const [flow, setConfig] = useState<SelfServiceRegistrationFlow | undefined>(
    undefined
  )
  const { project } = useContext(ProjectContext)
  const { setSession, isAuthenticated } = useContext(AuthContext)

  const initializeFlow = () =>
    newKratosSdk(project)
      .initializeSelfServiceRegistrationFlowWithoutBrowser()
      // The flow was initialized successfully, let's set the form data:
      .then(({ data: flow }) => {
        setConfig(flow)
      })
      .catch(console.error)

  // When the component is mounted, we initialize a new use login flow:
  useFocusEffect(
    React.useCallback(() => {
      initializeFlow()

      return () => {
        setConfig(undefined)
      }
    }, [project])
  )

  useEffect(() => {
    if (isAuthenticated) {
      navigation.navigate('Home')
    }
  }, [isAuthenticated])

  if (isAuthenticated) {
    return null
  }

  // This will update the registration flow with the user provided input:
  const onSubmit = (
    payload: SubmitSelfServiceRegistrationFlowBody
  ): Promise<void> =>
    flow
      ? newKratosSdk(project)
          .submitSelfServiceRegistrationFlow(flow.id, payload)
          .then(({ data }) => {
            // ORY Kratos can be configured in such a way that it requires a login after
            // registration. You could handle that case by navigating to the Login screen
            // but for simplicity we'll just print an error here:
            if (!data.session_token || !data.session) {
              const err = new Error(
                'It looks like you configured ORY Kratos to not issue a session automatically after registration. This edge-case is currently not supported in this example app. You can find more information on enabling this feature here: https://www.ory.sh/kratos/docs/next/self-service/flows/user-registration#successful-registration'
              )
              return Promise.reject(err)
            }

            // Looks like we got a session!
            return Promise.resolve({
              session: data.session,
              session_token: data.session_token
            })
          })
          // Let's log the user in!
          .then(setSession)
          .catch(
            handleFormSubmitError<SelfServiceRegistrationFlow | undefined>(
              setConfig,
              initializeFlow
            )
          )
      : Promise.resolve()

  return (
    <AuthLayout>
      <StyledCard>
        <AuthSubTitle>Create an account</AuthSubTitle>
        <SelfServiceFlow
          textInputOverride={(field, props) => {
            switch (getNodeId(field)) {
              case 'traits.email':
                return {
                  autoCapitalize: 'none',
                  autoCompleteType: 'email',
                  textContentType: 'username',
                  autoCorrect: false
                }
              case 'password':
                const iOS12Plus =
                  Platform.OS === 'ios' &&
                  parseInt(String(Platform.Version), 10) >= 12
                return {
                  textContentType: iOS12Plus ? 'newPassword' : 'password',
                  secureTextEntry: true
                }
            }
            return props
          }}
          flow={flow}
          onSubmit={onSubmit}
        />
      </StyledCard>

      <NavigationCard
        description="Already have an account?"
        cta="Sign in!"
        onPress={() => navigation.navigate({ key: 'Login' })}
      />

      <ProjectPicker />
    </AuthLayout>
  )
}

export default Registration

React Navigation Home Component Example

The Home component receives the user's authentication session and displays all relevant information. To learn more about Ory Kratos' Identity and User Management check out the Ory Kratos Identity Data Model.

// ...
const Home = () => {
  const navigation = useNavigation()
  const { isAuthenticated, session, sessionToken } = useContext(AuthContext)

  useEffect(() => {
    if (!isAuthenticated || !session) {
      navigation.navigate('Login')
    }
  }, [isAuthenticated, sessionToken])

  if (!isAuthenticated || !session) {
    return null
  }

  // Get the name, or if it does not exist in the traits, use the
  // identity's ID
  const { name: { first = String(session.identity.id) } = {} } = session
    .identity.traits as any

  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(session.identity.traits || '{}', null, 2)}
        </CodeBox>
        <StyledText variant="lead">
          You are signed in using an ORY Kratos Session Token:
        </StyledText>
        <CodeBox testID="session-token">{sessionToken}</CodeBox>
        <StyledText variant="lead">
          This app makes REST requests to ORY Kratos' Public API to validate and
          decode the ORY Kratos Session payload:
        </StyledText>
        <CodeBox testID="session-content">
          {JSON.stringify(session || '{}', null, 2)}
        </CodeBox>
      </StyledCard>
    </Layout>
  )
}

export default Home

React Navigation User Settings Component Example

The User Settings component performs a User Settings API Flow.

// ...
const Settings = () => {
  const navigation = useNavigation()
  const { project } = useContext(ProjectContext)
  const { isAuthenticated, sessionToken, setSession, syncSession } =
    useContext(AuthContext)
  const [flow, setFlow] = useState<SelfServiceSettingsFlow | undefined>(
    undefined
  )

  const initializeFlow = (sessionToken: string) =>
    newKratosSdk(project)
      .initializeSelfServiceSettingsFlowWithoutBrowser(sessionToken)
      .then(({ data: flow }) => {
        setFlow(flow)
      })
      .catch(console.error)

  useEffect(() => {
    if (sessionToken) {
      initializeFlow(sessionToken)
    }
  }, [project, sessionToken])

  useEffect(() => {
    if (!isAuthenticated) {
      navigation.navigate('Login')
    }
  }, [isAuthenticated])

  if (!flow || !sessionToken) {
    return null
  }

  const onSuccess = (result: SelfServiceSettingsFlow) => {
    if (result.state === SelfServiceSettingsFlowState.Success) {
      syncSession().then(() => {
        showMessage({
          message: 'Your changes have been saved',
          type: 'success'
        })
      })
    }
    setFlow(result)
  }

  const onSubmit = (payload: SubmitSelfServiceSettingsFlowBody) =>
    newKratosSdk(project)
      .submitSelfServiceSettingsFlow(flow.id, sessionToken, payload)
      .then(({ data }: any) => {
        onSuccess(data)
      })
      .catch(
        handleFormSubmitError(
          setFlow,
          () => initializeFlow(sessionToken),
          () => setSession(null)
        )
      )

  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

Adding Authentication to a React Native App From Scratch

Granted, using a template is the easiest way to get started. However, understanding how everything works even better. Let's have a look at the project set up process!

Assuming that you ran expo init with one of the default templates:

expo init login-signup-app --npm
cd login-signup-app

React Navigation with Authentication

To set up screen navigation, we use the standard React Native navigation component:

npm install @react-navigation/native @react-navigation/stack
expo install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

React Native with Expo Encrypted Credentials Storage

We will use expo-secure-store to securely store the user's session key in the encrypted device store (Android Keystore / Expo Secure Store). To install it we need to run:

expo install expo-secure-store @react-native-community/async-storage

# For iOS you also need to run:
npx pod-install

We're also adding @react-native-community/async-storage because expo-secure-store does not work on the Web.

Ory Kratos SDKs for React Native

We also need to install the Ory Kratos SDK. We'll also be making sure the app looks beautiful by installing the Ory Themes package and some fonts later.

# The package "url" is needed as it is not natively available in React Native.
npm install @ory/kratos-client@0.5.4-alpha.1 url

React Native Environment Variable Support

Next we want to set up support for environment variables which we will use in the next section. You can either follow the Expo guide on Environment Variables or do the following:

expo install expo-constants

Create a file called app.config.js in the project's root:

export 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/api/kratos/public',

    // 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
    }
  }
}

You can use this variable when initializing the Ory Kratos SDK:

import { Configuration, V0alpha2Api } from '@ory/kratos-client'
import Constants from 'expo-constants'
import axiosFactory from 'axios'
import { resilience } from './axios'

const axios = axiosFactory.create()
resilience(axios) // Adds retry mechanism to axios

// canonicalize removes the trailing slash from URLs.
const canonicalize = (url: string = '') => url.replace(/\/+$/, '')

// This value comes from ../../app.config.js
export const kratosUrl = (project: string = 'playground') => {
  const url = canonicalize(Constants.manifest?.extra?.kratosUrl) || ''

  if (url.indexOf('https://playground.projects.oryapis.com/') == -1) {
    // The URL is not from Ory, so let's just return it.
    return url
  }

  // We handle a special case where we allow the project to be changed
  // if you use an ory project.
  return url.replace('playground.', `${project}.`)
}

export const newKratosSdk = (project: string) =>
  new V0alpha2Api(
    new Configuration({
      basePath: kratosUrl(project),
      baseOptions: {
        // Setting this is very important as axios will send the CSRF cookie otherwise
        // which causes problems with ORY Kratos' security detection.
        withCredentials: false,

        // Timeout after 5 seconds.
        timeout: 10000
      }
    }),
    '',
    // Ensure that we are using the axios client with retry.
    axios
  )

Fonts and Other Dependencies

To make things a bit prettier, we are going to add the Ory Theme and some fonts:

npm install @ory/themes styled-components
expo install expo font @expo-google-fonts/rubik \
    @expo-google-fonts/roboto expo-status-bar

You are of course free to use your own themes for this but for the sake of completeness we added this to the guide.

Conclusion

We have now implemented Ory Kratos Authentication with Login, Registration, Profile Management in React Native!

Thanks for taking the time to follow this guide and hopefully it helps you build secure apps more smoothly. Should you have further questions or feedback, visit the community forum or chat.

Ory Kratos is open-source and freely available on github, please consider starring the repository. It is free and helps grow the project and community.

Sign up to our newsletter to be notified of new updates to Ory Kratos and other Ory projects.