Skip to content
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

"The getAccessToken method can only be used from the server side" when used in middleware.js #912

Closed
7 tasks done
robertwbradford opened this issue Nov 17, 2022 · 15 comments
Labels
enhancement New feature or request

Comments

@robertwbradford
Copy link
Contributor

robertwbradford commented Nov 17, 2022

Checklist

  • The issue can be reproduced in the sample app (or N/A).
  • I have looked into the README and have not found a suitable solution or answer.
  • I have looked into the examples and have not found a suitable solution or answer.
  • I have looked into the API documentation and have not found a suitable solution or answer.
  • I have searched the issues and have not found a suitable solution or answer.
  • I have searched the Auth0 Community forums and have not found a suitable solution or answer.
  • I agree to the terms within the Auth0 Code of Conduct.

Description

Hello, I see there is a withMiddlewareAuthRequired helper coming in v2.

We have a use case where we'd like to use getAccessToken in a Next.js middleware file but not require authentication. That is, auth is optional, but we will do additional things if a user is authenticated.

However, in a middleware.js file, the call to getAccessToken() is reporting: Error: The getAccessToken method can only be used from the server side.Seems that its check for the server environment is not accounting for the Next.js middleware feature.

Any ideas on how to accomplish this?

Reproduction

I have the following in a middleware.js file:

import { NextResponse } from "next/server";
import { getAccessToken } from "@auth0/nextjs-auth0";

export async function middleware(req) {
  const res = NextResponse.next();
  const { accessToken } = await getAccessToken(req, res);
  console.log("Middleware", accessToken);
}

SDK version

^1.9.2

Next.js version

^12.3.0

Node.js version

v16.16.0

@adamjmcgrath
Copy link
Contributor

Hi @robertwbradford - thanks for raising

Everything under @auth0/nextjs-auth0 needs to run on the Node.js runtime. As of as of 2.0.0-beta.4 everything under @auth0/nextjs-auth0/edge (it used to be under @auth0/nextjs-auth0/middleware) will run on the Edge runtime.

We don't have getAccessToken for the Edge runtime currently because the tools we use to refresh and verify the access token require Node. But, you can get the access token and expiry from the /edge getSession method (you just need to refresh it on a Node handler)

import { NextResponse } from "next/server";
import { getAccessToken } from "@auth0/nextjs-auth0/edge";

export async function middleware(req) {
  const res = NextResponse.next();
  const { accessToken, accessTokenExpiresAt } = await getSession(req, res);
  if (accessTokenExpiresAt * 1000 < Date.now()) {
    // redirect somewhere to refresh it
  } else {
    // do something with the access token
  }
}

@adamjmcgrath adamjmcgrath added enhancement New feature or request beta labels Nov 18, 2022
@adamjmcgrath adamjmcgrath removed the beta label Dec 14, 2022
@focux
Copy link

focux commented Dec 14, 2022

Hey @adamjmcgrath, is there a plan to support refreshing tokens on the edge in the short term?

@adamjmcgrath
Copy link
Contributor

adamjmcgrath commented Dec 15, 2022

Hi @focux - we don't currently have plans to support it in the short term. But we've left this feature request open to gauge support for it and hear about the different use cases - so if the above workaround doesn't work for you, we'd be interested to hear more about it.

@martinop
Copy link

do we have a way to get the token client side?

@adamjmcgrath
Copy link
Contributor

@martinop - see #358

@jonahallibone
Copy link

jonahallibone commented Feb 3, 2023

Definitely would be good to be able to refresh the token in edge environments, especially with the new Edge API Routes. The edge seems particularly well suited for actions such as these, which are simple, need to be fast, and can be executed during the lifetime of a Request.

@benevbright
Copy link
Contributor

@adamjmcgrath hi,

But, you can get the access token and expiry from the /edge getSession method (you just need to refresh it on a Node handler)

so "refresh it", you mean by just calling getAccessToken one time that is located in the node handler? Then, it will update the cookies so in the next call to edge function, edge function's getSession tokens will have updated AT?

I don't fully understand what you mean by "refresh it". The goal here would be to have an updated AT in edge function's getSession.

@adamjmcgrath
Copy link
Contributor

so "refresh it", you mean by just calling getAccessToken one time that is located in the node handler? Then, it will update the cookies so in the next call to edge function, edge function's getSession tokens will have updated AT?

Yes, this is the current workaround while we don't have the functionality to refresh the AT at the edge

@dkokotov
Copy link

dkokotov commented Jun 7, 2023

Just to follow up, I was able to implement this approach successfully. My use case was wanting to have an api proxy that runs on edge. My solution was to have both an edge and node version of this proxy, and the edge one will forward to the node one if the access token is no longer valid.

This works pretty well, though you have to be careful around set-cookie handling to have the set-cookie header returned correctly in the final response.

However, I would really like to see all functionality supported on edge - both refreshing the token and the core handlers for the full authentication flow. Clerk already supports this, and it is implemented (though not yet released) in NextAuth so it seems like the Edge APIs do have everything needed to support this.

@benevbright
Copy link
Contributor

@dkokotov which proxy library are you using for edge? Glad to hear the successful story. Thanks. 👍

@dkokotov
Copy link

dkokotov commented Jun 8, 2023

@benevbright I just wrote my own, it is not comprehensive, but it covers our use cases (primarily graphql, but some rest api as well).

Below is the edge version - it lives under /pages/api/proxy/[...path].ts. So client code will for example post graphql queries to /api/proxy/graphql and this will forward to /graphql on our backend with the correct auth header.

import { getSession } from '@auth0/nextjs-auth0/edge';

import { NextRequest, NextResponse } from 'next/server';
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';

export default async function handler(req: NextRequest) {
    const backendPath = req.nextUrl.pathname.replace(/^\/api\/proxy/, '');
    const backendPrefix = process.env.API_SERVER_URL_PREFIX;
    const backendUrl = `${backendPrefix}${backendPath}`;
    const nodeProxyUrl = req.url.replace('/api/proxy', '/api/node-proxy');
    const bodyText = req.method == 'GET' || req.method == 'HEAD' ? null : await req.text();

    const fakeResponse = new NextResponse(null);
    const session = await getSession(req, fakeResponse);
    if (!session) {
        return NextResponse.json(
            {
                error: 'not_authenticated',
                description: 'The user does not have an active session or is not authenticated'
            },
            { status: 401 }
        );
    }

    if (session.accessToken && session.accessTokenExpiresAt * 1000 > Date.now()) {
        const backendRequestHeaders = new Headers(req.headers);
        backendRequestHeaders.delete('Cookie');
        backendRequestHeaders.delete('Transfer-Encoding');
        backendRequestHeaders.set('Authorization', `Bearer ${session.accessToken}`);
        const backendResponse = await fetch(backendUrl, {
            body: bodyText,
            method: req.method,
            headers: backendRequestHeaders,
            redirect: 'manual'
        });

        // for unauthorized responses, retry with the node proxy, maybe the access token was bad after all
        if (backendResponse.status != 401) {
            const resp = new NextResponse(backendResponse.body, {
                status: backendResponse.status,
                headers: new Headers(backendResponse.headers)
            });
            // nextjs-auth0 updates the cookie each time we access the session (https://github.com/auth0/nextjs-auth0#caching-and-security)
            // so we want to merge in that set-cookie header set on the fake response passed to getSession.
            // note this will need to be updated once https://github.com/auth0/nextjs-auth0/pull/1236 is released
            const fakeResponseCookies = parseSetCookie(
                splitCookiesString(fakeResponse.headers.get('set-cookie') ?? '')
            );
            fakeResponseCookies.forEach((c) =>
                resp.cookies.set({
                    name: c.name,
                    value: c.value,
                    path: c.path,
                    expires: c.expires,
                    secure: c.secure,
                    httpOnly: c.httpOnly,
                    domain: c.domain,
                    maxAge: c.maxAge,
                    sameSite: c.sameSite as any
                })
            );

            return resp;
        }
    }

    const nodeProxyRequestHeaders = new Headers(req.headers);
    nodeProxyRequestHeaders.delete('Transfer-Encoding');

    const nodeProxyResponse = await fetch(nodeProxyUrl, {
        body: bodyText,
        method: req.method,
        headers: nodeProxyRequestHeaders,
        redirect: 'manual'
    });

    // just forwarding the response doesn't work, because the set-cookie gets munged into a single value
    // instead, need to manually parse it out and then re-set it on a new response.
    const nodeProxyResponseHeaders = new Headers(nodeProxyResponse.headers);
    const nodeProxyResponseCookies = parseSetCookie(
        splitCookiesString(nodeProxyResponseHeaders.get('set-cookie') ?? '')
    );
    nodeProxyResponseHeaders.delete('set-cookie');
    const response = new NextResponse(nodeProxyResponse.body, {
        status: nodeProxyResponse.status,
        headers: nodeProxyResponseHeaders
    });

    nodeProxyResponseCookies.forEach((c) =>
        response.cookies.set({
            name: c.name,
            value: c.value,
            path: c.path,
            expires: c.expires,
            secure: c.secure,
            httpOnly: c.httpOnly,
            domain: c.domain,
            maxAge: c.maxAge,
            sameSite: c.sameSite as any
        })
    );

    return response;
}

export const config = {
    runtime: 'edge'
};

And then there is another node-based proxy at /pages/api/node-proxy/[...path].ts, which is used as fallback in case the access token is expired. Here I just used http-proxy for simplicity:

import { withApiAuthRequired } from '@auth0/nextjs-auth0';
import { getApiAccessToken } from '@cnaught-inc/shared/util-auth0';

import httpProxy from 'http-proxy';

const baseProxyOptions = {
    changeOrigin: true,
    secure: false,
    autoRewrite: false
};

const proxy = httpProxy.createProxyServer();
export const config = {
    api: {
        bodyParser: false,
        externalResolver: true
    }
};

export default withApiAuthRequired(async function process(req, res) {
    const { accessToken } = await getApiAccessToken(req, res);
    req.headers['Authorization'] = `Bearer ${accessToken}`;
    req.headers['cookie'] = '';

    req.url = req.url.replace(/^\/api\/proxy/, '');

    proxy.web(req, res, {
        target: process.env.API_SERVER_URL_PREFIX,
        ...baseProxyOptions
    });
});

@benevbright
Copy link
Contributor

@dkokotov many many thanks! very cool :)

@adamjmcgrath adamjmcgrath mentioned this issue Jun 9, 2023
@nopitown
Copy link

I have this use case as well and it would be nice to have that support as well in the edge. Wondering if there are plans to do this soon.

@adamjmcgrath
Copy link
Contributor

@nopitown see #1257 (comment)

@adamjmcgrath
Copy link
Contributor

👋 Full support for the edge runtime, including getAccessToken has been added to https://github.com/auth0/nextjs-auth0/releases/tag/v3.0.0-beta.3

More info on the Beta can be found here #1235

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

8 participants