This is a Proof-of-Concept React Native Expo mobile application that uses JSON Web Tokens to authenticate to an Internet Computer canister.
The main idea behind this PoC is to have an off-chain OpenID authentication service (Auth0 in this case) that mints a JWT that the user can send to the canister to generate a delegated identity. This way, the user can identify themselves on the canister with the same principal across sessions. It offers a trade-off between ease of use for the end user and decentralization of the authentication process.
This PoC has the following components:
- React Native Expo mobile app: src/app
- off-chain TypeScript backend: src/app_backend
- IC Rust backend canister: src/ic_backend
- Auth0 authentication provider
Head over to the How it works section for more details.
-
Bun Javascript runtime
-
Rust with the
wasm32-unknown-unknown
target:rustup target add wasm32-unknown-unknown
-
dfx (better if installed with the dfx version manager -
dfxvm
) -
an Auth0 account
-
an Android/iOS device or simulator
Follow these steps to configure Auth0:
-
Create a Tenant and get your Auth0 Tenant domain, which looks like
<TENANT_NAME>.<TENANT_REGION>.auth0.com
-
In the Dashboard > Applications > YOUR_APP > Settings tab, set the Allowed Callback URLs and Allowed Logout URLs to:
io.icp0.jwtauthdemo.auth0://<YOUR_AUTH0_TENANT_DOMAIN>/ios/io.icp0.jwtauthdemo/callback
io.icp0.jwtauthdemo.auth0://<YOUR_AUTH0_TENANT_DOMAIN>/android/io.icp0.jwtauthdemo/callback
Where
<YOUR_AUTH0_TENANT_DOMAIN>
is the Auth0 Tenant domain andio.icp0.jwtauthdemo
is both the Android Package Name and iOS Bundle Identifier, as configured in the app.config.js file. -
In the Dashboard > Applications > YOUR_APP > Credentials tab, set the Authentication Method to None (instead of Client Secret (Post))
The 1st step of the Auth0 React Native Quickstart interactive guide can be helpful too.
Install the dependencies:
bun install
Copy the .env.example
file to .env
:
cp .env.example .env
and replace the values with your own.
Start the IC backend:
# in terminal 1
bun start:dfx
# in terminal 2
bun deploy:ic_backend
Start the off-chain backend:
# in terminal 3
bun start:app_backend
Start the mobile app:
# in terminal 4
cd src/app
cp ../../.env .env
bun expo prebuild
cd ../..
# if you want to start the app for Android
bun start:android
# if you want to start the app for iOS
bun start:ios
You may need to manually start the Android/iOS emulator.
See the expo start
CLI docs for more information.
Unit tests are available for the IC Rust backend canister. Simply run:
./scripts/unit-test.sh
Integration tests are available for the IC Rust backend canister. Simply run:
./scripts/integration-test.sh
This PoC is highly inspired by this discussion on the Internet Computer forum.
sequenceDiagram
participant M as Mobile app
participant A as Authentication Provider
participant C as Canister
participant B as Off-chain Backend
note over A,B: JWK data is shared
note over M: Generates session PK/SK
M->>+A: Request id_token with PK
A->>B: Request new/existing user
B->>A: User confirmed
A->>-M: Valid id_token(PK)
M->>+C: Request prepare_delegation with id_token(PK) signed with SK
note over C: Validate id_token against JWK
C-->>C: Extract sub claim from id_token
C-->>C: Create a canister signature and store in delegation map
C-->>C: Derive principal from canister signature
C-->>C: Assign principal to sub in users map
C->>-M: user_key, expiration
M->>+C: Request get_delegation with id_token(PK), expiration signed with SK
note over C: Validate id_token against JWK
C-->>C: Read delegation from delegation map
C->>-M: Signed Delegation
note over M: Session PK/SK is now delegated
note over M: User is authenticated
M-->>C: Request authenticated with delegation signed with SK
note over C: User identified by delegation principal
C-->>C: Read sub from users map
C-->>M: Confirm authentication
M-->>B: Generic request with ic_token
note over B: User identified by sub claim
B-->>M: ...
The main steps are:
-
The JWKs must be fetched from Auth0 and stored in the canister and off-chain backend.
In the current implementation, the canister fetches them once on deployment and every 1 hour using the HTTPS outcalls and Timers features.
-
The mobile app generates a new session PK/SK pair;
-
The mobile app requests an
id_token
from the authentication provider, setting thenonce
claim to the session PK (encoded as a hex string); -
The authentication provider creates the new user or fetches the existing user on the off-chain backend/database, then mints a valid
id_token
that contains thenonce
claim as requested; -
The mobile app sends an update call to the
prepare_delegation
method of the canister, with theid_token
as argument. This update call is signed with the session PK/SK pair; -
The canister performs the following operations:
a. Validates the
id_token
against the JWK and by verifies that:- it was issued by the JWKs fetched from Auth0
- it is not expired (
exp
claim) - it was not issued more than 10 minutes ago (
iat
claim) - the issuer is the expected Auth0 tenant (
iss
claim) - the audience is the expected Auth0 application id (
aud
claim) - the session self-authenticating principal derived from the session PK is equal to the caller (
nonce
claim)
b. Extracts the
sub
andnonce
claims from theid_token
c. Hashes the
sub
andnonce
claims together with a randomsalt
. The DER-encoding of this hash is theuser_key
d. Creates a canister signature for the
user_key
and stores it in thedelegation
mape. Derives a self-authenticating principal from the
user_key
. This is the principal with which the mobile app will authenticate to the canister and will be the same across sessions and canister upgrades.f. Assigns the principal to the
sub
claim in theusers
map. This map is used to retrieve the usersub
claim in all the methods that the user will use after completing the authentication flow, see step 7.If all these steps succeed, the canister returns the
user_key
and the expiration of the delegation, which is set to theexp
claim of theid_token
. -
The mobile app sends a query call to the
get_delegation
method of the canister, with theid_token
andexpiration
as arguments. This query call is signed with the session PK/SK pair.This method performs the same validation on the
id_token
as in the previous step and returns the delegation along with the canister signature. -
The mobile app can now create a delegated identity, with which it can send subsequent requests to the canister, for example to the
authenticated
method.The
authenticated
method is just a demo method to show that the user is authenticated with the delegation obtained from the canister and itssub
claim can be retrieved from theusers
map. -
The mobile app can also send authenticated requests to the off-chain backend, which can identify the user by the
sub
claim as well.
The canister_sig_util crate from the Internet Identity source code is used as an helper for the signatures map.
-
On the canister, periodically fetch the JSON Web Key Sets (JWKS) from Auth0 using the HTTPS outcalls and Timers features.
Right now, the JWKS are fetched at build time by the build-canister.sh script, stored in
data/jwks.json
and imported in the canister as raw bytes at compile time (source).Fetching the JWKS at runtime is needed because JWKs on Auth0 may rotate.
Related issue: #1.
-
Integration tests
Related PRs:
MIT License. See LICENSE.