Closed zatheg closed 1 month ago
Hey @zatheg! Thanks for reporting.
Could you enable debug mode and share the logs here?
Hi @awinogrodzki ,
Thank you for your prompt response.
I have enabled debug mode as you requested. Here are the logs:
ⓘ next-firebase-auth-edge: getTokens: Tokens successfully extracted from cookies
ⓘ next-firebase-auth-edge: getTokens: Cookies are marked as verified. Skipping verification
ⓘ next-firebase-auth-edge: getTokens: took 0.021ms
ⓘ next-firebase-auth-edge: getTokens: Tokens successfully extracted from cookies
ⓘ next-firebase-auth-edge: getTokens: Cookies are marked as verified. Skipping verification
ⓘ next-firebase-auth-edge: getTokens: took 0.016ms
ⓘ next-firebase-auth-edge: Missing authentication cookies
ⓘ next-firebase-auth-edge: Token is missing or has incorrect formatting. This is expected and usually means that user has not yet logged in
reason: MISSING_CREDENTIALS
ⓘ next-firebase-auth-edge: getTokens: took 0.001ms
Error getting thread messages Error: Unauthorized
at AuthCheck.getTokens (webpack-internal:///(rsc)/./src/lib/helpers/auth-check.ts:19:19)
at async AuthCheck.protect (webpack-internal:///(rsc)/./src/lib/helpers/auth-check.ts:37:24)
at async GET (webpack-internal:///(rsc)/./src/app/api/threads/[threadId]/messages/route.ts:60:25)
// ...rest of the stack trace
Additional Information:
getTokens()
is initially able to extract tokens from the cookies, but then reports missing authentication cookies.getTokens()
function and the AuthCheck
class work correctly in server actions and server components.Thanks for the details @zatheg.
That is peculiar indeed. getTokens
should never return undefined
. It should return null
when the token is missing.
Let's try some experiments to pin-point the place where the code breaks.
In API route handler, just before calling getTokens
, could you call cookies().get(serverConfig.cookieName)
and share the value? You can use https://jwt.io
to decode the cookie and obfuscate vulnerable data. I am interested in token structure
Hi @awinogrodzki ,
Following your instructions, I added a call to cookies().get(serverConfig.cookieName)
just before invoking getTokens()
in my API route handler. Here are the results:
In the API route handler, the cookie retrieval returns undefined
:
// route.ts
import { getTokens } from "next-firebase-auth-edge";
import { cookies } from "next/headers";
import { serverConfig } from "@/lib/firebase/config";
export async function GET(
request: NextRequest,
{ params }: { params: { threadId: string } }
) {
try {
// Retrieve the cookie value
const authCookie = cookies().get(serverConfig.cookieName);
console.log("Auth Cookie in API Route:", authCookie); // Outputs: undefined
const tokens = await AuthCheck.protect();
// ...rest of the code
} catch (error: any) {
console.error("Error getting thread messages", error);
return NextResponse.json(
{
error: "Error verifying assistant access",
code: "verify_assistant_access_error",
authorized: false,
},
{ status: 500 }
);
}
}
Output:
Auth Cookie in API Route: undefined
In contrast, within a server component, the cookie is successfully retrieved and contains a JWT token:
Decoded JWT Structure:
I used jwt.io to decode the JWT token obtained from the server component. Here's the obfuscated structure:
{
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...[rest of the token]...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...[rest of the token]..."
}
Thank you for your support!
Thanks @zatheg!
const authCookie = cookies().get(serverConfig.cookieName); console.log("Auth Cookie in API Route:", authCookie); // Outputs: undefined
This is interesting. It seems API Route does not have access to cookies. We are getting closer to the resolution.
Could you confirm that serverConfig.cookieName
is the same as in server component?
Also, could you share your middleware.ts
file?
Last question: is API route called under the same domain as the rest of the app?
Hi @awinogrodzki ,
Thank you for your continued assistance.
serverConfig.cookieName
Yes, I have confirmed that serverConfig.cookieName
is identical in both the server components and the API route handler. This ensures consistency in how the cookie is referenced across different parts of the application.
middleware.ts
)Here is my current middleware.ts
file:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import {
authMiddleware,
redirectToHome,
redirectToLogin,
} from "next-firebase-auth-edge";
import { clientConfig, serverConfig } from "./lib/firebase/config";
async function getEstablishmentSlugFromEstablishmentId(
establishmentId: string
): Promise<string> {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/establishments/slug/${establishmentId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const data = await res.json();
return data.slug;
}
const PUBLIC_PATHS = [
"/",
"/assistants",
"/assistant",
"/assistant/*",
"/assistants/*",
"/auth",
"/auth/*",
"/pricing",
"/pricing/*",
"/features",
"/features/*",
"/contact",
"/contact/*",
"/about",
"/about/*",
"/terms",
"/terms/*",
"/privacy",
"/privacy/*",
"/404",
"/500",
];
const isPublicPath = (path: string) => {
const regexPublicPaths = PUBLIC_PATHS.map((publicPath) => {
if (publicPath.includes("*")) {
return new RegExp(`^${publicPath.replace("*", ".*")}$`);
}
return new RegExp(`^${publicPath}$`);
});
return (
regexPublicPaths.some((regex) => regex.test(path)) ||
/^\/assistant\/[^/]+$/.test(path)
);
};
export async function middleware(request: NextRequest) {
const requestPath = request.nextUrl.pathname;
return authMiddleware(request, {
loginPath: "/api/login",
logoutPath: "/api/logout",
refreshTokenPath: "/api/refresh-token",
apiKey: clientConfig.apiKey,
cookieName: serverConfig.cookieName,
cookieSignatureKeys: serverConfig.cookieSignatureKeys,
cookieSerializeOptions: serverConfig.cookieSerializeOptions,
serviceAccount: serverConfig.serviceAccount,
handleValidToken: async ({ token, decodedToken }, headers) => {
if (isPublicPath(requestPath)) {
if (requestPath.startsWith("/auth")) {
const url = request.nextUrl.clone();
// if the user is logged in and tries to access the login page, redirect to the appropriate page
if (decodedToken.role === "global-admin") {
url.pathname = "/admin";
return NextResponse.redirect(url);
} else {
//get the slug of the establishment the user is associated with
const establishmentSlug =
await getEstablishmentSlugFromEstablishmentId(
decodedToken.establishmentId as string
);
url.pathname = `/establishment/${establishmentSlug}/chat`;
return NextResponse.redirect(url);
}
}
return NextResponse.next({
request: {
headers,
},
});
}
//Is on a private page
else {
const url = request.nextUrl.clone();
//Global admin have access to all pages so they can continue
if (decodedToken.role === "global-admin") {
return NextResponse.next({
request: {
headers,
},
});
}
//get the slug of the establishment the user is associated with
const establishmentSlug = await getEstablishmentSlugFromEstablishmentId(
decodedToken.establishmentId as string
);
//If the user tries to access an establishment page that is not his, replace the slug with his establishment slug
if (requestPath.includes("/establishment/")) {
const establishmentSlugFromPath = requestPath.split("/")[2];
if (establishmentSlugFromPath !== establishmentSlug) {
url.pathname = requestPath.replace(
establishmentSlugFromPath,
establishmentSlug
);
return NextResponse.redirect(url);
}
}
//if a non establishment admin tries to access an establishment dashboard, redirect to the chat page (which is the only page he has access to)
if (
requestPath.includes(
`/establishment/${establishmentSlug}/dashboard`
) &&
decodedToken.role !== "admin"
) {
url.pathname = `/establishment/${establishmentSlug}/chat`;
return NextResponse.redirect(url);
}
//Check if the user has completed the sign up process, if he is the only admin of the establishment, it means he needs to complete the onboarding process instead
if (
decodedToken.role !== "global-admin" &&
!decodedToken.hasCompletedSignUp
) {
console.log("User has not completed sign up process", decodedToken);
// if the user is not a global admin and has not completed the sign up process, redirect to the complete sign up page
url.pathname = "/auth/complete-signup";
return NextResponse.redirect(url);
}
}
return NextResponse.next({
request: {
headers,
},
});
},
handleInvalidToken: async (reason) => {
if (isPublicPath(requestPath)) {
return NextResponse.next();
}
return redirectToLogin(request, {
path: "/auth",
publicPaths: PUBLIC_PATHS,
});
},
handleError: async (_) => {
if (isPublicPath(requestPath)) {
return NextResponse.next();
}
return redirectToLogin(request, {
path: "/auth",
publicPaths: PUBLIC_PATHS,
});
},
});
}
export const config = {
matcher: [
"/",
"/((?!_next|favicon.ico|__/auth|__/firebase|api|.*\\.).*)",
"/api/login",
"/api/logout",
"/api/refresh-token",
],
};
Yes, the API route is called under the same domain as the rest of the application. There are no cross-domain requests involved, and all interactions occur within the same origin.
@zatheg I think I know where the error comes from.
In API route handler, you should use cookies
from inside incoming request:
export async function GET(request: NextRequest) {
const tokens = await getTokens(request.cookies, serverConfig);
}
You're passing a result of calling cookies()
imported from next/headers
, which is meant to be used in Server Components and Server Actions.
Let me know if that works!
I apologize, as I might've caused a confusion. I've noticed that the docs about library usage in API route handlers have the same mistake. I will fix the issue immediately!
Thank you for creating an issue!
Hi again @awinogrodzki ,
Thank you for the suggestion! I updated the API route handler to use request.cookies
directly as you mentioned, but I'm still encountering issues.
Here are the details:
API Route Code:
export async function GET(request: NextRequest, { params }: { params: { threadId: string } }) {
try {
console.log("Cookies", request.cookies); // Logs request.cookies
const tokens = await getTokens(request.cookies, {
debug: true,
...(serverConfig as any),
});
if (!tokens) {
console.error("No ID token provided");
return NextResponse.json(
{
error: "You must be authenticated to access this assistant",
code: "unauthenticated",
authorized: false,
},
{ status: 401 }
);
}
// Continue processing...
} catch (error: any) {
console.error("Error getting thread messages", error);
return NextResponse.json(
{
error: "Error getting thread messages",
code: "verify_assistant_access_error",
authorized: false,
},
{ status: 500 }
);
}
}
Printed Cookies in the API Route:
Cookies RequestCookies {
_parsed: Map(0) {},
_headers: Headers {
accept: '*/*',
'accept-encoding': 'gzip, deflate',
'accept-language': '*',
'cache-control': 'no-cache',
connection: 'keep-alive',
'content-type': 'application/json',
host: 'localhost:3000',
pragma: 'no-cache',
'sec-fetch-mode': 'cors',
'user-agent': 'node',
'x-forwarded-for': '::1',
'x-forwarded-host': 'localhost:3000',
'x-forwarded-port': '3000',
'x-forwarded-proto': 'http'
}
}
When I attempt to log the specific cookie with:
console.log("Cookies", request.cookies.get(serverConfig.cookieName));
It prints undefined
.
Additional Context:
request.cookies.get(serverConfig.cookieName)
returns undefined
.request.cookies
object don't seem to contain any actual cookies, just request-related headers like accept
, user-agent
, etc.I'm not sure why the cookies are not accessible in the API route, but it seems like the request is not including the cookie in the headers.
Good morning @zatheg!
In such case, I think the root-cause for the issue might lie within the structure of a code. See this:
https://github.com/vercel/next.js/issues/52209#issuecomment-1741833812
Is your API route called from the client-side (browser), or do you call it from inside the Middleware or Server Component?
I see that the user-agent of request to API route is node:
'user-agent': 'node',
You could try to forward the cookie header to the API route request, if the preceding request is made from the browser.
Eg. if you call your API route from inside Server Component, you could do something similar to this:
import {headers} from 'next/headers';
await fetch('/api/some-route', ({
headers: {
cookie: headers().get('cookie')
}
}))
I never tested it, but it should work fine unless Next.js is deliberately stripping cookie header on such requests, which I doubt
Thank you so much for your support. This is a working solution.
Awesome! Thanks for confirming! I will close the issue as it seems resolved.
Cheers!
I'm encountering an issue with
next-firebase-auth-edge
version 1.7.1 when trying to usegetTokens()
in an API route handler on Next.js 14. Despite following the documentation here, I'm getting an "Unauthorized" error becausegetTokens()
returnsundefined
.Code Snippets:
Here's the relevant part of my code:
Error Message:
Additional Information:
serverConfig
is correctly set up.getTokens()
returnsundefined
, leading to the "Unauthorized" error.getTokens()
function and theAuthCheck
class work correctly in server actions and server components, but not in API routes.Steps to Reproduce:
next-firebase-auth-edge
version 1.7.1 and configure it as per the documentation.getTokens()
in the API route.getTokens()
returnsundefined
and an "Unauthorized" error is thrown.Expected Behavior:
getTokens()
should return the authentication tokens extracted from the cookies, allowing the API route to authenticate the user successfully.Actual Behavior:
getTokens()
returnsundefined
, causing an "Unauthorized" error to be thrown in the API route handler.Environment:
Possible Cause:
It seems like
getTokens()
might not be working as expected in API route handlers in Next.js 14, even though the documentation indicates that it should. The fact that it works in server actions and server components suggests that there might be an issue specific to API routes.Request for Assistance:
Is this a known issue with
next-firebase-auth-edge
? Are there any workarounds or fixes available to getgetTokens()
working in API route handlers in Next.js 14?Note: Any guidance or assistance would be greatly appreciated.