Background: I have an app that is showing a list of movies, but what I want to do is to show the list only for users who are authorized (by checking token stored on the local-storage).
so the flow I want to achieve is:
user enters the app main page
check if has token in local storage
if yes check if it is authorized
if authorized = show list, else don't show
so what I do right now is in my main (wrapper) component right after I create my store:
const store = configureStore();
var token = localStorage.get(authConstants.LOCAL_STORAGE_TOKEN_KEY);
if(token){
store.dispatch(actions.checkInitialAuth(token));
}
checkInitialAuth actions is:
export function checkInitialAuth(token){
return dispatch => {
dispatch(requestLogin());
return fetch(authConstants.API_USER_DETAILS, {headers: { 'Authorization' : `Bearer ${token}`}})
.then(function(response){
if(!response.ok){
throw Error(response.statusText);
}
return response.json();
})
.then(function(user){
localStorage.set(authConstants.LOCAL_STORAGE_TOKEN_KEY, token);
dispatch(receiveLogin(user)); // <==========
dispatch(setTopMovies()); // <==========
})
.catch(function(err){
// TODO: handle errors
console.log(err);
});
}
}
so the question is, is it the right place to invoke the initial auth check right after the creating store and right before the element creation?
and now if I have to invoke more actions only if the user is authorized do I have to invoke them in the then inside the checkInitialAuth action? is it the right place to make all the action dispatch calls?
and last one, when the auth is wrong (I changed manually the token to be wrong on the local storage) the console.log(err) is logging as expected but I have also this annoying 401 error in the console, can I somehow avoid it?
thanks a lot!
is it the right place to invoke the initial auth check right after the creating store and right before the element creation?
Yes. Usually this is where all the initialization happens before store is passed to Provider.
is it the right place to make all the action dispatch calls?
This depends a lot on your application logic. You component can check if a user is authenticated itself to display the correct content for 'topMovies'. But nothing stops you from dispatch more actions here.
but I have also this annoying 401 error in the console, can I somehow avoid it?
It is the network request error. It is the response from the server your fecth is contacting. You can hide the error by change your Chrome Dev console filter if you really want to hide them (but why?).
Related
I'm pretty new in React-Native programming, but here is the context.
We are using React Query and Axios libraries in our project. As AuthManager we are using Keycloak and for the library managing auth status we have React Native Keycloak. We encounter a tedious problem with our server responding randomly 401 at our requests after a certain amount of time, bringing also to the app crash sometimes.
We reproduced the error making the Bearer Token of Keycloak expire after only 1 minute. This caused almost immediatly the 401 error and we wondered why this is happening.
Let's say we have a screen with some "Activities" and this screen is the first thing the user will see. For handling requests, in our code we use some custom hooks that reference useQuery, for example:
export function useActivities(): UseQueryResult<ActivityList> {
const { headers } = useHeaders();
return useQuery(
['activities', today.start],
() => getActivitiesList(headers), // Note 1
{
enabled: !!today.start,
}
);
}
The important point of it is that we useHeaders to get our updated headers with the Keycloak token and our realm settings. useHeaders is almost everywhere in our app.
export function useHeaders(): UseHeaders {
const { keycloak } = useKeycloak();
const KEYCLOAK_REALM = remoteConfig().getString('KEYCLOAK_REALM');
const headers = {
Authorization: `Bearer ${keycloak?.token}`,
Realm: KEYCLOAK_REALM,
};
return { headers };
}
Now, the getActivitiesList is simple as five:
async function getActivitiesList(headers: UseHeaders['headers']): Promise<ActivityList> {
const url = `${BASE_URL}${VERSION}/activities/grouped?end=${end}&start=${start}`;
// Note 2
return axios
.get(url, { headers })
.then((res) => res.data)
.catch((e) => console.error('Error fetching grouped activities:', e));
}
The problem with all of that is that whenever Keycloak will trigger the refresh token, the token inside keycloak object is changed, the headers inside useActivities are changed BUT if I print the headers inside my getActivitiesList (// Note 2), or even inside the query function (// Note 1), headers will not be updated. Sometimes it just causes to make two requests (one that fails and show error, the other one actually working), some other times the app crashes without any explain. This makes me wonder why the query function will not update its headers and passed the "old" headers inside getActivitiesList.
For now we are mitigating this problem in two different points.
After keycloak init, we pass immediatly to axios a global header with axios.defaults.headers.common.Realm = KEYCLOAK_REALM;
After receiving a valid token from Keycloak, we overwrite the Authorization header with a new one: axios.defaults.headers.common.Authorization = 'Bearer ${keycloak?.token}';
This is not a perfect solution and we are here to search some info about this problem.
Someone get something similar? How to manage the token refresh in useQuery function?
async handleSignInSignOutButtonClick() {
if (!this.isSignedIn) {
supabase.auth.signIn({ provider: "google" });
this.$store.commit("signIn", supabase.auth.session());
window.location.reload();
return;
}
await this.$store.commit("signOut");
supabase.auth.signOut();
window.location.reload();
},
The above function is triggered by a sign-in button, which is supposed to become a sign-out button and the icon of the user after logging in.
When The function fires, supabase redirects me to Google OAuth consent screen. However, after logging in and redirecting back to my app, the sign-in button stays there until I manually refresh the page.
What is wrong with my code...
There are a couple of things going on that you need to be aware of. For starters you are reloading your page when you don't need to in the handleSignInSignOutButtonClick() function.
When the authentication process begins, your app will be redirected to Google OAuth consent screen as you have discovered. Once the authentication is complete, you will be redirected back to your app and the reload occurs automatically.
The second point is that you can make use of the supabase.auth.onAuthStateChange() event to help you. My suggestion would be to listen for this event when you create your supabase client so it listens for the duration of your app instance. During that event handling, you can assign the user to the store (or anywhere you want to save the user data) based upon the state change. Your app can be reactive to state changes.
In your supabase client setup code:
const supabaseUrl = process.env.SUPABASE_URL // your supabaseUrl
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY // your supabaseKey
const supabase = createClient(supabaseUrl, supabaseAnonKey)
/**
* Set up the authentication state change listener
*
* #param {string} event The event indicates what state changed (SIGN_IN, SIGN_OUT etc)
* #param {object} session The session contains the current user session data or null if there is no user
*/
supabase.auth.onAuthStateChange((event, session) => {
const user = session?.user || null;
// Save your user to your desired location
$store.commit("user", user);
});
Now you have your user data being saved whenever the user logs in and a null being set for the user data when the user logs out. Plus any page refreshes are handled by the change state event listener or any other instance that might change the user state. For example, you could have other login or logout buttons and the single listener would pick them up.
Next is to deal with the actual process of logging in or out. In your component Vue file (from your example):
async handleSignInSignOutButtonClick() {
if ($store.state.user === null) {
await supabase.auth.signIn(
{ provider: "google" },
{ redirectTo: "where_to_go_on_login" }))
} else {
await supabase.auth.signOut()
$router.push("your_logged_out_page")
}
}
Finally for your button change state to indicate logged in or logged out, you can simply observe the store user state.
<button v-if="user">Sign Out</button>
<button v-else>Sign In</button>
This way your button will update whenever the user state changes. The user state changes whenever a user logs in or out, and your code is much more compact and readable.
Once final observation that you may already be doing anyway. I would recommend that you put all of your authentication code into a single file and expose the log in and log out functions for your app use as an export to use in component files. This way everything to do with login and logout is handled in a single location and this code is abstracted away from the component file. If you ever wanted to switch from Supabase you could easily update one or two files and everything else would just keep working.
I'm building a react native app in which users can login. After a user logs in, the server returns a auth token (jwt) which is stored on the device. This all works.
Now, the problem is with authenticating the user whenever something 'happens'.
After the user logs in and the token is stored, he is send to the screen 'Main'. On that screen (and all the other 'secured' screens), I want to check the auth token. I currently have the following function to do this (simplified):
// get the auth token out of the redux store
const authToken = useSelector(state => state.auth.authToken)
// the function to authenticate a user
const authenticateUser = () => {
try {
fetch('http://URL_TO_SERVER/auth/authenticate-user', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + authToken
}
})
.then(response => response.json())
.then(responseJSON => {
if (responseJSON.response === 'OK') {
// user is authenticated
} else {
// user is NOT authenticated
}
})
}
// only run the function once
useEffect(() => {
authenticateUser()
}, [])
This works, but there is a 'lag' (because fetch returns a promise). When I open the app and go to I screen whilst I'm not logged in, the screen is shown for just a second, and after that, I'm redirected to the login screen. So it works, but I'm not sure that this is the correct way to do so.
I could add a 'layer' to each screen and show a loading modal untill the authenticateUser-function has ran, but I don't think that's the correct way to do so.
My question is: what is the best way to secure screens in a react native app? Is the way I'm doing it OK, or should I switch to another method?
Thanks in advance!
EDIT: I have checked the following articles before asking this question.
Authentication with React Native and API backend
https://scotch.io/tutorials/react-native-app-with-authentication-and-user-management-in-15-minutes
You need to come up with better logic for your auth checks. Usually there is no need to check token every time you open a new screen. Even in your example, you log in and then get sent to a Main screen only to check auth token you've just received. Likewise, should user even see "go to main screen" button when user is clearly not logged in? Do you really want to check for token every time new screen opens in the app? What happens if token is valid by the time you open the screen, but, as the time passes and user stays on the same screen, token becomes invalid?
Try this:
create a part of redux store (e.g. reducer) that will handle current authentication state. Simple "isAuthenticated" flag will do for start
you can access this flag from any screen that is connected to redux
when receiving JWT you can read it's expiration date. So, if you refreshed the token 5 minutes ago and it's not going to expire for the next 2 hours, there is no need to check if token is valid, because you can just assume that by comparing expiration date with current date any time you want
use "isAuthenticated" flag to determine whether or not to show the "go to Main screen" button, so that logged out users will not even see that button
if you need even more control, hook into navigation to check where user is trying to navigate to and allow/deny that by checking against "isAuthenticated" flag in redux store. Take a look here and here
create a service to keep track on token expiration dates and refresh tokens ahead of time if needed, or periodically check if current token is valid, and set "isAuthenticated" to false if it isn't. Create wrappers around your network functions (fetch in your example) and notify this service if status 401 is received in response to any request to the server so that the service can either refresh the token and repeat request, or let user know that he was logged out
See what i believe is the best way toachieve it is by adding a splashscreen to the app, and there you can divert logic if the token exists you can redirect to Homescreen or loginScreen.
like this :
in Splashscreen.js
componentDidMount(){
AsyncStorage.getItem('token') ? navigation.navigate('Home'):navigation.navigate('Login')
}
hope it helps.feel free for doubts
How can I add check whether user is logged or not and in accordance with that, allow navigation to desired page or not? It wouldn't be good practice to add :
ionViewCanEnter() {
return this.auth.isAuthenticated();
}
check at the top of every component...
I recommend using an authentication token for your user login. This will allow you to locally store as a variable or local storage and you can implement in your service or provider to be used throughout the app. If you're uncertain with how they work there are plenty of resources online, but ultimately it comes down to your back-end server. Here's an example:Auth Token Example
Also, I would recommend you use *ngIf statement blocks in your html pages where the buttons navigate to the pages themselves and throw an alert if the user tries clicking on the button to navigate.
I have some sample code that can help guide you with this as well.
LoginPage.ts
// API POST authentication
this.API.validateUser(form.value).then((result) =>{
form.reset();//clears values of the form after data is saved to array
this.res = JSON.parse(result.toString());//converts result to array
//console.log(this.res);
if(this.res.token!=""){//sets authtoken to local storage
this.storage.set('authToken',this.res.token)
}
//console.log(localStorage);
if(this.res.status == true){
setTimeout(() => {
LoginPage.initialLogin = true;
this.navCtrl.push(MenuPage);
loading.dismiss();
}, 1000);
}
MenuPage.ts
// MenuPage.ts
/* calls local storage once user hits menupage*/
if(LoginPage.initialLogin==true){
//console.log('Initial Login is:',LoginPage.initialLogin);
this.storage.get('authToken').then((data)=>{//grabs local storage auth token
if(data!=null){
//console.log('GET request happened');
this.loggedIn = true;//User is logged in
this.reap.grabAPIData(data);//calls service to grab API data on initial login
}
});
}
else{
this.reap.getLocalStorage();
//console.log('Initial Login is:',LoginPage.initialLogin);
}
MenuPage.html
This is where you can use your value to determine what the user can see or not see. The button can be hidden or you can throw an alert in the .ts file that lets user know they aren't logged in.
<ion-item *ngIf="loggedIn" no-lines>
<button class="menuButton" ion-button large block (tap)="toNexPage()" >
Next page</button>
</ion-item>
If I want to return custom error from Rules I simply do callback(new UnauthorizedError('Custom error message here')) but how do I do the same thing with Hooks?
callback('error message');
callback(new Error('error message'));
Those didn't worked and "UnauthorizedError" is undefined in Hooks. Whatever I do, on front-end side I always get "WE'RE SORRY, SOMETHING WENT WRONG WHEN ATTEMPTING TO SIGN UP." and when I inspect result of requested I see that there is no difference, each time "InternalExtensibilityError" comes.
Why do I want to return error from Hooks? I run extra validation for sign-up there.
Now it is possible to send custom error messages in hooks.
I extracted below code snippet from Auth0's documentation on hooks.
module.exports = function (user, context, cb) {
const isUserDenied = ...; // determine if a user should be allowed to register
if (isUserDenied) {
const LOCALIZED_MESSAGES = {
en: 'You are not allowed to register.',
es: 'No tienes permitido registrarte.'
};
const localizedMessage = LOCALIZED_MESSAGES[context.renderLanguage] || LOCALIZED_MESSAGES['en'];
return cb(new PreUserRegistrationError('Denied user registration in Pre-User Registration Hook', localizedMessage));
}
};
Here is the original link (https://auth0.com/docs/hooks/extensibility-points/pre-user-registration)
At the moment, returning custom errors from hooks to the top-level API, /dbconnections/signup in this case is not possible in Auth0. This is documented in the bottom of this page.
Note that Hooks is still in Beta, and this enhancement request is one of the most asked for features and it is currently in our backlog. We cannot give an ETA for this yet. You can submit your feedback to the Product here.