Next.js (Part 3)

Neil HaddleyNovember 7, 2021

API routes and next-auth

Reactnext-jsapi-routesnext-authauthentication

Get Request

I used API routes to create API endpoints inside a Next.js app. I added a file to the /pages/api directory to create each endpoint.

Get Request

Get Request

Dynamic API routes

I made API routes dynamic, just like regular Next.js pages.

Dynamic API Route

Dynamic API Route

Unprotected pages

Some pages didn't require login to access.

Unprotected page

Unprotected page

next-auth

I used next-auth to prevent unauthorized access to protected API methods and pages. I installed it with npm install next-auth.

GitHub Id and GitHub Secret

Here I used GitHub as an authentication provider.

GitHub Developer Settings

GitHub Developer Settings

Register a new OAuth application

Register a new OAuth application

Note the Client ID and Client Secret

Note the Client ID and Client Secret

.env

I stored the Client ID and Client Secret values in an .env file.

[...nextauth]

I added a [...nextauth] API method.

_app.js

I added a next-auth/client Provider to _app.js.

Provider

Provider

Add code to prevent unauthorized access

I checked that a valid session existed before returning articles or article details, whether via a REST API call or a web page request.

/api/articles is now protected

/api/articles is now protected

/api/articles/4 is now protected

/api/articles/4 is now protected

page /protected is now protected (server-side)

page /protected is now protected (server-side)

page /protected is now protected (client-side)

page /protected is now protected (client-side)

Adding login and logout to home page

I added "Sign In" and "Sign Out" buttons to the home page.

Sign in

Sign in

Sign in with GitHub provider

Sign in with GitHub provider

Signed in

Signed in

authorized

authorized

authorized

authorized

authorized

authorized

next-auth database (optional)

Specifying a database is optional — if I didn't need to persist user data or support email sign-in, JSON Web Tokens would be used for session storage instead. To specify a database, I updated the [...nextauth].js file and the relevant environment variables.

accounts

accounts

users

users

session (with user id and provider)

session (with user id and provider)

pages

pages

articles.js and pagesapiarticlesindex.js

JAVASCRIPT
1// articles.js
2
3export const articles = [
4  {
5    "userId": 1,
6    "id": 1,
7    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
8    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
9  },
10  {
11    "userId": 1,
12    "id": 2,
13    "title": "qui est esse",
14    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
15  }
16]
17
18// /pages/api/articles/index.js
19import { articles } from '../../../articles'
20
21const index = async (req, res) => {
22
23    res.status(200).json(articles)
24
25}
26
27export default index

pagesapiarticlesid.js

JAVASCRIPT
1// /pages/api/articles/[id].js
2import {articles} from '../../../articles'
3
4const article = async (req, res) => {
5
6    const { query: { id } } = req
7
8    const article = articles.find(a => a.id.toString() === id)
9
10    if (!article) {
11        res.status(404).json({ message: `article ${id} not found` })
12    }
13
14    res.status(200).json(article)
15
16}
17
18export default article

pagesunprotected

JAVASCRIPT
1import { articles } from '../articles'
2
3function index() {
4
5    return (
6        <div>
7            <ul>
8                {articles.map(article => (<li key={article.id}>{article.title}</li>))}
9            </ul>
10        </div>
11    )
12}
13
14export default index

.env

TEXT
1//.env
2NEXTAUTH_GITHUB_ID=e95a2816a93e7daabc6c
3NEXTAUTH_GITHUB_SECRET=<secret>

apiauth...nextauth.js

JAVASCRIPT
1// /api/auth/[...nextauth].js
2import NextAuth from "next-auth";
3import Providers from "next-auth/providers";
4
5const options = {
6    // @link https://next-auth.js.org/configuration/providers
7    providers: [
8        Providers.GitHub({
9            clientId: process.env.NEXTAUTH_GITHUB_ID,
10            clientSecret: process.env.NEXTAUTH_GITHUB_SECRET,            
11        }),
12        /*Providers.Google({
13            clientId: process.env.NEXTAUTH_GOOGLE_ID,
14            clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET,
15        }),
16        Providers.Facebook({
17            clientId: process.env.NEXTAUTH_FACEBOOK_ID,
18            clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET,
19        }),*/
20    ]
21}
22
23export default (req,res) => NextAuth(req,res,options)

_app.js

JAVASCRIPT
1import { createGlobalStyle, ThemeProvider } from 'styled-components'
2import { Provider } from "next-auth/client";
3
4const GlobalStyle = createGlobalStyle`
5  body {
6    margin: 0;
7    padding: 0;
8    box-sizing: border-box;
9  }
10`
11
12const theme = {
13    colors: {
14        primary: '#0070f3',
15    },
16}
17
18export default function App({ Component, pageProps }) {
19    return (
20        <>
21            <Provider session={pageProps.session}>
22                <GlobalStyle />
23                <ThemeProvider theme={theme}>
24                    <Component {...pageProps} />
25                </ThemeProvider>
26            </Provider>
27        </>
28    )
29}

pagesapiarticlesindex.js updated

JAVASCRIPT
1import { articles } from '../../../articles'
2import { getSession } from 'next-auth/client'
3
4const index = async (req, res) => {
5
6    const session = await getSession({ req });
7    if (session) {
8        res.status(200).json(articles)
9    } else {
10        res.status(401).json({ message: `please log in` })
11    }
12
13}
14
15export default index

pagesapiarticlesid.js updated

JAVASCRIPT
1import { articles } from '../../../articles'
2import { getSession } from 'next-auth/client'
3
4const article = async (req, res) => {
5
6    const session = await getSession({ req });
7    if (session) {
8        const { query: { id } } = req
9        const article = articles.find(a => a.id.toString() === id)
10        if (!article) {
11            res.status(404).json({ message: `article ${id} not found` })
12        }
13        res.status(200).json(article)
14    } else {
15        res.status(401).json({ message: `please log in` })
16    }
17
18}
19
20export default article

pagesprotected.js

JAVASCRIPT
1import { getSession } from "next-auth/client"
2import { articles } from '../articles'
3
4function index({ session, articles }) {
5
6    // If no session exists, display access denied message
7    if (!session) { return (<div>You need to be logged on</div>) }
8
9    return (
10        <div>
11            <ul>
12                {articles.map(article => (<li key={article.id}>{article.title}</li>))}
13            </ul>
14        </div>
15    )
16}
17
18export default index
19
20export const getServerSideProps = async (context) => {
21
22    const session = await getSession(context);
23
24    // If no session exists, display access denied message
25    if (!session) {
26        return {
27            props: {
28                session
29            }
30        }
31    }
32
33    return {
34        props: {
35            session,
36            articles
37        }
38    }
39}

pagesprotected.js

JAVASCRIPT
1import { useSession } from "next-auth/client"
2import useSWR from 'swr'
3
4function index() {
5    
6    const [ session, loading ] = useSession()
7
8  // When rendering client side don't display anything until loading is complete
9  if (typeof window !== 'undefined' && loading) return null
10
11  // If no session exists, display access denied message
12  if (!session) { return  (<div>You need to be logged on</div>) }
13
14
15  const fetcher = (...args) => fetch(...args).then(res => res.json())
16
17    const { data, error } = useSWR('/api/articles', fetcher)
18
19    if (error) return <div>failed to load</div>
20    if (!data) return <div>loading...</div>
21    
22    return (
23        <div>
24            <ul>
25                {data.map(article => (<li key={article.id}>{article.title}</li>))}
26            </ul>
27        </div>
28    )
29
30}
31
32export default index

pagesindex.js

JAVASCRIPT
1import { signIn, signOut, useSession } from 'next-auth/client'
2
3export default function Home() {
4  const [session, loading] = useSession()
5
6  return (
7    <>
8      {!session && (<>
9        Not signed in
10        <br />
11        <button onClick={signIn}>Sign In</button>
12      </>)}
13      {session && (<>
14        Signed in as {session.user.email}
15        <br />
16        <button onClick={signOut}>Sign Out</button>
17      </>)}
18    </>
19  )
20}

...nextauth.js

JAVASCRIPT
1import NextAuth from "next-auth";
2import { session } from "next-auth/client";
3import Providers from "next-auth/providers";
4
5const options = {
6    // @link https://next-auth.js.org/configuration/providers
7    providers: [
8        Providers.GitHub({
9            clientId: process.env.NEXTAUTH_GITHUB_ID,
10            clientSecret: process.env.NEXTAUTH_GITHUB_SECRET,
11        }),
12        /*Providers.Google({
13            clientId: process.env.NEXTAUTH_GOOGLE_ID,
14            clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET,
15        }),
16        Providers.Facebook({
17            clientId: process.env.NEXTAUTH_FACEBOOK_ID,
18            clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET,
19        }),*/
20    ],
21    database: process.env.DB_URL,
22    session: {
23        jwt: true
24    },
25    jwt: {
26        secret: process.env.JWT_SECRET
27    },
28    callbacks: {
29        async jwt(token, user, account, profile, isNewUser) {
30            console.log("token", token)
31            console.log("user", user)
32            console.log("account", account)
33            console.log("profile", profile)
34            console.log("isNewUser", isNewUser)
35            if (user && user.id) {
36                token.id = user.id
37            }
38            if (account && account.provider) {
39                token.provider = account.provider
40            }
41            return token
42        },
43        async session(session, token) {
44            session.user.id = token.id
45            session.user.provider = token.provider
46            return session
47        }
48    }
49}
50
51export default (req, res) => NextAuth(req, res, options)

.env

TEXT
1NEXTAUTH_GITHUB_ID=e95a2816a93e7daabc6c
2NEXTAUTH_GITHUB_SECRET=<secret>
3
4DB_USER=<user>
5DB_PASSWORD=<password>
6DB_URL=mongodb+srv://$DB_USER:$DB_PASSWORD@cluster0.gdnd5.mongodb.net/nextauthDB?retryWrites=true&w=majority
7JWT_SECRET=<secret>
8NEXTAUTH_URL=http://localhost:3000