I am trying to get Auth0 to play nicely with React Navigation in a native project, I have followed this article Authentication flows | React Navigation as I am trying to separate out some routes in my native app:
- Loading (contains a stack to a loading page, used when retrieving token from storage and checking validity)
- Unauthenticated (contains the guest stack for welcome and login pages)
- User (contains bottom tabs and stacks to serve up main app functionality)
My App.js file looks like this which on the surface works fine, when I login it moves to the user navigation, logout reverts back to the guest navigation:
import * as React from 'react';
import { View } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import EncryptedStorage from 'react-native-encrypted-storage';
import { ActivityIndicator } from 'react-native-paper';
import Auth0 from "react-native-auth0";
import jwt_decode from "jwt-decode";
import WelcomePage from './src/surfaces/loggedout/welcome';
import { SocialFeed } from './src/surfaces/social';
const AuthContext = React.createContext();
const auth0 = new Auth0({
domain: 'xxxxxxxx.auth0.com',
clientId: 'xxxxxxxxxxxxxxxxxxxxxxxx'
});
function LoadingScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator animating={true} color='#02BFAB' />
</View>
);
}
const Stack = createNativeStackNavigator();
const Tab = createMaterialBottomTabNavigator();
export default function App() {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
console.log("State: " + JSON.stringify(state));
React.useEffect(() => {
async function bootstrapAsync() {
let session = await EncryptedStorage.getItem("user_session");
return await session;
}
bootstrapAsync()
.then(response => {
if (response) {
let obj = JSON.parse(response);
console.log('Session data: ' + JSON.stringify(obj));
return obj;
} else {
console.log('Session empty.');
return null;
};
})
.then(token => {
dispatch({ type: 'RESTORE_TOKEN', token: token.accessToken });
})
.catch(e => console.log(e));
}, []);
const authContext = React.useMemo(
() => ({
signIn: async () => {
auth0.webAuth
.authorize({
scope: "openid email"
})
.then(res => {
var idToken = res.idToken;
var decodedIdToken = jwt_decode(idToken);
console.log("ID token: " + idToken + ". Decoded: " + JSON.stringify(decodedIdToken));
async function storeUserSession() {
try {
let response = await EncryptedStorage.setItem(
"user_session",
JSON.stringify({
accessToken : res.accessToken,
idToken : res.idToken,
decodedToken : decodedIdToken
})
);
console.log("EncryptedStorage setItem response: " + response)
} catch (error) {
console.log("Error storing session in EncryptedStorage: " + JSON.stringify(error))
}
};
storeUserSession();
console.log("Logged in.");
return res.accessToken;
})
.then( token => {
dispatch({ type: 'SIGN_IN', token: token });
})
.catch(error => console.log("Auth0 webAuth error: " + JSON.stringify(error)))
},
signOut: async () => {
auth0.webAuth
.clearSession()
.then(success => {
async function clearStorage() {
try {
let response = await EncryptedStorage.clear();
console.log("EncryptedStorage clear response: " + response)
} catch (error) {
console.log("Error clearing EncryptedStorage: " + JSON.stringify(error))
}
};
clearStorage();
dispatch({ type: 'SIGN_OUT' });
console.log("Logged out.");
})
.catch(error => console.log("Auth0 clearSession error: " + JSON.stringify(error)))
},
}),
[]
);
if (state.isLoading) {
// Loading Spinner
return (
<AuthContext.Provider value={authContext}>
<Stack.Navigator>
<Stack.Screen name="Splash" options={{ headerShown: false }} component={LoadingScreen} />
</Stack.Navigator>
</AuthContext.Provider>
)
} else if (state.userToken == null) {
// Logged Out Stack
return (
<AuthContext.Provider value={authContext}>
<Stack.Navigator>
<Stack.Screen
name="Welcome"
component={WelcomePage}
initialParams={{ login: authContext.signIn }}
options={{
title: 'Sign in',
headerShown: false,
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
</Stack.Navigator>
</AuthContext.Provider>
)
} else {
// User Routes
return (
<AuthContext.Provider value={authContext} >
<Tab.Navigator>
<Tab.Screen name="Feed" component={SocialFeed} initialParams={{ logout: authContext.signOut }} />
</Tab.Navigator>
</AuthContext.Provider>
)
};
}
This is a little flawed however as passing the authContext signIn and signOut down into children is giving me non-serializable values warnings and as I build out my app functionality I am not sure if that will even be sustainable. I also need to be able to access the tokens via the state in other pages (ie. access the decoded id token in a child page so I can call a 3rd party API with a custom claim key value).
So my question is… is there a better way to do this? I have spent the best part of a day getting to this point but its not great
What I was trying to achieve was something similar to the useAuth0
hook from React SDK (auth0/auth0-react) to be able to access the authentication state for switching the Navigation Stacks and user (inc. itToken claims) for injecting Auth0 user data anywhere in my native project where I might need it.