RushLabs ID developer docs

RushLabs ID is the single sign-on service for RushLabs apps. It is a standard OAuth 2.0 + OpenID Connect provider: your app redirects the user here, we handle email/password + mandatory two-factor authentication, and you get back signed tokens describing who they are.

Endpoints

EndpointPurpose
GET /.well-known/openid-configurationOIDC discovery document
GET /.well-known/jwks.jsonPublic keys (RS256) for verifying tokens
GET /oauth/authorizeStart the authorization-code flow (PKCE S256)
POST /oauth/tokenExchange a code or refresh token for tokens
GET /oauth/userinfoProfile claims for a Bearer access token
POST /oauth/revokeRevoke a refresh token

Scopes

ScopeGrants
openidAn ID token with the user's stable sub
profilegiven_name, family_name, name
emailemail
offline_accessA refresh token

1 · Register your app

Client registration happens on the server (there is no self-serve portal). On the VPS:

cd /root/websites/auth.rushlabs.dev
bun run client:create -- --name "My App" \
  --redirect https://myapp.rushlabs.dev/auth/callback

This prints a client_id and (for confidential server-side apps) a client_secret shown once. Pass --public for SPAs/native apps — they get no secret and must use PKCE (the SSO library always uses PKCE). Pass --first-party to skip the consent screen for RushLabs-owned apps.

2 · Add the flow with @rushlabs/sso

The library lives at packages/rushlabs-sso in the auth repo and has zero dependencies (fetch + WebCrypto). Example with Hono on Bun:

import { Hono } from 'hono';
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
import { RushLabsSSO } from '@rushlabs/sso';

const sso = new RushLabsSSO({
  clientId: process.env.SSO_CLIENT_ID!,
  clientSecret: process.env.SSO_CLIENT_SECRET, // omit for --public clients
  redirectUri: 'https://myapp.rushlabs.dev/auth/callback',
});

const app = new Hono();

app.get('/auth/login', async (c) => {
  const { url, state, codeVerifier } = await sso.createAuthorization();
  setCookie(c, 'sso_tx', JSON.stringify({ state, codeVerifier }), {
    httpOnly: true, secure: true, sameSite: 'Lax', maxAge: 600, path: '/',
  });
  return c.redirect(url);
});

app.get('/auth/callback', async (c) => {
  const tx = JSON.parse(getCookie(c, 'sso_tx') ?? '{}');
  deleteCookie(c, 'sso_tx', { path: '/' });
  if (!tx.state || c.req.query('state') !== tx.state) return c.text('Bad state', 400);

  const tokens = await sso.exchangeCode(c.req.query('code')!, tx.codeVerifier);
  const user = await sso.verifyToken(tokens.id_token!);
  // user = { sub, email, given_name, family_name, name }
  // Create YOUR app session here (its own cookie), keyed by user.sub.
  return c.redirect('/');
});

3 · Verify tokens on API requests

Access and ID tokens are RS256 JWTs. Verify them locally (no network round-trip after the first JWKS fetch):

const claims = await sso.verifyToken(bearerToken);
// claims.sub is the stable RushLabs user id

Or call await sso.getUserInfo(accessToken) to fetch fresh profile data.

Raw flow (no library)

# 1. Redirect the browser to:
https://auth.rushlabs.dev/oauth/authorize?response_type=code
  &client_id=rl_...&redirect_uri=https://myapp/callback
  &scope=openid+profile+email&state=...&code_challenge=...&code_challenge_method=S256

# 2. Exchange the code:
curl -X POST https://auth.rushlabs.dev/oauth/token \
  -d grant_type=authorization_code -d code=... \
  -d redirect_uri=https://myapp/callback \
  -d client_id=rl_... -d client_secret=rls_... -d code_verifier=...

Accounts & 2FA

Every account uses email + password and mandatory TOTP two-factor authentication — users set it up during registration and confirm a code at each sign-in. Sessions on auth.rushlabs.dev are JWT httpOnly cookies, so returning users who are already signed in go straight through the authorize flow (first-party apps skip consent, too).

Access modes & RBAC

Each application has an access mode controlling who can complete the OAuth flow (set in /manage):

ModeWho can sign in
openAny RushLabs ID account (default)
allowlistOnly emails on the app's allowlist
role_basedOnly users holding at least one role for the app

Denied users are redirected back to your app with error=access_denied. Access is re-checked at every token exchange and refresh, so revocations take effect within the access-token lifetime (1 h) at worst.

Roles & dot-notation permissions

Roles are defined per application and contain dot-notation permissions namespaced by the app:

application.permission.child_permission
status.incidents.write        # exact permission
status.incidents.*            # subtree wildcard
status.*                      # everything in the app

When a signed-in user holds roles for your app, their tokens and the userinfo response carry two extra claims:

{
  "roles": ["ops"],
  "permissions": ["status.incidents.*", "status.services.read"]
}

Check them with the SSO library — hasPermission understands the wildcard rules:

import { hasPermission, hasRole } from '@rushlabs/sso';

const claims = await sso.verifyToken(bearerToken);
if (!hasPermission(claims, 'status.incidents.write')) return c.json({ error: 'forbidden' }, 403);
if (hasRole(claims, 'ops')) { /* ... */ }

Manage roles on the application page in /manage; grant them to users from each user's page.

Federated providers (Google, X, …)

The provider layer is pluggable. Deployments enable providers via AUTH_PROVIDERS in .env; only local ships enabled today. Adding a provider is a small module implementing IdentityProvider — see docs/adding-providers.md in the repo.