Think of a hybrid dApp (i.e. sports betting exchange) that uses Internet Computer canisters for on-chain logic (e.g., placing bets) and a traditional backend for heavy data like match history, odds, and analytics—things that are currently impractical to store fully on-chain.
The frontend interacts with both the canisters and the backend. Users sign in with ICP-compatible wallets like Plug or Internet Identity. The question is: how do you authenticate these principals when they make requests to the off-chain backend?
In particular:
How do you reliably and securely get their principal ID on the backend?
A few ideas I’ve explored:
- Frontend signs a message (either per-request or once to get a JWT), and sends it to the backend for verification.
- Problem: Not all wallets expose a signMessage method, so this isn’t universal.
- Use a canister as a trusted signer :
- The frontend calls a canister which verifies the user’s identity and returns a signed statement like “This principal is xxxxx-xx.”
- The frontend sends this signed message to the backend.
- The backend validates the message and signature using the canister’s public key and issues a JWT or session token.This solves the wallet inconsistency issue, but introduces two new problems:
- Problem 1: The canister must be public, or other dApps might abuse it (e.g., spam it or rely on it without cost).
- Problem 2: Secure validation. Symmetric encryption is risky (the key would have to be stored in the canister). Using sign_with_ecdsa + ecdsa_public_key seems better, but comes with cost (problem 1)
So: Is there a recommended or elegant way to authenticate principals off-chain in this kind of hybrid setup?
I’d love to hear how others have tackled this, especially in production apps. Maybe there’s a pattern I’m missing—or a library that simplifies this?
Thanks in advance!
1 Like
Both options are viable but the second option is definitely more straightforward.
How I would implement the second approach:
- Create session with JWT token on web2 backend.
- Call your canister backend through the wallet with the users subject id from the previous step as argument.
- Call your web2 backend to notify the previous step has completed.
- Your web2 backend calls the canister to get the principal registered for the users subject id.
The 2nd step can be restricted to actual valid subject ids by e.g. verifying the incoming JWT signature within the canister.
The 4th step can be restricted to only your web2 backend by checking if the caller principal is on an allowlist.
And yes a standardized sign message method isn’t available yet, it was de-prioritized a while back when the priority was on topics specific to the IC. You can find the current ICRC-32 draft here: GitHub - dfinity/wg-identity-authentication: Repository of the Identity and Wallet Standards Working Group
In case you’re interested in picking this up and would like to discuss the current status and work to be done to get it past the finish line, feel free to schedule a meeting: Newsletter for Identity & Wallet Standards Working Group
1 Like
Thanks. I built this small open-source POC based on your suggestions, in case it helps somebody:
https://github.com/ClainoLabs/ic-identity-certifier
2 Likes
And here’s a second project to reduce all those steps and streamline the process: https://github.com/ClainoLabs/asymmetric-identity-certifier
While AES encryption with a shared key does not protect from potentially malicious node providers, it should be sufficient to make your canister unusable to unauthorized DApps.
I never coded in Rust.
I tried doing it in Azle but it seems broken, and I couldn’t find anything for symmetric encryption in Motoko. So anybody, feel free to let me know or open a PR if you spot something wrong.