Gateway & Tower Auth
Overview
OAuth and Magic Link login gateway for Nexus (web) and Tower (Electron). Issues legacy API tokens after successful IdP authentication. Tower additionally uses an authorization-code exchange so long-lived tokens never appear in browser redirect URLs, plus explicit Tower URL registration with drift detection.
Design reflection (not final spec): neo-backend/docs/bcyip-token/.
Route map
All gateway/tower routes are under /api/v2 unless noted.
Public (no Bearer token)
| Method | Path | Purpose |
|---|---|---|
GET | /gateway/branding?company_id= | Company logo as logoDataUrl (from companies.customization.logo, handles BSON Binary) |
GET | /gateway/initiate?email=&redirect_uri=&state= | Start OAuth or Magic Link flow |
GET | /gateway/callback | OAuth IdP callback |
POST | /gateway/magic/validate | Complete Magic Link (body includes didToken, optional flowId) |
POST | /gateway/exchange | Tower exchanges short-lived code for API token |
Authenticated
| Method | Path | Purpose |
|---|---|---|
GET | /gateway/refresh | Refresh OAuth access token |
GET | /gateway/logout | Logout + IdP redirect |
DELETE | /gateway/cache | Clear OAuth config cache (ops) |
Tower registration (no user session — activation token)
| Method | Path | Purpose |
|---|---|---|
POST | /tower/activate | Register Tower origin on companies.towerUrl |
POST | /tower/heartbeat | Detect URL drift vs stored origin |
Flows
Nexus / browser login (no redirect_uri)
User → GET /gateway/initiate?email=...
→ OAuth or redirect to Nexus (Magic Link)
→ callback / magic/validate
→ legacy API token issued to client
Tower login (with redirect_uri)
If Tower origin is not registered → TowerUrlMismatch → browser redirect /activate?reason=url_mismatch.
Tower URL registration
Activate — POST /tower/activate:
{
"token": "<company-activation-token>",
"towerUrl": "https://tower.customer.example.com"
}
Response: { companyId, socialReason }. Normalizes URL to bare origin (no trailing slash).
Heartbeat — POST /tower/heartbeat (same body shape):
{
"token": "<company-activation-token>",
"towerUrl": "https://tower.customer.example.com"
}
Response when drift detected:
{
"urlChangePending": true,
"currentUrl": "https://old-origin.example.com"
}
BCYIP keeps the stored URL until operator re-activates explicitly.
Code exchange
POST /gateway/exchange:
{
"code": "<base64url-code-from-redirect>",
"companyId": "<mongo-company-id>"
}
Response:
{
"token": "<legacy-api-token-value>",
"email": "user@company.com"
}
Codes: 60s TTL, single-use, stored as SHA-256 hash in memory (AuthCodeService).
Key services
| Service | Role |
|---|---|
AuthCodeService | One-time code store |
GatewayFlowService | Binds flow to redirectUri, clientState, expected company + Tower URL |
AuthCompletionService | Shared OAuth/Magic completion; issues code or token |
TowerAuthService | Activate / heartbeat / token validation |
AuthService.findCompanyBranding() | Logo lookup |
AuthService.findCompanyByTowerUrl() | Origin → company for initiate validation |
Key files
src/domains/gateway/gateway.controller.tssrc/domains/gateway/services/—auth-code,auth-completion,gateway-flow,oauth,magic-linksrc/domains/gateway/exceptions/gateway-exception.filter.tssrc/domains/tower-auth/tower-auth.controller.tssrc/domains/tower-auth/tower-auth.service.tssrc/core/auth/auth.service.ts
Known limitations
- Gateway still issues the legacy opaque API token at end of flow — long-term JWT/refresh model not shipped (see
docs/bcyip-token/sso-plan-token-design.md)
Next steps
- Evolve token model per bcyip-token use cases
- SCIM / multi-provider flows