Bilko Mobile Phase 0 Auth Bridge — Status 2026-06-05
Bilko Mobile Phase 0 Auth Bridge — Status 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. The mobile app build should not be dispatched until the remaining gates below are closed.
What was changed
Documentation and architecture
Updated or created:
docs/mobile/MOBILE-ARCHITECTURE.mddocs/mobile/MOBILE-IMPL-SPEC-PHASE1.mddocs/mobile/MOBILE-PRD.mddocs/mobile/README.mddocs/INDEX.md/Users/makinja/system/specs/bilko-tech-stack.mddocs/architecture/ADR-037-BILKO-MOBILE-NATIVE-ENTRA-AUTH.mddocs/backend/MOBILE-ENTRA-AUTH-BRIDGE-SPEC.md
Key doc decisions:
- Native iOS/Android app is the target, not PWA.
- React Native + 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
/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. - Verifies configured issuer and audience.
- Extracts verified subject from
sub, withoidfallback.
apps/api/src/main/resources/db/migration/V64__entra_external_identities.sql- Adds
entra_external_identitiesmapping table. - Maps verified
issuer + subjectto 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)
- Adds
apps/api/src/main/kotlin/no/alai/bilko/db/AuthUserRepository.kt- Adds
findByEntraIdentity(issuer, subject). - Adds
markEntraLogin(issuer, subject).
- Adds
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.
- Adds
apps/api/src/main/kotlin/no/alai/bilko/plugins/DI.kt- Wires
EntraExternalIdServiceintoAuthService.
- Wires
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.
- Adds
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/sessionmissingidTokenreturns 400/auth/entra/sessionmissing Entra config returns 503CONFIGURATION_ERROR/auth/mobile/refreshmissingrefreshTokenreturns 400/auth/mobile/refreshwith 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
#102962was 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-trafficstep buildsENTRA_ENV_VARS. If all three substitutions are non-__UNSET__and non-empty, they are appended to--set-env-varsas;ENTRA_EXTERNAL_ID_ISSUER=...;ENTRA_EXTERNAL_ID_AUDIENCE=...;ENTRA_EXTERNAL_ID_JWKS_URL=.... OtherwiseENTRA_ENV_VARS=""— the env-vars string is unchanged and current stage behaviour is preserved exactly. - Values are provisioned in the
bilko-stage-auto-deploytrigger 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
- Force active review path for MC
#102962or run a direct independent Securion/QA validation with evidence. - Push/merge the clean Phase 0 branch through the gated path.
- Trigger a stage build and confirm
MC#102996: Entra metadata presentlog line appears and the three vars are visible in the Cloud Run revision env. - Run
/api/v1/auth/entra/sessionagainst a real Entra test tenant token. - Run
/api/v1/auth/mobile/refreshwith real issued mobile refresh token. - Close Securion MC
#102989with passing evidence. - Only after PASS evidence, dispatch native mobile implementation.
No comments to display
No comments to display