Alternate User Authentication method in Canister

OK I’ve been thinking about this today. For reference, I’m building a native mobile app (in React Native) that talks to an IC canister backend.

Can someone tell me whether this would work, i.e. is secure? It’s a traditional email-password login flow instead of the Internet Identity flow.

  1. End user enters email and password in mobile app, and submits.
  2. Mobile app tries logging in using Firebase Auth (via their React Native SDK). If credentials are accepted, Firebase Auth returns a JWT ID token.
  3. Mobile app calls a login method on a custom IC canister, passing both the JWT and the Firebase Auth public key as arguments [1]. The identity (containing the principal) used to authenticate the request will be generated by the mobile app using Ed25519KeyIdentity.generate() and saved in local (mobile) storage.
  4. The canister validates the signature on the JWT using the Firebase public key [2], and then retrieves the uid (i.e. unique ID for the user). It saves the mapping between the principal in the request and the uid in some Map variable.
  5. Whenever the mobile app wants to do something on behalf of the logged in user, it no longer needs to pass the JWT and simply makes the request to the canister with the same identity/principal it generated in step 3. Basically, it’s a session ID.
  6. When the end user logs out, the mobile app calls signOut using the Firebase SDK. It then calls some logout method on the canister, which deletes the mapping from principal to uid. Lastly, it removes the identity from local storage.

The alternative is to completely do away with all IC identities/principals, use AnonymousIdentity for all canister-bound requests, and pass the JWT ID token to authenticate every should-be-authenticated request. The benefit is that it’s stateless, since the canister no longer needs to store any mapping and therefore we can get rid of the login and logout canister methods. Also, the Firebase SDK will automatically handle persisting tokens to local storage.

The drawback is that if someone happens to steal the ID token (even over HTTPS or some other way), then they have complete access to that end user’s info (until the token expires), whereas stealing a principal doesn’t matter if you can’t steal the associated private key. All they could do is replay the exact same request. Also, there’s probably a slightly performance drawback with validating JWT on every request—but on the other hand, you don’t need to validate the sender_sig on every request since we are using the anonymous principal.

I’m actually leaning towards the alternative option… Please tell me if I’m thinking about this correctly or am missing an obvious security risk.

[1] The public key is deliberately passed to the canister, because otherwise it would need to rely on an oracle to fetch the public key from an external Firebase URL. Instead, the mobile app client can fetch it.
[2] There’s an open-source Rust SDK for JWTs, but none for Motoko.

3 Likes