![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
A centralized OAuth & authentication service designed to provide unified login across multiple products with configurable branding, UI, and security features.
Unlike Other Authenticator is a stateless, API-first authentication service that enables:
- Unified authentication across 4–5+ products with a single account per email
- Multiple auth methods: Email/password, Google, Apple, Facebook, GitHub, LinkedIn
- Configurable branding: Per-client UI theming, logos, colors, and language support
- Optional 2FA: TOTP-based two-factor authentication
- Secure configuration: Tamper-proof JWT-based config delivery
- Zero admin UI: Client onboarding through signed configuration only
- Client Identification: Each client is identified by a verified domain. Backend-only calls use per-domain client secrets managed in Admin > Domains & Secrets.
- Config Delivery: All client configuration is delivered as a signed JWT. The OAuth server verifies the JWT signature before trusting any config.
- OAuth Flow: Uses the standard authorization code flow. Client popup redirects with a code, which the client backend exchanges for an access token.
- Token Pair: Access tokens remain short-lived JWTs (15–60 minutes). Client backends also receive rotating refresh tokens for server-side session renewal.
- Email is the canonical user identifier
- No email enumeration protection across all flows
- All client config is signed and verified
- Everything UI-related is templated and config-driven
- No avatars stored locally (external URLs only)
- Generic error messages only (no information leakage)
Configuration is delivered as a signed JWT with the following properties:
domain— Client domain (e.g.,app.example.com)redirect_urls— Array of allowed OAuth redirect URLsenabled_auth_methods— Array of enabled methods:["email_password", "google", "apple", "facebook", "github", "linkedin"]; social provider names here are both enabled and allowedui_theme— Complete theme object (colors, radii, typography, logo URL, density)language_config— Single language string or array of language codes
2fa_enabled— Boolean to enable/disable 2FA (default:false)debug_enabled— Boolean to enable debug endpoints (default:false)user_scope—"global"(default) or"per_domain"(isolate users per domain)language— Selected language (must be inlanguage_configif provided)org_features— Organisation/team/group feature configuration (see below)
Enable organisations, teams, and groups by adding org_features to the config:
{
"org_features": {
"enabled": true,
"groups_enabled": false,
"max_teams_per_org": 100,
"max_members_per_org": 1000,
"max_team_memberships_per_user": 50,
"org_roles": ["owner", "admin", "member"]
}
}When enabled, the access token JWT includes an org claim with the user's organisation, team, and group memberships. Groups are managed exclusively through the Internal API (/internal/org/) using signed requests. See Section 24 of the brief for the full specification.
All config JWTs must be signed and include the client domain. Expected claims:
domain— Client domain; must match the hostname ofconfig_urlexp— Optional expiration (configs are verified on every request)
- Node.js 18+ and npm
- PostgreSQL 14+
- Social provider OAuth credentials (Google, Apple, Facebook, GitHub, LinkedIn)
- SMTP server (optional, for email functionality)
git clone https://github.com/yourusername/unlike-other-authenticator.git
cd unlike-other-authenticatornpm installThis installs dependencies for both the API and Auth workspaces.
Create .env files in both /API and /Auth directories.
# Required
SHARED_SECRET=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/auth_db
# Social OAuth Providers
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# ... (Apple, Facebook, GitHub, LinkedIn credentials)
# Email Service (optional)
EMAIL_PROVIDER=smtp # or 'disabled'
EMAIL_FROM=noreply@yourservice.com
EMAIL_REPLY_TO=support@yourservice.com
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-smtp-username
SMTP_PASSWORD=your-smtp-password
# Optional Configuration
ACCESS_TOKEN_TTL=30 # minutes (15-60)
LOG_RETENTION_DAYS=90# Generate Prisma client
npm run prisma:generate --workspace API
# Run database migrations
npm run prisma:migrate:dev --workspace API# Terminal 1: Run API server
npm run dev:api
# Terminal 2: Run Auth UI
npm run dev:authThe API runs on http://localhost:3000 by default.
The Auth UI runs on http://localhost:5173 by default.
npm run buildThis builds both API and Auth workspaces.
# Apply production migrations
npm run prisma:migrate:deploy --workspace API
# Start API server
npm run start --workspace API/API — Node.js OAuth/auth server (Fastify)
/Auth — React auth UI (Vite + Tailwind)
/Docs — Full specification and architecture docs
brief.md — Complete product specification
techstack.md — Technology stack and structure
Auth/
architecture-api.md — API layered architecture
architecture-auth.md — Auth UI component architecture
Admin/ — Admin panel documentation
/Admin — Admin panel UI (React CSR)
CLAUDE.md — Agent/contributor instructions
The API follows a layered architecture:
Request → Routes → Middleware → Services → Database (Prisma)
- Routes (
/src/routes): Thin handlers that validate input and call services - Middleware (
/src/middleware): Config verification, domain auth, error handling - Services (
/src/services): Business logic for auth, users, tokens, social providers, email - Utils (
/src/utils): Pure helper functions (hashing, validation, errors)
Key Rules:
- No code file longer than 500 lines
- Thin routes, fat services
- All errors are generic to users, detailed in internal logs
- Prisma for all database access
The Auth UI is a React application with config-driven theming:
PopupContainer
└── ThemeProvider (config-driven)
└── I18nProvider (language-driven)
└── AuthLayout
└── [Page Components]
- Components (
/src/components): Reusable UI primitives (buttons, cards, inputs) and auth forms - Pages (
/src/pages): Auth flow screens (login, register, 2FA setup, etc.) - Theme (
/src/theme): Maps config theme to Tailwind classes - i18n (
/src/i18n): Translation loading with AI fallback for missing keys
Key Rules:
- Tailwind-only styling (no other CSS frameworks)
- All theming from config (no hardcoded brand styles)
- One component per file for reusable components
- No component file longer than 500 lines
Core Tables:
users— User accounts (email, password hash, name, pronouns preset/custom fields, avatar URL, 2FA settings)domain_roles— Per-domain role assignments (superuser vs user)login_logs— Audit trail of authentication eventsverification_tokens— One-time tokens for email verification and password reset
Organisation Tables (opt-in via org_features config):
organisations— Tenant organisations, scoped per domainorg_members— User-to-org membership with configurable rolesteams— Named groups of users within an organisationteam_members— User-to-team membership with lead/member rolesgroups— Named collections of teams (enterprise feature)group_members— User-to-group membership with admin flag
User Scope:
- Global (default): One email = one user across all domains
- Per-domain: Same email on different domains = separate user records
Pronouns Model:
- Use
pronouns_presetplus optionalpronouns_custom - Recommended presets:
he_him,she_her,they_them,any_pronouns,ask_me,prefer_not_to_say,custom - Require
pronouns_customonly whenpronouns_preset = custom
- No email enumeration: All responses are generic ("Check your email")
- Shared secret: Single global secret (never exposed, env var only)
- Config integrity: All configs signed with JWT, verified on every request
- Domain verification: Runs on each auth initiation (not cached)
- Social email trust: Only provider-verified emails accepted
- Short-lived access tokens: Access tokens expire in 15–60 minutes and are renewed through rotating refresh tokens
- Generic errors: All user-facing error messages are non-specific
# Development
npm run dev:api # Start API server in watch mode
npm run dev:auth # Start Auth UI dev server
# Building
npm run build # Build both workspaces
# Code Quality
npm run lint # Lint all workspaces
npm run format # Format code with Prettier
npm run typecheck # TypeScript type checking
# Testing
npm run test # Run tests in all workspaces
# Database
npm run prisma:generate --workspace API # Generate Prisma client
npm run prisma:migrate:dev --workspace API # Create and apply migration
npm run prisma:studio --workspace API # Open Prisma Studio# Run all tests
npm test
# Run API tests only
npm test --workspace API
# Watch mode (during development)
npm test -- --watch --workspace APITests are written using Vitest and cover:
- Unit tests for all services
- Integration tests for API endpoints
- Security tests (enumeration protection, generic errors, token validation)
On your client backend:
import jwt from 'jsonwebtoken';
const config = {
domain: 'app.example.com',
redirect_urls: ['https://app.example.com/auth/callback'],
enabled_auth_methods: ['email_password', 'google'],
ui_theme: {
colors: { primary: '#3b82f6', secondary: '#64748b' },
borderRadius: '0.5rem',
// ... full theme config
},
language_config: ['en', 'es'],
2fa_enabled: true,
user_scope: 'global'
};
const configJWT = jwt.sign(config, process.env.CONFIG_PRIVATE_KEY_PEM, {
algorithm: 'RS256',
keyid: process.env.CONFIG_KEY_ID
});
// Serve this JWT at a URL accessible to the auth serverOn your client frontend:
const configUrl = 'https://app.example.com/api/auth-config'; // serves the JWT
const authUrl = `https://auth.yourservice.com/oauth/authorize?config_url=${encodeURIComponent(configUrl)}`;
window.open(authUrl, 'oauth', 'width=500,height=700');On your client backend:
// Callback route receives the authorization code
app.get('/auth/callback', async (req, res) => {
const { code } = req.query;
// Exchange code for an access token + refresh token pair
const response = await fetch('https://auth.yourservice.com/auth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${hashDomainAndSecret(domain, sharedSecret)}`
},
body: JSON.stringify({
code
})
});
const { access_token, refresh_token } = await response.json();
// Verify and decode the JWT access token
const user = jwt.verify(access_token, process.env.SHARED_SECRET);
// Store the refresh token server-side only (for example an HttpOnly cookie)
res.cookie('refresh_token', refresh_token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
// Set session and redirect
req.session.userId = user.id;
res.redirect('/dashboard');
});POST /auth/login— Email/password loginPOST /auth/register— Email registration (sends verification email)POST /auth/verify-email— Verify email with tokenPOST /auth/reset-password— Request password resetGET /auth/callback/:provider— Social OAuth callbackPOST /auth/token— Exchange authorization code or refresh token for a token pairPOST /auth/revoke— Revoke the refresh-token family for logout
POST /2fa/setup— Initiate 2FA enrollment (returns QR code)POST /2fa/verify— Verify TOTP code during setup or loginPOST /2fa/reset— Email-based 2FA reset
GET /domain/users— List users for domain (requires domain hash token)GET /domain/logs— Get login logs for domainGET /domain/debug— Debug endpoints (superuser only)
These endpoints require org_features.enabled: true in the config JWT.
User-Facing (require domain hash token + user access token):
POST /org/organisations— Create an organisation (auto-creates default team)GET /org/organisations/:orgId— Get organisation detailsPUT /org/organisations/:orgId— Update organisationDELETE /org/organisations/:orgId— Delete organisation (owner only)GET /org/organisations/:orgId/members— List membersPOST /org/organisations/:orgId/members— Add member (by userId)POST /org/organisations/:orgId/transfer-ownership— Transfer ownershipGET /org/organisations/:orgId/teams— List teamsPOST /org/organisations/:orgId/teams— Create teamGET /org/organisations/:orgId/groups— List groups (read-only)GET /org/me— Current user's org context
Internal API (require domain hash token only, no user token):
POST /internal/org/organisations/:orgId/groups— Create groupPUT /internal/org/organisations/:orgId/groups/:groupId— Update groupDELETE /internal/org/organisations/:orgId/groups/:groupId— Delete groupPOST /internal/org/organisations/:orgId/groups/:groupId/members— Add group memberPUT /internal/org/organisations/:orgId/groups/:groupId/members/:userId— Toggle is_adminDELETE /internal/org/organisations/:orgId/groups/:groupId/members/:userId— Remove group memberPUT /internal/org/organisations/:orgId/teams/:teamId/group— Assign/unassign team to group
See Section 24 of the brief for the full specification.
GET /health— Health check endpoint
UOA's display-ready BillingStatementV1 contract lives in the MIT-licensed
@unlikeotherai/billing-statement-protocol
workspace alongside the complete customer-action contract: hosted redirect,
cancellation selection, preview and exact confirm_action, confirmation
request/response, and the minimal error envelope. It exports public TypeScript
types and exact runtime schemas, plus generated JSON Schema, OpenAPI 3.1, and
synthetic conformance artifacts. It contains no private server imports,
credentials, or production tenant data and can be packed or vendored by
open-source consumers before registry publication.
The same artifacts are available without authentication at:
GET /schemas/billing-statement-v1.jsonGET /schemas/billing-statement-v1.example.jsonGET /schemas/billing-statement-v1.openapi.jsonGET /schemas/billing-consumer-actions-v1.jsonGET /schemas/billing-consumer-actions-v1.example.jsonGET /schemas/billing-consumer-actions-v1.openapi.json
Backend feature authorization uses
GET /apps/:appId/flags?domain=…&userId=…&teamId=… with the product's
domain-hash credential. :appId is opaque UOA App.id, userId is the UOA
token subject, and teamId is the exact active UOA team. Capability checks
require explicit true; empty/missing/error results fail closed.
See CLAUDE.md for contributor guidelines and Docs/brief.md for the complete specification.
Key Rules:
- Read the brief before making changes
- Never remove content from
Docs/brief.mdunless explicitly instructed - Follow architecture patterns in
Docs/Auth/architecture-api.mdandDocs/Auth/architecture-auth.md - No code file longer than 500 lines
- Keep the codebase reusable and clean
- Security first: no enumeration, no information leakage, all config verified
MIT — See LICENSE file for details.
For issues and questions:
Built with: Node.js, TypeScript, Fastify, React, Tailwind CSS, PostgreSQL, Prisma








