Different Principal on Each Login with Same Google Account

I’m building an ICP app with Google OAuth and having an issue with principals changing on every login.

When I login with the same Google account, logout, then login again, I get a completely different principal each time. This causes all user data to be lost since it’s tied to the principal.

My approach:

  1. User logs in with Google OAuth, I get their JWT token with a sub field (Google’s unique user ID)
  2. Backend extracts the sub from JWT and derives a deterministic principal using SHA224(DER_encoded_pubkey) + 0x02 suffix
  3. Backend creates a DER-encoded Ed25519 public key from SHA256(sub + origin)
  4. Backend returns both the deterministic principal and the public key to frontend
  5. Frontend creates a delegation chain with this deterministic public key
  6. Frontend calls DelegationIdentity.fromDelegation(randomSessionKey, delegationChain)

The backend consistently returns the same principal for the same user, but the frontend identity.getPrincipal() gives different values each login.

However, when I call identity.getPrincipal(), it returns a different principal every time I login, even though the backend is giving me the same deterministic public key.

My question is: does DelegationIdentity.fromDelegation() derive the principal from the random session key or from the public key in the delegation chain? If it uses the session key, how can I make it use the deterministic public key instead?

For now I’m working around this by using email as the primary identifier instead of principals, but I’d like to understand why the deterministic principal approach isn’t working.

Environment: Internet Computer, Rust backend with ic-cdk, React frontend with @dfinity/agent and @dfinity/identity, Google OAuth 2.0

Relevant code:

  • Backend: src/backend/src/lib.rs (prepare_delegation and get_delegation functions, derive_user_principal and derive_user_pubkey functions)
  • Frontend: src/frontend/utils/oauthDelegation.ts (loginWithOAuth function)

Full code available at: GitHub - aliscie2/weeekaly.com

Here’s an example on how to derive principals from JWTs: GitHub - ilbertt/ic-react-native-jwt-auth: React Native (Expo) + JWT Authentication + Rust ICP canister.

Can this help?

1 Like

The Root Cause

In IC’s delegation model, the principal is derived from whoever signs the request (the session key), not from the delegation chain. The delegation chain only proves authority - it doesn’t determine identity.


The Solution

Changed the backend to use the session principal (from ic_cdk::caller()) instead of deriving a principal from email. This aligns with how DelegationIdentity works in the frontend.


Key Changes Made

Backend (lib.rs)

  • prepare_delegation() - Now uses ic_cdk::caller() to get the session principal and returns it

  • get_delegation() - Returns the session public key as user_canister_pubkey

  • Token storage functions - Simplified to only store tokens for the session principal

  • Removed unused derive_user_principal() and derive_user_pubkey() functions


How It Works Now

  1. Frontend stores/reuses session key in localStorage → Same session key every login

  2. Session key signs requests → Backend receives same session principal via caller()

  3. Backend maps email ↔ session principal for lookups

  4. Frontend creates DelegationIdentity from same session key → Same principal

But still same issue


Backend - Session Principal Storage (lib.rs)

#[update]
async fn prepare_delegation(req: PrepareDelegationRequest) -> Result {
    // Extract email from JWT
    let (user_id, email, name) = verify_jwt_token(&req.id_token)?;
    let email_str = email.clone().ok_or("Email is required")?;
    
    // ✅ USE SESSION PRINCIPAL (from caller)
    let session_principal = ic_cdk::caller();
    
    ic_cdk::println!("🔍 Email: {}", email_str);
    ic_cdk::println!("🔍 Session Principal: {}", session_principal.to_text());
    
    // Store email ↔ session principal mapping
    EMAIL_TO_PRINCIPAL_MAP.with(|m| {
        m.borrow_mut().insert(
            StorableString(email_str.clone()),
            StorableString(session_principal.to_text())
        )
    });
    
    // Return session principal (NOT derived from email)
    Ok(PrepareDelegationResponse {
        expire_at,
        user_principal: session_principal.to_text(),
    })
}


Backend - Delegation Response (lib.rs)

#[query]
fn get_delegation(req: GetDelegationRequest) -> Result {
    let session = SESSIONS.with(|s| {
        s.borrow().get(&req.session_public_key).cloned()
    }).ok_or("Session not found")?;
    
    let delegation = Delegation {
        pubkey: req.session_public_key.clone(),
        expiration: req.expire_at,
        targets: req.targets,
    };
    
    let signature = sign_delegation(&delegation)?;
    
    // ✅ Return session public key (NOT derived key)
    Ok(GetDelegationResponse {
        signed_delegation: SignedDelegation { delegation, signature },
        user_canister_pubkey: req.session_public_key.clone(),
    })
}


Frontend - Session Key Management (oauthDelegation.ts)

// Load or generate session key
let sessionKey: Ed25519KeyIdentity;
const storedSessionKeyJson = localStorage.getItem(STORAGE_KEY_SESSION_KEY);

if (storedSessionKeyJson) {
    // ✅ REUSE existing session key for deterministic principal
    sessionKey = Ed25519KeyIdentity.fromJSON(storedSessionKeyJson);
    console.log("♻️ Reusing session key");
} else {
    // Generate new session key on first login
    sessionKey = Ed25519KeyIdentity.generate();
    localStorage.setItem(STORAGE_KEY_SESSION_KEY, JSON.stringify(sessionKey.toJSON()));
    console.log("🆕 Generated new session key");
}

const sessionPublicKey = sessionKey.getPublicKey().toDer();


Frontend - Identity Creation (oauthDelegation.ts)

// Create delegation chain
const delegationChain = DelegationChain.fromDelegations(
    [{
        delegation: {
            pubkey: Uint8Array.from(signed_delegation.delegation.pubkey),
            expiration: BigInt(signed_delegation.delegation.expiration),
            targets: /* … */
        },
        signature: Uint8Array.from(signed_delegation.signature).buffer
    }],
    Uint8Array.from(user_canister_pubkey).buffer // From backend
);

// ⚠️ Principal is derived from sessionKey, NOT from user_canister_pubkey!
const identity = DelegationIdentity.fromDelegation(sessionKey, delegationChain);

console.log("🔑 Frontend Principal:", identity.getPrincipal().toText());
console.log("🔑 Backend Principal:", user_principal); // Should match!

That is backwards. The purpose of using the delegation is to get the same principal every time, no matter which session key is used.

Reusing the session key won’t work if the user switches devices or clears browsing data (aside from being less secure).
If the delegation chain is constructed correctly, you should not have to do these additional hacks and always get the same principal.

1 Like