Reputation: 21685
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.
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
}
)
}
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.
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.
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.
NONE
, as I want the auth state to be persisted on the client.Any help debugging this would be greatly appreciated!
Upvotes: 1
Views: 260
Reputation: 21685
Not the solution I was looking for, but I did find a viable workaround. The idea is to refactor my onCall()
functions into onRequest()
functions and handle the authorization mechanism myself.
So, my createPost()
function goes from this
// BEFORE
// functions/index.js (firebase cloud functions)
// ... (imports & setup code here)
export const createPost = onCall({ cors: true }, async (request) => {
...
})
to this
// AFTER
// functions/index.js (firebase cloud functions)
// ... (imports & setup code here)
export const createPost = onRequest({ cors: true }, async (req, res) => {
const tokenId = req.get('Authorization').split('Bearer ')[1];
return getAuth().verifyIdToken(tokenId)
.then((decoded) => {
...
res.status(200).send(decoded)
})
.catch((err) => res.status(401).send(err))
})
And the way I invoke it from the client goes from this
// BEFORE
// Header.js (client side header)
// ... (imports & setup code here)
const createPost = httpsCallable(functions, 'createPost')
createPost({ authorId: sessionCookie.uid })
to this
// AFTER
// Header.js (client side header)
// ... (imports & setup code here)
auth.currentUser.getIdToken()
.then(token => {
return fetch(
'https://us-central1-scipress-dev.cloudfunctions.net/createPost',
{
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token
},
method: 'post',
body: JSON.stringify({ data: "just a test" })
})
})
Upvotes: 0