Nuxt - is it possible to check if a user is logged in from SSR? - vue.js

I created a Nuxt app that uses Django on the backend, i'm using the standard Django Session Authentication, so when i log in from Nuxt, a session cookie is set in my browser.
I've been trying for days to find a way to restrict some pages to authenticated users only, but i don't seem to find any working approach to do that. I need to check if the user is logged in before the page is loaded, so i tried to use a middleware but middleware won't work at all because the middleware is executed from server side (not client side) so there won't be any cookie in the request.
At this point, is there any other way to do this from SSR? Here is my request:
export default async function (context) {
axios.defaults.withCredentials = true;
return axios({
method: 'get',
url: 'http://127.0.0.1:8000/checkAuth',
withCredentials: true,
}).then(function (response) {
//Check if user is authenticated - response is always False
}).catch(function (error) {
//Handle error
});
}

If you are running Nuxt in SSR mode as server, you can access the cookie headers to find out if the user has a certain cookie. Packages like cookieparser (NPM) can easily do that for you.
But as you already found out, you can't do that in a middleware. What you could use instead is the nuxtServerInit action in your store (Docs). This action is run on the server and before any middleware gets executed. In there you can use cookieparser to get the user's cookies, authenticate them and save the any information you need in the store.
Later you can access the store in your middleware and for example redirect the user.

actually you can get cookies in a middleware.... Ill put my example, but the answer above is more correct .
middleware/auth.js
import * as cookiesUtils from '~/utils/cookies'
export default function ({ route, req, redirect }) {
const isClient = process.client
const isServer = process.server
const getItem = (item) => {
// On server
if (isServer) {
const cookies = cookiesUtils.getcookiesInServer(req)
return cookies[item] || false
}
// On client
if (isClient) {
return cookiesUtils.getcookiesInClient(item)
}
}
const token = getItem('token')
const { timeAuthorized } = cookiesUtils.authorizeProps(token)
const setRedirect = (routeName, query) => {
return redirect({
name: routeName,
query: query
? {
redirect: route.fullPath
}
: null
})
}
// strange bug.. nuxt cant redirect '/' to '/login'
if (route.path === '/') {
setRedirect('users')
}
if (!route.path.match(/\/login\/*/g) && !timeAuthorized) {
setRedirect('login', true)
}
}
utils/cookies.js
import Cookie from 'js-cookie'
import jwtDecoded from 'jwt-decode'
/*
TOKEN
*/
// Get server cookie
export const getcookiesInServer = (req) => {
const serviceCookie = {}
if (req && req.headers.cookie) {
req.headers.cookie.split(';').forEach((val) => {
const parts = val.split('=')
serviceCookie[parts[0].trim()] = (parts[1] || '').trim()
})
}
return serviceCookie
}
// Get the client cookie
export const getcookiesInClient = (key) => {
return Cookie.get(key) || false
}
export const setcookiesToken = (token) => {
Cookie.set('token', token)
}
export const removecookiesToken = () => {
Cookie.remove('token')
}
export const authorizeProps = (token) => {
const decodeToken = token && jwtDecoded(token)
const timeAuthorized = (decodeToken.exp > Date.now() / 1000) || false
return {
timeAuthorized
}
}

Related

How do I write tests for pages middleware (Next 12)?

I have added some logic in my pages middleware (using Next 12) and would like to add tests now but am pretty lost on how to get that started. Can someone direct me to a tutorial or resource that shows a complete example of middleware being tested?
Specifically this is what my middleware is doing:
export function middleware(request: NextRequest) {
// Redirect a user if they don't have an auth token and are not the admin role
if (request.nextUrl.pathname.startsWith('/admin')) {
const authTokenCookie = request.cookies.token;
const parsedToken = authTokenCookie ? jose.decodeJwt(authTokenCookie) : null;
const role = typeof parsedToken === 'object' ? parsedToken?.role : null;
if (!authTokenCookie || !role || role !== USER_ROLES.admin) {
return NextResponse.redirect(new URL('/', request.url));
}
}
// Redirect the user if a query parameter is present
if (request.nextUrl.pathname === '/' && request.nextUrl.searchParams.has('directToStore')) {
NextResponse.redirect(new URL('/store', request.url));
}
return NextResponse.next();
}
This is how I ended up testing my middleware:
import { middleware } from '../pages/_middleware';
import { NextResponse, NextRequest } from 'next/server';
import * as jose from 'jose';
describe('Middleware', () => {
const redirectSpy = jest.spyOn(NextResponse, 'redirect');
afterEach(() => {
redirectSpy.mockReset();
});
it('should redirect to the homepage if visiting an admin page as a user without an auth token', async () => {
const req = new NextRequest(new Request('https://www.whatever.com/admin/check-it-out'), {});
req.headers.set(
'cookie',
'token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTg3NjczMjYsImV4cCI6MTY5MDMwMzMyNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIn0.G7rkptAKt1icBp92KcHYpGdcWOnn4gO8vWiCMtIHc0c;',
);
const { role } = jose.decodeJwt(req.cookies.token);
await middleware(req);
expect(role).toEqual('user');
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/', req.url));
});
it('should redirect to the store if the directToStore query param is set', async () => {
const req = new NextRequest(new Request('https://www.whatever.com'), {});
req.nextUrl.searchParams.set('directToStore', 'true');
await middleware(req);
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/store', req.url));
});
});

Nextjs Auth0 get data in getServerSideProps

Im using Auth0 to authenticate users.
Im protected api routes like this:
// pages/api/secret.js
import { withApiAuthRequired, getSession } from '#auth0/nextjs-auth0';
export default withApiAuthRequired(function ProtectedRoute(req, res) {
const session = getSession(req, res);
const data = { test: 'test' };
res.json({ data });
});
My problem is when I'm trying to fetch the data from getServerSideProps I'm getting 401 error code.
If I use useEffect Im able to get data from api route.
Im trying to fetch the data like this:
export const getServerSideProps = withPageAuthRequired({
async getServerSideProps(ctx) {
const res = await fetch('http://localhost:3000/api/secret');
const data = await res.json();
return { props: { data } };
},
});
Im getting the following response:
error: "not_authenticated", description: "The user does not have an active session or is not authenticated"
Any idea guys? Thanks!!
When you call from getServerSideProps the protected API end-point you are not passing any user's context (such as Cookies) to the request, therefore, you are not authenticated.
When you call from useEffect it runs inside your browser, which attaches all cookies to the request, one of them is the session cookie.
You need to forward the session cookie that was passed to the getServerSideProps (by the browser) to the API call.
export const getServerSideProps = withPageAuthRequired({
async getServerSideProps(ctx) {
const res = await fetch('http://localhost:3000/api/secret', {
headers: { Cookie: ctx.req.headers.cookie },
// ---------------------------^ this req is the browser request to the getServersideProps
});
const data = await res.json();
return { props: { data } };
},
});
For more info.
#auth0/nextjs-auth0 has useUser hook. This example is from: https://auth0.com/blog/ultimate-guide-nextjs-authentication-auth0/
// pages/index.js
import { useUser } from '#auth0/nextjs-auth0';
export default () => {
const { user, error, isLoading } = useUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;
if (user) {
return (
<div>
Welcome {user.name}! Logout
</div>
);
}
// if not user
return Login;
};
Note that authentication takes place on the server in this model,
meaning that the client isn't aware that the user is logged in. The
useUser hook makes it aware by accessing that information in the
initial state or through the /api/auth/profile endpoint, but it won't
expose any id_token or access_token to the client. That information
remains on the server side.
Custom HOF:
// getData is a callback function
export const withAuth = (getData) => async ({req, res}) => {
const session = await auth0.getSession(req);
if (!session || !session.user) {
res.writeHead(302, {
Location: '/api/v1/login'
});
res.end();
return {props: {}};
}
const data = getData ? await getData({req, res}, session.user) : {};
return {props: {user: session.user, ...data}}
}
Example of using:
export const getServerSideProps = withAuth(async ({req, res}, user) => {
const title = await getTitle();
return title;
});

Get localStorage in NextJs getInitialProps

I working with localStorage token in my next.js application. I tried to get the localStorage on page getInitialProps but, it returns undefined.
Here is an example,
Dashboard.getInitialProps = async () => {
const token = localStorage.getItem('auth');
const res = await fetch(`${process.env.API_URL}/pages/about`, {
headers: { 'Authorization': token }
});
const data = await res.json();
return { page: data };
}
For the initial page load, getInitialProps will run on the server
only. getInitialProps will then run on the client when navigating to a
different route via the next/link component or by using next/router. Docs
This means you will not be able to access localStorage(client-side-only) all the time and will have to handle it:
Dashboard.getInitialProps = async ({ req }) => {
let token;
if (req) {
// server
return { page: {} };
} else {
// client
const token = localStorage.getItem("auth");
const res = await fetch(`${process.env.API_URL}/pages/about`, {
headers: { Authorization: token },
});
const data = await res.json();
return { page: data };
}
};
If you want to get the user's token for the initial page load, you have to store the token in cookies instead of localStorage which #alejandro also mentioned in the comment.

Cannot Access themes.json using shopify api and nodejs

I am not able to access the themes.json of my development store using shopify api and nodejs.
Here is what I am doing:
app.get('/shopify/examplePage', (req, res) => {
const { shop, hmac, code, state } = req.query;
const stateCookie = cookie.parse(req.headers.cookie).state;
// Verifying Cookie
if (state !== stateCookie) {
return res.status(403).send('Request origin cannot be verified');
}
// Verifying Hmac
if (shop && hmac && code) {
const map = Object.assign({}, req.query);
delete map['hmac'];
const message = querystring.stringify(map);
const generatedHash = crypto
.createHmac('sha256', apiSecret)
.update(message)
.digest('hex');
if(generatedHash !== hmac){
return res.status(400).send('HMAC verification failed');
}
// Appending Access Token to the shop Url
const accessTokenRequestUrl = 'https://' + shop + '/admin/oauth/access_token';
const accessTokenPayload = {
client_id: apiKey,
client_secret: apiSecret,
code
};
// Making an API Request And getting an API response
request.post(accessTokenRequestUrl, {json: accessTokenPayload })
// Promise for Access Token Response
.then((accessTokenResponse) => {
const accessToken = accessTokenResponse.access_token;
// Request URL for Products
const apiRequestUrl = 'https://' + shop + '/admin/api/2019-04/themes.json'
console.log(apiRequestUrl);
const apiRequestHeader = {
'X-Shopify-Access-Token': accessToken
};
request.get(apiRequestUrl, { headers: apiRequestHeader })
.then((apiResponse) => {
let example = JSON.parse(apiResponse);
res.send(example);
// End API Response
res.end(apiResponse)
}).catch((error) => {
res.status(error.statusCode).send(error.error.error_descripton)
});
}).catch((error) => {
res.status(error.statusCode).send(error.error.error_descripton)
})
} else {
res.status(400).send('Required parameters missing');
}
});
There is this error showing that the access to {ngrok}.ngrok.io was denied while I can access product.json & shop.json with the help of same code
Denied means your API key doesn’t have access. If this is a public app you need to add read_themes to your scopes. If it is a private app you need to go to the app setup and add theme access.

next js redux-observable persistent auth token using cookie

I have been trying to implement react server-side-rendering using next, and redux-observable, now i want to implement auth
On signin
click signin
dispatch signin
set signin type
set signin data
call backend api auth/signin
if the response says that token is expired
call backed api auth/refresh using refreshToken
set cookie based on auth/refresh response token
set auth data based on auth/refresh response
else
set cookie based on auth/signin response token
set auth data based on auth/signin response
On accessing pages that needs auth
check for cookies called token
if exists
call backed api auth/me to authorize
if the response says that token is expired
call backed api auth/refresh using refreshToken
set cookie based on auth/refresh response token
set auth data based on auth/refresh
else
set auth data based on auth/me response
else
redirect to signin
Steps above happens inside the epics, as follows
/epics/signin.js
export const signinEpic = (action$, store) => action$
.ofType(SIGNIN)
.mergeMap(() => {
const params = { ... }
return ajax(params)
.concatMap((response) => {
const { name, refreshToken } = response.body
if (refreshToken && name === 'TokenExpiredError') {
const refreshParams = { ... }
return ajax(refreshParams)
.concatMap((refreshResponse) => {
setToken(refreshResponse.body.auth.token)
const me = { ... }
return [
authSetMe(me),
signinSuccess(),
]
})
.catch(error => of(signinFailure(error)))
}
const me = { ... }
setToken(response.body.auth.token)
return [
authSetMe(me),
signinSuccess(),
]
})
.catch(error => of(signinFailure(error)))
})
I did some console.log(Cookies.get('token')) to ensure that the cookie gets saved, and it prints the token just fine, saying that its there, but when i checked under browser console > Application > Cookies, nothing is there
So in auth epic below, the getToken() will always return '' which will always dispatch authMeFailure(error)
/epics/auth.js
// this epic will run on pages that requires auth by dispatching `authMe()`
export const authMeEpic = action$ => action$
.ofType(AUTH_ME)
.mergeMap(() => {
const params = {
...,
data: {
...
Authorization: getToken() ? getToken() : '', // this will always return ''
},
}
return ajax(params)
.mergeMap((response) => {
const { name, refreshToken } = response.body
if (refreshToken && name === 'TokenExpiredError') {
const refreshParams = { ... }
return ajax(refreshParams)
.mergeMap((refreshResponse) => {
setToken(refreshResponse.body.auth.token)
const me = { ... }
return authMeSuccess(me)
})
.catch(error => of(authMeFailure(error)))
}
const me = { ... }
setToken(response.body.auth.token)
return authMeSuccess(me)
})
.catch(error => of(authMeFailure(error)))
})
I use js-cookie for getting and setting cookies
EDIT: i actually prepared an auth lib containing getToken, setToken and removeToken, as follows
import Cookies from 'js-cookie'
export const isAuthenticated = () => {
const token = Cookies.get('token')
return !!token
}
export const getToken = () => Cookies.get('token')
export const setToken = token => Cookies.set('token', token)
export const removeToken = () => Cookies.remove('token')
and yes, i could have just used the setToken() on the epics, was just trying to directly test the cookie set method
UPDATE:
it seems that despite its not being in Console > Application > Cookies, its exists on every pages as it's printing the correct token if i do console.log(getToken()) inside the component render method
But every time i refresh the page, its gone. Kind of like it is being stored in a redux state, which is weird
UPDATE #2:
ok i think i manage to make it work, it turns out that we need 2 types of cookie, server side (the one's generated on refresh) and a client side (persist on navigating), so the reason that i wasn't able to get the token on epics its because it was not passed from the server side (at least this is my understanding)
Inspired by this issue comment on github
yarn add cookie-parser
on ./server.js (you need to have a custom server to be able to do this)
const cookieParser = require('cookie-parser')
...
server.use(cookieParser())
on ./pages/_document.js
export default class extends Document {
static async getInitialProps(...args) {
// ...args in your case would probably be req
const token = args[0].req ? getServerToken(args[0].req) : getToken()
return {
...
token,
}
}
render() {
...
}
}
on ./lib/auth.js or on any place you put your token methods
export const getServerToken = (req) => {
const { token = '' } = req.cookies
return token
}
export const getToken = () => {
return Cookies.get('token') ? Cookies.get('token') : ''
}
I am not 100% understand how this is solving my problem, but i am gonna leave it like this for now