Search code examples
authenticationdjango-rest-frameworknext.jsnext-auth

Is this the correct and secure way to connect Next.js + NextAuth with a Django Rest Framework API?


I have been working on a Next.js app with a custom backend using Django Rest Framework, with the main focus on authentication with social platforms (Google, Github etc). Here's the flow I am wanting to use:

  1. Have the NextAuth do the heavy lifting for social authentication. It gets back, for instance, an access token and an id token when the user wants to login with his/her Google account.
  2. Put the id token and access token given back by Google into the NextAuth session object.
  3. In the frontend, use those two tokens in the session object to make a POST request to the DRF backend which essentially accepts the access token and id token and returns a access token and a refresh token. NB. The DRF backend has dj-rest-auth and django-allauth setup to handle social authentication.
  4. The DRF backend sends back the tokens in the form of HTTPOnly cookies. So, next time I want to make a request to the DRF API, the cookies should be passed along the request.

Is this correct and secure, or am I shooting myself in the foot?

My code for context:

index.tsx

import React, { useEffect } from "react";
import { signIn, signOut, useSession } from "next-auth/client";
import { Typography, Button, Box } from "@material-ui/core";
import { makeUrl, BASE_URL, SOCIAL_LOGIN_ENDPOINT } from "../urls";
import axios from "axios";
axios.defaults.withCredentials = true;

function auth() {
  const [session, loading] = useSession();

  useEffect(() => {
    const getTokenFromServer = async () => {
      // TODO: handle error when the access token expires
      const response = await axios.post(
        // DRF backend endpoint, api/social/google/ for example
        // this returns accessToken and refresh_token in the form of HTTPOnly cookies
        makeUrl(BASE_URL, SOCIAL_LOGIN_ENDPOINT, session.provider),
        {
          access_token: session.accessToken,
          id_token: session.idToken,
        },
      );
    };

    if (session) {
      getTokenFromServer();
    }
  }, [session]);

  return (
    <React.Fragment>
      <Box
        display="flex"
        justifyContent="center"
        alignItems="center"
        m={5}
        p={5}
        flexDirection="column"
      >
        {!loading && !session && (
          <React.Fragment>
            <Typography variant="button">Not logged in</Typography>
            <Button
              variant="outlined"
              color="secondary"
              onClick={() => signIn()}
            >
              Login
            </Button>
          </React.Fragment>
        )}
        {!loading && session && (
          <React.Fragment>
            <Typography>Logged in as {session.user.email}</Typography>
            <pre>{JSON.stringify(session, null, 2)}</pre>
            <Button
              variant="outlined"
              color="primary"
              onClick={() => signOut()}
            >
              Sign Out
            </Button>
          </React.Fragment>
        )}
      </Box>
    </React.Fragment>
  );
}

export default auth;

api/auth/[...nextauth].ts

import NextAuth from "next-auth";
import { InitOptions } from "next-auth";
import Providers from "next-auth/providers";
import { NextApiRequest, NextApiResponse } from "next";
import axios from "axios";

import { BASE_URL, SOCIAL_LOGIN_ENDPOINT, makeUrl } from "../../../urls";
import { AuthenticatedUser, CustomSessionObject } from "../../../types";
import { GenericObject } from "next-auth/_utils";

const settings: InitOptions = {
  providers: [
    Providers.Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      authorizationUrl:
        "https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code",
    }),
  ],

  secret: process.env.NEXT_AUTH_SECRET,

  session: {
    maxAge: 6 * 60 * 60, // 6 hours
  },

  callbacks: {
    async signIn(user: AuthenticatedUser, account, profile) {
      if (account.provider === "google") {
        const { accessToken, idToken, provider } = account;
        user.accessToken = accessToken;
        user.idToken = idToken;
        user.provider = provider;
        return true;
      }

      return false;
    },

    async session(session: CustomSessionObject, user: AuthenticatedUser) {
      session.accessToken = user.accessToken;
      session.idToken = user.idToken;
      session.provider = user.provider;
      return session;
    },

    async jwt(token, user: AuthenticatedUser, account, profile, isNewUser) {
      if (user) {
        token.accessToken = user.accessToken;
        token.idToken = user.idToken;
        token.provider = user.provider;
      }

      return token;
    },
  },
};

export default (req: NextApiRequest, res: NextApiResponse) => {
  return NextAuth(req, res, settings);
};

I am stressed out by the fact that whether the session object is secure enough to store the tokens. I also want to implement a mechanism to refresh the access tokens using the refresh token when the access token expires.


Solution

  • I ended up solving this issue over time. I wrote a two-part article outlining how I solved it, which can be found here and here