Skip to main content

Bilko Mobile Phase 0 Auth Bridge — Status 2026-06-05

Bilko

null

Mobile
Phase

Update 0 Auth Bridge2026-07-10StatusT1 2026-06-05

Executive summary

Bilko mobile direction is native iPhone + Samsung/Android, with React Native + Expo as the implementation path. The PWA/mobile-web direction is superseded for the companion mobile app.

Phase 0 backend/auth work is implemented locally and targeted tests are green. TheCIAM mobile app buildprovisioning shouldCOMPLETE not(MC be dispatched until the remaining gates below are closed.

What was changed#105193)

Documentation and architecture

Updated or created:

  • docs/mobile/MOBILE-ARCHITECTURE.md
  • Canonical
  • docs/mobile/MOBILE-IMPL-SPEC-PHASE1.md
  • docs/mobile/MOBILE-PRD.md
  • docs/mobile/README.md
  • docs/INDEX.md
  • /Users/makinja/system/specs/bilko-tech-stack.md
  • docs/architecture/ADR-037-BILKO-MOBILE-NATIVE-ENTRA-AUTH.md
  • docs/backend/MOBILE-ENTRA-AUTH-BRIDGE-SPEC.md

Key doc decisions:

  • Native iOS/Androidmobile app isregistration: the916bb9f3-658d-4729-b5a0-64b1f157c8c2 target,"Bilko notMobile PWA.
  • (Native)"
  • React Nativesingle-tenant (AzureADMyOrg), public client, PKCE, redirects com.alai.bilko://auth + Expo is the chosen native cross-platform path.
  • Entra External ID + OIDC Authorization Code + PKCE is the target mobile/customer login model.
  • Phase 1 uses existing msauth.com.alai.bilko:/api/v1/* endpoints; a dedicated /mobile/* BFF is deferred.
  • Mobile refresh must not depend on browser cookies.
  • Email claims from Entra are not trusted for login mapping.

Backend/auth implementation

Added or changed:

  • apps/api/src/main/kotlin/no/alai/bilko/auth/EntraExternalIdService.kt
    • Verifies Microsoft Entra External ID JWTs.
    • Fails closed if issuer/audience/JWKS config is missing.
    • Enforces RS256 and kid.
    • VerifiesStray configuredduplicate issuer31d2eb67 and(invalid audience.AzureADandPersonalMicrosoftAccount audience for CIAM, no API perms) deleted 2026-07-10.
    • ExtractsAdmin consent verified subject(Graph fromoauth2PermissionGrants, subAllPrincipals): bilko-api access_as_user + Graph openid profile offline_access email User.Read.
    • apps/mobile/eas.json: client ID wired in all 3 build profiles; EXPO_PUBLIC_API_URL on Azure stage FQDN. PR 90 squash-merged to main (1cd39b59); ENTRA_SCOPES now includes api://fe39e0f5-513e-40af-93f0-c3ee624df56c/access_as_user, withusePKCE: true.
    • Securion (Parisa) S3: PASS-WITH-FINDINGS; F1 open → live decoded access token must carry oid fallback.
    • +
    email claim — verified in T3 MC #105194 (EAS build + live CIAM login).
  • apps/api/src/main/resources/db/migration/V64__entra_external_identities.sqlP2P
      peer
    • Addspre-verify (Proveo native agent): PASS 7/7. Evidence: entra_external_identities mapping table.
    • Maps verified issuer + subject to existing Bilko user.
    • Adds SECURITY DEFINER functions with fixed SET search_path = public, pg_temp:
      • bilko_auth.find_user_by_entra_identity(text, text)
      • bilko_auth.mark_entra_login(text, text)
  • apps/api/src/main/kotlin/no/alai/bilko/db/AuthUserRepository.kt
    • Adds findByEntraIdentity(issuer, subject)~/system/evidence/105193/.
    • AddsLive probes 2026-07-10: stage API markEntraLogin(issuer,GET subject)/api/v1/health = 200; CIAM OIDC discovery = 200 (authorize/token endpoints resolve).
  • apps/api/src/main/kotlin/no/alai/bilko/auth/AuthService.kt
    • Adds createSessionFromEntraIdToken(idToken).
    • Maps verified Entra identity to active Bilko user/org/role.
    • Issues existing Bilko access + refresh tokens.
  • apps/api/src/main/kotlin/no/alai/bilko/plugins/DI.kt
    • Wires EntraExternalIdService into AuthService.
  • apps/api/src/main/kotlin/no/alai/bilko/routes/AuthRoutes.kt
    • Adds POST /api/v1/auth/entra/session.
    • Adds POST /api/v1/auth/mobile/refresh.
    • Preserves existing web cookie refresh endpoint POST /api/v1/auth/refresh.
    • Hardened bad-body handling by removing printStackTrace() and detailed parser error echo from register bad-body response.

Tests added or extended

  • apps/api/src/test/kotlin/no/alai/bilko/auth/EntraExternalIdServiceTest.kt
    • valid token
    • wrong audience
    • wrong issuer
    • expired token
    • HS256 rejection
    • missing sub/oid
    • missing config fail-closed
  • apps/api/src/test/kotlin/no/alai/bilko/db/AuthUserRepositoryTest.kt
    • Flyway migration execution on disposable PostgreSQL/Testcontainers DB, including V64.
    • mapped Entra identity returns user
    • unmapped identity returns null
    • inactive mapped user returns null
    • soft-deleted mapped user returns null
    • org/role/status assertions
    • markEntraLogin() metadata update
  • apps/api/src/test/kotlin/no/alai/bilko/auth/AuthServiceTest.kt
    • refresh-token rotation/reuse rejection test
  • apps/api/src/test/kotlin/no/alai/bilko/routes/AuthRoutesHttpIntegrationTest.kt
    • /auth/entra/session missing idToken returns 400
    • /auth/entra/session missing Entra config returns 503 CONFIGURATION_ERROR
    • /auth/mobile/refresh missing refreshToken returns 400
    • /auth/mobile/refresh with structurally valid but stale/non-DB refresh JTI returns 401
    • web cookie refresh/logout stale-token regression remains covered

Validation evidence

Evidence files:

  • /Users/makinja/system/evidence/bilko-mobile-doc-review-20260604.md
  • /Users/makinja/system/evidence/bilko-mobile-auth-local-security-audit-20260604.md
  • /Users/makinja/system/evidence/securion-review-check-102962-20260605.md

Commands recorded as green in evidence:

  • ./gradlew compileKotlin --no-daemon → BUILD SUCCESSFUL
  • ./gradlew compileKotlin compileTestKotlin --no-daemon → BUILD SUCCESSFUL
  • ./gradlew test --tests no.alai.bilko.auth.EntraExternalIdServiceTest --no-daemon → BUILD SUCCESSFUL
  • ./gradlew integrationTest --tests no.alai.bilko.db.AuthUserRepositoryTest --no-daemon → BUILD SUCCESSFUL
  • ./gradlew integrationTest --tests no.alai.bilko.auth.AuthServiceTest --no-daemon → BUILD SUCCESSFUL
  • ./gradlew integrationTest --tests no.alai.bilko.routes.AuthRoutesHttpIntegrationTest --no-daemon → BUILD SUCCESSFUL
  • Combined targeted run:
    • ./gradlew compileKotlin compileTestKotlin test --tests no.alai.bilko.auth.EntraExternalIdServiceTest integrationTest --tests no.alai.bilko.db.AuthUserRepositoryTest --tests no.alai.bilko.auth.AuthServiceTest --tests no.alai.bilko.routes.AuthRoutesHttpIntegrationTest --no-daemon → BUILD SUCCESSFUL

Where we stand

Done locally

  • Mobile architecture direction cleaned up and made native-first.
  • Entra External ID bridge implemented.
  • Mobile-safe body refresh endpoint implemented.
  • V64 migration implemented and executed via Testcontainers/Flyway integration test.
  • Targeted local tests passed.
  • Local security audit completed with no critical/high local findings; one route error-handling hygiene issue was fixed.

Still blocked

  • No independent Securion/QA domain PASS has been received.
  • MC #102962 was checked and is still open in /Users/makinja/system/evidence/securion-review-check-102962-20260605.md.
  • Real Entra staging trigger substitutions are provisioned/verified, but the current Azure tenant is AAD (alemalai.onmicrosoft.com), not a separate CIAM customer tenant. Customer-facing CIAM tenant policy/MFA/passwordless configuration remains a later production gate.
  • No live environment/browser/mobile-device verification has been performed for this auth bridge.

Decision

Do not dispatch mobile app build yet.

Next concrete step is to stop waiting for passive task pickup and run/obtain a real independent security/QA review, then provision/test real Entra External ID config. After those pass, dispatch the native React Native + Expo mobile build.

Stage deploy — substitution wiring (MC #102996, 2026-06-05)

infrastructure/gcp/cloudbuild-stage.yaml is now substitution-ready for Entra External ID metadata:

  • Three substitutions added to the substitutions: block: _ENTRA_EXTERNAL_ID_ISSUER, _ENTRA_EXTERNAL_ID_AUDIENCE, _ENTRA_EXTERNAL_ID_JWKS_URL, all defaulting to __UNSET__.
  • A conditional block in the deploy-api-no-traffic step builds ENTRA_ENV_VARS. If all three substitutions are non-__UNSET__ and non-empty, they are appended to --set-env-vars as ;ENTRA_EXTERNAL_ID_ISSUER=...;ENTRA_EXTERNAL_ID_AUDIENCE=...;ENTRA_EXTERNAL_ID_JWKS_URL=.... Otherwise ENTRA_ENV_VARS="" — the env-vars string is unchanged and current stage behaviour is preserved exactly.
  • Values are provisioned in the bilko-stage-auto-deploy trigger as of 2026-06-05:
    • _ENTRA_EXTERNAL_ID_ISSUER=https://login.microsoftonline.com/3454a03f-20b4-4bda-a116-2293c459aecd/v2.0
    • _ENTRA_EXTERNAL_ID_AUDIENCE=95b2a55f-8f48-4c9c-b4f8-eb455e3bdfd7
    • _ENTRA_EXTERNAL_ID_JWKS_URL=https://login.microsoftonline.com/3454a03f-20b4-4bda-a116-2293c459aecd/discovery/v2.0/keys
  • No stage smoke has been run against Entra yet. Stage will only receive these env vars after the committed Cloud Build wiring is pushed/merged and the trigger deploys a new API revision.

Immediate next actions

  1. Force active review path for MC #102962 or run a direct independent Securion/QA validation with evidence.
  2. Push/merge the clean Phase 0 branch through the gated path.
  3. Trigger a stage build and confirm MC#102996: Entra metadata present log line appears and the three vars are visible in the Cloud Run revision env.
  4. Run /api/v1/auth/entra/session against a real Entra test tenant token.
  5. Run /api/v1/auth/mobile/refresh with real issued mobile refresh token.
  6. Close Securion MC #102989 with passing evidence.
  7. Only after PASS evidence, dispatch native mobile implementation.