Skip to content

Auth Adapter Stack (Code Level)

C4 Level 4 detail of the PSI authentication system. Shows the layered adapter architecture, plugin system, and OIDC provider implementations.

Class Hierarchy

classDiagram
    class AuthAdapter {
        <<interface>>
        +getSession(request) Promise~Session~
        +getUser(userId) Promise~User~
        +getUserByEmail(email) Promise~User~
        +createUser(data) Promise~User~
        +deleteUser(userId) Promise~void~
        +createCustomToken(userId) Promise~string~
        +listUsers() Promise~User[]~
        +importUsers(users) Promise~void~
    }

    class BetterAuthAdapter {
        #betterAuth: BetterAuth
        #dbAdapter: DatabaseAdapter
        +getSession(request)
        +getUser(userId)
        +getUserByEmail(email)
        +createUser(data)
        +deleteUser(userId)
        #initBetterAuth(plugins)
    }

    class FirebaseAuthAdapter {
        -firebaseAdmin: FirebaseAdmin
        +createCustomToken(userId)
        +listUsers()
        +importUsers(users)
        #initBetterAuth(plugins)
    }

    AuthAdapter <|.. BetterAuthAdapter : implements
    BetterAuthAdapter <|-- FirebaseAuthAdapter : extends

    class BetterAuthPlugin {
        <<interface>>
        +id: string
        +init(ctx): void
    }

    class FirebaseIdTokenPlugin {
        +id: "firebase-id-token"
        +init(ctx)
        -verifyIdToken(token) Promise~DecodedToken~
    }

    class GenericOidcPlugin {
        +id: "generic-oidc"
        +init(ctx)
        -verifyJwt(token, jwksUri) Promise~JwtPayload~
        -fetchJwks(uri) Promise~JWKS~
    }

    BetterAuthAdapter --> BetterAuthPlugin : uses plugins
    BetterAuthPlugin <|.. FirebaseIdTokenPlugin
    BetterAuthPlugin <|.. GenericOidcPlugin

Token Verification Chain

sequenceDiagram
    participant Client as PSI Frontend
    participant Router as Hono Router
    participant AuthAdapter as FirebaseAuthAdapter
    participant BetterAuth as better-auth Core
    participant FBPlugin as firebaseIdToken Plugin
    participant OIDCPlugin as genericOidc Plugin
    participant FBAdmin as Firebase Admin SDK
    participant IdP as OIDC Provider JWKS

    Client->>Router: Request with Authorization: Bearer <token>

    Router->>AuthAdapter: getSession(request)
    AuthAdapter->>BetterAuth: Process session request

    BetterAuth->>FBPlugin: Try verify as Firebase ID token
    FBPlugin->>FBAdmin: verifyIdToken(token)

    alt Firebase ID token valid
        FBAdmin-->>FBPlugin: DecodedToken {uid, email, ...}
        FBPlugin-->>BetterAuth: Session {user, isValid: true}
    else Not a Firebase token
        FBAdmin-->>FBPlugin: Error: invalid token
        FBPlugin-->>BetterAuth: Pass to next plugin

        BetterAuth->>OIDCPlugin: Try verify as OIDC JWT
        OIDCPlugin->>IdP: GET /.well-known/jwks.json
        IdP-->>OIDCPlugin: JWKS with public keys
        OIDCPlugin->>OIDCPlugin: Verify JWT signature + claims (via jose)

        alt OIDC JWT valid
            OIDCPlugin-->>BetterAuth: JwtPayload {sub, email, iss, ...}
        else Invalid JWT
            OIDCPlugin-->>BetterAuth: Authentication failed
        end
    end

    BetterAuth-->>AuthAdapter: Session or error
    AuthAdapter-->>Router: User identity or 401

SSO Token Conversion (Full Detail)

sequenceDiagram
    participant User
    participant Frontend as PSI Frontend
    participant IdP as OIDC Provider
    participant Backend as PSI Backend
    participant AuthAdapter as FirebaseAuthAdapter
    participant OIDC as genericOidc Plugin
    participant FBAuth as Firebase Admin
    participant JWKS as Provider JWKS Endpoint

    User->>Frontend: Click SSO login button
    Frontend->>IdP: Redirect to authorization endpoint
    Note over IdP: client_id, redirect_uri,<br/>scope=openid email profile,<br/>response_type=id_token (fragment)<br/>or response_type=code (code mode)

    IdP->>User: Login form
    User->>IdP: Credentials
    IdP->>Frontend: Redirect with id_token (fragment) or code

    alt Code mode (Radio-Canada)
        Frontend->>Backend: POST /api/auth/convertToken {code}
        Backend->>IdP: Exchange code for tokens
        IdP-->>Backend: {id_token, access_token}
    else Fragment mode (ZDF, CBC, RTBF, etc.)
        Frontend->>Backend: POST /api/auth/convertToken {jwt: id_token}
    end

    Backend->>AuthAdapter: convertToken(jwt)
    AuthAdapter->>OIDC: Verify JWT
    OIDC->>JWKS: Fetch public keys
    JWKS-->>OIDC: JWKS
    OIDC->>OIDC: Verify signature, iss, aud, exp

    AuthAdapter->>FBAuth: getOrCreateUser(email, displayName)
    FBAuth-->>AuthAdapter: Firebase user record
    AuthAdapter->>FBAuth: createCustomToken(uid)
    FBAuth-->>AuthAdapter: Custom token

    Backend-->>Frontend: {loginToken: customToken}

    Frontend->>FBAuth: signInWithCustomToken(loginToken)
    FBAuth-->>Frontend: Firebase session (ID token)

    Note over Frontend: All subsequent API calls use<br/>Firebase ID token in Authorization header

OIDC Provider Configurations

graph LR
    subgraph "genericOidc Plugin"
        Verify[JWT Verification]
    end

    subgraph "Provider Implementations"
        ZDF[ZDF Keycloak<br/>psi-login.zdf.de<br/>login.zdf.digital]
        CBC[CBC Azure AD B2C<br/>login.cbc.radio-canada.ca]
        RC[Radio-Canada Azure AD B2C<br/>login.cbc.radio-canada.ca]
        RTBF[RTBF Login<br/>login.rtbf.be]
        NPO[NPO ID<br/>npoid-sandbox.npoid-dev.nl]
        SRG[SRG SSR Account<br/>account-int.srgssr.ch]
    end

    subgraph "Provider Helpers"
        AzureHelper[azure-ad-b2c.ts<br/>B2C tenant config]
        CBCHelper[cbc-rc.ts<br/>CBC/RC-specific config]
    end

    Verify --> ZDF
    Verify --> CBC
    Verify --> RC
    Verify --> RTBF
    Verify --> NPO
    Verify --> SRG

    CBC --> AzureHelper
    RC --> AzureHelper
    AzureHelper --> CBCHelper

Client-Side Auth

classDiagram
    class ClientAuthAdapter {
        <<interface>>
        +getCurrentUser() User | null
        +getIdTokenAsync() Promise~string~
        +signOutAsync() Promise~void~
        +signInWithGoogleAsync() Promise~void~
        +signInWithTokenAsync(token: string) Promise~void~
        +onAuthStateChanged(callback) Unsubscribe
    }

    class FirebaseAuthAdapter {
        -auth: FirebaseAuth
        +getCurrentUser()
        +getIdTokenAsync()
        +signOutAsync()
        +signInWithGoogleAsync()
        +signInWithTokenAsync(token)
        +onAuthStateChanged(callback)
    }

    class NoopAdapter {
        +getCurrentUser() null
        +getIdTokenAsync() ""
        +signOutAsync()
        +signInWithGoogleAsync()
        +signInWithTokenAsync()
        +onAuthStateChanged()
    }

    ClientAuthAdapter <|.. FirebaseAuthAdapter : default
    ClientAuthAdapter <|.. NoopAdapter : testing

    note for FirebaseAuthAdapter "Wraps Firebase Auth SDK.\nSupports Google popup sign-in\nand custom token sign-in (SSO)."

Admin Authorization

graph TD
    A[API Request] --> B{Is adminFunction?}
    B -->|No| C[Execute publicFunction]
    B -->|Yes| D[Check admin role]
    D --> E[Read: silo/siloKey/module/admin/userRoles/email]
    E --> F{Role exists?}
    F -->|Yes: Owner| G[Execute adminFunction]
    F -->|No| H[Return 403 Forbidden]

Further Reading