@antzsoft/wso2-auth-reactnative
v1.4.6
Published
WSO2 Identity Server App-Native Authentication for React Native — browserless, no WebView, full PKCE + auto token refresh
Readme
@antzsoft/wso2-auth-reactnative
WSO2 Identity Server App-Native Authentication for React Native — fully browserless, no WebView, pure REST. Supports both Expo (managed & bare) and React Native CLI (bare) projects.
What This Package Does
- Login via WSO2 App-Native Authentication API (PKCE S256, no browser redirect)
- Auto token refresh — proactive refresh 60 s before expiry + on app foreground
- Session restore on app launch — loads stored tokens, refreshes if expired
- Logout — revokes refresh token + terminates WSO2 SSO session
- Change Password — with optional OTP verification (SMS / email) controlled by WSO2 governance config
- Forgot Password — built-in WebView modal that opens inside the app, auto-closes when WSO2 redirects to the login page after a successful reset
- Human-readable errors — all WSO2 error codes mapped to plain English
- ALB sticky session — captures
AWSALBcookie from/authorizeand replays it on/authnso both requests hit the same WSO2 node behind an AWS Application Load Balancer - Secure token storage — pluggable adapter:
expo-secure-storefor Expo,react-native-keychainfor bare RN CLI
Requirements
| Requirement | Version | |---|---| | React Native | ≥ 0.76 | | React | ≥ 18 | | WSO2 Identity Server | 7.x | | Expo SDK (Expo only) | ≥ 52 | | react-native-webview (for Forgot Password modal) | ≥ 13.0.0 |
Installation
Expo (Managed or Bare)
npx expo install @antzsoft/wso2-auth-reactnative expo-crypto expo-secure-store react-native-webviewReact Native CLI (Bare)
npm install @antzsoft/wso2-auth-reactnative react-native-keychain react-native-webview
cd ios && pod install
expo-cryptois required for PKCE key generation. Hermes does not exposecrypto.getRandomValuesas a browser global, so the package usesexpo-cryptointernally.
react-native-webviewis required for the built-inopenPasswordRecovery()modal. If you only usegetPasswordRecoveryUrl()+Linking.openURLyou can skip it.
Quick Start — Expo
1. Configure WSO2 Console
In WSO2 Console → Applications → your app:
- Application type: Mobile Application
- Allowed grant types: Authorization Code
- Enable App-Native Authentication (under Sign-in Method)
- Token type: JWT (required for change-password OTP exclusion feature)
- Redirect URI: your custom scheme, e.g.
antzmobile://auth/callback
2. Wrap your app with AuthProvider
// App.tsx
import { AuthProvider } from '@antzsoft/wso2-auth-reactnative';
import { expoSecureStoreAdapter } from '@antzsoft/wso2-auth-reactnative/expo';
export default function App() {
return (
<AuthProvider
config={{
baseUrl: 'https://auth.antzsystems.com',
tenantDomain: 'dev', // your WSO2 tenant: "dev" | "uat" | "prod"
clientId: 'YOUR_CLIENT_ID', // from WSO2 Console → Applications → Protocol
redirectUri: 'antzmobile://auth/callback',
scopes: ['openid', 'profile', 'email', 'phone'],
storageAdapter: expoSecureStoreAdapter,
}}
>
<YourNavigator />
</AuthProvider>
);
}3. Use the useAuth hook
import { useAuth } from '@antzsoft/wso2-auth-reactnative';
export function LoginScreen() {
const { login, status, error } = useAuth();
async function handleLogin() {
try {
await login({ username: 'john', password: 'secret' });
// status becomes 'authenticated', navigate to home
} catch (err) {
// err.message is already human-readable
Alert.alert('Login failed', err.message);
}
}
return (
<Button title="Sign In" onPress={handleLogin} disabled={status === 'loading'} />
);
}4. Add Expo config plugins
In app.json / app.config.js:
{
"expo": {
"plugins": [
"expo-secure-store"
]
}
}Quick Start — React Native CLI (Bare)
1. Configure WSO2 Console
Same as Expo above.
2. Wrap your app with AuthProvider
// App.tsx
import { AuthProvider } from '@antzsoft/wso2-auth-reactnative';
import { rnKeychainAdapter } from '@antzsoft/wso2-auth-reactnative/rn';
export default function App() {
return (
<AuthProvider
config={{
baseUrl: 'https://auth.antzsystems.com',
tenantDomain: 'dev',
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'antzmobile://auth/callback',
scopes: ['openid', 'profile', 'email', 'phone'],
storageAdapter: rnKeychainAdapter,
}}
>
<YourNavigator />
</AuthProvider>
);
}3. Link native dependencies
cd ios && pod install4. Add expo-crypto as a dependency
Even in bare RN CLI projects, expo-crypto is used internally for PKCE:
npm install expo-crypto
cd ios && pod installAPI Reference
useAuth() hook
Returns the full auth context. Must be called inside <AuthProvider>.
const {
// State
status, // 'idle' | 'loading' | 'authenticated' | 'unauthenticated'
user, // AuthUser | null — decoded from JWT: sub, username, email, orgName, etc.
tokens, // TokenSet | null — accessToken, refreshToken, idToken, expiresAt
error, // string | null — last error message
// Actions
login, // (credentials: { username, password }) => Promise<void>
logout, // () => Promise<void>
sendChangePasswordOtp, // () => Promise<{ otpEnabled: boolean }>
changePassword, // (payload: ChangePasswordPayload) => Promise<void>
getPasswordRecoveryUrl, // () => string — raw URL (use with Linking.openURL if preferred)
openPasswordRecovery, // () => Promise<void> — opens built-in WebView modal
refreshTokens, // () => Promise<void>
clearError, // () => void
} = useAuth();useAccessToken() hook
Returns the current access token string. Throws if not authenticated.
const accessToken = useAccessToken();Feature Guide
Login
const { login } = useAuth();
await login({ username: '[email protected]', password: 'MyPassword123!' });Internally runs the full WSO2 App-Native flow:
POST /oauth2/authorizewithresponse_mode=direct+ PKCE challengePOST /oauth2/authnwith credentials (sticky cookie replayed for ALB affinity)POST /oauth2/tokenwith authorization code + PKCE verifier
Logout
const { logout } = useAuth();
await logout();
// Revokes refresh token + terminates WSO2 SSO session + clears local storageChange Password (OTP flow auto-detected)
const { sendChangePasswordOtp, changePassword } = useAuth();
// Step 1 — check if OTP is required
const { otpEnabled } = await sendChangePasswordOtp();
if (!otpEnabled) {
// OTP disabled for this app — change directly
await changePassword({ currentPassword: 'old', newPassword: 'new' });
} else {
// OTP sent to user's mobile/email — collect it, then:
await changePassword({ currentPassword: 'old', newPassword: 'new', otp: '123456' });
}Error codes you may receive from changePassword:
| Code | Meaning |
|---|---|
| INVALID_CREDENTIALS | Current password is wrong |
| INVALID_OTP | OTP code is incorrect |
| OTP_EXPIRED | OTP has expired (5 min TTL) |
| OTP_MAX_ATTEMPTS | 3 wrong codes — request a new OTP |
| PASSWORD_POLICY_VIOLATION | New password fails WSO2 policy |
All errors have .message set to a human-readable string.
Forgot Password
WSO2 password recovery is browser-based. The package ships a built-in WebView modal (WKWebView on iOS, Android WebView on Android) that:
- Slides up over the app — user never leaves
- Shows a toolbar with a ‹ Back button (web history only) and a single Close button
- Auto-closes when WSO2 redirects to the login page after a successful reset
- Is themed via the
themefield inAuthProviderconfig
Basic usage
Just call openPasswordRecovery() — no arguments needed:
import { useAuth } from '@antzsoft/wso2-auth-reactnative';
export function ForgotPasswordScreen() {
const { openPasswordRecovery } = useAuth();
async function handleReset() {
await openPasswordRecovery();
// Promise resolves when modal closes — either via Close button or auto-close after reset.
// Both paths resolve the same promise. To distinguish them, use openPasswordRecovery()
// with onComplete / onDismiss callbacks (see below).
Alert.alert('Done', 'If your password was reset, please sign in again.');
}
return <Button title="Reset Password" onPress={handleReset} />;
}Theming the modal
Pass a theme object in your AuthProvider config:
<AuthProvider
config={{
baseUrl: 'https://auth.antzsystems.com',
tenantDomain: 'dev',
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'antzmobile://auth/callback',
storageAdapter: expoSecureStoreAdapter,
theme: {
primaryColor: '#6C47FF', // toolbar background colour — default: "#6C47FF"
recoveryTitle: 'Reset Password', // toolbar title — default: "Reset Password"
closeLabel: 'Close', // close button (top-right) — default: "Close"
},
}}
>
<YourNavigator />
</AuthProvider>All theme fields are optional — defaults are used for anything not provided.
Using the raw URL instead
If you prefer to handle browser opening yourself (e.g. with Linking.openURL or a custom flow):
import { Linking } from 'react-native';
const { getPasswordRecoveryUrl } = useAuth();
await Linking.openURL(getPasswordRecoveryUrl());
// Opens: https://auth.antzsystems.com/t/dev/accounts/recovery?flowType=PASSWORD_RECOVERYInstalling react-native-webview
The built-in modal requires react-native-webview:
# Expo
npx expo install react-native-webview
# React Native CLI
npm install react-native-webview
cd ios && pod installToken Refresh
Token refresh is fully automatic — you do not need to call anything yourself. The package handles it in three ways:
1. On app launch (session restore)
When AuthProvider mounts, it loads the stored tokens from secure storage and immediately checks if the access token is expired. If it is, it silently refreshes before setting status = 'authenticated'. If the refresh token is also dead, the user is set to unauthenticated and must log in again.
2. Proactive timer — 60 seconds before expiry
As soon as tokens are set (after login or any refresh), a timer is scheduled to fire 60 seconds before the access token expires. This means the refresh happens in the background before the token actually expires — the user never hits a 401.
3. On app foreground
Every time the app comes back to the foreground (e.g. user switches back from another app), the package checks whether the current token has expired. If it has, it refreshes immediately. This covers the case where the app was backgrounded long enough for the token to expire.
What happens if refresh fails?
If the refresh token is expired or revoked (e.g. WSO2 session invalidated server-side), the package:
- Clears the stored tokens from secure storage
- Sets
status = 'unauthenticated'
Your app reacts to this via the status field — no callbacks or events needed.
The recommended pattern is a root navigator that switches between auth and app screens based on status:
// RootNavigator.tsx
import React from 'react';
import { useAuth } from '@antzsoft/wso2-auth-reactnative';
import { ActivityIndicator, View } from 'react-native';
export function RootNavigator() {
const { status } = useAuth();
if (status === 'idle' || status === 'loading') {
// Session restore in progress — show a splash/loading screen
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
// status === 'authenticated' → show app screens
// status === 'unauthenticated' → show login screen
// React Navigation re-renders automatically when status changes
return status === 'authenticated' ? <AppStack /> : <AuthStack />;
}This pattern handles all four cases automatically:
| status | Cause | What to show |
|---|---|---|
| idle | Before session restore runs | Splash / loading |
| loading | Session restore in progress | Splash / loading |
| authenticated | Valid tokens | App screens |
| unauthenticated | Not logged in, logout, or refresh token expired/revoked | Login screen |
If you use navigation.navigate or navigation.replace instead, make sure to handle the unauthenticated transition in a screen that is always mounted:
// In a screen that stays mounted (e.g. HomeScreen)
import { useEffect } from 'react';
import { useAuth } from '@antzsoft/wso2-auth-reactnative';
import { useNavigation } from '@react-navigation/native';
const { status } = useAuth();
const navigation = useNavigation();
useEffect(() => {
if (status === 'unauthenticated') {
navigation.replace('Login');
}
}, [status]);Note: When the refresh token expires mid-session,
statustransitions directly from'authenticated'to'unauthenticated'. Your navigator or screen will re-render and redirect to login automatically.
Manual refresh
refreshTokens() is available if you ever need to force a refresh manually, but this is rarely needed:
const { refreshTokens } = useAuth();
await refreshTokens();Configuring the refresh buffer
By default the proactive timer fires 60 seconds before the access token expires. You can change this via refreshBufferSeconds in AuthProvider config:
<AuthProvider
config={{
baseUrl: 'https://auth.antzsystems.com',
tenantDomain: 'dev',
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'antzmobile://auth/callback',
storageAdapter: expoSecureStoreAdapter,
refreshBufferSeconds: 30, // refresh 30s before expiry instead of 60s
}}
>Important: refreshBufferSeconds must be less than your WSO2 refresh token expiry time. If your refresh token expires in 180s and you set refreshBufferSeconds: 180, the refresh will be attempted the moment the access token is issued — effectively refreshing in a tight loop.
A safe rule of thumb: refreshBufferSeconds < refreshTokenExpiry / 2.
WSO2 token expiry settings
Make sure your WSO2 application has sensible expiry values. Set under Applications → Protocol → OAuth/OIDC:
| Token | Recommended | |---|---| | Access Token Expiry | 3600 s (1 hour) | | Refresh Token Expiry | 86400 s (24 hours) or longer |
If the access token expiry is very short (e.g. 60 s), the proactive timer will fire almost immediately after every login — not harmful but noisy.
Auth State & User Info
const { status, user, tokens } = useAuth();
if (status === 'authenticated') {
console.log(user.username); // e.g. "john"
console.log(user.email); // e.g. "[email protected]"
console.log(user.orgName); // e.g. "Antz Systems"
console.log(tokens.accessToken);
console.log(tokens.expiresAt); // ms timestamp
}Adapters
Storage Adapters
expoSecureStoreAdapter — for Expo projects
import { expoSecureStoreAdapter } from '@antzsoft/wso2-auth-reactnative/expo';Uses expo-secure-store which maps to iOS Keychain and Android Keystore.
rnKeychainAdapter — for React Native CLI (bare) projects
import { rnKeychainAdapter } from '@antzsoft/wso2-auth-reactnative/rn';Uses react-native-keychain which maps to iOS Keychain and Android Keystore.
Custom storage adapter
import type { StorageAdapter } from '@antzsoft/wso2-auth-reactnative';
const myAdapter: StorageAdapter = {
getItem: async (key) => { /* return string | null */ },
setItem: async (key, value) => { /* store string */ },
removeItem: async (key) => { /* delete key */ },
};Configuration Reference
interface Wso2AuthConfig {
/** WSO2 IS base URL. No trailing slash. */
baseUrl: string;
/** Tenant slug: "dev" | "uat" | "prod" */
tenantDomain: string;
/** OAuth2 client_id from WSO2 Console → Applications → Protocol */
clientId: string;
/**
* Redirect URI registered in WSO2 Console.
* Must match exactly. In App-Native flow it is never actually followed,
* but WSO2 validates it is registered.
* Example: "antzmobile://auth/callback"
*/
redirectUri: string;
/** OAuth2 scopes. Default: ["openid", "profile", "email", "phone"] */
scopes?: string[];
/** Storage adapter for secure token persistence. Required — no default. */
storageAdapter: StorageAdapter;
/**
* Override the storage key. Useful for multi-account setups.
* Default: "antz_wso2_token_set"
*/
storageKey?: string;
/**
* How many seconds before access token expiry to proactively refresh.
* The refresh timer fires this many seconds early so the new token is
* ready before the old one is rejected by the server.
*
* Must be less than your WSO2 refresh token expiry time.
* Default: 60
*/
refreshBufferSeconds?: number;
/**
* Visual theme for the built-in PasswordRecoveryModal.
* All fields are optional — defaults are used for anything not provided.
*/
theme?: {
/** Toolbar background colour. Default: "#6C47FF" */
primaryColor?: string;
/** Toolbar title. Default: "Reset Password" */
recoveryTitle?: string;
/** Close button label (top-right). Default: "Close" */
closeLabel?: string;
};
}Error Handling
All errors thrown by this package are instances of Wso2ApiError:
import { Wso2ApiError } from '@antzsoft/wso2-auth-reactnative';
try {
await login({ username, password });
} catch (err) {
if (err instanceof Wso2ApiError) {
console.log(err.message); // Human-readable: "Incorrect username or password."
console.log(err.code); // WSO2 code: "ABA-60003"
console.log(err.httpStatus); // HTTP status: 401
}
}You can also use humanizeWso2Error directly:
import { humanizeWso2Error } from '@antzsoft/wso2-auth-reactnative';
const msg = humanizeWso2Error('INVALID_CREDENTIALS', undefined, undefined, 401);
// → "Current password is incorrect."WSO2 Console Setup Checklist
- [ ] Application type: Mobile Application
- [ ] Allowed grant types: Authorization Code
- [ ] Enable App-Native Authentication under Sign-in Method → Add Sign-in Step → App Native
- [ ] Access Token type: JWT (Applications → Protocol → OAuth/OIDC → Access Token)
- [ ] Add redirect URI (e.g.
antzmobile://auth/callback) - [ ] For change password OTP: enable under Resident IDP → Login & Registration → Change Password Settings
Troubleshooting
"Invalid authentication request" on login
→ The redirectUri in your config doesn't match exactly what's registered in WSO2 Console.
Login succeeds immediately without entering credentials (SUCCESS_COMPLETED)
→ Normal — WSO2 reused an existing SSO session. The package handles this automatically.
sendChangePasswordOtp always returns { otpEnabled: false }
→ OTP is not enabled in WSO2 Console for this tenant. Enable it under Resident IDP → Login & Registration → Change Password Settings.
changePassword throws "Current password is incorrect."
→ The currentPassword field is wrong. This is code INVALID_CREDENTIALS.
Token expires immediately → Check that your WSO2 application has a reasonable Access Token Expiry (default is 3600 seconds). Set under Applications → Protocol → OAuth/OIDC.
expo-crypto not found error on bare RN CLI
→ Run npm install expo-crypto && cd ios && pod install.
Forgot password modal is blank or shows an error page
→ Make sure react-native-webview is installed and native modules are linked (pod install on iOS).
Package Structure
@antzsoft/wso2-auth-reactnative
├── AuthProvider React context provider — wrap your root component
│ └── renders PasswordRecoveryModal internally (themed via config.theme)
├── useAuth() Primary hook — all state and actions
├── useAccessToken() Returns the raw access token string
├── PasswordRecoveryModal WebView modal component (also exported for direct use)
├── Wso2ApiError Error class with .code, .message, .httpStatus
├── humanizeWso2Error() Maps WSO2 error codes to human-readable strings
│
├── /expo Expo storage adapter (expo-secure-store)
│ └── expoSecureStoreAdapter
│
└── /rn React Native CLI storage adapter (react-native-keychain)
└── rnKeychainAdapterLicense
MIT — © Antz Systems
