awinogrodzki / next-firebase-auth-edge

Next.js Firebase Authentication for Edge and Node.js runtimes. Compatible with latest Next.js features.
https://next-firebase-auth-edge-docs.vercel.app/
MIT License
533 stars 46 forks source link

Updating claims without a network call #268

Open Immortalin opened 1 month ago

Immortalin commented 1 month ago

I am using firebase purely as an authentication provider, with a separate database as a source of truth for user roles. I am trying to set claims in the session cookie on login or session creation, similar to nextauth i.e. without making a separate network call to firebase or updating custom claims on the firebase side.

E.g. https://authjs.dev/guides/role-based-access-control

Is this currently possible? There doesn't seem to be a callback/method to check for initial login on the middleware/server side, and the closest method I found for updating the session cookie refreshNextResponseCookie still involves a call to firebase which I am trying to minimize to reduce latency.

awinogrodzki commented 1 month ago

Hey @Immortalin,

NextAuth and next-firebase-auth-edge are different libraries, each one comes with a trade-off:

NextAuth allows you to manage user data and session using database of your choice.

next-firebase-auth-edge is using Firebase Authentication service as "external database".

similar to nextauth i.e. without making a separate network call to firebase or updating custom claims on the firebase side.

In NextAuth, whenever you update user data, you are making a call to database, which can be distributed or local. You have more control over your infrastructure and if the database is inside internal network, the calls are usually much faster than an external call to Firebase Authentication. The tradeoff here is that you have additional upkeep cost of infrastructure, and more complex configuration.

In next-firebase-auth-edge, you don't need to run your own infrastructure to manage user and session, but the cost is having to call Firebase Authentication services each time you want to make changes to user data or token structure.

You can have the best of both worlds, if you delegate access management to some internal database and skip custom claims. Assuming you have some local DB, like Mongo, you can store user role/permissions against User ID available in the token. The tradeoff here is that you would need to scan the DB/cache every time you need to read user permissions

Immortalin commented 1 month ago

Hi, I am referring specifically to adding claims to the JWT without updating firebase. Would it be possible to do that in next-firebase-auth? I am not referring to database backed sessions here. Nextauth's sign-in callback allows you to update the JWT/session cookie claims without requiring database calls. I am trying to do the same with next-firebase-edge.

awinogrodzki commented 1 month ago

@Immortalin,

I am referring specifically to adding claims to the JWT without updating firebase

I assume you are referring to ID Token here. ID Token is not generated by the library, it's generated and signed by Google. There is no way to update the claims within ID Token, as only Google knows private keys to sign the token against.

next-firebase-auth-edge generates custom token, which is then exchanged for ID Token/Refresh token pair using API call to Google whenever there is a change to token structure.

ID Token needs to be generated by Google, otherwise the token would not be compatible with official Firebase Authentication libraries.

Nextauth's sign-in callback allows you to update the JWT/session cookie claims without requiring database calls.

NextAuth is able to do that, because NextAuth has access to private key the token is signed against.


Here is a documentation of verifying ID token by third party libraries: https://firebase.google.com/docs/auth/admin/verify-id-tokens

Finally, ensure that the ID token was signed by the private key corresponding to the token's kid claim. Grab the public key from https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com and use a JWT library to verify the signature.

The private key Google is referring to is only known by them, hence, they expose public keys to allow library authors to verify the token

Immortalin commented 1 month ago

Ah I see, I thoughtnext-firebase-edge would generate its own signed cookies. I guess I should generate my own additional signed cookies with a JWT if I want stateless RBAC.

Immortalin commented 1 month ago

@awinogrodzki would it be possible to add claims to the cookie issued by this library? I suppose manually setting an authCookie could be sufficient.

awinogrodzki commented 4 weeks ago

The cookie is designed to store id, refresh and custom tokens, without any additional information.

I think I understand your problem correctly: Your source of truth about user roles is an external DB and you want to avoid duplicating the access roles in Firebase. You also want to avoid an unnecessary network call. This is very valid use case, that the library does not yet support.

I would gladly help with this use case. Let's design some solution. Here's some rules that we ought not to break:

  1. We cannot modify the structure of ID Token – it is signed by Firebase services. Ie. if you want to save the claims based on roles that you have in external DB, you need to make the call and keep the roles in sync. If you modify the structure of the ID token on fly, it will be rejected.

  2. We also cannot save access details in custom token for the same reasons – basically, custom token is exchanged for ID token and refresh tokens. Whenever ID token and custom token become expired, we use refresh token to securely extend the session and regenerate both tokens.

  3. Storing roles in Cookies should be done responsibly. We need to include this data in cookie signature, so end-user is not able to change their roles by tampering with their cookies.

I think I could introduce something called session metadata or cookie metadata, where you could store additional data associated with the session. The data would be incorporated into cookie signature for security.

Would similar API to this be simple enough?

//middleware.ts
export async function middleware(request: NextRequest) {
  return authMiddleware(request, {
    //... other options
    getSessionMetadata: async ({decodedToken}) => {
      const roles = await getRoles(decodedToken.uid);

      return {roles};
    }
  });
}

The metadata could be extracted using getTokens function

//page.tsx
import {getTokens} from 'next-firebase-auth-edge';

const {metadata} = await getTokens(cookies(), options);

Are you using enableMultipleCookies option? This functionality could impact the size of a cookie

Immortalin commented 4 weeks ago

Yup that's looks perfect