Skip to content

Authentication

The platform is the identity provider; no service builds a parallel identity path.

Updated View as Markdown

The platform is the identity provider. No service creates a separate identity path. The Security Team must review changes involving authentication, authorisation, cryptography, personal data, or a customer-facing API.

Flow Used for
Authorization code with PKCE Our own browser, mobile and desktop clients, public clients that ship no secret
Authorization code with client secret Third-party server-side integrations, where the partner’s backend exchanges the code
JWT bearer grant (RFC 7523) Partner SSO, where a trusted partner asserts a user’s identity
Short grant code Single-use handoff between our own surfaces
OIDC relying party Accepting a partner identity provider; we validate their token and issue our own
  • Never ask a user for their NinjaTrader password. Delegated access is the purpose of OAuth.
  • Keep client secrets only in a backend. Anything sent to a user is public.
  • Register redirect URIs and match them exactly. Authorization codes are single-use and short-lived.
  • Access tokens are short-lived, and refresh tokens rotate. The client handles refreshing. A resource server returns 401 invalid_token and nothing else.
  • Never log a token. Remove query strings before sending anything to an error tracker or analytics platform. Auth flows put exchangeable secrets there, and redaction cannot remove data from inside a URL. See observability for the redaction pipeline.

Our hardening follows the OAuth 2.0 Security Best Current Practice (RFC 9700). Native clients follow OAuth 2.0 for Native Apps (RFC 8252): use the system browser, PKCE, and a registered redirect URI.

Starting a flow: state, nonce and PKCE

Every authorization-code flow uses an unguessable state. Generate it for each request and store it server-side or in a signed cookie. On the callback, reject requests whose returned state does not match the stored value, then discard it. This prevents CSRF, and the comparison uses constant time.

sequenceDiagram
participant U as User agent
participant C as Client
participant A as Auth server
participant R as Resource server
C->>C: generate state and PKCE verifier
C->>A: authorize with code_challenge S256 and state
A->>U: login and consent
U->>A: approve
A->>C: redirect with code and state
C->>C: verify state matches
C->>A: exchange code and verifier for token
A->>C: access token and refresh token
C->>R: request with Bearer token
R->>R: validate signature, iss, aud, exp
R->>C: response

Public clients use PKCE with code_challenge_method=S256. Reject plain. It provides no protection. Create a new verifier for every request and derive the challenge with SHA-256.

For OIDC relying-party flows, also send a nonce and check it in the returned ID token. This ties the token to this request and prevents token replay.

Validating a bearer token

Every service that accepts a bearer token is a resource server. It validates the token locally on each request instead of introspecting it each time. Pin the algorithm and never accept none. Require a kid that resolves to a known key and verify the signature. Match iss and aud exactly. Check exp and iat with minimal clock skew. Skipping any of these checks is a known type of vulnerability. See the OWASP JSON Web Token Cheat Sheet for the alg=none and key-confusion attacks these checks prevent.

Find JWKS through .well-known metadata, cache keys by kid, and fetch them again when kid is unknown. This lets keys rotate without redeploying resource servers. Fail closed if fetching fails. Never accept an unverified token. The web trader’s auth-service (packages/tradovate/auth-service/src/ops/jwks.ts in NT-NinjaTrader/ninja-web-trader) is the standard implementation for local validation and key caching.

Between services, do not forward a user’s token across a trust boundary. Use an OAuth 2.0 Token Exchange (RFC 8693) so the receiving service gets a token minted for its own audience. Authorise the caller for the resource instead of treating a valid token as permission.

Token lifetimes and rotation

Access tokens last minutes. Refresh tokens last longer and have both an idle cap and an absolute cap. Rotate the refresh token on every refresh. If someone presents a refresh token that was already rotated, treat it as theft. Revoke the whole token family and require authentication again, as described in the RFC 6819 replay defence. Quietly rotating the token again would hide the attack.

A resource server checks only exp, so a short access-token lifetime is how revocation reaches it. Set lifetimes so a revoked session ends quickly without checking introspection on every request.

Client-side token storage

Store tokens in the platform secure store. Never store them in application code or plain files.

  • Mobile and desktop use the OS keystore, Keychain, Android Keystore, or the platform secure-storage API.
  • Browser clients avoid localStorage for refresh tokens. Prefer a backend-for-frontend that holds the refresh token and sets HttpOnly, Secure, SameSite cookies, or hold the access token in memory only.

Sessions, logout and revocation

Logout must end the session, not only clear the client. Support OIDC RP-initiated logout and back-channel logout so a session ended at the identity provider also ends at relying parties. A resource server knows that a token is dead through its short exp; it does not run a revocation check on every request.

Require step-up authentication for sensitive actions: funding, withdrawals, or changing authentication settings. Ask for fresh authentication with max_age. Check the acr and amr claims to confirm the assurance level before allowing the action.

Service-to-service and higher-assurance controls

For service-to-service auth on GKE, connect workload identity to GCP Workload Identity instead of using long-lived service-account keys. This matches the least-privilege and secret-storage approach on the security page.

Rate-limit and monitor the token and authorization endpoints for credential stuffing and code-injection attempts. Connect this to the edge rate limiting described on the security page.

Sender-constrained tokens, DPoP (RFC 9449) or mTLS-bound tokens (RFC 8705), tie a token to its holder, so a stolen bearer token is useless. They are planned for high-value APIs. Bearer tokens with short exp are not the final limit.

Examples

These examples are for illustration, not a walkthrough. Each snippet shows the form of a check in the language that implements it.

Validating a bearer token

A resource server pins the algorithm, finds the key by kid, matches iss and aud, allows minimal clock skew, and fails closed. The examples use java-jwt (Scala/JVM), jose (TypeScript), and PyJWT (Python).

Starting a flow: state and PKCE

This is for client-side use on our browser and native surfaces. Verify state on the callback with a constant-time comparison (TypeScript/Node shown):

import { timingSafeEqual } from 'node:crypto';

// Constant-time compare with a length guard.
// timingSafeEqual throws on unequal-length buffers, so guard first.
function safeEqual(a: string, b: string): boolean {
  const ab = Buffer.from(a);
  const bb = Buffer.from(b);
  if (ab.length !== bb.length) return false;
  return timingSafeEqual(ab, bb);
}

// on /callback
const expected = req.session.oauthState; // set before redirect to /authorize
const got = String(req.query.state ?? '');
if (!expected || !safeEqual(expected, got)) {
  return res.status(400).send('state_mismatch');
}
delete req.session.oauthState; // single use

Create the PKCE challenge from a new verifier with SHA-256:

import crypto from 'node:crypto';

const base64url = (b: Buffer) => b.toString('base64url');
const verifier = base64url(crypto.randomBytes(32));
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());

const authUrl = `${authorize}?response_type=code&client_id=${clientId}`
  + `&redirect_uri=${encodeURIComponent(redirectUri)}`
  + `&code_challenge=${challenge}&code_challenge_method=S256` // never 'plain'
  + `&state=${state}`;

See also

  • Observability — the redaction pipeline that removes tokens from logs and query strings.
  • Security — least privilege, secret storage, edge rate limiting, and Workload Identity.
  • Open questions — token lifetimes, browser refresh-token storage, and sender-constrained tokens.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close