Skip to main content
Version: 1.1.0

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)

MethodPathPurpose
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/callbackOAuth IdP callback
POST/gateway/magic/validateComplete Magic Link (body includes didToken, optional flowId)
POST/gateway/exchangeTower exchanges short-lived code for API token

Authenticated

MethodPathPurpose
GET/gateway/refreshRefresh OAuth access token
GET/gateway/logoutLogout + IdP redirect
DELETE/gateway/cacheClear OAuth config cache (ops)

Tower registration (no user session — activation token)

MethodPathPurpose
POST/tower/activateRegister Tower origin on companies.towerUrl
POST/tower/heartbeatDetect 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

ActivatePOST /tower/activate:

{
"token": "<company-activation-token>",
"towerUrl": "https://tower.customer.example.com"
}

Response: { companyId, socialReason }. Normalizes URL to bare origin (no trailing slash).

HeartbeatPOST /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

ServiceRole
AuthCodeServiceOne-time code store
GatewayFlowServiceBinds flow to redirectUri, clientState, expected company + Tower URL
AuthCompletionServiceShared OAuth/Magic completion; issues code or token
TowerAuthServiceActivate / heartbeat / token validation
AuthService.findCompanyBranding()Logo lookup
AuthService.findCompanyByTowerUrl()Origin → company for initiate validation

Key files

  • src/domains/gateway/gateway.controller.ts
  • src/domains/gateway/services/auth-code, auth-completion, gateway-flow, oauth, magic-link
  • src/domains/gateway/exceptions/gateway-exception.filter.ts
  • src/domains/tower-auth/tower-auth.controller.ts
  • src/domains/tower-auth/tower-auth.service.ts
  • src/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