TLDR;
When my users sign in, I give them a session cookie. When they attempt to hit a callable function, they get a 403 error stating that the request was unauthenticated.
Authentication
I'm using Firebase + Next.js 13 (with app/
dir). When my users sign in, I issue them a session cookie as described here. That process looks like this
// sign in function (handled client-side)
signInWithEmailAndPassword(auth, data.email, data.password)
.then(async (userCredential) => {
const { user } = userCredential
// Get the user's ID token and send it to the sessionLogin endpoint to set the session cookie
return user.getIdToken()
.then(idToken => {
return fetch('/api/sessionLogin', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ idToken })
})
})
})
As you can see, this process makes a POST request to /api/sessionLogin
, an HTTP function that I'm hosting on Vercel (i.e. not hosted on Firebase). Here's what that looks like
// sessionLogin API endpoint (handled server-side)
import { NextResponse } from "next/server"
import { adminAuth } from "../../../../firebase/firebaseAdmin"
export async function POST(request) {
// Get the ID token passed
const body = await request.json()
const idToken = body.idToken.toString()
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000
return adminAuth
.createSessionCookie(idToken, { expiresIn })
.then(
(sessionCookie) => {
// Instantiate the response
const response = new NextResponse(null, { status: 200, statusText: "OK" })
// Set cookie policy for session cookie
response.cookies.set({
name: "sessionCookie",
value: sessionCookie,
maxAge: expiresIn,
sameSite: "lax",
httpOnly: true,
secure: true,
path: "/"
})
return response
},
(error) => {
const response = new NextResponse(null, { status: 401, statusText: "UNAUTHORIZED REQUEST!" })
return response
}
)
}
Demo:
This part appears to work fine. I can sign in without error and I receive the session cookie.
The session cookie is clearly set, below.
Callable function
Next, my user attempts to run a callable function. This is where the error occurs.
// Header.js (client side header)
// ... (imports & setup code here)
const createPost = httpsCallable(functions, 'createPost')
createPost({ authorId: sessionCookie.uid })
// functions/index.js (firebase cloud functions)
// ... (imports & setup code here)
initializeApp()
setGlobalOptions({ region: "us-central1" })
export const createPost = onCall({ cors: true }, async (request) => {
...
})
^ Notice I'm setting { cors: true }
, so cross-origin requests should be allowed.
Demo
There are two requests. My understanding is that the first request is the "preflight" request which checks if it's safe to send the real request. Notice that both requests fail.
This is the preflight request. It fails with a 403.
This is the actual request. It fails with a CORS error
localhost/:1 Access to fetch at 'https://us-central1-scipress-dev.cloudfunctions.net/createPost' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Lastly, when I check the function logs, I see this.
The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header.
Additional Notes
- This error does not occur when I develop locally with Firebase emulators
- Two ways in which I've deviated from the session cookie docs are
- I do not use a csrfToken
- I do not set the auth persistence to
NONE
, as I want the auth state to be persisted on the client.
Any help debugging this would be greatly appreciated!