Skip to Content
Decision RecordsADR-010: Signals OAuth Implicit Flow

ADR-010: Backend vs Frontend Handling of the Signals OAuth Implicit Flow

FieldValue
StatusOpen
Date2026-04-08
Related SADSAD-001
Related ADRADR-002, ADR-003, ADR-006, ADR-008

Context

External Actions require a Signals access token to read/write data back to the ELN. Signals only supports the OAuth implicit grant, which returns the token in the URL fragment (#access_token=...). URL fragments are never sent to the server — they are only accessible to JavaScript running in the browser.

The backend needs this token (to store in ElastiCache and use for API calls), but it cannot receive it directly from the redirect. JavaScript in the browser must extract it and relay it to the backend.

The question is: should the callback page that extracts the token be served by the frontend (React) or the backend (FastAPI HTML response)? Both work, but they have different security implications.

Decision

Pending. The dual OAuth flow itself is not in question — Roche identity (Janus, Authorization Code Grant + PKCE) and Signals API token (implicit grant) are both required. The open decision is whether the Signals OAuth callback page is served by the frontend (React) or the backend (FastAPI HTML response).

The flow (Option A: backend-served callback)

The flow (Option B: frontend-served callback)

Redirect URLs and provisioned client IDs

Based on the modular monolith approach (ADR-006), a single application handles all External Actions under one hostname:

Environment labelSignals tenantRedirect URLClient ID
LOCALroche-orca-signalsnotebookhttp://localhost:3000/auth/signals-callbackclient-989c0537-e804-4516-84b7-f2085d3acbf4
LOCALroche-dia-devhttp://localhost:3000/auth/signals-callbackclient-989c0537-e804-4516-84b7-f2085d3acbf4
xRED DEVroche-orca-signalsnotebookhttps://xred-eln-integration.minerva.sandbox.pcloud.roche.com/auth/signals-callbackclient-989c0537-e804-4516-84b7-f2085d3acbf4
LEM DEVroche-dia-devhttps://xred-eln-integration.minerva.sandbox.pcloud.roche.com/auth/signals-callbackclient-989c0537-e804-4516-84b7-f2085d3acbf4
LEM TESTroche-dia-tst-signalsnotebookhttps://xred-eln-integration.apps.uat.minerva.roche.com/auth/signals-callbackclient-64bf3274-5eb6-49d9-8139-f66e980318c0
LEM TRAININGroche-glbtrnhttps://xred-eln-integration.apps.uat.minerva.roche.com/auth/signals-callbackclient-180fbda6-a5d1-466a-bfb1-32760fa7ae10
LEM PRODroche-dia-signalsnotebookhttps://xred-eln-integration.apps.minerva.roche.com/auth/signals-callbackclient-180fbda6-a5d1-466a-bfb1-32760fa7ae10

These URLs must be registered exactly with Revvity when requesting the Signals Client ID. Provisioned mapping was validated on 2026-04-22.

Client IDs are provisioned per Revvity cluster and shared across environments in the same cluster:

  • signalsresearch: client-989c0537-e804-4516-84b7-f2085d3acbf4
  • srpste3: client-64bf3274-5eb6-49d9-8139-f66e980318c0
  • srpe3: client-180fbda6-a5d1-466a-bfb1-32760fa7ae10

Open dependency:

  • Canonical OAuth authorization endpoint URLs per tenant are not yet documented as explicit URLs in this repository.

The callback page

The /auth/signals-callback route serves a page (either a React route or a FastAPI HTML response) with JavaScript that:

  1. Extracts access_token from window.location.hash
  2. Sends it to the backend via POST /api/auth/token
  3. Redirects the user to the integration UI once the backend confirms storage
// Callback page JS (simplified) const hash = window.location.hash.substring(1); const params = new URLSearchParams(hash); const token = params.get('access_token'); fetch('/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }) }) .then(() => { window.location.href = '/'; });

Token storage

Both tokens are stored in ElastiCache (ADR-003):

TokenSourceTTLUsed for
Roche identity tokenJanus1 hour (refresh token: 30 days)Calls to internal Roche systems
Signals access tokenSignals OAuth30 days (invalidated after 30 days of inactivity)Read/write back to Signals via MuleSoft

A session cookie links the user’s browser to their token pair in ElastiCache.

Rationale

Backend redirect vs frontend relay

The Signals OAuth implicit grant returns the token in the URL fragment (#access_token=...). URL fragments are never sent to the server in HTTP requests — they exist only in the browser. This is a fundamental constraint of the implicit grant (RFC 6749 §4.2).

This means a pure backend callback is not possible. Two approaches exist:

Option A: Backend serves the callback page (FastAPI HTML response)

FastAPI serves a minimal HTML page on /auth/signals-callback with an inline <script> that extracts the token and POSTs it to /api/auth/token.

  • Minimal attack surface — no dependencies loaded on the callback page
  • No frontend routing or build changes needed
  • Backend-only actions (e.g. refresh triggers) don’t require frontend deployment

Option B: Frontend serves the callback page (React route)

A React route handles /auth/signals-callback — a component extracts the token, shows a loading spinner, and POSTs it to /api/auth/token.

  • Better UX — branded spinners, progress indicators, error states
  • Consistent styling with the rest of the application
  • Full React bundle loaded during token extraction (larger attack surface)

Both options require JavaScript in the browser to extract the fragment. However, the security properties differ:

ConcernFrontend callback (React)Backend callback (FastAPI HTML)
Attack surfaceFull React bundle loaded on callback page — all dependencies, polyfills, third-party code presentMinimal HTML page with a single inline script — no dependencies loaded
XSS exposureIf any React dependency has an XSS vulnerability, the token is exposedIsolated page with no dependencies — much smaller XSS surface
CSP controlHarder to lock down CSP when the full app bundle is loadedCan set a strict CSP (no external scripts, no inline except the extraction)
Token lifetime in memoryToken exists in React’s JS context alongside the full app stateToken exists only in the inline script, page navigates away immediately
Third-party scriptsAny analytics, error tracking, or A/B testing loaded by React could see the fragmentNo third-party scripts loaded
Build complexityCallback is part of the frontend build — changes to the app could affect itCallback is a standalone HTML string in FastAPI — decoupled from frontend
User experienceFull React — can show branded spinners, progress indicators, error states with consistent app stylingBare HTML — user sees a brief flash of a minimal page before redirect
Backend-only actionsNeed to deploy frontend changes even if the action has no UI (e.g. a refresh or sync trigger)Just add a backend endpoint — no frontend deployment needed

The trade-off is security vs UX: the backend option minimises what runs in the browser during token extraction (smaller attack surface), while the frontend option provides a polished user experience with consistent app styling.

Two separate OAuth flows are required

Janus authenticates the user’s Roche identity. The Signals API requires a Signals-issued token — a separate credential that authorises API access to the specific Signals tenant. These are two different identity systems that cannot be substituted. Both tokens are needed for External Actions that read from internal systems (Roche token) and write back to Signals (Signals token).

Security Considerations

Token exposure in the browser

The Signals access token is visible in the browser’s URL bar (in the fragment) and in JavaScript memory during the callback. This is inherent to the implicit grant — there is no way to avoid it. Mitigations:

  • The callback page should immediately extract the token and clear the fragment (window.location.hash = '') to minimise the exposure window
  • The callback page should have no third-party scripts, analytics, or external resources that could intercept the fragment

Token in transit (browser → backend)

The token travels from the browser to the backend in a POST /api/auth/token request body. This is over HTTPS (TLS), so it is encrypted in transit. However:

  • The token is in plaintext in the request body — if the backend logs request bodies, the token would appear in logs. The /api/auth/token endpoint must not log the request body.
  • CSRF protection is needed on this endpoint — without it, a malicious page could POST a forged token. Use a CSRF token or verify the Origin/Referer header.

Token storage (ElastiCache)

The Signals token is stored in ElastiCache alongside the Roche identity token. Both are sensitive credentials. See ADR-003 and ADR-009 for the shared Redis concerns (memory pressure, security boundary).

  • ElastiCache encryption-at-rest and encryption-in-transit must be enabled
  • Tokens should have a TTL matching the session duration
  • The session cookie linking the browser to the token pair must be Secure, HttpOnly, and SameSite=Lax (or Strict)

Implicit grant limitations

The implicit grant is considered less secure than the authorization code grant (RFC 6749 §10.16):

  • No refresh tokens — if the token expires (30 days of inactivity), the user must re-authorise
  • Token exposed in URL fragment — visible in browser history and potentially in Referer headers (mitigated by immediate extraction + fragment clearing)
  • No client authentication — the Client ID is public, anyone can initiate the flow (the redirect URI whitelist is the only protection)

These are inherent to Signals only supporting the implicit grant. For the Roche identity leg (Janus), we use the more secure Authorization Code Grant + PKCE.

XSS risk

If the application is vulnerable to XSS, an attacker could inject JavaScript that reads the token from the fragment before the callback page processes it, or intercept the POST to the backend. Standard XSS mitigations apply:

  • Content Security Policy (CSP) headers
  • No inline scripts on the callback page beyond the token extraction
  • Input sanitisation on all user-facing content

Consequences

  • The callback page is a thin relay — minimal code, but it must be maintained and kept free of third-party scripts
  • The /api/auth/token endpoint must not log request bodies
  • CSRF protection required on the token relay endpoint
  • Session cookies must be Secure, HttpOnly, SameSite
  • The implicit grant does not support refresh tokens — token expiry requires re-authorisation
  • Redirect handling remains centralised in one callback path (/auth/signals-callback) with three provisioned Client IDs (one per Revvity cluster), shared across environments in each cluster
  • Adding a new integration does not require a per-integration Client ID or redirect URI
Last updated on