Skip to main content
Version: 1.1.0

Gateway Branding, OAuth Code Flow, and Tower Auth

Version: 1.1.0 Date: 2026-07-15

Note: This change shipped without a .unreleased/ fragment in neo-backend; this entry is reconstructed from merge commit d9ca567 (feat/tower-nexus-login) and the gateway/tower-auth implementation.

Goal: Support Nexus and Tower login flows with per-company branding, a secure OAuth-style authorization-code exchange for Tower (without exposing long-lived tokens in redirect URLs), and explicit Tower URL registration with drift detection when an operator moves Tower to a new origin.

Solution: Extend the existing gateway domain with public branding, flow-scoped OAuth/magic completion, and in-memory one-time auth codes. Add a dedicated tower-auth domain for activation and heartbeat. Document the token design space under docs/bcyip-token/.

Features:

Public company branding

  • GET /gateway/branding?company_id= — public route (excluded from auth middleware)
  • Returns logoDataUrl from companies.customization.logo (handles BSON Binary logo payloads)
  • Used by Nexus login UI before the user authenticates

OAuth / Magic login with optional code flow (Tower)

  • GET /gateway/initiate?email=&redirect_uri=&state= — when redirect_uri is present:
    • Validates redirect shape via AuthCompletionService.assertValidRedirectShape
    • Resolves company by Tower origin (findCompanyByTowerUrl) — rejects unknown origins with TowerUrlMismatch
    • Creates an opaque flowId via GatewayFlowService binding redirectUri, clientState, expectedCompanyId, expectedTowerUrl
  • OAuth and Magic Link callbacks complete through AuthCompletionService.complete() — can redirect back to Tower with a short-lived code instead of a raw token in the URL
  • POST /gateway/exchange — Tower exchanges { code, companyId } for { token, email }
    • Codes are 60s TTL, single-use, stored hashed (SHA-256 of code) in AuthCodeService
  • GatewayExceptionFilter maps TowerUrlMismatch to redirect /activate?reason=url_mismatch for browser flows

Tower URL registration and drift

  • POST /tower/activate — validates activation token, registers normalized Tower origin on companies.towerUrl (idempotent re-activation with same URL)
  • POST /tower/heartbeat — compares announced URL to stored URL; returns urlChangePending: true when they differ (silent reject — operator must re-activate explicitly)
  • normalizeTowerUrl() — canonical bare origin (lowercased scheme/host, no trailing slash)

Design documentation

  • New docs/bcyip-token/ folder — use cases (always-on session, Tower ingestion token, platform company session, document read scope, etc.) and authorization model notes

Implementation Details:

  • AuthCodeService — in-memory store, periodic purge, codes never logged in plaintext
  • GatewayFlowService — binds OAuth/magic completion to expected company + Tower URL
  • AuthCompletionService — shared completion path for OAuth callback and magic validate
  • TowerAuthService.resolveCompanyFromToken() — shared token validation for activate/heartbeat
  • TowerAuthModule no longer exports TowerAuthService (no external consumers)

Files Modified:

  • src/domains/gateway/gateway.controller.ts - Branding, initiate with flow, POST /exchange
  • src/domains/gateway/services/auth-code.service.ts - One-time code store (new)
  • src/domains/gateway/services/auth-completion.service.ts - Login completion + redirect (new)
  • src/domains/gateway/services/gateway-flow.service.ts - Flow state for Tower redirect (new)
  • src/domains/gateway/services/oauth.service.ts - Flow-aware OAuth callback
  • src/domains/gateway/services/magic-link.service.ts - Flow-aware magic validation
  • src/domains/gateway/exceptions/gateway-exception.filter.ts - TowerUrlMismatch handling
  • src/domains/tower-auth/tower-auth.controller.ts - Activate + heartbeat routes (new)
  • src/domains/tower-auth/tower-auth.service.ts - URL registration and drift detection (new)
  • src/domains/tower-auth/tower-auth.module.ts - Module wiring (new)
  • src/core/auth/auth.service.ts - findCompanyBranding(), findCompanyByTowerUrl()
  • src/core/error/error-reference.ts - TowerUrlMismatch error type
  • src/app.module.ts - Register TowerAuthModule
  • docs/bcyip-token/README.md and use-case docs (new)

Open questions (see docs/bcyip-token/sso-plan-token-design.md):

  • Legacy opaque API token still issued at end of gateway flow — long-term JWT/refresh model not yet shipped