Authentication workflow in react/redux - authentication

I am trying to setup a new front-end project using react, react-router and redux and I'm stumbling on setting up a login workflow that works well and with many pages.
Here is the logic that I want to have:
Have a variable in the redux store that says if I'm logged in or not. To simplify let's say that we have logged: true/false.
When logged is set to true from the login page, redirect to the page we were redirected from, or redirect to / if not defined. The previous location is stored in nextPathname.
When logged is set to false in a component that requires auth, redirect to /.
My main problem with this workflow is that I want to dispatch an action (set the logged value) and then do a redirection.
Here are a few alternatives that I have explored:
Check if logged==true in my login component's componentWillReceiveProps and redirect if it's the case. This works well for the login workflow but if I have to implement such a logic for the logout component, I have to do it for each component that requires auth. (Mixins don't work thanks to ES6 react components...).
Handle the redirections in a middleware: when the action is about login, run the action and then redirect. However I don't know if this is a best practice or not.
So my question is: how do I make the auth work without too much overhead on all pages that are restricted?
A bit of code:
app.js:
function requireAuth(nextState, replace) {
if(store.getState().Login.logged === false) {
replace({
pathname: "/login",
state: { nextPathname: nextState.location.pathname }
});
}
}
const history = syncHistoryWithStore(browserHistory, store);
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={Archives} onEnter={requireAuth}></Route>
<Route path="/login" component={LoginComponent}></Route>
</Router>
</Provider>,
document.getElementById("app")
);
The LoginComponent:
class LoginComponent extends React.Component<LoginProps, any> {
componentWillReceiveProps(newProps) {
if(newProps.logged) {
let nextPath = "/";
const {location, history} = newProps;
if (location.state && location.state.nextPathname) {
nextPath = location.state.nextPathname;
}
history.pushState({}, nextPath);
}
}
render() {
return <div>This is a login form</div>
}
}

The way you'll want to approach this, I believe, is by using a 'RequireAuth' higher order component to wrap your components that the user needs to be authenticated to access. Here's an example of such, just pay attention to the auth components in the client folder: https://github.com/joshuaslate/mern-starter

Related

Reevaluate Nuxt.js middleware without a route change

I'm wondering if it's possible to essentially "reevaluate" the middleware conditions without actually changing the current route.
The middleware's purpose is to prevent non-logged-in users from accessing the "dashboard".
My issue is, a user could become logged in or logged out without necessarily changing route but they wouldn't be redirected until they try and change pages.
I have a VueX action that triggers when the user's auth state changes but this (from what I can see), can't access the redirect or route variables.
// /mixins/auth.js
const reevaluateAuthStatus = (store, redirect, route) => {
console.log(route)
const redirectPolicy = route.meta.map((meta) => {
if (meta.auth && typeof meta.auth.redirectPolicy !== 'undefined') { return meta.auth.redirectPolicy[0] }
return []
})
const user = store.getters['auth/getUser']
if (redirectPolicy.includes('LOGGEDOUT')) {
if (user) {
return redirect('/dashboard')
}
} else if (redirectPolicy.includes('LOGGEDIN')) {
if (!user) {
return redirect('/login')
}
}
}
module.exports = {
reevaluateAuthStatus
}
// /middleware/auth.js
import { reevaluateAuthStatus } from '../mixins/auth'
export default function ({ store, redirect, route }) {
reevaluateAuthStatus(store, redirect, route)
}
Appreciate any help on this :)
You cannot re-evaluate a middleware AFAIK because it's mainly this (as stated in the documentation)
middlewares will be called [...] on the client-side when navigating to further routes
2 clean ways you can still achieve this IMO:
use some websockets, either with socket.io or something similar like Apollo Subscriptions, to have your UI taking into account the new changes
export your middleware logic to some kind of call, that you could trigger again by calling the $fetch hook again or any other data-related fetching hook in Nuxt
Some more ugly solutions would probably be:
making an internal setInterval and check if the actual state is still valid every 5s or so
move to the same page you are actually on with something like this.$router.go(0) as somehow explained in the Vue router documentation
Still, most of the cases I don't think that this one may be a big issue if the user is logged out, because he will just be redirected once he tries something.
As if the user becomes logged-in, I'm not even sure on which case this one can happen if he is not doing something pro-active on your SPA.
I don't know if it's relevant or not, but I solved a similar problem this way:
I have a global middleware to check the auth status. It's a function that receives Context as a parameter.
I have a plugin that injects itself into context (e.g. $middleware).
The middleware function is imported here.
In this plugin I define a method that calls this middleware passing the context (since the Plugin has Context as parameter as well): ctx.$middleware.triggerMiddleware = () => middleware(ctx);
Now the middleware triggers on every route change as intended, but I can also call this.$middleware.triggerMiddleware() everywhere I want.

Running Nuxt middleware client side after static rendering

We're switching from SPA to statically generated, and are running into a problem with middleware.
Basically, when Nuxt is statically rendered, middleware is run on the build server first, and then is run after each page navigation client side. The important point is that middleware is not run client side on first page load. This is discussed here
We work around this for some use cases by creating a plugin that uses the same code, since plugins are run on the first client load.
However, this pattern doesn't work well for this use case. The following is an example of the middleware that we want to use:
// middleware/authenticated.js
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
// Inside a component
<template>
<h1>Secret page</h1>
</template>
<script>
export default {
middleware: 'authenticated'
}
</script>
This example is taken directly from the Nuxt docs.
When rendered statically, this middleware is not called on first page load, so a user might end up hitting their dashboard before they've logged in, which causes problems.
To add this to a plugin, the only way I can think to do this is by adding a list of authenticated_routes, which the plugin could compare to and see if the user needs to be authed.
The problem with that solution though is that we'd then need to maintain a relatively complex list of authed pages, and it's made worse by having dynamic routes, which you'd need to match a regex to.
So my question is: How can we run our authenticated middleware, which is page specific, without needing to maintain some list of routes that need to be authenticated? Is there a way to actually get the middleware associated to a route inside a plugin?
To me it is not clear how to solve it the right way. We are just using the static site generation approach. We are not able to run a nuxt middleware for the moment. If we detect further issues with the following approach we have to switch.
One challenge is to login the user on hot reload for protected and unprotected routes. As well as checking the login state when the user switches the tabs. Maybe session has expired while he was on another tab.
We are using two plugins for that. Please, let me know what you think.
authRouteBeforeEnter.js
The plugin handles the initial page load for protected routes and checks if the user can access a specific route while navigating around.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes"
export default ({ app, store }) => {
app.router.beforeEach(async (to, from, next) => {
if(to.name === 'logout'){
await store.dispatch('app/shutdown', {userLogout:true})
return next('/')
}
if(PROTECTED_ROUTES.includes(to.name)){
if(document.cookie.indexOf('PHPSESSID') === -1){
await store.dispatch('app/shutdown')
}
if(!store.getters['user/isLoggedIn']){
await store.dispatch('user/isAuthenticated', {msg: 'from before enter plugin'})
console.log('user is logged 2nd try: ' + store.getters['user/isLoggedIn'])
return next()
}
else {
/**
* All fine, let him enter
*/
return next()
}
}
return next()
})
}
authRouterReady.js
This plugin ment for auto login the user on unprotected routes on initial page load dnd check if there is another authRequest required to the backend.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes";
export default function ({ app, store }) {
app.router.onReady(async (route) => {
if(PROTECTED_ROUTES.includes(route.name)){
// Let authRouterBeforeEnter.js do the job
// to avoid two isAuthorized requests to the backend
await store.dispatch('app/createVisibilityChangedEvent')
}
else {
// If this route is public do the full init process
await store.dispatch('app/init')
}
})
}
Additionally i have added an app module to the store. It does a full init process with auth request and adding a visibility changed event or just adds the event.
export default {
async init({ dispatch }) {
dispatch('user/isAuthenticated', {}, {root:true})
dispatch('createVisibilityChangedEvent')
},
async shutdown({ dispatch }, {userLogout}) {
dispatch('user/logout', {userLogout}, {root:true})
},
async createVisibilityChangedEvent({ dispatch }) {
window.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
console.log('visible changed');
await dispatch('user/isAuthenticated', {}, {root:true})
}
})
},
}

How to prevent visitor from seeing app before redirection?

I am building a SPA using Vue-CLI with a client-side OAuth 2.0 javascript library called JSO. It uses HTML 5.0 localStorage to cache Access Tokens.
In my full app, I have everything functioning properly with the exception of the following issue:
When the user arrives at my app for the first time, he catches a quick glimpse of the app and then automatically is redirected to a third party authentication login screen. I don't want that "quick glimpse" to happen -- I need to have the user immediately redirected to the third party login page BEFORE he sees any part of my app.
So, I thought I'd set up Global Before Guards using Vue-Router like so:
From: Main.js
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: {
requiresAuth: true
}
},
...more routes...and they all require auth...
]
router.beforeEach((to, from, next) => {
const token = window.localStorage.getItem('my-token-example')
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
if (token == null) {
client.getToken()
next(false)
}
} else {
next()
}
})
new Vue({
created: function() {
//check for response parameters if user has an auth token (uses JSO plugin)
//if token received, then it is stashed into localStorage
client.callback()
},
render: h => h(App),
router: router
}).$mount('#app')
Example App.vue:
<template>
<div id="app">
<Header />
<routerView />
</div>
</template>
Info on client.callback():
I catch the headers response when user is returning to app
Info on client.getToken():
I get the token payload
Notes: The JSO auth docs state:
"You may also ensure that a token is available early in your application, to force all user interaction and redirection to happen before your application is fully loaded. To do that make a call to getToken, and wait for the callback before you continue.
REMEMBER to ALWAYS call the callback() function to process the response from the OAuth server, before you use getToken(), if not you will end up in an redirect_loop"
Does anyone have any suggestions on how I can prevent the user from seeing my app before he is redirected to the auth login site? Update: I think I see the problem...is the global guards only affecting the section of the app within the <RouterView /> component? Hence, we see the header and banner of my app before redirection?
I solved this. I had to simply do a v-if on my app.vue file like: <div v-if="token !== null>
That hides the app template until token is received.

Angular 5/6: protect route (route guard) without redirecting to error route

I have a bit of a pickle.
I am using Route guard (implementing CanActivate interface) to check if user is granted access to particular route:
const routes: Routes = [
{
path: '',
component: DashboardViewComponent
},
{
path: 'login',
component: LoginViewComponent
},
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']},
canActivate: [RouteGuard]
},
{
path: '**',
component: ErrorNotFoundViewComponent
}
];
Now it works great in protecting the '/protected/foo' route from activating, but I would like to tell the user that route he is trying to access is forbidden (similar to 403 Forbidden you may get from server).
The problem:
How do I show the user this special error view without redirecting him to error route which seams to be the preferred option by so many sources I have found?
And how do I still use my RouteGuard without actually loading the forbidden route, because if I check access inside my FooViewComponent and display different view it kind of defeats point of having RouteGuard in the first place.
Ideally I would like to have my RouteGuard not only returning false in canActivate() method, but also replace component completely with say ErrorForbiddenViewComponent. But I have no idea how to do it, or is it event possible. Any alternatives?
This is how my route guard looks now:
import {Injectable} from '#angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '#angular/router';
import {AuthService} from '../services/auth.service';
#Injectable()
export class RouteGuard implements CanActivate {
constructor(
private router: Router,
private auth: AuthService
) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const { auth, router } = this;
const { allowAccessTo } = next.data;
const identity = auth.getIdentity();
if (
identity &&
allowAccessTo.indexOf(identity.role)
) {
// all good, proceed with activating route
return true;
}
if (identity) {
// TODO show ErrorForbiddenViewComponent instead of redirecting
console.log('403 Forbidden >>', next);
}
else {
// not logged in: redirect to login page with the return url
const [returnUrl, returnQueryParams] = state.url.split('?');
console.log('401 Unauthorised >>', returnUrl, returnQueryParams, next);
router.navigate(['/login'], {queryParams: {returnUrl, returnQueryParams}});
}
return false;
}
}
So I am just preventing route from loading, but I am not redirecting. I only redirect non logged visitors to login route.
Reasoning:
Routes should reflect certain state of application - visiting a route
url should recreate that state
To have error routes (except for 404 Not Found) would mean your application can actually recreate error states. This makes no sense
as why would you keep error state as state of your application? For
debugging purpose one should use logs (console or server), revisiting
error page (i.e. page refresh) might interfere with that.
Also by redirecting to error route app should provide some insights of error to user. For that matter either some parameter would need to
be passed via url or (far worse) keeping the error sate in some error
service and retrieve it upon accessing error route.
Also, ignoring the RouteGuard and just loading the component and checking access inside it may result in some extra dependencies
loaded which would not be used anyway (as user is not allowed), makes
the whole lazy loading much harder.
Does anyone have some kind of solution for this? I also wonder how come that after Angular 2+ being around for so long nobody had this kind of situation before? Everybody is just ok with redirecting?
Also keep in mind that although I am currently using the FooViewComponent synchronously, that may change in future!
I had once worked on the similar problem.
Sharing my stackblitz poc where I have created -
Authenticated Component (with guard)
Login Component
Permission Guard
Route (/auth route is provided with PermissionGuardService guard)
The guard is evaluating the user type and handling the redirection / error accordingly.
The use cases are -
User is not logged in (shows a toast with log in message)
User is not admin (shows a toast with unauthorised message)
User is admin (show a toast with success messaage)
I have stored the user in local storage.
EDIT - DEMO
Let me know if you need a special handling in it and I will update the code base.
Cheers!
After looking at angular2 example provided by Tarun Lalwani in comments of question and after taking deeper look into Dynamic component loader article on Angular docs I have managed to apply it to my code:
I no longer use my RouteGuard when specifying routes:
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']}, // admin only
canActivate: [RouteGuard]
},
Instead I have created special RouteGuardComponent and here is how I use it:
{
path: 'protected/foo',
component: RouteGuardComponent,
data: {component: FooViewComponent, allowAccessTo: ['Administrator']}
},
This is the code of RouteGuardComponent:
#Component({
selector: 'app-route-guard',
template: '<ng-template route-guard-bind-component></ng-template>
// note the use of special directive ^^
})
export class RouteGuardComponent implements OnInit {
#ViewChild(RouteGuardBindComponentDirective)
bindComponent: RouteGuardBindComponentDirective;
// ^^ and here we bind to that directive instance in template
constructor(
private auth: AuthService,
private route: ActivatedRoute,
private componentFactoryResolver: ComponentFactoryResolver
) {
}
ngOnInit() {
const {auth, route, componentFactoryResolver, bindComponent} = this;
const {component, allowAccessTo} = route.snapshot.data;
const identity = auth.getIdentity();
const hasAccess = identity && allowAccessTo.indexOf(identity.role);
const componentFactory = componentFactoryResolver.resolveComponentFactory(
hasAccess ?
component : // render component
ErrorForbiddenViewComponent // render Forbidden view
);
// finally use factory to create proper component
routeGuardBindComponentDirective
.viewContainerRef
.createComponent(componentFactory);
}
}
Also, this requires special directive to be defined (I am sure this can be done some other way, but I have just applied that Dynamic component example from Angular docs):
#Directive({
selector: '[route-guard-bind-component]'
})
export class RouteGuardBindComponentDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}
It isn't full answer to my own question (but its a start), so if somebody provides something better (i.e. a way to still use canActivate and ability to lazy load) I'll make sure to take that into account.
Your RouteGuard can inject whatever service you're using for modal windows, and the .canActivate() can pop the modal without redirection to inform the user without disturbing the current state of the app.
We use toastr and its angular wrapper for this, since it creates a modeless pop-up that self-dismisses after so-many seconds, no OK/Cancel buttons needed.
I've recently come across the same problem. In the end, I couldn't manage to do this using CanActivate guard, so I've implemented the authorisation logic in the component that holds the <router-outlet>.
Here is its template:
<div class="content">
<router-outlet *ngIf="(accessAllowed$ | async) else accessDenied"></router-outlet>
</div>
<ng-template #accessDenied>
<div class="message">
<mat-icon>lock</mat-icon>
<span>Access denied.</span>
</div>
</ng-template>
And its source code:
import { ActivatedRoute, ActivationStart, Router } from '#angular/router';
import { filter, switchMap, take } from 'rxjs/operators';
import { merge, Observable, of } from 'rxjs';
import { Component } from '#angular/core';
#Component({
selector: 'app-panel-content',
templateUrl: './content.component.html',
styleUrls: ['./content.component.scss'],
})
export class PanelContentComponent {
/**
* A stream of flags whether access to current route is permitted.
*/
accessAllowed$: Observable<boolean>;
constructor(
permissions: UserPermissionsProviderContract, // A service for accessing user permissions; implementation omitted
route: ActivatedRoute,
router: Router,
) {
const streams: Observable<boolean>[] = [];
/*
The main purpose of this component is to replace `<router-outlet>` with "Access denied"
message, if necessary. Such logic will be universal for all possible route components, and
doesn't require any additional components - you will always have at least one component with
`<router-outlet>`.
This component contains `<router-outlet>`, which by definition means that all possible authorisable
routes are beneath it in the hierarchy.
This implicates that we cannot listen to `route.data` observable of `ActivatedRoute`, because the route
itself in this component will always be the parent route of the one we need to process.
So the only real (the least hacky, IMO) solution to access data of child routes is to listen to
router events.
However, by the time an instance of this component is constructed, all routing events will have been
triggered. This is especially important in case user loads the page on this route.
To solve that, we can merge two streams, the first one of which will be a single access flag
for **activated route**, and the second will be a stream of flags, emitted from router
events (e.g. caused by user navigating through app).
This approach requires that the authorised route is bottom-most in the hierarchy, because otherwise the
last value emitted from the stream created from router events will be `true`.
*/
const deepestChild = this.findDeepestTreeNode(route);
const currentData = deepestChild.routeConfig.data;
// `data.authActions` is just an array of strings in my case
if (currentData &&
currentData.authActions &&
Array.isArray(currentData.authActions) &&
currentData.authActions.length > 0) {
streams.push(
// `hasPermissions(actions: strings[]): Observable<boolean>`
permissions.hasPermissions(currentData.authActions).pipe(take(1))
);
} else {
// If the route in question doesn't have any authorisation logic, simply allow access
streams.push(of(true));
}
streams.push(router.events
.pipe(
filter(e => e instanceof ActivationStart),
switchMap((event: ActivationStart) => {
const data = event.snapshot.data;
if (data.authActions &&
Array.isArray(currentData.authActions) &&
data.authActions.length > 0) {
return permissions.hasPermissions(data.authActions);
}
return of(true);
}),
));
this.accessAllowed$ = merge(...streams);
}
/**
* Returns the deepest node in a tree with specified root node, or the first
* encountered node if there are several on the lowest level.
*
* #param root The root node.
*/
findDeepestTreeNode<T extends TreeNodeLike>(root: T): T {
const findDeepest = (node: T, level = 1): [number, T] => {
if (node.children && node.children.length > 0) {
const found = node.children.map(child => findDeepest(child as T, level + 1));
found.sort((a, b) => a[0] - b[0]);
return found[0];
} else {
return [level, node];
}
};
return findDeepest(root)[1];
}
}
interface TreeNodeLike {
children?: TreeNodeLike[];
}
I've explained the approach in comments in the source code, but in short: access authorisation data in route.data using router events, and replace <router-outlet> with an error message if access is denied.

Checking auth token valid before route enter in Vue router

I have a simple use case, where my application is using vue-router and vuex. Then store contains a user object which is null in the beginning. After the user is validated from the server it sends back an user object which contains a JWT auth token which is assigned to the user object in the store. Now lets assume that the user came back after 3 hours and tried to visit a route or perform any other action, considering that the auth token has expired by then, what would be the best way to check that(need to call axios post to check it) and redirect user to the login page. My app will have loads of components so I know I can write logic to check the token valid in the mounted hook of each component but that would mean repeating it all of the components. Also I don't want to use the beforeEach navigation guard because I cannot show any visual feedback to the user like checking... or loading....
I do something similar in one of my projects, it's actually deceptively difficult to handle these types of situations, but you can add a beforeEnter guard to your protected routes, then redirect if the authentication failed.
const guard = function(to, from, next) {
// check for valid auth token
axios.get('/api/checkAuthToken').then(response => {
// Token is valid, so continue
next();
}).catch(error => {
// There was an error so redirect
window.location.href = "/login";
})
};
Then on your route you can do:
{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
guard(to, from, next);
}
},
You may notice I've used location.href rather than router.push. I do that because my login form is csrf protected, so I need a new csrf_token.
Your other issue is going to be if the user tries to interact with your page without changing the route (i.e. they click a button and get a 401 response). For this I find it easiest to check authentication on each axios request and redirect to login when I receive a 401 response.
In terms of adding a loading spinner during the guard check you can simply add a loading flag to your vuex store then import your store into your router. Honestly though I wouldn't bother, on a decent production server the check will be done so quickly that the user is unlikely to ever see it.
Try Vue.JS Mixins
You can define a Global Mixin and use it via Vue.use(myMixin) - then all Components will inherit this mixin. If you define a mounted or probably better activated hook on the mixin, it will be called on every component.
There you can use everything a component can do - this will point to your component. And if the component also defines a hook itself, the mixin hook of the same type will run before the components own hook.
Or try a single top-level login component
We used a little different solution - we have a single component which handles everything login-related, which exists outside of the router-view in the parent index.html. This component is always active and can hide the div router-view and overlay a loading message or a login-screen. For an intranet-application this component will also use polling to keep the session alive as long as the browser stays open.
You can load of your router-navigation to this component. - So a child-component which wants to trigger a router-navigation just sets a global reactive property navigateTo which is watched by the top level authentication component. This will trigger an authentication check, possibly a login-workflow and after that the top-level component will call $router.push() With this approach you have complete control over any navigation.
You can use interceptors to silently get the auth token when some request happens.
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const rToken = window.localStorage.getItem('rToken');
return axios.post('url/to/get/refresh/token', { rToken })
.then(({data}) => {
window.localStorage.setItem('token', data.token);
window.localStorage.setItem('rToken', data.refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
return axios(originalRequest);
});
}
return Promise.reject(error);
});
Because you use vuex, you can add some state like isLoading or isChecking.
And in your router.beforeEach, you can check and set isLoading or isChecking follow your current checking state. Then you can show loading message follow this state.
In our route.js we check in beforeEnter hooks the user has token or
not.
route.js
{
path: '/dashboard',
name: dashboard,
meta: {
layout: 'home-layout'
},
components: {
default: Dashboard,
header: UserHeader
},
beforeEnter: ifAuthenticated,
}
route.js
const ifAuthenticated = (to, from, next) => {
if (localStorage.getItem(token)) {
next();
return;
}
router.push({
name: 'login',
params: {
returnTo: to.path,
query: to.query,
},
});
};