Steam

Steam OpenID 2.0

Code Exchange for web and mobile — Steam uses OpenID 2.0 (not OAuth2), never returns email, optional Web API key for profile enrichment, API reference, and frontend examples.

Overview

Steam login is optional and controlled by STEAM_AUTH_ENABLED. When disabled, Steam endpoints return 503 Service Unavailable. Unlike OAuth2 providers, Steam uses OpenID 2.0 at https://steamcommunity.com/openid/login — there is no client secret and Steam never returns an email address. The API assigns a stable synthetic email {steamId}@users.noreply.steamcommunity.com for account upsert. JWTs are never placed in the redirect URL; the API issues a one-time exchangeCode (Redis, 60 seconds). The same POST /auth/exchange endpoint is shared with other social providers.

  • • Feature-flagged via environment
  • • Custom Steam OpenID 2.0 passport strategy + SteamAuthGuard (not OAuth2)
  • • Identity verified via check_authentication POST to steamcommunity.com/openid/login
  • • SteamID64 extracted from openid.claimed_id — stored as steamId
  • • No email from Steam — synthetic {steamId}@users.noreply.steamcommunity.com
  • • Optional persona name via GET https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/ when STEAM_API_KEY is set
  • • Redirect allowlist prevents open redirects
  • • Same token response shape as POST /auth/login

Steam setup

Steam OpenID login does not require a Steamworks partner account. Optionally register a Web API key at steamcommunity.com/dev/apikey to enrich profiles with GetPlayerSummaries (persona name). Set STEAM_CALLBACK_URL and STEAM_REALM in your environment — the API redirects users to https://steamcommunity.com/openid/login for sign-in.

  1. Open https://steamcommunity.com/dev/apikey (optional — for GetPlayerSummaries profile enrichment)
  2. Register a domain and copy the Web API key into STEAM_API_KEY (OpenID works without it)
  3. Set STEAM_CALLBACK_URL to your Nest API callback (e.g. http://localhost:3000/api/v1/auth/steam/callback)
  4. Set STEAM_REALM to your API origin (e.g. http://localhost:3000/) — passed as openid.realm
  5. Ensure STEAM_REDIRECT_ALLOWLIST includes your SPA or mobile deep-link URLs
  6. Enable STEAM_AUTH_ENABLED=true in .env

Environment variables

Loaded from .env via src/config/steam-openid.config.ts (namespace steamOpenId on ConfigService). See .env.example. STEAM_API_KEY is optional — OpenID identity works without it.

Environment.env
STEAM_AUTH_ENABLED=true
STEAM_API_KEY=your-steam-web-api-key
STEAM_CALLBACK_URL=http://localhost:3000/api/v1/auth/steam/callback
STEAM_REALM=http://localhost:3000/
STEAM_REDIRECT_ALLOWLIST=myapp://success,http://localhost:5173/auth/callback
STEAM_OAUTH_DEFAULT_ROLES=

Default roles for new Steam users

New Steam users receive roles: [] by default (same as password register). Set STEAM_OAUTH_DEFAULT_ROLES in .env to a comma-separated list of Role values (e.g. user or user,manager). Existing users linked by steamId keep their current roles.

Sequence

  1. Client opens GET /api/v1/auth/steam?redirect=<allowlisted URL>
  2. API redirects to Steam OpenID (checkid_setup); user signs in on Steam
  3. Browser hits GET /api/v1/auth/steam/callback with openid.* query params; API verifies via check_authentication
  4. API optionally calls GetPlayerSummaries, upserts user by steamId, stores exchangeCode in Redis, redirects to redirect?code=...
  5. Client POSTs { code } to /api/v1/auth/exchange and receives JWTs

API reference

1. Start OpenID login

Redirects the browser to https://steamcommunity.com/openid/login with OpenID 2.0 checkid_setup (not an OAuth2 authorize URL). Query redirect must match a prefix in STEAM_REDIRECT_ALLOWLIST (or omit to use the first allowlist entry). Uses STEAM_REALM and STEAM_CALLBACK_URL.

HTTPGET /api/v1/auth/steam
GET /api/v1/auth/steam?redirect=http%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback

400 invalid redirect · 503 feature disabled

2. Callback

Handled by the API. Steam returns openid.mode=id_res and related openid.* parameters. The API POSTs check_authentication to verify the response, extracts SteamID64 from openid.claimed_id, then redirects to your app with ?code= (exchange code, not an OAuth authorization code).

HTTPGET /api/v1/auth/steam/callback
GET /api/v1/auth/steam/callback?openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.mode=id_res&openid.claimed_id=https%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F76561198012345678&state=...
→ 302 Location: http://localhost:5173/auth/callback?code=EXCHANGE_CODE

3. Exchange code

Public endpoint shared with other social providers. Consumes the code (single use) and returns the same shape as login.

HTTP RequestPOST /api/v1/auth/exchange
POST /api/v1/auth/exchange
Content-Type: application/json

{
  "code": "EXCHANGE_CODE"
}
HTTP Response200 OK
{
  "user": {
    "id": "...",
    "email": "[email protected]",
    "firstName": "PlayerName",
    "lastName": "",
    "roles": [],
    "steamId": "76561198012345678",
    "isActive": true
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

401 invalid/expired/used code · 503 social auth disabled

Frontend — Web (SPA)

Same pattern as OAuth providers: start login in the browser; your callback route reads code from the query string and calls exchange. The user object includes steamId and a synthetic email — do not use it for outbound mail.

JavaScriptStart Steam login
const API = 'http://localhost:3000/api/v1';
const redirect = encodeURIComponent(
  'http://localhost:5173/auth/callback'
);

// Full-page redirect to Nest → Steam OpenID → back to SPA
window.location.href = `${API}/auth/steam?redirect=${redirect}`;
JavaScriptCallback page /auth/callback
// /auth/callback on the SPA
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) throw new Error('Missing exchange code');

const res = await fetch('http://localhost:3000/api/v1/auth/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code }),
});
if (!res.ok) throw new Error(await res.text());

const { user, accessToken, refreshToken } = await res.json();
// persist tokens as your app requires
sessionStorage.setItem('accessToken', accessToken);
// Steam has no real email — use steamId as the stable identifier
console.log('signed in', user.steamId);
  • • Add your frontend callback URL to STEAM_REDIRECT_ALLOWLIST (e.g. http://localhost:5173/auth/callback)
  • • STEAM_CALLBACK_URL must point at the Nest API, not the SPA
  • • Steam never returns a real email — user.email is synthetic and not deliverable
  • • Store accessToken / refreshToken securely (memory + httpOnly cookie patterns as you prefer)

Frontend — Mobile (deep link)

Use a custom scheme (or universal link) in the allowlist. After Steam OpenID, the API redirects to myapp://success?code=.... The app opens, parses the code, and calls exchange.

React NativeOpen OpenID in browser
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';

const API = 'https://api.example.com/api/v1';
const redirect = encodeURIComponent('myapp://success');

async function signInWithSteam() {
  const url = `${API}/auth/steam?redirect=${redirect}`;
  await WebBrowser.openAuthSessionAsync(url, 'myapp://success');
}
React NativeHandle deep link + exchange
import * as Linking from 'expo-linking';

async function exchangeFromUrl(url) {
  const { queryParams } = Linking.parse(url);
  const code = queryParams?.code;
  if (!code) return;

  const res = await fetch(`${API}/auth/exchange`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code }),
  });
  const data = await res.json();
  // data.accessToken, data.refreshToken, data.user
  return data;
}

Linking.addEventListener('url', ({ url }) => {
  exchangeFromUrl(url);
});
  • • Register the scheme in the OS / Expo app.json
  • • Prefer Linking.addEventListener('url', ...) and getInitialURL()
  • • Exchange over HTTPS against your API base URL
  • • Identify users by steamId — not by synthetic email

Security notes

  • • Steam uses OpenID 2.0, not OAuth2 — no client secret; verify responses with check_authentication
  • • Redirect URLs must match STEAM_REDIRECT_ALLOWLIST prefixes
  • • Exchange codes are single-use and expire in 60 seconds (Redis)
  • • Steam never provides email — synthetic address is for internal upsert only, not notifications
  • • Existing accounts are linked by steamId; email-based linking does not apply to Steam-only users
  • • Steam-only users have password null — password login / change-password will fail until a password is set

User model

Postgres entity / interface fields used by Steam login:

  • • steamId — unique nullable SteamID64 (from openid.claimed_id)
  • • email — synthetic {steamId}@users.noreply.steamcommunity.com (Steam does not expose email)
  • • password — nullable for OpenID-only accounts
  • • isActive — set true for new Steam users
  • • roles — from STEAM_OAUTH_DEFAULT_ROLES env (default [])

Related