I am currently implementing an authentication flow using Nextjs and an api using Expressjs.
I am looking to store a JWT token
as an auth token in memory
that I can periodically refresh using a refresh token stored in an HTTPOnly cookie
.
For my implementation, I took as a reference the nice OSS project here.
My issue here is that when I am storing the auth token in inMemoryToken
during login, the value is stored only available client side but still available server side and vice versa.
Another example is when I am disconnecting:
inMemoryToken
is equal to something server side- user click the logout button and
logout()
is called front side andinMemoryToken = null
- on page change,
getServerSideProps()
is called on the server butinMemoryToken
on the server is still equal to the previous value and therefore my user still appear connected.
Here is the Nextjs
code
//auth.js
import { Component } from 'react';
import Router from 'next/router';
import { serialize } from 'cookie';
import { logout as fetchLogout, refreshToken } from '../services/api';
let inMemoryToken;
export const login = ({ accessToken, accessTokenExpiry }, redirect) => {
inMemoryToken = {
token: accessToken,
expiry: accessTokenExpiry,
};
if (redirect) {
Router.push('/');
}
};
export const logout = async () => {
inMemoryToken = null;
await fetchLogout();
window.localStorage.setItem('logout', Date.now());
Router.push('/');
};
const subMinutes = (dt, minutes) => {
return new Date(dt.getTime() - minutes * 60000);
};
export const withAuth = (WrappedComponent) => {
return class extends Component {
static displayName = `withAuth(${Component.name})`;
state = {
accessToken: this.props.accessToken,
};
async componentDidMount() {
this.interval = setInterval(async () => {
inMemoryToken = null;
const token = await auth();
inMemoryToken = token;
this.setState({ accessToken: token });
}, 60000);
window.addEventListener('storage', this.syncLogout);
}
componentWillUnmount() {
clearInterval(this.interval);
window.removeEventListener('storage', this.syncLogout);
window.localStorage.removeItem('logout');
}
syncLogout(event) {
if (event.key === 'logout') {
Router.push('/');
}
}
render() {
return (
<WrappedComponent
{...this.props}
accessToken={this.state.accessToken}
/>
);
}
};
};
export const auth = async (ctx) => {
console.log('auth ', inMemoryToken);
if (!inMemoryToken) {
inMemoryToken = null;
const headers =
ctx && ctx.req
? {
Cookie: ctx.req.headers.cookie ?? null,
}
: {};
await refreshToken(headers)
.then((res) => {
if (res.status === 200) {
const {
access_token,
access_token_expiry,
refresh_token,
refresh_token_expiry,
} = res.data;
if (ctx && ctx.req) {
ctx.res.setHeader(
'Set-Cookie',
serialize('refresh_token', refresh_token, {
path: '/',
expires: new Date(refresh_token_expiry),
httpOnly: true,
secure: false,
}),
);
}
login({
accessToken: access_token,
accessTokenExpiry: access_token_expiry,
});
} else {
let error = new Error(res.statusText);
error.response = res;
throw error;
}
})
.catch((e) => {
console.log(e);
if (ctx && ctx.req) {
ctx.res.writeHead(302, { Location: '/auth' });
ctx.res.end();
} else {
Router.push('/auth');
}
});
}
const accessToken = inMemoryToken;
if (!accessToken) {
if (!ctx) {
Router.push('/auth');
}
}
return accessToken;
};
//page index.js
import Head from 'next/head';
import { Layout } from '../components/Layout';
import { Navigation } from '../components/Navigation';
import { withAuth, auth } from '../libs/auth';
const Home = ({ accessToken }) => (
<Layout>
<Head>
<title>Home</title>
</Head>
<Navigation />
<div>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
</Layout>
);
export const getServerSideProps = async (ctx) => {
const accessToken = await auth(ctx);
return {
props: { accessToken: accessToken ?? null },
};
};
export default withAuth(Home);
Part of the express js code:
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
....
const refreshToken = uuidv4();
const refreshTokenExpiry = new Date(new Date().getTime() + 10 * 60 * 1000);
res.cookie('refresh_token', refreshToken, {
maxAge: 10 * 60 * 1000,
httpOnly: true,
secure: false,
});
res.json({
access_token: accessToken,
access_token_expiry: accessTokenExpiry,
refresh_token: refreshToken,
user,
});
});
app.post('/api/refresh-token', (req, res) => {
const refreshToken = req.cookies['refresh_token'];
.....
const newRefreshToken = uuidv4();
const newRefreshTokenExpiry = new Date(
new Date().getTime() + 10 * 60 * 1000,
);
res.cookie('refresh_token', newRefreshToken, {
maxAge: 10 * 60 * 1000,
httpOnly: true,
secure: false,
});
res.json({
access_token: accessToken,
access_token_expiry: accessTokenExpiry,
refresh_token: newRefreshToken,
refresh_token_expiry: newRefreshTokenExpiry,
});
});
app.post('/api/logout', (_, res) => {
res.clearCookie('refresh_token');
res.sendStatus(200);
});
What I understand is that even if the let inMemoryToken
is declared once, two separate instance of it will be available at runtime, one client side and one server side, and modifying on doesn't affect the other. Am I right?
In this case, how to solved this since the auth method can be called on the server but also on the client?
from Nextjs - Auth token stored in memory + refresh token in HTTP Only cookie
No comments:
Post a Comment