# React Native App Auth React Native App Auth is a React Native bridge library for the AppAuth-iOS and AppAuth-Android SDKs, enabling OAuth 2.0 and OpenID Connect authentication in mobile applications. It follows the best practices outlined in RFC 8252 (OAuth 2.0 for Native Apps) by using secure system browsers (`ASWebAuthenticationSession`/`SFSafariViewController` on iOS and Custom Tabs on Android) instead of embedded WebViews, and supports PKCE (Proof Key for Code Exchange) for enhanced security in public clients. The library supports any OAuth provider that implements the OAuth2 specification, with built-in support for OpenID Connect discovery. It has been tested with numerous providers including Google, Microsoft, Okta, Auth0, Keycloak, AWS Cognito, GitHub, Spotify, and many others. The library only supports the Authorization Code Flow grant type and is compatible with React Native 0.63 and later, with Expo SDK 53+ support through a config plugin. ## Installation ```bash yarn add react-native-app-auth # or npm install react-native-app-auth --save ``` ## authorize - Perform OAuth Authorization The main function for user authentication that executes the complete login flow, opening a secure browser for user consent and exchanging the authorization code for tokens. ```typescript import { authorize, AuthConfiguration, AuthorizeResult } from 'react-native-app-auth'; // Configuration for an OpenID Connect provider (uses autodiscovery) const config: AuthConfiguration = { issuer: 'https://demo.duendesoftware.com', clientId: 'interactive.public', redirectUrl: 'io.identityserver.demo:/oauthredirect', scopes: ['openid', 'profile', 'email', 'offline_access'], // Optional: customize behavior usePKCE: true, // Enable PKCE (default: true) useNonce: true, // Include nonce for ID token validation (default: true) iosPrefersEphemeralSession: true, // Private browsing on iOS connectionTimeoutSeconds: 30, // Request timeout additionalParameters: { prompt: 'consent', // Force consent screen }, }; try { const result: AuthorizeResult = await authorize(config); // result contains: // - accessToken: string - The OAuth access token // - accessTokenExpirationDate: string - ISO date when token expires // - refreshToken: string - Token for refreshing access // - idToken: string - OpenID Connect ID token (JWT) // - tokenType: string - Usually "Bearer" // - scopes: string[] - Granted scopes // - authorizationCode: string - The authorization code // - authorizeAdditionalParameters: object - Extra params from auth endpoint // - tokenAdditionalParameters: object - Extra params from token endpoint console.log('Access Token:', result.accessToken); console.log('Expires:', result.accessTokenExpirationDate); console.log('ID Token:', result.idToken); } catch (error) { // error.code contains the error type: // - 'authentication_failed' - User cancelled or auth failed // - 'token_exchange_failed' - Code exchange failed // - 'service_configuration_fetch_error' - Discovery failed // - 'browser_not_found' - No suitable browser (Android) console.error('Authorization failed:', error.message, error.code); } ``` ## refresh - Refresh Access Token Exchanges a refresh token for a new access token without requiring user interaction. ```typescript import { refresh, AuthConfiguration, RefreshResult } from 'react-native-app-auth'; const config: AuthConfiguration = { issuer: 'https://demo.duendesoftware.com', clientId: 'interactive.public', redirectUrl: 'io.identityserver.demo:/oauthredirect', scopes: ['openid', 'profile', 'email', 'offline_access'], }; try { const result: RefreshResult = await refresh(config, { refreshToken: 'existing_refresh_token_from_authorize', }); // result contains: // - accessToken: string - New access token // - accessTokenExpirationDate: string - New expiration date // - refreshToken: string | null - New refresh token (if rotated) // - idToken: string - New ID token // - tokenType: string - Token type // - additionalParameters: object - Extra response params // Note: Some providers rotate refresh tokens const newRefreshToken = result.refreshToken || existingRefreshToken; console.log('New Access Token:', result.accessToken); console.log('New Expiration:', result.accessTokenExpirationDate); } catch (error) { // error.code may be 'token_refresh_failed' or 'invalid_grant' console.error('Token refresh failed:', error.message); // Typically requires re-authentication } ``` ## revoke - Revoke Token Invalidates an access token or refresh token at the OAuth provider, useful for implementing logout or security measures. ```typescript import { revoke, BaseAuthConfiguration } from 'react-native-app-auth'; const config: BaseAuthConfiguration = { issuer: 'https://demo.duendesoftware.com', clientId: 'interactive.public', }; try { // Revoke the access token await revoke(config, { tokenToRevoke: 'token_to_invalidate', sendClientId: true, // Include client_id in request includeBasicAuth: false, // Use Basic auth header }); console.log('Token revoked successfully'); // For complete logout, revoke both tokens await revoke(config, { tokenToRevoke: accessToken, sendClientId: true }); await revoke(config, { tokenToRevoke: refreshToken, sendClientId: true }); } catch (error) { console.error('Token revocation failed:', error.message); } ``` ## logout - End Session (OpenID Connect) Performs RP-Initiated Logout as per OpenID Connect specification, ending the user's session at the identity provider. ```typescript import { logout, EndSessionConfiguration, EndSessionResult } from 'react-native-app-auth'; const config: EndSessionConfiguration = { issuer: 'https://demo.duendesoftware.com', clientId: 'interactive.public', // Or specify endpoint manually: // serviceConfiguration: { // endSessionEndpoint: 'https://provider.com/connect/endsession', // }, }; try { const result: EndSessionResult = await logout(config, { idToken: 'id_token_from_authorize', postLogoutRedirectUrl: 'io.identityserver.demo:/logout-callback', }); // result contains: // - idTokenHint: string - The ID token used // - postLogoutRedirectUri: string - Where user was redirected // - state: string - State parameter for verification console.log('Logged out successfully'); // Clear local auth state } catch (error) { // error.code may be 'end_session_failed' console.error('Logout failed:', error.message); } ``` ## register - Dynamic Client Registration Dynamically registers a new OAuth client with the provider, useful for multi-tenant applications. ```typescript import { register, RegistrationConfiguration, RegistrationResponse } from 'react-native-app-auth'; const registrationConfig: RegistrationConfiguration = { issuer: 'https://provider-with-registration.com', redirectUrls: [ 'com.myapp://oauth-callback', 'com.myapp://oauth-callback-alt', ], responseTypes: ['code'], // Default grantTypes: ['authorization_code'], // Default tokenEndpointAuthMethod: 'client_secret_basic', additionalParameters: { client_name: 'My Application', logo_uri: 'https://myapp.com/logo.png', client_uri: 'https://myapp.com', policy_uri: 'https://myapp.com/privacy', tos_uri: 'https://myapp.com/terms', }, }; try { const result: RegistrationResponse = await register(registrationConfig); // result contains: // - clientId: string - Assigned client ID for future auth calls // - clientSecret?: string - Client secret (if issued) // - clientIdIssuedAt?: string - When client ID was issued // - clientSecretExpiresAt?: string - When secret expires (0 = never) // - registrationAccessToken?: string - Token for registration management // - registrationClientUri?: string - URI for registration management // Store clientId securely for future authorization calls console.log('Registered client ID:', result.clientId); } catch (error) { // error.code may be 'registration_failed', 'invalid_redirect_uri', // or 'invalid_client_metadata' console.error('Registration failed:', error.message); } ``` ## prefetchConfiguration - Android Performance Optimization Pre-fetches the OpenID Connect discovery document and warms up Chrome Custom Tabs for faster authorization on Android. ```typescript import { prefetchConfiguration, AuthConfiguration } from 'react-native-app-auth'; const config: AuthConfiguration = { warmAndPrefetchChrome: true, // Required for prefetch to work issuer: 'https://demo.duendesoftware.com', clientId: 'interactive.public', redirectUrl: 'io.identityserver.demo:/oauthredirect', scopes: ['openid', 'profile'], connectionTimeoutSeconds: 5, }; // Call on app startup or before auth is needed // This is fire-and-forget, no await needed prefetchConfiguration(config); // Later, authorize() will be faster const result = await authorize(config); ``` ## Service Configuration - Manual Endpoint Setup For OAuth providers that don't support OpenID Connect discovery, manually specify the endpoints. ```typescript import { authorize, AuthConfiguration } from 'react-native-app-auth'; // Configuration for non-OpenID Connect providers (e.g., GitHub, Spotify) const config: AuthConfiguration = { clientId: 'your_client_id', clientSecret: 'your_client_secret', // Only if required by provider redirectUrl: 'com.myapp://oauth', scopes: ['user', 'repo'], // Provider-specific scopes serviceConfiguration: { authorizationEndpoint: 'https://github.com/login/oauth/authorize', tokenEndpoint: 'https://github.com/login/oauth/access_token', revocationEndpoint: 'https://api.github.com/applications/{client_id}/token', // Optional endpoints: // registrationEndpoint: 'https://provider.com/register', // endSessionEndpoint: 'https://provider.com/logout', }, // GitHub doesn't support PKCE usePKCE: false, additionalParameters: { allow_signup: 'false', }, }; const result = await authorize(config); ``` ## Google OAuth Configuration Complete configuration example for Google OAuth with platform-specific setup. ```typescript import { authorize, refresh, revoke, AuthConfiguration } from 'react-native-app-auth'; // Replace with your Google OAuth Client ID const GOOGLE_OAUTH_APP_GUID = '123456789012-abcdefghijklmnopqrstuvwxyz123456'; const googleConfig: AuthConfiguration = { issuer: 'https://accounts.google.com', clientId: `${GOOGLE_OAUTH_APP_GUID}.apps.googleusercontent.com`, redirectUrl: `com.googleusercontent.apps.${GOOGLE_OAUTH_APP_GUID}:/oauth2redirect/google`, scopes: ['openid', 'profile', 'email'], }; // Full authentication flow async function authenticateWithGoogle() { try { // 1. Authorize const authState = await authorize(googleConfig); console.log('Logged in!', authState.accessToken); // 2. Later, refresh the token const refreshedState = await refresh(googleConfig, { refreshToken: authState.refreshToken, }); console.log('Refreshed!', refreshedState.accessToken); // 3. On logout, revoke tokens await revoke(googleConfig, { tokenToRevoke: refreshedState.refreshToken || authState.refreshToken, }); console.log('Logged out!'); } catch (error) { console.error('Auth error:', error.message); } } // Android: Add to android/app/build.gradle: // android { // defaultConfig { // manifestPlaceholders = [ // appAuthRedirectScheme: 'com.googleusercontent.apps.YOUR_GOOGLE_OAUTH_APP_GUID' // ] // } // } ``` ## Expo Setup with Config Plugin Simplified setup for Expo SDK 53+ using the built-in config plugin. ```json // app.json { "expo": { "plugins": [ [ "react-native-app-auth", { "redirectUrls": ["com.myapp.auth://oauth"] } ] ] } } ``` ```typescript // App.tsx import { authorize, AuthConfiguration } from 'react-native-app-auth'; const config: AuthConfiguration = { issuer: 'https://your-oauth-provider.com', clientId: 'your-client-id', redirectUrl: 'com.myapp.auth://oauth', // Must match app.json scopes: ['openid', 'profile', 'email'], }; async function login() { try { const result = await authorize(config); console.log('Access token:', result.accessToken); } catch (error) { console.error('Auth error:', error); } } // Run: npx expo prebuild --clean // This auto-configures iOS Info.plist and Android build.gradle ``` ## Complete App Example with State Management Full working example demonstrating authorization, token refresh, and revocation with React state management. ```typescript import React, { useState, useCallback, useEffect } from 'react'; import { View, Button, Text, Alert } from 'react-native'; import { authorize, refresh, revoke, prefetchConfiguration, AuthConfiguration, AuthorizeResult, } from 'react-native-app-auth'; const config: AuthConfiguration = { issuer: 'https://demo.duendesoftware.com', clientId: 'interactive.public', redirectUrl: 'io.identityserver.demo:/oauthredirect', scopes: ['openid', 'profile', 'email', 'offline_access'], }; interface AuthState { accessToken: string; refreshToken: string; accessTokenExpirationDate: string; } export default function App() { const [authState, setAuthState] = useState(null); // Prefetch on mount for faster auth useEffect(() => { prefetchConfiguration({ ...config, warmAndPrefetchChrome: true, }); }, []); const handleLogin = useCallback(async () => { try { const result = await authorize({ ...config, iosPrefersEphemeralSession: true, }); setAuthState({ accessToken: result.accessToken, refreshToken: result.refreshToken, accessTokenExpirationDate: result.accessTokenExpirationDate, }); } catch (error: any) { Alert.alert('Login Failed', error.message); } }, []); const handleRefresh = useCallback(async () => { if (!authState?.refreshToken) return; try { const result = await refresh(config, { refreshToken: authState.refreshToken, }); setAuthState(prev => ({ ...prev!, accessToken: result.accessToken, refreshToken: result.refreshToken || prev!.refreshToken, accessTokenExpirationDate: result.accessTokenExpirationDate, })); } catch (error: any) { Alert.alert('Refresh Failed', error.message); } }, [authState]); const handleLogout = useCallback(async () => { if (!authState?.accessToken) return; try { await revoke(config, { tokenToRevoke: authState.accessToken, sendClientId: true, }); setAuthState(null); } catch (error: any) { Alert.alert('Logout Failed', error.message); } }, [authState]); return ( {authState ? ( <> Access Token: {authState.accessToken.substring(0, 20)}... Expires: {authState.accessTokenExpirationDate}