Skip to content

feat: Add React Native Web Support for Auth0 with Class-Based Implementation #1221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 40 additions & 12 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- A0Auth0 (5.0.0-beta.2):
- A0Auth0 (5.0.0-beta.3):
- Auth0 (= 2.10)
- DoubleConversion
- glog
Expand Down Expand Up @@ -1363,7 +1363,7 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- react-native-safe-area-context (5.4.0):
- react-native-safe-area-context (5.4.1):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1378,8 +1378,8 @@ PODS:
- React-hermes
- React-ImageManager
- React-jsi
- react-native-safe-area-context/common (= 5.4.0)
- react-native-safe-area-context/fabric (= 5.4.0)
- react-native-safe-area-context/common (= 5.4.1)
- react-native-safe-area-context/fabric (= 5.4.1)
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
Expand All @@ -1389,7 +1389,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/common (5.4.0):
- react-native-safe-area-context/common (5.4.1):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1413,7 +1413,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/fabric (5.4.0):
- react-native-safe-area-context/fabric (5.4.1):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1760,7 +1760,31 @@ PODS:
- React-logger (= 0.79.2)
- React-perflogger (= 0.79.2)
- React-utils (= 0.79.2)
- RNScreens (4.10.0):
- RNGestureHandler (2.26.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNScreens (4.11.1):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1784,9 +1808,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNScreens/common (= 4.10.0)
- RNScreens/common (= 4.11.1)
- Yoga
- RNScreens/common (4.10.0):
- RNScreens/common (4.11.1):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1889,6 +1913,7 @@ DEPENDENCIES:
- ReactAppDependencyProvider (from `build/generated/ios`)
- ReactCodegen (from `build/generated/ios`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNScreens (from `../node_modules/react-native-screens`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)

Expand Down Expand Up @@ -2043,13 +2068,15 @@ EXTERNAL SOURCES:
:path: build/generated/ios
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNScreens:
:path: "../node_modules/react-native-screens"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
A0Auth0: 6b64eb955d9bf2d80bd147ae26c9ac9c69e66d97
A0Auth0: 8a3cbbc2f85fcfcca13c3bc804955599cc94a8bf
Auth0: 2876d0c36857422eda9cb580a6cc896c7d14cb36
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
Expand Down Expand Up @@ -2090,7 +2117,7 @@ SPEC CHECKSUMS:
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
react-native-safe-area-context: 562163222d999b79a51577eda2ea8ad2c32b4d06
react-native-safe-area-context: 5594ec631ede9c311c5c0efa244228eff845ce88
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
Expand Down Expand Up @@ -2122,7 +2149,8 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 04d5eb15eb46be6720e17a4a7fa92940a776e584
ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba
ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0
RNScreens: 5621e3ad5a329fbd16de683344ac5af4192b40d3
RNGestureHandler: c7986dd1eb909e329882af067c05db0702a659b3
RNScreens: 482e9707f9826230810c92e765751af53826d509
SimpleKeychain: 768cf43ae778b1c21816e94dddf01bb8ee96a075
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
Expand Down
11 changes: 6 additions & 5 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
"postinstall": "npm run pods"
},
"dependencies": {
"@react-navigation/native": "^7.1.8",
"@react-navigation/native-stack": "^7.3.12",
"@auth0/auth0-spa-js": "^2.2.0",
"@react-navigation/native": "^7.1.13",
"@react-navigation/stack": "^7.3.6",
"react": "19.0.0",
"react-native": "0.79.2",
"react-native-paper": "^5.14.0",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.10.0",
"react-native-gesture-handler": "^2.26.0",
"react-native-safe-area-context": "^5.4.1",
"react-native-screens": "^4.11.1",
"react-native-web": "^0.20.0"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {
type NavigationProp,
NavigationContainer,
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createNativeStackNavigator();
const Stack = createStackNavigator();

const Home = ({ navigation }: { navigation: NavigationProp<any> }) => {
const { authorize, clearSession, user, getCredentials, error } = useAuth0();
Expand Down
1 change: 0 additions & 1 deletion example/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const babelLoaderConfiguration = {
path.resolve(__dirname, 'node_modules/@react-navigation'),
path.resolve(__dirname, 'node_modules/react-native-safe-area-context'),
path.resolve(__dirname, 'node_modules/react-native-screens'),
path.resolve(__dirname, 'node_modules/react-native-paper'),
path.resolve(__dirname, 'node_modules/react-native-vector-icons'),
],
use: {
Expand Down
46 changes: 46 additions & 0 deletions src/auth/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Auth0Client, User as SpaUser } from '@auth0/auth0-spa-js';
import type { Credentials, User, UserInfoOptions } from '../types';

class NotImplementedError extends Error {
constructor(methodName: string) {
super(
`${methodName} is not implemented for the web. In a browser environment, you should always use the interactive, redirect-based 'authorize' method for authentication flows.`
);
this.name = 'NotImplementedError';
}
}

class Auth {
constructor(private client: Auth0Client) {}

/**
* Retrieves the user's profile information.
* The token is managed internally by auth0-spa-js.
*/
async userInfo(_options: UserInfoOptions): Promise<SpaUser | undefined> {
return this.client.getUser();
}

// The methods below are not recommended for SPAs and are not implemented by auth0-spa-js.
// We throw a specific error to guide the developer towards the correct, secure flow.
async passwordRealm(): Promise<Credentials> {
throw new NotImplementedError('auth.passwordRealm');
}
async exchange(): Promise<Credentials> {
throw new NotImplementedError('auth.exchange');
}
async refreshToken(): Promise<Credentials> {
throw new NotImplementedError('auth.refreshToken');
}
async revoke(): Promise<void> {
throw new NotImplementedError('auth.revoke');
}
async createUser(): Promise<Partial<User>> {
throw new NotImplementedError('auth.createUser');
}
async resetPassword(): Promise<void> {
throw new NotImplementedError('auth.resetPassword');
}
}

export default Auth;
15 changes: 7 additions & 8 deletions src/auth0.ts β†’ src/auth0/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Auth from './auth';
import CredentialsManager from './credentials-manager';
import Users from './management/users';
import WebAuth from './webauth';
import addDefaultLocalAuthOptions from './utils/addDefaultLocalAuthOptions';
import type { Auth0Options } from './types';
import Auth from '../auth';
import CredentialsManager from '../credentials-manager';
import Users from '../management/users';
import WebAuth from '../webauth';
import addDefaultLocalAuthOptions from '../utils/addDefaultLocalAuthOptions';
import type { Auth0Options } from '../types';

/**
* Auth0 for React Native client
Expand Down Expand Up @@ -32,8 +32,7 @@ class Auth0 {
this.auth = new Auth({ baseUrl: domain, clientId, headers, ...extras });
this.webAuth = new WebAuth(this.auth, localAuthenticationOptions);
this.credentialsManager = new CredentialsManager(
domain,
clientId,
this.auth,
localAuthenticationOptions
);
this.options = options;
Expand Down
48 changes: 48 additions & 0 deletions src/auth0/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Auth from '../auth';
import CredentialsManager from '../credentials-manager';
import WebAuth from '../webauth';
import type { Auth0Options } from '../types';
import { Auth0Client, type Auth0ClientOptions } from '@auth0/auth0-spa-js';
import type UsersApi from '../management/users';
/**
* Auth0 for React Native client
*/
class Auth0 {
public auth: Auth;
public webAuth: WebAuth;
public credentialsManager: CredentialsManager;
private client: Auth0Client;
private domain: string;

constructor(options: Auth0Options) {
this.domain = options.domain;
const clientOptions: Auth0ClientOptions = {
authorizationParams: {
redirect_uri: window.location.origin, // Default, can be overridden in authorize()
},
// For best security, tokens are stored in memory by default.
// useRefreshTokens: true and cacheLocation: 'localstorage' can be used for persistence.
cacheLocation: 'memory',
...options,
};

this.client = new Auth0Client(clientOptions);

this.auth = new Auth(this.client);
this.webAuth = new WebAuth(this.client, this.domain);
this.credentialsManager = new CredentialsManager(this.client);

// Automatically handle the redirect callback when the app loads
this.webAuth.handleRedirect();
}

/**
* Creates a client for the Auth0 Management API.
* Requires a token with the appropriate permissions.
*/
users(token: string): UsersApi {
return new UsersApi(this.domain, token);
}
}

export default Auth0;
8 changes: 4 additions & 4 deletions src/credentials-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { _ensureNativeModuleIsInitializedWithConfiguration } from '../utils/nati
import type { LocalAuthenticationOptions } from './localAuthenticationOptions';
import A0Auth0 from '../specs/NativeA0Auth0';
import type { NativeModuleError } from '../internal-types';
import type Auth from '../auth';

class CredentialsManager {
private domain;
Expand All @@ -14,12 +15,11 @@ class CredentialsManager {
* @ignore
*/
constructor(
domain: string,
clientId: string,
auth: Auth,
localAuthenticationOptions?: LocalAuthenticationOptions
) {
this.domain = domain;
this.clientId = clientId;
this.domain = auth.domain;
this.clientId = auth.clientId;
this.localAuthenticationOptions = localAuthenticationOptions;
}

Expand Down
70 changes: 70 additions & 0 deletions src/credentials-manager/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Credentials } from '../types';
import type { Auth0Client, GetTokenSilentlyOptions } from '@auth0/auth0-spa-js';

class CredentialsManager {
constructor(private client: Auth0Client) {}

/**
* In auth0-spa-js, credentials are saved automatically after a successful login flow.
* This method is a no-op to maintain API compatibility.
*/
async saveCredentials(_credentials: Credentials): Promise<void> {
console.warn(
'CredentialsManager.saveCredentials is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically.'
);
return Promise.resolve();
}

/**
* Retrieves the stored credentials. This is analogous to getTokenSilently in auth0-spa-js.
*/
async getCredentials(
_scope?: string,
_minTtl: number = 0,
parameters: Record<string, unknown> = {}
): Promise<Credentials> {
const options: GetTokenSilentlyOptions = {
authorizationParams: {
...parameters,
},
detailedResponse: true,
};

const tokenResponse = await this.client.getTokenSilently(options);

const idTokenClaims = await this.client.getIdTokenClaims();
if (!idTokenClaims || !idTokenClaims.exp) {
throw new Error('ID token or expiration claim is missing.');
}

const { id_token, access_token, scope } = tokenResponse;

return {
idToken: id_token,
accessToken: access_token,
scope: scope,
tokenType: 'Bearer',
expiresAt: idTokenClaims.exp,
};
}

/**
* Checks if valid, non-expired credentials exist.
*/
async hasValidCredentials(_minTtl: number = 0): Promise<boolean> {
// Note: minTtl is not directly applicable in the same way,
// as getTokenSilently handles expiry checks. isAuthenticated is the closest equivalent.
return this.client.isAuthenticated();
}

/**
* Clears the stored credentials from the local cache.
* This is equivalent to a local-only logout in auth0-spa-js.
*/
async clearCredentials(): Promise<void> {
// `logout({ openUrl: false })` clears local state without redirecting.
await this.client.logout({ openUrl: false });
}
}

export default CredentialsManager;
Loading