# 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.md`
- `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/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`, with `oid` fallback.
- `apps/api/src/main/resources/db/migration/V64__entra_external_identities.sql`
  - Adds `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)`.
  - Adds `markEntraLogin(issuer, subject)`.
- `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.