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. The hash of
(domain + shared secret)becomes the client ID. - 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", "google", "apple", "facebook", "github", "linkedin"]ui_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)allowed_social_providers— Array of social provider names to enableuser_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 with the shared secret using HS256. Expected claims:
aud— Auth service identifier (set inAUTH_SERVICE_IDENTIFIERenv var)exp— 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
AUTH_SERVICE_IDENTIFIER=auth.yourservice.com
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
architecture-api.md — API layered architecture
architecture-auth.md — Auth UI component architecture
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, 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
- 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', '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.SHARED_SECRET, {
audience: 'auth.yourservice.com',
algorithm: 'HS256'
});
// 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
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/architecture-api.mdandDocs/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