Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NextAuth with external API that provides access and refresh JWT HttpOnly cookies #6544

Closed
pixeliner opened this issue Jan 29, 2023 · 0 comments
Labels
question Ask how to do something or how something works

Comments

@pixeliner
Copy link

Question 💬

I'm trying to figure out how the flow works when the API (written in NestJs) provides Authentication cookies.

Here are the authentication endpoints:

/signin sets the Access- (Max-Age: 240) and Refresh token cookie (Max-Age: 60000)
/refresh sets a new Access token Cookie
The UseGuards use the Passport 'jwt' and 'jwt-refresh-token' guards

@HttpCode(200)
@Post('/signin')
async signIn(
  @Body() authCredentialsDto: AuthCredentialsDto,
  @Req() request: IRequestWithUser,
): Promise<UserModel> {
  const response = await this.authService.signIn(authCredentialsDto);

  request.res.setHeader('Set-Cookie', [...response.cookies]);

  return response.user;
}

@UseGuards(RestJwtRefreshGuard)
@Get('refresh')
async refresh(@Req() req: IRequestWithUser): Promise<UserModel> {
  const accessTokenCookie = await this.authService.getCookieWithJwtAccessToken(
    req.user.id,
  );

  req.res.setHeader('Set-Cookie', accessTokenCookie);

  return req.user;
}

@UseGuards(RestJwtAuthGuard)
@Get()
authenticate(@RestCurrentUser() user: UserModel, @RestCsrfToken() csrfToken) {
  return {
    user,
    csrfToken,
  };
}

[...nextauth] file:

import axios from 'axios';
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { RestUserModel } from '../../../types/models';
import createAxiosRequest from '../../../utils/axios';
import logger from '../../../utils/logger';
import { getData, postData } from '../../../utils/request';

const request = createAxiosRequest();

type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions

const nextAuthOptions: NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse): NextAuthOptions => {
  return {
    providers: [
      CredentialsProvider({
        name: "Credentials",
        credentials: {
          email: { label: "Email", type: "email", placeholder: "jhon@doe.com" },
          password: {  label: "Password", type: "password" }
        },
        async authorize(credentials) {
          try {
            const response = await postData<RestUserModel>('/auth/signin', {
              body: {
                email: credentials?.email,
                password: credentials?.password,
              },
            })

            logger.info("Authorization was successful!");

            const cookies = response.headers['set-cookie'] || [];
            res.setHeader('Set-Cookie', cookies)
            
            return response.data
          } catch (error: any) {
            logger.error(`NextAuth authorize error: ${error.message}`);
            throw error
          } 
        }
      })
    ],
    callbacks: {
      async signIn({ user }) {
        if (!(user.role === "admin" || user.role === "manager")) return false
        return true
      },
      async jwt({ token, user }) {
        if (user) {
          return { user };
        }

        // I suppose that if the cookie expires it's just a simple check of whether it still exists or not
        if (req.cookies['Authentication']) return token;

        try {
          // This gives a Set-Cookie with a new Access token
         const response = await getData<RestUserModel>('http://localhost:32000/api/auth/refresh')

          logger.debug('JWT Callback RETURN', token)
          
          return {
            ...token,
            user: response.data
          }
        } catch (error) {
          return {
            ...token,
            error: "RefreshAccessTokenError",
          }
        }
      },
    },
    session: {
      strategy: "jwt"
    },
    events: {
      async signOut() {
        res.setHeader("Set-Cookie", [
          "Authentication=deleted;Max-Age=0;path=/;",
          "Refresh=deleted;Max-Age=0;path=/;"
        ]);
      },
    }
  }
}

const Auth = (req: NextApiRequest, res: NextApiResponse) => {
  return NextAuth(req, res, nextAuthOptions(req, res))
}

export default Auth;

Data retrieval

Now most data is retrieved within the getServerSideProps and some of the search data is done on the frontend.
On the getServerSideProps I get it by using:

const res = await getData<Paginated<RestUserModel>>("/users", {cookies: req.headers.cookie});
return { props: { usersPaginated: res.data } }

The getData is an extension on:

const fetchData = async <T>(method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE", endpoint: string, { locale = "en", data, cookies }: RequestDetails): Promise<AxiosResponse<T, any>> => {
  logger.info(`fetchData cookies: [${endpoint}] - ${cookies}`);
  logger.info(`fetchData body: [${endpoint}] - ${body}`);
  const res = await axios({
    withCredentials: true,
    url: `${process.env.NEXT_PUBLIC_EQUMEDIA_API}${endpoint}`,
    method,
    headers: {
      method,
      "accept-language": locale,
      "cookie": `frontend_cookie=app-ecosystem; ${cookies || ''}`,
    },
    data: body ? JSON.stringify(body) : null,
    ...(method === "GET" && { params: data }),
    ...(method === "POST" && { data })
  });

  return res;
};

So as you can see I pass through the cookies as default

Issue

Signing in seems to work great, navigating between pages also, until my token expires.
After some fiddling I seem to have 2 problems or maybe misunderstandings about the topic:

  1. Even after token expiry it seems the cookie is still there, shouldn't the cookie be deleted when Max-Age is reached? I would think (as you can see in the code of the nextauth file) I could just check if it's defined.
  2. Another problem is, I'm not using a sessions check on a page, just SSR. So it's not reaching the async jwt() callback. How should take this on?

How to reproduce ☕️

Refresh or navigate page after token expiry

Contributing 🙌🏽

No, I am afraid I cannot help regarding this

@pixeliner pixeliner added the question Ask how to do something or how something works label Jan 29, 2023
@nextauthjs nextauthjs locked and limited conversation to collaborators Jan 29, 2023
@balazsorban44 balazsorban44 converted this issue into discussion #6545 Jan 29, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Ask how to do something or how something works
Projects
None yet
Development

No branches or pull requests

1 participant