Provide a comprehensive description of the most common authentication patterns for human users in full-stack web application. Next.js is taken as an example as it provides all relevant features (API routes, web pages, and middlewares acting as proxy) for authentication.
This guide is targeted at Next.js users, therefore it should be understandable by frontend developers with basic backend knowledge.
Users can authenticate themselves using password and email. A user access a "/private" page. We want to cover 2 scenarios:
- Check if they are logged in and redirect them, server-side, before the page is even loaded.
Scenario 1 is mandatory when the content of the page is sensitive, eg statically rendering private content. It supposes using Next.js server-side feature (mainly middlewares). It is appliable to other full-stack frameworks such as Meteor.
- Check client-side, for non-sensitive client.
Scenario 2 is appropriate when the content is not sensitive. It is not using Next.js server-side feature, so it should work for other client-only frameworks such as Gatsby, Create-React-App.
- "Remember me mode": user stays authenticated for at least one month. If not ticked, user is logged out at the end of the session.
- Token revokation: admin can log out an user (eg after an usurpation is identified)
We don't want to limit ourselves to setup server-to-server communication, or client-to-api communication. We also want to secure web pages. This is important, as many existing documentation are only explaining how to secure an API, and not how to secure web pages. We don't focus on patterns using a 3rd party (Oauth) yet. We don't introduce roles or tenants.
This is a summary only for the purpose of this guide. JWT are a vast subject. More information can be found here: https://jwt.io/
A JSON Web Token is a token that encode informations about the user. Typically, you would store a few non-sensitive meta-information, like the user email, name, organization, unique id.
You can do 2 operations on an existing token:
- Decoding: it means getting the information on the token. Decoding a token DOESN'T mean it's valid or that the user is authenticated. Because anyone can craft a token that can be decoded! Think of it as a convenience to get basic user info from the token
- Verifying: it means checking the token validity Verifying the token DOES MEAN it's valid and the user is authenticated. That's what you want to do to use to authenticate someone.
Asymmetrical auth token with public/private RSA256 Symmetrical auth tokens with secret => this is used in
Token stored in the database Access + refresh token
TODO: find the right naming for those patterns
In order to verify a token, you need a key. In a symmetrical approach, there is one secret key, used to created and verify token. This mean this operation can only be done server-side, by the authentication server.
In an assymetrical approach, the website owner, you, has a secret key used to create tokens. And the client application have a public key, used to verify the token. It means an assymetrical approach let's anyone check the token validity. That's nice if you are using a 3rd party authentication service like FusionAuth. It means you don't even need to call it to check the token. Technically, even a browser can do it.
(copy pasted from stack overflow) This question might need an updated answer, now middlewares are there in Next.js 12 (october 2021): https://nextjs.org/docs/middleware
Most of the answer from @Yilmaz from april 2020 is still relevant. However, previously, we had to use getInitialProps
in _app
to process the request OR a custom server.
This is no longer the case.. Using a middleware let's you achieve a similar purpose, with cleaner code. Because middleware are specifically designed for such use cases.
Here, I suppose you get a JWT access token using an asymetrical algorithm like RS256, exactly like in this previous answer.
Here is a possible implementation:
import { NextFetchEvent, NextRequest, NextResponse } from "next/server";
const removeCookie = (res: NextResponse, cookieName: string) => {
res.headers.append("Set-Cookie", `${cookieName}=; Max-Age=-1; Path=/`);
return res;
};
export default async function middleware(
req: NextRequest,
ev: NextFetchEvent
) {
const { pathname } = req.nextUrl;
const isPublic = isPublicRoute(pathname);
if (isPublic) {
return NextResponse.next();
}
const accessToken = req.cookies[TOKEN_PATH];
if (!accessToken) {
return NextResponse.redirect(LOGIN_HREF);
}
const isValidToken = await checkAccessToken(accessToken);
if (!isValidToken) {
let res = NextResponse.redirect(LOGIN_HREF);
res = removeCookie(res, TOKEN_PATH);
return res;
}
return NextResponse.next();
}
In my example, the checkAccessToken
should verify the token (not decode, verify the signature).
This is where things are the most complicated imo.
When using the RSA256 algorithme you also get a PUBLIC certificate (in addition to the SECRET key that must be... kept secret). Eventhough you do the check in the middleware
, which is private and server-only code, that's good news because it means you could even use it in the browser, in theory.
So, you can either fetch
the token validation endpoint provided by your auth server, or verify the token yourself.
Fetching is not the recommended option because it might break Vercel/Next edge capabilities and add latency, according to the documentation.
I must admit that I did not succeed to verify the token yet using Next.js :) I'll update this answer if I manage to have a code sample that works.
This doesn't change with middlewares. You store your access token as an httpOnly
cookie. When logging out, you unset this cookie.
Managing those Set-Cookies headers are the responsibility of your auth server.
This is a basic workflow but it should work. You can then add a refresh token in the mix with a similar approach.
Using httpOnly cookie is meant to prevent XSS attack, which happens when people force running some JavaScript code in your browser, for instance through an unsanitized script or image url.
It should be possible to handle the token storage client-side, as long as you fingerprint the browser as well. This way, the token only works for a specific browser, and the user have to login again if they swap browsers. You can store the print in the server database, or within the token itself, and verify it in the request.
- If you verify the token in your middleware, there is no immediate revokation mechanism for the access token. Because there is no call to a database.
Therefore, in this scenario, you'd want to opt-in for short lived access token (eg 5 minutes) coupled with a refresh token. You can revoke the refresh token, so basically revoking works but takes a few minutes.
- If a 3rd party server verifies the token: then it could check for blacklisted tokens.
Also, some piece of advice: most articles, tutorials etc. online are focused on server-to-server communication. Or client-to-API. They completely suck when it comes to check authentication before accessing web pages.
For instance, setting the Authorization
header is not possible in the browser. It works only when communicating with an API. Cookies are mandatory for web pages.
Even then, if this API is meant to be called from a browser, it should preferably accept a cookie.
When discussing with experts on the field, you need to always clarify the Next.js use case.
Some frameworks seem to prefer relying on the database. They store a hashed token in the db, which acts as a session. If you want to check auth, you need a server that will check the user's token against the stored token (= checking that there is an active session with this token).
I am thinking of Meteor for instance.
I couldn't find the name of this mechanism and its actual relation to JWT however. Are they simply variations of the JWT approach?
Next.js official authentication doc is not showing middlewares at the time of writing, but instead use getServerSideProps
. I really don't like this pattern.
It uses a kind of session system but I am not clear about the internals of it, I am not even sure of the name (is that session-based auth?).
Vercel edge handles examples shows how to secure an API route, but not a page (at the time of writing)
We talk about redirection more than authentication.
When using a JWT token with a public certificate, technically, you can authenticate client-side, but use cases are limited. Here, the goal is only to redirect non-authenticated users as soon as possible using frontend code.
See this ticket for some advanced pattern for Next.js inspired by Vercel dashboard: VulcanJS/vulcan-next#71
https://nextjs.org/docs/authentication TODO: describe the pattern selected by Next.js official doc more thoroughly.
The "With Passport" example is using a symmetrical token.
https://next-auth.js.org/ TODO: describe the pattern selected by Next auth
Remix, Blitz, Redwood...
The "Jokes" tutorial includes an authentication example with detailed explanations of the auth flow.
https://remix.run/docs/en/v1/tutorials/jokes#authentication
Sessions are documented here: https://remix.run/docs/en/v1.0.3/api/remix#sessions
- The token is hashed using a secret variable. It contains the user id.
- The server checks the user id against the database to confirm existence.
- It does describe an API authentication in this example (securing the creation of a new joke), however, since it relies correctly on cookies and Set-Header it will work ok for pages as well.
Session-based authentication stores the session information in a database, instead of storing them in a JSON web token. Then the sessionId is stored in a cookie, similarly to how we store user information in the JWT.
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
https://evertpot.com/jwt-is-a-bad-default/
https://github.com/vercel/next.js/blob/canary/examples/with-passport
https://nextjs.org/docs/authentication
https://stackoverflow.com/questions/49920234/how-to-implement-authentication-in-next-js
https://supertokens.io/: creator of Super Tokens contributed to Blitz.js auth implementation