user authentication in a react native app - react-native

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

Related

Vuex JWT Refresh token

I want to know how to keep user logged in all the time, so that when his token about to expire he could refresh it and would not receive 401 status when entering guarded routes and so that his data could be reflected on page whenever he refreshed it.
My assumption is that once App.vue is opened it should fetch data from server, confirming that user is logged and requesting user data, and eventually storing it in Vuex or localStorage.
Hope I am on the right way to solution
fetch('http://localhost:9000/api/refresh',{
method:'POST',
mode:'cors',
body:JSON.stringify({refreshToken :localStorage.getItem('refreshToken')}),
headers:{'Content-Type':'application/json'}
}
).then(res=>{
if(res.status===200){
return res.json()
}
else return
})
.then(({token})=>{localStorage.setItem('token',token)
Store.dispatch('authorize',JSON.parse(localStorage.getItem('user')))
return })

Vue,js used with Supabase - can't update signIn button after logging in with Oauth

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.

Auto Sign In with Cognito / Amplify Authenticator Components?

I'm using the Amplify authenticator component (described here) with Cognito User Pools.
Right now, adding the basic authenticator to html takes the user through the following process automatically:
1) Sign up
2) Enter verification code sent to email
3) Sign-in: Re-enter user name and password
This is based on just adding to html:
<amplify-authenticator></amplify-authenticator>
So new users sign up, and then right away need to sign in. It would be better if they were automatically signed-in, so that upon entering their verification code they went right into the app. This would be a common authentication flow.
Is there a way to have automatic sign-in like this while still using the authenticator components?
I see a github discussion about this topic here, but it is not resolved.
I have figured it out for my purposes. The key is that the Amplify components (authenticator, as well as the individual components) give out information about the state after the user takes action. You can listen for this info, and then programmatically sign in the user once you get the "signIn" state.
These are the states, as described here (I am using Angular):
'signUp' //when you want to show signUp page
'confirmSignUp' //the user has probably just submitted info for signing up, and now you want to show the "confirm" page--like the page where they can enter a verification code
'signIn' //this is the key for these purposes. This is when you want to show the sign in page. In the basic authenticator structure, this happens just after the user has been signed up. So this is where you can automatically sign in the user with Auth.signIn, like below.
'confirmSignIn' //I assume this is after user has submitted the sign in form and you want a confirm page to show
'signedIn' //presumably after user has been fully signed in (not confirmed)
'forgotPassword' //presumably when the forgotPassword form loads (not confirmed)
'requireNewPassword' //presumably when user clicks 'reset password' (not confirmed)
So, using Authenticator, you can listen for the 'signIn' event and automatically sign in the user then. Note however that, while this will sign in the user automatically, Authenticator may still show the sign in page while this action is being processed. You can hide this page, or just use the individual Amplify components, like I show in #2 below.
Even better would be if Amplify/Cognito could just have an option for the developer to select automatic sign in after sign up when they set up Cognito. Automatic sign in on sign up would be in line with modern authentication practice, and would be better for user flow. I do not find this option anywhere. If anyone at Amplify/Cognito is listening, please consider adding this feature.
Note: There seem to be some issues that can arise when using these components. For example--with the authenticator, if the user submits info to sign up, but then leaves the page before entering their verification code: the next time they try to access the app they may have trouble, because they can't go through the sign up process again (a user has already been created), but they also can't sign in (because they've never verified). Some more discussion on that is here.
Additionally, I have found error messages to be an issue. These Amplify components automatically show error messages, and some of them from Cognito are technical messages I would never want a user to see (stuff that talks about "user id" and lambda functions, depending on the random error). There may be a way to customize these, like described here, but be sure you test a lot of different scenarios to see what might happen in terms of the error messages.
If these issues prove problematic for you (like they have for me), you may want to use your own forms instead, and then use Auth.signIn() and related Amplify methods, instead of these components.
But, for auto-signing-in with Amplify components, here is code that worked for me:
1. Using Amplify's Authenticator component:
html
<amplify-authenticator></amplify-authenticator>
ts:
import { AmplifyService } from 'aws-amplify-angular';
import Auth from '#aws-amplify/auth';
Export class AuthComponent implements OnInit {
state: any;
newUser: any;
username: any;
password: any;
constructor(public amplifyService: AmplifyService){
this.amplifyService.setAuthState(this.authState) //may be not required
this.amplifyService.authStateChange$ //listening for state changes
.subscribe(authState => {
this.state = authState.state
if (this.state === 'confirmSignUp'){
console.log('user just signed up, now on verify code form')
this.newUser = authState.user
this.password = this.newUser.username
let checkPassword = this.newUser.username.password
if (checkPassword != 'undefined') {
this.password = checkPassword
/*Note here: I have coded it this way because it looks like the authenticator runs two events when the user hits "sign up". In the first event, you can get the user's password, to be used in the Auth.signIn() function below. In the second event, you can only get the user's username (and password would show up as undefined if you grabbed it there). So we need a way to get the password from the first event.*/
}
}
if ((this.newUser) && (this.state === 'signIn')){//this.newUser included because otherwise this event will fire anytime the sign in page loads--even for returning users trying to sign in (who you would not want to sign in automatically)
console.log('user has just finished signing up)
Auth.signIn(this.username, this.password).then(()=>{//there might be more parameters, like email, first name and last name, phone number, etc. here--depends on your Cognito settings
console.log('should be signed in now! You can navigate away from this page')
}).catch((error)=>{
console.log('error here = ' + error.message + ', error code = ' + error.code)
})
}
})
}
}
Like I mentioned above, this will show the "sign in" page while the Auth.signIn() function is processing. To avoid that, you could have an *ngIf, saying hide the page when this.state = "signIn".
2. Using Individual Amplify Auth Components:
In the below, the page will load with the sign up form.
Once the user enters their details and clicks sign up, the page will show the "confirm sign up" form, which is where the user enters a verification code that Cognito has sent to him/her (depending on your Cognito settings).
Then, once the user is signed up, you can get the "signIn" state like above, and automatically sign the user in:
html:
<amplify-auth-sign-up [authState]="authState" *ngIf="showSignUp"></amplify-auth-sign-up>
<amplify-auth-confirm-sign-up [authState]="authState" *ngIf="showVerify"></amplify-auth-confirm-sign-up>
ts:
import { AmplifyService } from 'aws-amplify-angular';
import { AuthState } from 'aws-amplify-angular/dist/src/providers';
import Auth from '#aws-amplify/auth';
Export class AuthComponent implements OnInit {
public authState: AuthState
public newUser: any
public username: any
public password: any
public state: any
public showSignUp = true
public showVerify = false
constructor(public amplifyService: AmplifyService) {
this.authState ={//the individual Amplify components require a state be set
user: null,
state: 'signUp'
}
this.amplifyService.setAuthState(this.authState) //this might not be required
this.amplifyService.authStateChange$ //listening for state changes.
.subscribe(authState => {
this.state = authState.state
if (this.state === 'confirmSignUp'){//get change in state
this.newUser = authState.user
this.username = this.newUser.username
let checkPassword = this.newUser.username.password
if (checkPassword != 'undefined') {
this.password = checkPassword
/*Note here: I have coded it this way because it looks like the authenticator runs two events when the user hits "sign up". In the first event, you can get the user's password, to be used in the Auth.signIn() function below. In the second event, you can only get the user's username (and password would show up as undefined if you grabbed it there). So we need a way to get the password from the first event.*/
}
this.authState ={
user: authState.user,
state: 'confirmSignUp'
}
this.showSignUp = false
this.showVerify = true
}
if ((this.newUser) && (this.state === 'signIn')){//this.newUser included because otherwise this event will fire anytime the sign in page loads--even for returning users trying to sign in (who you would not want to sign in automatically)
console.log('user has just finished signing up)
Auth.signIn(this.username, this.password).then(()=>{//there might be more parameters, like email, first name and last name, phone number, etc. here--depends on your Cognito settings
console.log('should be signed in now! You can navigate away from this page')
}).catch((error)=>{
console.log('error here = ' + error.message + ', error code = ' + error.code)
})
}
})
}
}

Ionic 3 app - allow navigation to page if user logged (check on one place, rather on every component)

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>

Initial authentication check in ReactJS and Redux

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?).