How to set middleware for specific route in Nuxt - vue-router

I am trying to protect api route in my Nuxt.js app using Nuxt auth module. In my nuxt.config.js I have:
serverMiddleware: [
{ path: '/api', handler: '~/api/index.js' }
]
And I want to use this:
router: {
middleware: ['auth']
}
How to add middleware only for specific route which isn't page component?

Thats not the way to protect an API route. I guess you are using an express.js server here.
For example: You have a API endpoint /getUserData. With express.js it would look something like this:
app.get("/getUserData/:user", ()=> {
res.json({
message: "I am a protected API endpoint."
});
});
To protect this endpoint you should create an middleware that checks if the user is allowed to enter this API endpoint or not. Lets create an example. First create an function and lets call it "auth":
function auth(req, res, next){
let user = req.params.user;
if(user == "admin"){
next(); //This means to continue its request
}else {
res.status(403).json({ message: "Sorry, but you are not allowed }); //Status 403 means forbidden.
}
}
Ok, now we got an very simple authentification middleware we just need to implement it to every API endpoint we need.
app.get("/getUserData/:user", auth, ()=> {
res.json({
message: "I am a protected API endpoint."
});
});
I have added the auth middleware to this route to protect it.
You can test it now. With localhost:3000/api/getUserData/admin you should see the message. If you choose something different then admin you will get an error

You can add
<script>
export default {
middleware: 'authenticated'
}
</script>
in the routes you want to protect
ps: a route is a page component in nuxt

Related

Vue router allways redirecting to error page

i am trying to setup a redirect when the user is not logged in. But when i do it like in my example the URL changes but i get This page could not be found from nuxt. The code is inside an login.js inside the plugins folder. Then i included this in the nuxt config like this.
plugins: [
'~/plugins/login.js'
],
And here is the actual code for handling redirecting
export default ({ app, store }) => {
app.router.beforeEach((to, from, next) => {
const loggedIn = store.state.account.loggedInAccount
if (!loggedIn) {
if (to.path !== '/redirect') {
next({ path: '/redirect' })
} else {
next()
}
} else {
next()
}
})
}
It looks like the routes are not mounted yet.
You should try to use middleware. It is the conventional way to implement the beforeEach function as mentioned by the official docs. You can read about it from here. If have access to the route object, store object and redirect function inside the middleware, so use redirect to direct to the other routes after validation.

Middleware executing before Vuex Store restore from localstorage

In nuxtjs project, I created an auth middleware to protect page.
and using vuex-persistedstate (also tried vuex-persist and nuxt-vuex-persist) to persist vuex store.
Everything is working fine when navigating from page to page, but when i refresh page or directly land to protected route, it redirect me to login page.
localStorage plugin
import createPersistedState from 'vuex-persistedstate'
export default ({ store }) => {
createPersistedState({
key: 'store-key'
})(store)
}
auth middleware
export default function ({ req, store, redirect, route }) {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect(`/auth/login?redirect=${route.fullPath}`)
}
return Promise.resolve()
}
I solved this problem by using this plugin vuex-persistedstate instead of the vuex-persist plugin. It seems there's some bug (or probably design architecture) in vuex-persist that's causing it.
With the Current approach, we will always fail.
Actual Problem is Vuex Store can never be sync with server side Vuex store.
The fact is we only need data string to be sync with client and server (token).
We can achieve this synchronization with Cookies. because cookies automatically pass to every request from browser. So we don't need to set to any request. Either you just hit the URL from browser address bar or through navigation.
I recommend using module 'cookie-universal-nuxt' for set and remove of cookies.
For Setting cookie after login
this.$cookies.set('token', 'Bearer '+response.tokens.access_token, { path: '/', maxAge: 60 * 60 * 12 })
For Removing cookie on logout
this.$cookies.remove('token')
Please go through the docs for better understanding.
Also I'm using #nuxt/http module for api request.
Now nuxt has a function called nuxtServerInit() in vuex store index file. You should use it to retrieve the token from request and set to http module headers.
async nuxtServerInit ({dispatch, commit}, {app, $http, req}) {
return new Promise((resolve, reject) => {
let token = app.$cookies.get('token')
if(!!token) {
$http.setToken(token, 'Bearer')
}
return resolve(true)
})
},
Below is my nuxt page level middleware
export default function ({app, req, store, redirect, route, context }) {
if(process.server) {
let token = app.$cookies.get('token')
if(!token) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Not Provided'}})
} else if(!isTokenValid(token.slice(7))) { // slice(7) used to trim Bearer(space)
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
}
return Promise.resolve()
}
else {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath}})
// return redirect(`/auth/login?redirect=${route.fullPath}`)
} else if (!isTokenValid(store.state.auth.tokens.access_token)) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
// return redirect(`/auth/login?redirect=${route.fullPath}&message=Token Expired`)
} else if (isTokenValid(store.state.auth.tokens.refresh_token)) {
return redirect(`/auth/refresh`)
} else if (store.state.auth.user.role !== 'admin')
return redirect(`/403?message=Not having sufficient permission`)
return Promise.resolve()
}
}
I have write different condition for with different source of token, as in code. On Server Process i'm getting token from cookies and on client getting token store. (Here we can also get from cookies)
After this you may get Some hydration issue because of store data binding in layout. To overcome this issue use <no-ssr></no-ssr> wrapping for such type of template code.

Restrict routing per role

Is it possible to restrict certain routes and / or change components for any route base on a value such as the logged in user's role?
I want this so an administrator's default page is different to a normal user, but also perhaps to restrict access if the route is set manually in the browser.
Or is it a better solution to have a base component that redirects to another route?
I understand that restricting access via the router does not replace real account security, but would seem like a first good step in preventing users guessing restricted routes.
Here is a great example of implementing authentication and authorization with vue router: https://scotch.io/tutorials/vue-authentication-and-route-handling-using-vue-router
Basically you can check premissions before letting user open the protected component. The easiest way to achieve this is using router guards. In your router definitions:
{
path: '/proctected',
beforeEnter(to, from, next) {
if (isAuthenticated()) {
if (!hasPermissionsNeeded(to)) {
next('/page-to-show-for-no-permission');
} else {
next();
}
} else {
next('/page-to-show-for-unauthenticated-users');
}
}
}
This guard will protect from entering /proctected url. Here you can check the working codepen: https://codepen.io/anon/pen/JwxoMe
Below an example of guard for all routes:
router.beforeEach((to, from, next) => {
if (isAuthenticated()) {
if (!hasPermissionsNeeded(to)) {
next('/page-to-show-for-no-permission');
} else {
next();
}
} else {
next('/page-to-show-for-unauthenticated-users');
}
})
More about router guards: https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard

Nuxt js <nuxt-link /> authentication

I have a route in nuxt that has to be accessible only by logged in users: /dashboard/secret.
In /dashboard page I have a link like this:
<nuxt-link to="/dashboard/secret">Link to "secret" page</nuxt-link>
When clicked, nuxt will fetch that page from
myapp.com/_nuxt/pages_dashboard_secret.js
How can I add authentication for that nuxt route to stop people from manually going to that url and reading the contents?
Yes the actual secret data will be taken from external api which will validate user token, but still it seems wrong that people can see even the html of this page
if you just want to protect a js file, it would be wrong to do it like this. But if you mean you just want to protect a route from being accessed manually by the users, you must try Nuxt Middlewares and write a middleware for authentication and user fetching.
The middleware structure can be as simple as this:
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
and you can simply use it like this in your root (or secretPage) layout:
<template>
<h1>Secret page</h1>
</template>
<script>
export default {
middleware: 'authenticated'
}
</script>
You can use nuxt/auth package, that is the case for your work and can be used as a plugin and module, you can check has it for the be accessible page or not, it runs automatically and has semantic structure.
You cannot keep your secret on client side (in your JS code) everyone using your application can get it from his browser. You need to keep secret keys on server side and make some validation endpoint to provide if user is valid or not or just keep his session after login.
you can use middleware in nuxt framework. Also, route has a information about url and request. You can make a logic by using them.
https://nuxtjs.org/docs/directory-structure/middleware/
middleware/auth.js
export default async function ({store, from, route, req}) {
if (process.client) {
if (route.name === 'dashboard-room-id' && from.name === route.name)
return
else await store.dispatch('checkSession', route)
}
}
save the token in the store on nuxtServerInit or whenever you get it.
on /dashboard/secret page check in the fetch method if there is a token set.
if token is set, fetch your data otherwise redirect the use somewhere else
https://nuxtjs.org/examples/auth-routes/#redirect-user-if-not-connected
For such a guard of pages, the middleware is the sure way to do it.
Create a middleware file in the middleware directory
Add your middleware logic as described here https://nuxtjs.org/api/pages-middleware/
Then add the middleware option in your page component
as it is mentioned that the routing should be done on the server, in case you just want to handle it if I have this
store/index.js action
async nuxtServerInit({ dispatch, commit }, { req }) {
try {
if (process.server && process.static) { return }
if (!req.headers.cookie) {
console.log('return ')
return
}
const parsed = cookieparser.parse(req.headers.cookie)
const accessTokenCookie = parsed.__session
if (!accessTokenCookie) { return }
const decoded = JWTDecode(accessTokenCookie)
if (userData.exists) {
commit('setState', { name: 'user',
value: {
uid: decoded.user_id,
email: decoded.email,
...userData.data()
} })
}
} catch (e) {
console.log(e)
}
},
//Login firebase
async fireLogin({ dispatch }, { singInWith, account }) {
const resp = await this.$firebase.auth()signInWithEmailAndPassword(account.email, account.password)
const token = await resp.user.getIdToken()
Cookie.set('__session', token)
return { email: resp.user.email, uid: resp.user.uid }
}
Middleware/auth.js
export default function({ store, route, redirect }) {
const user = store.state.user
const blockedRoute = /\/admin\/*/g
const homeRoute = '/'
if (!user && route.path.match(blockedRoute)) {
redirect('/')
}
/*if (user && route.path === homeRoute) {
redirect('/admin')
}*/
}
nuxt.config
router: {
middleware: [
'authenticated'
]
},
you can set the middleware for current page
middle ware
export default context => {
//set Condition and logic
};
route page :
middleware: 'name of middle ware'
i can suggest three solutions:
1.Get pathname in your js codes and then check the url that client using to access your page , for example if pathname is
/dashboard/secret and user is logged in then show the page
for checking pathname u can use these cods:
$nuxt.$route.path
//or good old pure js ;)
window.location.pathname
2.check if user truly logged in (backend & frontend)
for that u can use nuxt-auth and sync it to your backend as well.
for example if you using laravel , u can use laravel passport ,
in that case when the request sended to the backend route, you can check if user is logged in to the backend as well.
Ps:This way is more secure and of course in every backend language this process can be different, but surely all of them will have the same capability.
3.using .htaccess :
Do not allow the user to view the file directly from the server path
Read more

Auth0 callback URL mismatch

I am doing LinkedIn authentication with auth0 in a react app. I have set localhost:3000/upload in callback urls in settings, hopping that after users login at localhost:3000/login, they would be redirected to localhost:3000/upload. However, I always get this error: url localhost:3000/login is not in the list of callback urls. Why would auth0 expect to return to the page where you just logged in after logging in. Shouldn't it be some different url. It just does not make sense to me.
Edit:
export default class AuthService {
constructor(clientId, domain) {
// Configure Auth0
const options = {
allowedConnections: ['linkedin'],
auth: {
params: {responseType: 'code'}
}
};
this.lock = new Auth0Lock(clientId, domain, options)
// Add callback for lock `authenticated` event
this.lock.on('authenticated', this._doAuthentication.bind(this))
// binds login functions to keep this context
this.login = this.login.bind(this)
this.loggedIn = this.loggedIn.bind(this)
}
_doAuthentication(authResult){
// Saves the user token
console.log(authResult);
this.setToken(authResult.idToken)
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
console.log('Error loading the Profile', error)
} else {
console.log(profile)
}
})
}
//....
Please ensure two things:
1). In your react app code
responseType: 'code'
2). On the Auth0 dashboard, under Settings -> Allowed Callback URLs put your callback entry (localhost:3000/upload) - which I think you have done but just in case.
Let me know if you are still having problems.
Make sure that there is no special hidden characters or space between the commas between the URLs when you paste it into the Auth0 Setting site. I didn't realise about this util I put every urls into Vim to check and see that there are such above cases
In the call to AuthProvider, make sure to use to same callback url as the one in Auth0 settings:
const uri='http://localhost:3000/upload';
<Auth0Provider
domain={domain}
clientId={clientId}
redirectUri={uri}>
To cause a redirect to a different URL after a successful authentication, you need to provide the redirectUrl to Lock, like this:
// Configure Auth0
const options = {
allowedConnections: ['linkedin'],
auth: {
responseType: 'code',
redirectUrl: 'http://localhost:3000/upload'
}
};
this.lock = new Auth0Lock(clientId, domain, options)
(Also notice that the responseType option goes under auth, not under auth.params.)
If you do the redirect, you won't reach the event handler you defined in your login page. You will need to either add an event handler in your destination page (and use responseType:token) or handle authentication results in your server code (this is what you will normally be doing if you are requesting a responseType: code).
the reason why you should set the callback Url in auth0 settings, because any one can use your client id and send request to google or linkedin, get the response to anywhere they set. but with this setting only you can access that response.
once your app is authorized to pull the data from linkedin, linkedin will send the data to where you specified. you should create a page to handle the response from Linkedin server. Let's name that page callback.js and this will be an example of response object.
accessToken: "hNuPLKTZHiE9_lnED0JIiiPNjlicRDp"
appState: null
expiresIn: 7200
idToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRXdSVUl5TURVeE4wSkJPRFZEUlRKRU1EVkZNemsxTXpNNU5VTXlNRGt6T0VWQlJqUkZRUSJ9.eyJodHRwOi8vbG9jYWxob3N0OjMwMDAvcm9sZSI6InNpdGVPd25lciIsImdpdmVuX25hbWUiOiJvbWFyIiwiZmFtaWx5X25hbWUiOiJpYm8iLCJuaWNrbmFtZSI6Im9tYXJpYm8xOTgyIiwibmFtZSI6Im9tYXIgaWJvIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250BQUFBQUkvQUFBQUFBQUFBQUEvQUNIaTNyLTEwLTEyVDIyOjU4OjAxLjgzM1oiLCJpc3MiOiJodHRwczovL3BvcnRmb2xpby15aWxtYXouYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTE0MDY0NTA2ODI2OTgwNTA5ODY3IiwiYXVkIjoiUEdVY242RjRRS21PRkJhb1k0UFdCeWpjVzIyT09vNGMiLCJpYXQiOjE1NzA5MjEwODIsImV4cCI6MTU3MDk1NzA4MiwiYXRfaGFzaCI6InN0R1l5SnJaMHNnbVYzSWNLWjlPeFEiLCJub25jZSI6InRrOV95b096enRmVThVVjFVMlVFR3IyMW5ORW5abjk4In0.TYS7mM8N2d7jEHFdWQGTSeAAUaDt4-0SMUG3LrcQ1r3xzY0RMGsUsEszj5xqk1GE0cIlFS10xCOYKsuHSwsFLomC1EbLjntjkledHtfD0MW84cMoXN6a-x-1-bNwl3lMYJ98qklTrNvTvkQJ6DWhei3hJ8rs8dnbNyCfckNVU6ptJU-9ef1DwWfHRomW5LQ6WSDRHZScW697gdgBEMU-Nd2SddyHhQe0kVh6lKdcbnskEAyCJLE07jfM40RQI_8LJouFcpoyImcXSDZlKv90tYfVDq9_TwE3GNaSz5I5snn0457oCgz0vuX0JoCUiaDuTIX7XiyXnozW_DxGMuhk4w"
idTokenPayload: {http://localhost:3000/role: "siteOwner", given_name: "me", family_name: "you", nickname: "nck", name: "nm", …}
refreshToken: null
scope: null
state: "xkEbffzXbdOYPLkXOUkrQeb0Jysbnlfy"
tokenType: "Bearer"
//THIS CODE IS FOR NEXT.JS9
//auth.js
class Auth0 {
constructor() {
this.auth0 = new auth0.WebAuth({
domain: "portfolio-ys.auth0.com",
clientID: "PGUWJQKmOFBaoY4PWByjcW22OOo4c",
redirectUri: "http://localhost:3000/callback",
responseType: "token id_token",
scope: "openid profile"
});
this.handleAuthentication = this.handleAuthentication.bind(this);
}
//there are too many methods are defined here i put only relevant ones
handleAuthentication() {
return new Promise((resolve, reject) => {
this.auth0.parseHash((err, authResult) => {
console.log(authResult);
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
resolve();
} else if (err) {
reject(err);
}
});
});
}
setSession function is where you set the cookies based on response object. I use js-cookie package to set the cookie.
setSession(authResult) {
const expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
Cookies.set("user", authResult.idTokenPayload);
Cookies.set("jwt", authResult.idToken);
Cookies.set("expiresAt", expiresAt);
}
}
const auth0Client = new Auth0();
export default auth0Client;
callback.js
import React from "react"
import auth0Client from "./auth0"
import {withRouter} from "next/router"
class Callback extends React.Component{
async componentDidMount(){
await auth0Client.handleAuthentication()
this.props.router.push('/')
}
render() {
return (
<h1>verifying logging data</h1>
)
}
}
export default withRouter(Callback) //this allows us to use router
I had similar issue "callback URL mismatch" and resolved it by running the application over https with a trusted certificate.
Here is a snippet from Auth0 applications settings section about callback URL, which says "Make sure to specify the protocol (https://) otherwisw the callback may fail in some cases."
If you're using the Android(Kotlin) SDK of auth0, I noticed that during runtime, the requested URL is being changed. e.g. app://{your_auth0_domain}/android/{package_name}/callback://{your_auth0_domain}/android/app://{your_auth0_domain}/android//callback
Originally URL was
app://{your_auth0_domain}/android/{package_name}/callback
and SDK is appending "://{your_auth0_domain}/android/app://{your_auth0_domain}/android//callback" this extra part.
Solution: Either put the same URL in auth0 setting dashboard as it showing in your logs
or
WebAuthProvider
.login(account)
.withScheme("app") // instead of complete URL, put only the remaining part from the URL,
.start(this, object : Callback<Credentials, AuthenticationException> {}
I hope it will definitely help android/app developer.