JWT-based auth with 13 OAuth providers, TOTP/SMS MFA, magic links, RBAC, and session management — production-ready and self-hosted.
# Auth starts automatically with nself start
nself start
# Check auth service status
nself status | grep auth
# Auth service is at:
# Local: http://auth.local.nself.org (proxied by Nginx)
# Port: 4000 (internal, not exposed)
# Image: nhost/hasura-auth:0.36.0# Enable OAuth providers
nself auth provider enable google --client-id $GOOGLE_CLIENT_ID --client-secret $GOOGLE_CLIENT_SECRET
nself auth provider enable github --client-id $GITHUB_CLIENT_ID --client-secret $GITHUB_CLIENT_SECRET
# Enable MFA
nself auth mfa enable --type totp
# Configure email (required for magic links, password reset)
nself auth email configure --provider smtp --host smtp.mailgun.org --port 587Authentication flow:
Client
|
| POST /auth/signin/email-password
| POST /auth/signin/provider/{provider}
| POST /auth/signin/magic-link
v
Nginx (auth.{domain})
|
v
nhost/hasura-auth:0.36.0 (port 4000, 127.0.0.1 only)
|
+-- Validates credentials
+-- Issues JWT + refresh token
+-- Writes session to np_auth_refresh_tokens
|
v
Client stores JWT
|
| Authorization: Bearer <jwt>
v
Hasura GraphQL API
|
+-- Validates JWT signature (HASURA_GRAPHQL_JWT_SECRET)
+-- Extracts x-hasura-* claims
+-- Applies RBAC permissions
+-- Enforces RLS policies
v
PostgreSQL# .env.dev (team defaults — replace HASURA_GRAPHQL_JWT_SECRET in .env.secrets)
AUTH_JWT_SECRET=your-256-bit-secret-here
HASURA_GRAPHQL_JWT_SECRET='{"type":"HS256","key":"your-256-bit-secret-here"}'
# JWT token TTL
AUTH_ACCESS_TOKEN_EXPIRES_IN=900 # 15 minutes (default)
AUTH_REFRESH_TOKEN_EXPIRES_IN=43200 # 30 days in minutes// JWT payload structure
{
"sub": "uuid-of-user",
"iat": 1715000000,
"exp": 1715000900,
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": ["user", "me"],
"x-hasura-default-role": "user",
"x-hasura-user-id": "uuid-of-user",
"x-hasura-source-account-id": "primary",
"x-hasura-tenant-id": "uuid-of-tenant" // Cloud only
}
}# Inject custom claims via Hasura actions
AUTH_CUSTOM_CLAIMS_WEBHOOK=https://api.nself.org/auth/custom-claims// Custom claims webhook handler
export async function customClaimsHandler(userId: string) {
const user = await db.select().from(npUsers)
.where(eq(npUsers.id, userId))
.limit(1)
return {
'x-hasura-source-account-id': user[0]?.sourceAccountId ?? 'primary',
'x-hasura-tenant-id': user[0]?.tenantId ?? null,
'x-hasura-plan': user[0]?.plan ?? 'free',
}
}# Enable email/password auth (default: enabled)
AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED=true # require email verification
AUTH_PASSWORD_MIN_LENGTH=8
AUTH_GRAVATAR_ENABLED=true# REST API
# Sign up
POST /auth/signup/email-password
{"email": "user@example.com", "password": "secure123", "options": {"displayName": "Alice"}}
# Sign in
POST /auth/signin/email-password
{"email": "user@example.com", "password": "secure123"}
# Response
{
"session": {
"accessToken": "eyJ...",
"refreshToken": "uuid...",
"accessTokenExpiresIn": 900,
"user": { "id": "uuid", "email": "user@example.com" }
}
}13 providers are supported out of the box.
| Provider | Enable flag | Required env vars |
|---|---|---|
AUTH_PROVIDER_GOOGLE_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET | |
| GitHub | AUTH_PROVIDER_GITHUB_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET |
| Apple | AUTH_PROVIDER_APPLE_ENABLED=true | _CLIENT_ID, _KEY_ID, _PRIVATE_KEY, _TEAM_ID |
AUTH_PROVIDER_FACEBOOK_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET | |
AUTH_PROVIDER_TWITTER_ENABLED=true | _CONSUMER_KEY, _CONSUMER_SECRET | |
AUTH_PROVIDER_LINKEDIN_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET | |
| Discord | AUTH_PROVIDER_DISCORD_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET |
| Spotify | AUTH_PROVIDER_SPOTIFY_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET |
| Twitch | AUTH_PROVIDER_TWITCH_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET |
| GitLab | AUTH_PROVIDER_GITLAB_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET |
| Bitbucket | AUTH_PROVIDER_BITBUCKET_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET |
| Windows Live | AUTH_PROVIDER_WINDOWSLIVE_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET |
| WorkOS | AUTH_PROVIDER_WORKOS_ENABLED=true | _CLIENT_ID, _CLIENT_SECRET, _DEFAULT_CONNECTION |
# OAuth sign-in URL (redirect user to this)
GET /auth/signin/provider/google?redirectTo=https://app.nself.org/dashboard
# After OAuth, user is redirected to:
# https://app.nself.org/dashboard?refreshToken=uuid&...# Enable (requires email configured)
AUTH_EMAIL_PASSWORDLESS_EMAIL_ENABLED=true# Request a magic link
POST /auth/signin/passwordless/email
{"email": "user@example.com", "options": {"redirectTo": "https://app.nself.org"}}
# User receives email with link:
# https://auth.nself.org/verify?ticket=otp:uuid...&type=signinPasswordless&redirectTo=...
# Verify the ticket (happens automatically via the link click)
GET /auth/verify?ticket=otp:uuid...&type=signinPasswordless
# Response: redirects to redirectTo with session in fragment/query# Enable TOTP globally
AUTH_MFA_TOTP_ENABLED=true# 1. Generate TOTP secret for user
POST /auth/mfa/totp/generate
Authorization: Bearer <access-token>
# Returns: { "totpSecret": "BASE32...", "qrCodeDataUrl": "data:image/png..." }
# 2. User scans QR code in authenticator app
# 3. Activate TOTP with first code
POST /auth/mfa/totp/activate
Authorization: Bearer <access-token>
{"code": "123456"}
# 4. On future sign-in, after email/password step:
POST /auth/signin/mfa/totp
{"ticket": "mfaTotp:uuid...", "otp": "123456"}# Enable SMS MFA (requires Twilio or similar)
AUTH_MFA_SMS_ENABLED=true
AUTH_SMS_PROVIDER=twilio
AUTH_SMS_TWILIO_ACCOUNT_SID=ACxxx
AUTH_SMS_TWILIO_AUTH_TOKEN=xxx
AUTH_SMS_TWILIO_MESSAGING_SERVICE_ID=MGxxx# Add phone number to account
POST /auth/user/phone
Authorization: Bearer <access-token>
{"phoneNumber": "+15551234567"}
# Verify phone
POST /auth/user/phone/verify
{"code": "123456"}
# On sign-in with MFA:
POST /auth/signin/mfa/sms
{"ticket": "mfaSms:uuid...", "otp": "123456"}# Default roles (built in)
# user — authenticated user (default)
# me — alias for current user (for self-service queries)
# admin — elevated access
# anonymous — unauthenticated
# Custom roles
AUTH_DEFAULT_ROLE=user
AUTH_ALLOWED_ROLES=user,admin,moderator,premium# Assign role to user
nself auth user role add user@example.com --role admin
# Remove role
nself auth user role remove user@example.com --role admin
# List user roles
nself auth user role list user@example.com// Hasura table permission — different rows per role
// 'user' role: can only see own documents
// SELECT permission:
{
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
// 'admin' role: can see all documents in their source_account
{
"source_account_id": {
"_eq": "X-Hasura-Source-Account-Id"
}
}
// 'me' role: user profile self-service
{
"id": {
"_eq": "X-Hasura-User-Id"
}
}# Refresh access token (before it expires)
POST /auth/token
{"refreshToken": "uuid..."}
# Returns new accessToken + refreshToken
# Sign out (revoke refresh token)
POST /auth/signout
Authorization: Bearer <access-token>
{"refreshToken": "uuid..."}
# Sign out all sessions (revoke all refresh tokens)
POST /auth/signout
Authorization: Bearer <access-token>
{"all": true}-- Session storage in PostgreSQL
-- np_auth_refresh_tokens table (managed by hasura-auth)
SELECT id, user_id, expires_at, created_at
FROM auth.refresh_tokens
WHERE user_id = $1
ORDER BY created_at DESC;
-- Revoke all sessions for a user
DELETE FROM auth.refresh_tokens WHERE user_id = $1;# List users
nself auth user list
nself auth user list --role admin
# Get user details
nself auth user get user@example.com
# Disable user (blocks sign-in without deleting)
nself auth user disable user@example.com
# Enable user
nself auth user enable user@example.com
# Delete user (soft delete)
nself auth user delete user@example.com
# Force password reset (sends email)
nself auth user password-reset user@example.com# Template locations
AUTH_EMAIL_TEMPLATES_DIR=./email-templates
# Template files:
# email-templates/verify-email.html
# email-templates/reset-password.html
# email-templates/magic-link.html
# email-templates/invite.html
# Override sender
AUTH_EMAIL_FROM=noreply@nself.org
AUTH_EMAIL_FROM_NAME="nSelf Team"| Setting | Default | Description |
|---|---|---|
AUTH_PASSWORD_HIBP_ENABLED | false | Check passwords against HaveIBeenPwned breach database |
AUTH_PASSWORD_MIN_LENGTH | 8 | Minimum password length |
AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED | true | Require email verification before sign-in |
AUTH_ANONYMOUS_USERS_ENABLED | false | Allow anonymous sessions |
AUTH_CLIENT_URL | Your app URL | Validates redirectTo URLs (whitelist) |
AUTH_ALLOWED_REDIRECT_URLS | CLIENT_URL | Comma-separated allowed redirect destinations |
AUTH_TOKEN_EXPIRY_SECONDS | 900 | Access token TTL in seconds |
AUTH_JWT_SECRET on a schedule — old tokens become invalid immediately. Coordinate with a deployment.AUTH_ALLOWED_REDIRECT_URLS explicitly — open redirectors allow phishing attacks using your domain.AUTH_MFA_TOTP_ENABLED=true and require TOTP for the admin role.127.0.0.1.