Over the past couple of releases, Next.js has made some impressive additions which have transformed the way I develop with it. One of my favourite new features is the getServerSideProps
function; a function which can be appended to any page component, is executed on the server for each request, and injects the resulting data into the page as props
.
Why do I like this so much? Well, put simply, it makes my life dramatically easier - and no piece of functionality better illustrates this than the ubiquitous user sign in...
Up until now, authentication within a general React/SPA project has been a complicated task, fraught with danger. In basic cases, it involves various hooks and API calls; in more extreme cases, jargonistic acronyms like JWT and PKCE come into play. But not anymore! With getServerSideProps
, secure server sessions are back on the menu. Hear that thud? That's the sound of 100s of lines of redundant code dropping out of my project.
The code
Starting with a clean Next.js app, adding user sign in requires just three parts:
- A page for the user sign in form.
- An API endpoint for validating the user credentials and setting the user session.
- A page which validates the user session or rejects the request.
We'll start with the sessions API endpoint, and by creating pages/api/sessions.js
:
import { withIronSession } from "next-iron-session";
const VALID_EMAIL = "chris@decimal.fm";
const VALID_PASSWORD = "opensesame";
export default withIronSession(
async (req, res) => {
if (req.method === "POST") {
const { email, password } = req.body;
if (email === VALID_EMAIL && password === VALID_PASSWORD) {
req.session.set("user", { email });
await req.session.save();
return res.status(201).send("");
}
return res.status(403).send("");
}
return res.status(404).send("");
},
{
cookieName: "MYSITECOOKIE",
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false
},
password: process.env.APPLICATION_SECRET
}
);
Let's break this down:
- There are two prerequisites here: first we use the fantastic Next Iron Session package to simplify dealing with sessions, so you'll need to
npm install --save next-iron-session
; secondly you'll need to add a 32 character string calledAPPLICATION_SECRET
to your.env
, which is used to secure the session content. - My credentials check is very crude (
email === VALID_EMAIL && password === VALID_PASSWORD
) to keep the example simple; in reality you'd likely be doing a datastore lookup (and please use password hashing). - Next.js API routes aren't scoped by HTTP verb, hence I've added the
if (req.method === "POST")
check to lock this down a little.
Next we're going to create our private page, pages/private.jsx
:
import React from "react";
import { withIronSession } from "next-iron-session";
const PrivatePage = ({ user }) => (
<div>
<h1>Hello {user.email}</h1>
<p>Secret things live here...</p>
</div>
);
export const getServerSideProps = withIronSession(
async ({ req, res }) => {
const user = req.session.get("user");
if (!user) {
res.statusCode = 404;
res.end();
return { props: {} };
}
return {
props: { user }
};
},
{
cookieName: "MYSITECOOKIE",
cookieOptions: {
secure: process.env.NODE_ENV === "production" ? true : false
},
password: process.env.APPLICATION_SECRET
}
);
export default PrivatePage;
So what's happening here?
- Firstly, we're using
getServerSideProps
to check for the existence of the user session - which would have been set by our sessions API endpoint. - If there's no session, we're sending an empty 404 back to the browser. You could redirect to the sign in page instead, but I prefer this approach as it deters snooping bots/crawlers.
- Finally, we're piping the contents of the user session into the page component as a prop.
So now we have a private page, and an API endpoint to open it up - we just need to add our sign in form to bring it all together, pages/signin.jsx
:
import React, { useRef } from "react";
import { useRouter } from "next/router";
const SignInPage = () => {
const router = useRouter();
const emailInput = useRef();
const passwordInput = useRef();
const handleSubmit = async (e) => {
e.preventDefault();
const email = emailInput.current.value;
const password = passwordInput.current.value;
const response = await fetch("/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
if (response.ok) {
return router.push("/private");
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Email: <input type="text" ref={emailInput} />
</label>
</div>
<div>
<label>
Password: <input type="password" ref={passwordInput} />
</label>
</div>
<div>
<button type="submit">Sign in</button>
</div>
</form>
);
};
export default SignInPage;
It might seem like there's a lot going on with this one, but it's actually the most basic piece:
- We're rendering out a form; using
ref
s to capture the email and password inputs, and registering anonSubmit
callback. - That onSubmit callback is then using
fetch
to call our sessions API endpoint with the supplied value. - If the sessions API endpoint responds with an
ok
header, we're assuming the user session has been set successfully, and redirecting the user to our private page.
But hey... If we're just checking the users details exist in the session, what's to stop someone creating a fake session and pretending to be someone else?
Very good question! Remember that 32 character APPLICATION_SECRET
we added to our .env
? That's used to encrypt the session contents, so it's not readable (or spoofable) to the outside world. All the client will see is something like this:
Just remember: it's called APPLICATION_*SECRET*
for a reason, keep it that way.
That's all folks
That's it; we've added a functional, and secure user sign in function to our Next.js app - with minimal code overhead.
What's next?
I've kept the code deliberately blunt for the purpose of this article; there are some obvious and immediate improvements required to take this forward into a full application:
- We don't want to be repeating the session lookup for all our secured pages - we could write a decorator/HoC that wraps
getServerSideProps
and performs the session validation in a reusable way. - I've not abstracted the iron session cookie name (
cookieName: "MYSITECOOKIE"
) - this encourages developer error and bugs, so should be moved to a shared constant or the env. - Our sign in form doesn't provide any kind of validation messaging to the end user; we could add a
useState
hook to display helpful errors. - We've not added user sign out functionality; that could be added for
DELETE
calls to the sessions API endpoint.
Top comments (13)
Love the simplicity. However, after I am successfully authenticated, my user session is not available inside my getServerSideProps function, so a 404 is always returned even after a successful authentication and redirect. Any thoughts on why:
const user = req.session.get("user");
would work in the sessions.js file but be unavailable in the getServerSideProps function?
HI, @tsm012. How did you solve the above issue? I am experiencing the same issue.
Create a util in
utils/cookies.js
Implement lookup logic
Attempt to access the session value
At this point, the session is empty e.g
{ }
In my case, different
cookieNames
property.because i just copied and paste code above.
Returning a 404 for an unauthenticated user - surely this would be a 401/403. In your demo, SSR would just show a 404 which doesn't make any sense.
redirecting/or using res.end within getServerSideProps will result in the error: 'ERR_HTTP_HEADERS_SENT' so don't see how that works either.
It's quite common to return a 404 on protected endpoints for an unauthed user - it makes it harder to "profile" an application from the outside. Github do this, for instance. Feel free to implement differently though, for instance redirecting to the sign in screen - it's just a demo afterall...
I'm using
res.statusCode
,res.send
insidegetServerSidePros
without issue; are you basing your assumption on a statically compiled next app?Thanks for the reply Chris.
There is an RFC for this issue. Take a look at the codesandbox demo and you will see
Maybe this is handled somehow in next-iron-session
Thanks Samuel - that is very bizarre! I'm using this exact code successfully in a project at the moment, but you're right - it is an issue in the codesandbox demo... I'll do some digging.
It seems to be an RFC at the moment and would be a great solution to be able to set headers - which could allow a redirect in
getServerSidePros
which would be great.Currently, I have to use
getInitialProps
in a HOC and check if we SSR/client and do something like:Is it possible to use
withIronSession
in_app.jsx
and pass thesession:true|false
topageProps
to wrap around each and every possible page in an easier way? That being said, it'd be inside thefunction App({ Component, pageProps }){...
. Please tell me "YES", I need this answer and how :')Very great, simple and useful example, but am not able to understand that how we could write a decorator/HoC that wraps getServerSideProps and performs the session validation in a reusable way, can anyone here give an example of that related to this example here.
This is great, thanks Chris! And thanks for the introduction to
next-iron-session
Yes succinct and too the points ;) more words could be cutdown at what is it explanation blocks. :)