Client side OAuth2 app with VueJS - Server component needed? - vue.js

I am new to building JavaScript client-side apps with OAuth2.
I am using a JavaScript library that uses localStorage for OAuth implementation which implements Implicit Flow where only a client-id is needed -- no client-secret.
My corporation uses an enterprise identity provider/auth server (not Social networks) for issuing tokens. My custom app checks for the token on return and then proceeds with navigation using Vue Router's navigation guards.
Question:
From a security perspective, is the below code a good practical starting point? I plan to convert this into a JavaScript single page application (Webpack) where the app won't load protected pages unless a token is found (just the welcome/landing page is shown with an lOGIN button for auth). Is some kind of server validation necessary or is the client library good enough?
Here's a simple example of my auth flow (using Google as the Auth
server for demonstration purposes)
let client = new jso.JSO({
providerID: "google",
default_lifetime: 1800,
client_id: '<my-app-id>',
redirect_uri: "http://localhost/overview",
authorization: "https://accounts.google.com/o/oauth2/auth",
scopes: { request: ["https://www.googleapis.com/auth/userinfo.profile"] }
})
// check the browser response params when user returns
client.callback()
const LandingPage = Vue.component('landing-page', {
template: `<div>
<h2>Thisis the landing page.</h2>
<button v-on:click="oauthLogin">Login</button>
</div>`,
methods: {
oauthLogin() {
// get the token from the response params
client.getToken()
}
}
})
const OverView = { template: '<div><h1>Welcome!</h1><p>Here is what site is about.</p></div>' }
const PResource = { template: '<h2>Protected Resource Content Here</h2>' }
const router = new VueRouter({
mode: 'history',
routes: [
// {
// path: '*',
// component: NotFound
// },
{
path: '/',
component: LandingPage,
meta: {
requiresAuth: false
}
},
{
path: '/overview',
component: OverView,
meta: {
requiresAuth: true
}
},
{
path: '/protected-resource',
component: PResource,
meta: {
requiresAuth: true
}
}
]
})
router.beforeEach((to, from, next) => {
const token = window.localStorage.getItem('tokens-google')
if (to.matched.some(record => record.meta.requiresAuth)) {
//if token exists, continue user to protected resource
if (token !== null) {
next()
} else {
//if no token exists, send to a landing page
next({
path: '/',
query: {
redirect: to.fullPath
}
})
}
} else {
// if token exists and user navigates to root, redirect to /overview
if (token !== null && to.path == '/') {
next({ path: '/overview' })
} else {
next() // make sure to always call next()!
}
}
})
var app = new Vue({
el: '#app',
computed: {
checkToken() {
return window.localStorage.getItem('tokens-google')
}
},
router
})
Thanks!

Related

How to restrict going back to login page after logged in in vue?

I want restrict users from going to back to login page after he/she logs in. How to do this using guard in routes ?
My code :
guard.js
export default function guard(to, from, next) {
const token = localStorage.getItem('_utoken');
if (token) {
next();
} else {
next('/login');
}
}
and in routes.js I used beforeEnter:guard inside every object except login route object like this
{
path:'/home,
name: 'Home,
component: Home,
beforeEnter: guard,
}
If token exists restrict from going to login page or signup page .
Instead of adding guard into each and every route, you can add a global guard to all routes:
router.beforeEach((to, from, next) =>
{
const token = localStorage.getItem('_utoken');
if (!to.meta.public)
{
// page requires authentication - if there is none, redirect to /login
if (token) next();
else next('/login');
}
else
{
// Login is supposedly public - skip navigation if we have a token
if (token ? to.path !== '/login' : true) next();
}
});
{
path: '/home',
name: 'Home,
component: Home,
},
{
path: '/login',
name: 'Login',
component: Login,
meta:
{
public: true
}
}

How to disable access to login and signup page when user is logged in?

i am confused why is this not working.
Routes
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home, name: 'Home', meta: { requiresAuth: true }},
{ path: '/adminarea', component: Admin, name:"Admin", meta: { requiresAuth: true }},
{ path: '/login', component: Login, name: 'Login'},
{ path: '/signup', component: Register, name: 'Signup'},
]
});
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!store.getters.getAuth) {
next({ name: 'Login' })
} else {
next();
}
} else {
next()
}
});
to.matched.some(record => record.meta.requiresAuth this line should allow only routes with meta requiresAuth but i don't know why is it allowing other routes as well.
I can manually access login and signup pages.
I cannot figure out what is wrong here.
I included a disableIfLoggedIn value in the router meta section
{
name: 'login',
path: '/login',
component: Login,
meta: {
disableIfLoggedIn: true
}
},
which will check before every route navigation
import {store} from "./store"; // import the store to check if authenticated
router.beforeEach((to, from, next) => {
// if the route is not public
if (!to.meta.public) {
// if the user authenticated
if (store.getters.isAuthenticated) { // I declared a `getter` function in the store to check if the user is authenticated.
// continue to the route
next();
} else {
// redirect to login
next({name: 'login'});
}
}
next();
});
Inside the store declare a getter function to check if authenticated or not
const getters = {
isAuthenticated: state => {
return state.isAuth; // code to check if authenticated
}
};
The meta value disableIfLoggedIn can be set to true in any of the router object. Then it won't be accessible after logged in.

How to create a middleware or how to manage the below route for front end and admin

How can I manage the url for front and admin panel Via Middleware in Vue.
This is the code I have written in router/index.js file:
const router = new VueRouter({ mode: 'history', routes });
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const loggedIn = localStorage.getItem('usertoken') == null ? false : true;
const user = JSON.parse(localStorage.getItem('user'));
//this is for admin
next('/admin/login')
next('/admin/home');
//this is my front URL
next('/terms-condition');
next('/home');
next()
})
export default router;
See the below code it may helps you
/**
* middleware for authentication
*/
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const loggedIn = localStorage.getItem('usertoken') == null ? false : true;
const user = JSON.parse(localStorage.getItem('user'));
if (to.meta.portal == 'admin') {
if (to.meta.auth) {
if (!loggedIn) {
next('/admin/login')
} else if (loggedIn) {
next();
}
} else {
if (!loggedIn) {
next();
} else if (loggedIn) {
if (user.role_id == '1') {
next('/admin/home');
} else {
next('/');
}
}
}
} else if (to.meta.portal == 'front') {
if (loggedIn) {
if (user.role_id == '1') {
next('/admin/home');
} else {
next('/');
}
} else if (!loggedIn) {
if (to.path == "/admin") {
next('/admin/login');
} else {
next();
}
}
}
next()
})
export default router;
And you need to create two router files one for front and other for admin:
//front route file will look like
export default [{
path: '/',
meta: { auth: false, portal: 'front' },
component: () => import('#/components/layouts/front/main.vue'),
children: [
{
path: '/',
name: 'front-home',
title: 'Dashboard',
meta: { auth: false, portal: 'front' },
}
]
}]
//admin router file will be like
export default [
{
path: 'user',
name: 'users',
title: 'Users',
meta: { auth: true, portal: 'admin' },
component: () => import('#/components/templates/admin/user'),
}
]
Main difference is the portal that defines which portal will access by the respective route.Without portal inside meta it won't work.
The way you have implemented is correct
Once the user is successfully logged in , use if else condition to redirect to admin panel, also use Navigation guards given in vue-router
https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard
This help to prevent the other user to use this url directly

How to not be able to access manually /login after you logged in - VueJS?

I'm learning VueJS from an Udemy course. In the module about authentication, the instructor didn't make the whole process, so I had to try it by my self for 2 days but I succeeded 90%.
The backend is on firebase, so after login with correct data, I get back a token that I send it to local storage.
With the code that I make it, you can't see the dashboard if you are not authenticated(even you try the route manually), but what I don't like is that you can see the login page after you are authenticated(if you type /signin).
This last part is not normal to be. So if you are authenticated, when you try manually to go to /signin, you can.
In the router.js:
const routes = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: WelcomePage },
{ path: '/signup', component: SignupPage },
{ path: '/signin', component: SigninPage },
{ path: '/dashboard', component: DashboardPage}
]
});
routes.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/signin', '/signup'];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('token');
console.log(loggedIn);
if (authRequired && !loggedIn) {
return next('/signin');
}
next();
});
And in store.js, inside login action:
if (localStorage.getItem('token')) {
router.replace("/dashboard");
}
Any idea what to do to /login and /register routes after login so to not be able to see them, even you manually try these routes?
If the user will try manually /signin or /signup, I want to be redirected to /dashboard.
In my vue application I just use the router.beforeEach method with meta data plus the state of my token which I pull from my store.
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
var token = store.getters.loggedIn;
if (!token ) {
next({
path: '/login',
})
} else {
next()
}
}else if (to.matched.some(record => record.meta.requiresVisitor)) {
if (token) {
next({
path: '/',
})
} else {
next()
}
}
})
It checks each time the route changes.
The requiresVisitor meta is something I placed in my router object
{
// this is can only be viewed if not logged in.
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'),
props: true,
meta: {
requiresVisitor: true,
layout: "landing",
},
},
{
// this can only be viewed if logged in.
path: '/',
name: 'dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'),
props: true,
meta: {
requiresAuth: true,
layout: "default",
},
},
you can read more about route guards Here
theses methods are typically used in the entry point to the app main.js or in your router.js file.
You can add a per-route guard (https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard) to make the logic only run when /signin and /signup is matched, but if you want to keep it in the loop that runs over all routes you're on the right track -- you just need to invert your logic.
So what you want do is to add another if statement, checking if isLoggedIn is true, and that you're trying to access a public page, and in that case redirect the user to the /dashboard route.
if (!authRequired && loggedIn) {
next('/dashboard');
return;
}
Best example of redirection using beforeEach
const routes = [
{
path: "/",
name: "login",
component: Login,
meta:{requiresVisitor: true},
},
{
path: "/dashboard",
name: "dashboard",
component: Dashboard,
meta:{requiresVisitor: false},
}];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
export default router;
//Check the user is loged in If yes then move to next url.If not loged
in so move in else and check the meta and move into next other wise is
redirect my index url
router.beforeEach((to, from, next) => {
const isLogged = JSON.parse(localStorage.getItem('username'));
if (isLogged) next()
else{
if(to.meta.requiresVisitor) next()
else next('/')
}
})

How to get something to run before vue-router

I am building my first SPA and I am facing some issues. This is how it is designed:
Laravel & Laravel Views handle the login and registration related pages.
SPA starts at the user logged in page.
My app.js defines a default VueJS app in which I use the mounted() method to set the state (VUEX) of the logged in user. Ideally, all it does is get the user details via an axios call to the Laravel backend and populate the VUEX state.
I use beforeEnter() methods in the route definitions to ensure only authorized people can navigate to the route.
This is where I face the problem. When a user logs in, it seems like router is executed before the vuex is set. Say I have a url /dashboard and /user/1. When I try to go to user/1 it works perfectly if it is after I load the application. But, if I refresh the webpage when I am in user/1, then router beforeEnter cannot find vuex state of the user so it redirects user to dashboard. This is because when the router runs beforeEnter, if it is a fresh page load, it wouldn't have access to the user Vuex state or it has access, but the value isn't set yet.
Because of this my biggest problem is I can't link to a route page directly since it always lands in dashboard and then the user will have to go to the route for it to work. How can I handle this situation?
This is what I ended up doing. I defined a function with which I initialized Vue.
At the end of the app.js I used Axios to retrieve the current user via Ajax. In the then method of the promise, I set the store with the user details I received in the promise and then call the function that I defined for Vue initialization above. This way when vue is initialized the store user already has the data.
The code change was very minimal and I didn't have to change the existing axios implementation.
This is my new implementation:
Axios.get('/api/user/info')
.then(response => {
(new Vue).$store.commit('setUser', response.data);
initializeVue();
})
.catch(error => initializeVue());
function initializeVue()
{
window.app = new Vue({
el: '#app',
router,
components: {
UserCard,
Sidebar,
},
methods: mapMutations(['setUser']),
computed: mapState(['user']),
});
}
I use $root as a bus and turn to VueX as a last resort, Here is some code i have stripped out of a plugin i am working on, I have adapted it slightly for you to just drop in to your code.., Should get you going.
This configuration supports VUE Cli.
Don't worry about session expiery, an interceptor will watching for a 401 response from Laravel will do to prompt the user to re-authenticate.
Ditch the axios configuration in bootstrap.js and replace it with this setup and configure Access-Control-Allow-Origin, the wildcard will do for local dev.
axios.defaults.withCredentials = true;
axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': undefined,
'Access-Control-Allow-Origin': '*'
};
axios.interceptors.response.use(
function (response) {
if(response.headers.hasOwnProperty('x-csrf-token')) {
axios.defaults.headers['X-CSRF-TOKEN'] = response.headers['x-csrf-token'];
}
return response;
},
function (error) {
if(typeof error !== 'object' || !error.response) {
return Promise.reject(error);
}
if(error.response.hasOwnProperty('status')) {
switch(error.response.status) {
case 401:
case 419:
// DO RE-AUTHENTICATE CALL
break;
}
}
return Promise.reject(error);
}
);
For the rest...
In main.js
data() {
return {
user: {},
authenticating: false
}
},
computed: {
isAuthenticated() {
// Check a credential only an authorized user would have.
if(this.$router.app.hasOwnProperty('user') === false || this.$router.app.user === null) {
return false;
}
return this.$router.app.user.hasOwnProperty('id');
}
},
methods: {
checkAuth: function () {
this.$set(this.$router.app, 'authenticating', true);
axios.get('/auth/user').then(response => {
this.$set(this.$router.app, 'user', response.data.user);
if (this.$router.app.isAuthenticated()) {
this.$router.push(this.$router.currentRoute.query.redirect || '/', () => {
this.$set(this.$router.app, 'authenticating', false);
});
}
}).catch(error => {
// TODO Handle error response
console.error(error);
this.$set(this.$router.app, 'user', {});
}).finally(() => {
this.$set(this.$router.app, 'authenticating', false);
});
},
login: function (input) {
axios.post('/login', input).then(response => {
this.$set(this.$router.app, 'user', response.data.user);
this.$router.push(this.$router.currentRoute.query.redirect || '/');
}).catch(error => {
// TODO Handle errors
console.error(error);
});
},
logout: function () {
axios.post('/logout').then(response => {
this.$set(this.$router.app, 'user', {});
this.$nextTick(() => {
window.location.href = '/';
});
}).catch(error => {
console.error(error);
});
},
}
beforeCreate: function () {
this.$router.beforeResolve((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth) && !this.$router.app.isAuthenticated()) {
next({
name: 'login',
query: {
redirect: to.fullPath
}
});
return;
}
next();
});
}
In Auth/LoginController.php add method
public final function authenticated(Request $request)
{
return response()->json([
'user' => Auth::user()
]);
}
Create app/Http/Middleware/AfterMiddleware.php
It will pass back a new CSRF token only when it changes rather than on every request. The axios interceptor will ingest the new token when detected.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Cookie;
class AfterMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(Cookie::get('X-CSRF-TOKEN',false) !== csrf_token())
return $next($request)->header('X-CSRF-TOKEN',csrf_token());
return $next($request);
}
}
You effectively can replace the static login form with a Vue login form with this setup.
Here is what router setup looks like:
new Router({
mode: 'history',
routes: [
{
path: '/login',
name: 'login',
component: AuthLogin,
meta: {
requiresAuth: false,
layout: 'auth'
}
},
{
path: '/login/recover',
name: 'login-recover',
component: AuthLoginRecover,
meta: {
requiresAuth: false,
layout: 'auth'
}
},
{
path: '/',
name: 'index',
component: Dashboard,
meta: {
requiresAuth: true,
layout: 'default'
}
},
{
path: '/settings',
name: 'settings',
component: Settings,
meta: {
requiresAuth: true,
layout: 'default'
}
}
]
});