Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1699999999HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"error_description": "Too many requests. Please retry after 60 seconds.",
"retry_after": 60
}RATE_LIMIT_BY=ipRATE_LIMIT_BY=clientRATE_LIMIT_BY=user# Token endpoint: 60 requests per minute
RATE_LIMIT_TOKEN_MAX=60
RATE_LIMIT_TOKEN_WINDOW=60
# Login endpoint: 10 requests per minute
RATE_LIMIT_LOGIN_MAX=10
RATE_LIMIT_LOGIN_WINDOW=60
# Password reset: 3 requests per hour
RATE_LIMIT_PASSWORD_RESET_MAX=3
RATE_LIMIT_PASSWORD_RESET_WINDOW=3600RATE_LIMIT_WHITELIST=10.0.0.0/8,192.168.1.0/24{
"client_id": "trusted-client",
"rate_limits": {
"token": {
"max": 1000,
"window": 60
}
}
}async function makeRequest(url, options, retries = 3) {
const response = await fetch(url, options);
if (response.status === 429) {
if (retries > 0) {
const retryAfter = response.headers.get('Retry-After') || 60;
await sleep(retryAfter * 1000);
return makeRequest(url, options, retries - 1);
}
throw new Error('Rate limit exceeded');
}
return response;
}RATE_LIMIT_STORE=redis
REDIS_URL=redis://localhost:6379GET /auth/{provider}# Basic social login
curl -I "https://auth.example.com/auth/google"
# With redirect URL
curl -I "https://auth.example.com/auth/google?forward_url=aHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20vZGFzaGJvYXJk"GET /auth/{provider}/callbackPOST /auth/{provider}/unlinkcurl -X POST "https://auth.example.com/auth/google/unlink" \
-H "Cookie: session=..." \
-H "Content-Type: application/json"{
"sub": "google-user-id",
"email": "user@gmail.com",
"email_verified": true,
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"picture": "https://lh3.googleusercontent.com/..."
}{
"id": 12345678,
"login": "johndoe",
"email": "john@example.com",
"name": "John Doe",
"avatar_url": "https://avatars.githubusercontent.com/..."
}{
"id": "facebook-user-id",
"email": "john@example.com",
"name": "John Doe",
"picture": {
"data": {
"url": "https://graph.facebook.com/..."
}
}
}{
"sub": "linkedin-member-id",
"email": "john@example.com",
"email_verified": true,
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"picture": "https://media.licdn.com/..."
}{
"sub": "apple-user-id",
"email": "john@example.com",
"email_verified": true,
"name": "John Doe"
}POST /device HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
client_id=cli_client&scope=openid%20profile{
"device_code": "e2623df1-8594-47b4-b528-41ed3daecc1a",
"user_code": "WDJB-MJHT",
"verification_uri": "https://auth.example.com/activate",
"verification_uri_complete": "https://auth.example.com/activate?user_code=WDJB-MJHT",
"expires_in": 300,
"interval": 5
}To sign in, visit:
https://auth.example.com/activate
And enter the code:
WDJB-MJHT
Waiting for authorization...POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=e2623df1-8594-47b4-b528-41ed3daecc1a
&client_id=cli_client{
"error": "authorization_pending",
"error_description": "The user has not yet completed authorization"
}{
"error": "slow_down",
"error_description": "Polling too frequently"
}{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"scope": "openid profile"
}{
"error": "access_denied",
"error_description": "The user denied authorization"
}{
"error": "expired_token",
"error_description": "The device code has expired"
}const CLIENT_ID = 'cli_client';
const AUTHORITY_URL = 'https://auth.example.com';
async function login() {
// Step 1: Get device code
const deviceResponse = await fetch(`${AUTHORITY_URL}/device`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: CLIENT_ID,
scope: 'openid profile email'
})
});
const device = await deviceResponse.json();
// Step 2: Display to user
console.log('\nTo sign in, visit:');
console.log(` ${device.verification_uri}`);
console.log('\nAnd enter the code:');
console.log(` ${device.user_code}`);
console.log('\nWaiting for authorization...\n');
// Step 3: Poll for token
const interval = device.interval * 1000;
const expiresAt = Date.now() + (device.expires_in * 1000);
while (Date.now() < expiresAt) {
await sleep(interval);
const tokenResponse = await fetch(`${AUTHORITY_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: device.device_code,
client_id: CLIENT_ID
})
});
const result = await tokenResponse.json();
if (result.access_token) {
console.log('Login successful!');
return result;
}
if (result.error === 'authorization_pending') {
process.stdout.write('.');
continue;
}
if (result.error === 'slow_down') {
await sleep(5000); // Additional delay
continue;
}
throw new Error(result.error_description || result.error);
}
throw new Error('Device code expired');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Run
login()
.then(tokens => console.log('Access token:', tokens.access_token))
.catch(err => console.error('Error:', err.message));import requests
import time
CLIENT_ID = 'cli_client'
AUTHORITY_URL = 'https://auth.example.com'
def login():
# Step 1: Get device code
response = requests.post(
f'{AUTHORITY_URL}/device',
data={
'client_id': CLIENT_ID,
'scope': 'openid profile email'
}
)
device = response.json()
# Step 2: Display to user
print(f"\nTo sign in, visit:\n {device['verification_uri']}")
print(f"\nAnd enter the code:\n {device['user_code']}")
print("\nWaiting for authorization...")
# Step 3: Poll for token
interval = device['interval']
expires_at = time.time() + device['expires_in']
while time.time() < expires_at:
time.sleep(interval)
response = requests.post(
f'{AUTHORITY_URL}/token',
data={
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'device_code': device['device_code'],
'client_id': CLIENT_ID
}
)
result = response.json()
if 'access_token' in result:
print("\nLogin successful!")
return result
if result.get('error') == 'authorization_pending':
print('.', end='', flush=True)
continue
if result.get('error') == 'slow_down':
time.sleep(5)
continue
raise Exception(result.get('error_description', result.get('error')))
raise Exception('Device code expired')
if __name__ == '__main__':
tokens = login()
print(f"Access token: {tokens['access_token']}")Enter the code displayed on your device:
┌─────────────────────────────────┐
│ WDJB-MJHT │
└─────────────────────────────────┘
[Continue]POST /token HTTP/1.1
Host: auth.example.com
Authorization: Basic YWJjMTIzOnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=read%20write{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}Authorization: Basic base64(client_id:client_secret)POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=abc123
&client_secret=your_secretconst CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const AUTHORITY_URL = 'https://auth.example.com';
async function getAccessToken() {
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
const response = await fetch(`${AUTHORITY_URL}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${credentials}`
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'read write'
})
});
const data = await response.json();
return data.access_token;
}
// Use the token
async function callApi() {
const token = await getAccessToken();
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}import requests
from requests.auth import HTTPBasicAuth
CLIENT_ID = 'your_client_id'
CLIENT_SECRET = 'your_client_secret'
AUTHORITY_URL = 'https://auth.example.com'
def get_access_token():
response = requests.post(
f'{AUTHORITY_URL}/token',
data={
'grant_type': 'client_credentials',
'scope': 'read write'
},
auth=HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET)
)
return response.json()['access_token']
# Use the token
def call_api():
token = get_access_token()
response = requests.get(
'https://api.example.com/data',
headers={'Authorization': f'Bearer {token}'}
)
return response.json()# Get token
TOKEN=$(curl -s -X POST https://auth.example.com/token \
-u "client_id:client_secret" \
-d "grant_type=client_credentials" \
-d "scope=read" \
| jq -r '.access_token')
# Use token
curl https://api.example.com/data \
-H "Authorization: Bearer $TOKEN"class TokenCache {
constructor() {
this.token = null;
this.expiresAt = null;
}
async getToken() {
// Return cached token if still valid
if (this.token && Date.now() < this.expiresAt - 60000) {
return this.token;
}
// Get new token
const response = await fetchToken();
this.token = response.access_token;
this.expiresAt = Date.now() + (response.expires_in * 1000);
return this.token;
}
}
const tokenCache = new TokenCache();# Request specific scopes
grant_type=client_credentials&scope=read%20write
# If scope omitted, uses client's default scopes
grant_type=client_credentialsHTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"scope": "openid profile email",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGhvcml0eS1rZXktMSJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLXV1aWQiLCJhdWQiOiJhYmMxMjMiLCJleHAiOjE2OTk5OTk5OTksImlhdCI6MTY5OTk5NjM5OSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCJ9.signature{
"alg": "RS256",
"typ": "JWT",
"kid": "authority-key-1"
}{
"iss": "https://auth.example.com",
"sub": "user-uuid",
"aud": "abc123",
"exp": 1699999999,
"iat": 1699996399,
"scope": "openid profile email",
"client_id": "abc123"
}{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"scope": "openid profile email",
"id_token": "..."
}{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"scope": "openid profile email"
}GET /api/resource HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...GET /api/resource?access_token=eyJhbGciOiJSUzI1NiIs...HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
}
function validateToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'your_client_id'
}, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
}POST /token/introspect HTTP/1.1
Authorization: Basic YWJjMTIzOnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
token=eyJhbGciOiJSUzI1NiIs...{
"active": true,
"client_id": "abc123",
"username": "user@example.com",
"scope": "openid profile email",
"sub": "user-uuid",
"exp": 1699999999
}Cache-Control: no-store
Pragma: no-cache{
"keys": [
{
"kty": "RSA",
"kid": "authority-key-1",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZpt...",
"e": "AQAB"
}
]
}const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 600000, // 10 minutes
rateLimit: true,
jwksRequestsPerMinute: 10
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
return;
}
callback(null, key.getPublicKey());
});
}
function verifyToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com'
}, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
}import jwt
from jwt import PyJWKClient
jwks_client = PyJWKClient(
"https://auth.example.com/.well-known/jwks.json"
)
def verify_token(token):
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer="https://auth.example.com",
audience="your_client_id"
)import (
"github.com/golang-jwt/jwt/v5"
"github.com/MicahParks/keyfunc/v2"
)
func verifyToken(tokenString string) (*jwt.Token, error) {
jwks, err := keyfunc.Get("https://auth.example.com/.well-known/jwks.json", keyfunc.Options{})
if err != nil {
return nil, err
}
return jwt.Parse(tokenString, jwks.Keyfunc, jwt.WithIssuer("https://auth.example.com"))
}{
"alg": "RS256",
"typ": "JWT",
"kid": "authority-key-1"
}const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 600000, // 10 minutes
timeout: 30000 // 30 seconds timeout
});const crypto = require('crypto');
async function manualVerify(token) {
const [headerB64, payloadB64, signatureB64] = token.split('.');
// Decode header to get kid
const header = JSON.parse(Buffer.from(headerB64, 'base64url'));
// Fetch JWKS
const response = await fetch('https://auth.example.com/.well-known/jwks.json');
const jwks = await response.json();
// Find matching key
const jwk = jwks.keys.find(k => k.kid === header.kid);
if (!jwk) throw new Error('Key not found');
// Convert JWK to PEM
const pem = jwkToPem(jwk);
// Verify signature
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(`${headerB64}.${payloadB64}`);
const signature = Buffer.from(signatureB64, 'base64url');
const isValid = verifier.verify(pem, signature);
if (!isValid) throw new Error('Invalid signature');
return JSON.parse(Buffer.from(payloadB64, 'base64url'));
}┌─────────────────────────────────────────┐
│ Network Security (HTTPS) │
├─────────────────────────────────────────┤
│ Rate Limiting │
├─────────────────────────────────────────┤
│ Input Validation │
├─────────────────────────────────────────┤
│ Authentication (Password + MFA) │
├─────────────────────────────────────────┤
│ Session Management │
├─────────────────────────────────────────┤
│ Authorization (Scopes) │
├─────────────────────────────────────────┤
│ Audit Logging │
└─────────────────────────────────────────┘Attempt 1-4: Normal login
Attempt 5: Account locked (30 min)
Attempt 6+: Lock timer resetsRequest with Refresh Token A
↓
Issue new Access Token
Issue new Refresh Token B
Invalidate Refresh Token AClient generates: state=abc123
Client sends in request
Authority returns: state=abc123
Client verifies matchcode_verifier = random()
code_challenge = SHA256(code_verifier)
Authorization: Send code_challenge
Token exchange: Prove with code_verifierMy Test App


git clone https://github.com/azutoolkit/authority.git
cd authoritydocker-compose up -dPORT=4001 docker-compose up -ddocker-compose logs dbdocker-compose logs authority# Clone the repository
git clone https://github.com/azutoolkit/authority.git
cd authority
# Start with Docker
docker-compose up -d
# Visit http://localhost:4000

User → Your App → Authority → User authenticates → Your Backend → TokensClient generates: code_verifier (random)
Client sends: code_challenge = SHA256(code_verifier)
Authority returns: authorization code
Client proves: code_verifier
Authority validates: SHA256(code_verifier) == code_challenge
Authority issues: tokensYour Service → Authority (client_id + secret) → Token → Your Service → APIDevice → Authority: Request device code
Authority → Device: device_code + user_code + URL
Device → User: "Visit URL, enter code"
User → Browser → Authority: Enter code, authenticate
Device → Authority: Poll for token
Authority → Device: Access tokenApp → User: Enter credentials
App → Authority: username + password
Authority → App: Tokens<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<style>
.btn-google {
display: inline-flex;
align-items: center;
padding: 12px 24px;
background: #4285f4;
color: white;
border-radius: 4px;
text-decoration: none;
font-family: sans-serif;
font-weight: 500;
}
.btn-google:hover {
background: #357abd;
}
.btn-google svg {
margin-right: 12px;
}
</style>
</head>
<body>
<h1>Welcome to My App</h1>
<a href="http://localhost:4000/auth/google" class="btn-google">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z" fill="#4285F4"/>
<path d="M9.003 18c2.43 0 4.467-.806 5.956-2.18l-2.909-2.26c-.806.54-1.836.86-3.047.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 009.003 18z" fill="#34A853"/>
<path d="M3.964 10.712A5.41 5.41 0 013.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 000 9c0 1.452.348 2.827.957 4.042l3.007-2.33z" fill="#FBBC05"/>
<path d="M9.003 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.464.891 11.428 0 9.002 0A8.997 8.997 0 00.957 4.958L3.964 7.29c.708-2.127 2.692-3.71 5.036-3.71z" fill="#EA4335"/>
</svg>
Sign in with Google
</a>
</body>
</html>// Encode your callback URL
const callbackUrl = 'http://localhost:3000/dashboard';
const encodedUrl = btoa(callbackUrl);
// Build the auth URL with forward_url
const authUrl = `http://localhost:4000/auth/google?forward_url=${encodedUrl}`;
// Use this URL for your button
document.querySelector('.btn-google').href = authUrl;http://localhost:4000/auth/google/callbackconst params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email', // OpenID Connect scopes
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}function parseIdToken(idToken) {
const parts = idToken.split('.');
const payload = JSON.parse(atob(parts[1]));
return payload;
}
// Result:
// {
// "iss": "http://localhost:4000",
// "sub": "user-uuid",
// "aud": "your_client_id",
// "exp": 1699999999,
// "iat": 1699996399,
// "name": "John Doe",
// "email": "john@example.com",
// "email_verified": true
// }async function validateIdToken(idToken) {
const payload = parseIdToken(idToken);
// 1. Verify issuer
if (payload.iss !== AUTHORITY_URL) {
throw new Error('Invalid issuer');
}
// 2. Verify audience
if (payload.aud !== CLIENT_ID) {
throw new Error('Invalid audience');
}
// 3. Verify expiration
if (payload.exp < Date.now() / 1000) {
throw new Error('Token expired');
}
// 4. Verify signature (use JWKS)
await verifySignature(idToken);
return payload;
}async function getUserInfo(accessToken) {
const response = await fetch(`${AUTHORITY_URL}/userinfo`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch user info');
}
return response.json();
}
// Result:
// {
// "sub": "user-uuid",
// "name": "John Doe",
// "given_name": "John",
// "family_name": "Doe",
// "email": "john@example.com",
// "email_verified": true,
// "picture": "https://..."
// }class AuthSession {
constructor() {
this.tokens = null;
this.user = null;
}
login(tokens, user) {
this.tokens = tokens;
this.user = user;
// Store refresh token securely (httpOnly cookie recommended)
localStorage.setItem('auth_session', JSON.stringify({
accessToken: tokens.access_token,
expiresAt: Date.now() + (tokens.expires_in * 1000),
user: user
}));
}
getAccessToken() {
const session = JSON.parse(localStorage.getItem('auth_session'));
if (!session) return null;
if (Date.now() > session.expiresAt) {
// Token expired - need to refresh
return null;
}
return session.accessToken;
}
getUser() {
const session = JSON.parse(localStorage.getItem('auth_session'));
return session?.user;
}
logout() {
localStorage.removeItem('auth_session');
this.tokens = null;
this.user = null;
}
}
const auth = new AuthSession();<div id="profile" style="display: none;">
<img id="avatar" src="" alt="Profile picture">
<h2 id="name"></h2>
<p id="email"></p>
<button id="logout">Logout</button>
</div>
<script>
function showProfile(user) {
document.getElementById('profile').style.display = 'block';
document.getElementById('avatar').src = user.picture || '/default-avatar.png';
document.getElementById('name').textContent = user.name;
document.getElementById('email').textContent = user.email;
}
document.getElementById('logout').onclick = () => {
auth.logout();
window.location.href = '/';
};
</script>function logout() {
auth.logout();
window.location.href = '/';
}function logoutWithRedirect() {
auth.logout();
const params = new URLSearchParams({
post_logout_redirect_uri: 'http://localhost:3000',
client_id: CLIENT_ID
});
window.location.href = `${AUTHORITY_URL}/logout?${params}`;
}class AuthClient {
constructor(config) {
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.authorityUrl = config.authorityUrl;
this.session = new AuthSession();
}
async login() {
const codeVerifier = this.generateCodeVerifier();
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
const state = this.generateCodeVerifier();
sessionStorage.setItem('pkce_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `${this.authorityUrl}/authorize?${params}`;
}
async handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state');
}
const codeVerifier = sessionStorage.getItem('pkce_verifier');
const tokens = await this.exchangeCode(code, codeVerifier);
const user = await this.getUserInfo(tokens.access_token);
this.session.login(tokens, user);
// Clean up
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
window.history.replaceState({}, '', window.location.pathname);
return user;
}
async exchangeCode(code, codeVerifier) {
const response = await fetch(`${this.authorityUrl}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
code_verifier: codeVerifier
})
});
return response.json();
}
async getUserInfo(accessToken) {
const response = await fetch(`${this.authorityUrl}/userinfo`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
return response.json();
}
logout() {
this.session.logout();
}
isAuthenticated() {
return this.session.getAccessToken() !== null;
}
getUser() {
return this.session.getUser();
}
// PKCE helpers
generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return this.base64URLEncode(array);
}
async generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return this.base64URLEncode(new Uint8Array(hash));
}
base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
}
// Usage
const auth = new AuthClient({
clientId: 'your_client_id',
redirectUri: 'http://localhost:3000/callback',
authorityUrl: 'http://localhost:4000'
});
if (window.location.search.includes('code=')) {
auth.handleCallback().then(user => {
console.log('Logged in as:', user.name);
});
}curl http://localhost:4000/.well-known/jwks.json{
"keys": [
{
"kty": "RSA",
"kid": "authority-key-1",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'http://localhost:4000/.well-known/jwks.json',
cache: true,
rateLimit: true
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
}
async function validateToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'http://localhost:4000'
}, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
}import jwt
import requests
from jwt import PyJWKClient
jwks_client = PyJWKClient("http://localhost:4000/.well-known/jwks.json")
def validate_token(token):
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer="http://localhost:4000"
)async function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.substring(7);
try {
const decoded = await validateToken(token);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// Use it
app.get('/api/protected', authMiddleware, (req, res) => {
res.json({ message: `Hello, ${req.user.sub}` });
});const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';async function introspectToken(token) {
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
const response = await fetch('http://localhost:4000/token/introspect', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${credentials}`
},
body: new URLSearchParams({
token: token,
token_type_hint: 'access_token'
})
});
return response.json();
}{
"active": true,
"client_id": "my-client",
"username": "user@example.com",
"scope": "read write",
"sub": "user-uuid",
"exp": 1699999999
}{
"active": false
}function requireScopes(...requiredScopes) {
return async (req, res, next) => {
const token = req.headers.authorization?.substring(7);
if (!token) {
return res.status(401).json({ error: 'Missing token' });
}
try {
const decoded = await validateToken(token);
const tokenScopes = decoded.scope?.split(' ') || [];
const hasAllScopes = requiredScopes.every(
scope => tokenScopes.includes(scope)
);
if (!hasAllScopes) {
return res.status(403).json({
error: 'Insufficient scope',
required: requiredScopes,
provided: tokenScopes
});
}
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
}
// Use it
app.get('/api/data', requireScopes('read'), (req, res) => {
res.json({ data: '...' });
});
app.post('/api/data', requireScopes('write'), (req, res) => {
res.json({ created: true });
});
app.delete('/api/users/:id', requireScopes('admin'), (req, res) => {
res.json({ deleted: true });
});function scheduleTokenRefresh(tokens) {
const expiresIn = tokens.expires_in * 1000; // Convert to ms
const refreshAt = expiresIn - 60000; // Refresh 1 minute early
setTimeout(async () => {
const newTokens = await refreshAccessToken(tokens.refresh_token);
scheduleTokenRefresh(newTokens);
}, refreshAt);
}app.use((err, req, res, next) => {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'token_expired',
message: 'Access token has expired'
});
}
next(err);
});git clone https://github.com/azutoolkit/authority.git
cd authority
docker-compose up -d# Server
CRYSTAL_ENV=production
PORT=4000
BASE_URL=https://auth.example.com
# Database
DATABASE_URL=postgres://auth_user:secure_password@db:5432/authority_db
# Security
SECRET_KEY=your-256-bit-secret-key-here
# Token Lifetimes (seconds)
ACCESS_TOKEN_TTL=3600
CODE_TTL=600
DEVICE_CODE_TTL=300
# SSL (optional - use reverse proxy recommended)
SSL_CERT=
SSL_KEY=openssl rand -hex 32version: '3.8'
services:
authority:
image: azutoolkit/authority:latest
ports:
- "4000:4000"
environment:
- CRYSTAL_ENV=production
- DATABASE_URL=postgres://auth_user:${DB_PASSWORD}@db:5432/authority_db
- SECRET_KEY=${SECRET_KEY}
- BASE_URL=${BASE_URL}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=auth_user
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=authority_db
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U auth_user -d authority_db"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
postgres_data:
redis_data:docker-compose up -dserver {
listen 443 ssl http2;
server_name auth.example.com;
ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
location / {
proxy_pass http://localhost:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name auth.example.com;
return 301 https://$server_name$request_uri;
}# docker-compose.yml addition
labels:
- "traefik.enable=true"
- "traefik.http.routers.authority.rule=Host(`auth.example.com`)"
- "traefik.http.routers.authority.tls=true"
- "traefik.http.routers.authority.tls.certresolver=letsencrypt"docker-compose exec authority crystal run src/db/migrate.crdocker-compose exec db pg_dump -U auth_user authority_db > backup.sqldocker-compose exec -T db psql -U auth_user authority_db < backup.sqlcurl http://localhost:4000/healthdocker-compose logs -f authoritydocker stats authorityservices:
authority:
deploy:
replicas: 3docker-compose logs authoritydocker-compose ps db
docker-compose logs dbPORT=4001 docker-compose up -d# Create database
psql -c "CREATE DATABASE authority_db;"
# Create user (optional)
psql -c "CREATE USER authority WITH PASSWORD 'secure_password';"
psql -c "GRANT ALL PRIVILEGES ON DATABASE authority_db TO authority;"createdb authority_db# Basic format
DATABASE_URL=postgres://user:password@host:port/database
# Examples
DATABASE_URL=postgres://localhost:5432/authority_db
DATABASE_URL=postgres://authority:password@localhost:5432/authority_dbDATABASE_URL=postgres://user:password@host:5432/authority_db?sslmode=requirecrystal run src/db/migrate.crcrystal run src/db/seed.crpg_dump -U authority authority_db > backup.sqlpg_dump -U authority authority_db | gzip > backup.sql.gzpsql -U authority authority_db < backup.sqlgunzip -c backup.sql.gz | psql -U authority authority_dbDATABASE_URL=postgres://user:password@host:5432/authority_db?initial_pool_size=10&max_pool_size=50# pgbouncer.ini
[databases]
authority_db = host=localhost port=5432 dbname=authority_db
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
pool_mode = transaction
max_client_conn = 200
default_pool_size = 20DATABASE_URL=postgres://user:password@localhost:6432/authority_db# postgresql.conf
# Memory
shared_buffers = 256MB
effective_cache_size = 768MB
work_mem = 16MB
# Connections
max_connections = 100
# Write-ahead log
wal_buffers = 16MB
checkpoint_completion_target = 0.9-- Index on frequently queried columns
CREATE INDEX idx_tokens_user_id ON access_tokens(user_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);pg_isready -h localhost -p 5432# pg_hba.conf
local all all trust
host all all 127.0.0.1/32 md5createdb authority_dbGRANT ALL PRIVILEGES ON DATABASE authority_db TO authority;
GRANT ALL ON ALL TABLES IN SCHEMA public TO authority;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO authority;# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: authoritykubectl apply -f namespace.yaml# Generate secret key
kubectl create secret generic authority-secrets \
--namespace authority \
--from-literal=secret-key=$(openssl rand -hex 32) \
--from-literal=db-password=$(openssl rand -hex 16)# postgres.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: authority
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
value: authority
- name: POSTGRES_DB
value: authority_db
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: authority-secrets
key: db-password
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command: ["pg_isready", "-U", "authority"]
initialDelaySeconds: 30
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: authority
spec:
ports:
- port: 5432
selector:
app: postgres
clusterIP: None# authority.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: authority
namespace: authority
spec:
replicas: 3
selector:
matchLabels:
app: authority
template:
metadata:
labels:
app: authority
spec:
containers:
- name: authority
image: azutoolkit/authority:latest
ports:
- containerPort: 4000
env:
- name: CRYSTAL_ENV
value: production
- name: PORT
value: "4000"
- name: BASE_URL
value: "https://auth.example.com"
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: authority-secrets
key: secret-key
- name: DATABASE_URL
value: "postgres://authority:$(DB_PASSWORD)@postgres:5432/authority_db"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: authority-secrets
key: db-password
livenessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: authority
namespace: authority
spec:
ports:
- port: 80
targetPort: 4000
selector:
app: authority# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: authority
namespace: authority
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- auth.example.com
secretName: authority-tls
rules:
- host: auth.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: authority
port:
number: 80kubectl apply -f namespace.yaml
kubectl apply -f postgres.yaml
kubectl apply -f authority.yaml
kubectl apply -f ingress.yamlhelm repo add authority https://azutoolkit.github.io/authority-helm
helm repo updatehelm install authority authority/authority \
--namespace authority \
--create-namespace \
--set ingress.enabled=true \
--set ingress.hosts[0].host=auth.example.com \
--set postgresql.enabled=truereplicaCount: 3
image:
repository: azutoolkit/authority
tag: latest
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: auth.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: authority-tls
hosts:
- auth.example.com
postgresql:
enabled: true
auth:
database: authority_db
username: authority
primary:
persistence:
size: 10Gi
redis:
enabled: true
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80helm install authority authority/authority \
--namespace authority \
--create-namespace \
-f values.yamlpostgresql:
enabled: false
externalDatabase:
host: your-postgres.xxx.rds.amazonaws.com
port: 5432
database: authority_db
username: authority
existingSecret: authority-db-secretredis:
enabled: true
architecture: replication
replica:
replicaCount: 2apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: authority-pdb
namespace: authority
spec:
minAvailable: 2
selector:
matchLabels:
app: authorityapiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: authority
namespace: authority
spec:
selector:
matchLabels:
app: authority
endpoints:
- port: http
path: /metricskubectl logs -f -l app=authority -n authoritykubectl get pods -n authority
kubectl describe pod authority-xxx -n authoritykubectl exec -it authority-xxx -n authority -- sh
curl postgres:5432kubectl get events -n authority --sort-by='.lastTimestamp'# Local development
DATABASE_URL=postgres://localhost:5432/authority_db
# With credentials
DATABASE_URL=postgres://user:password@localhost:5432/authority_db
# With SSL
DATABASE_URL=postgres://user:password@host:5432/authority_db?sslmode=requireopenssl rand -hex 32# .env.local
CRYSTAL_ENV=development
PORT=4000
BASE_URL=http://localhost:4000
DATABASE_URL=postgres://localhost:5432/authority_db
SECRET_KEY=development-key-not-for-production
CRYSTAL_LOG_LEVEL=debug# .env
CRYSTAL_ENV=production
PORT=4000
HOST=0.0.0.0
BASE_URL=https://auth.example.com
DATABASE_URL=postgres://user:password@db.example.com:5432/authority_db?sslmode=require
SECRET_KEY=your-256-bit-production-secret-key
ACCESS_TOKEN_TTL=3600
SESSION_DURATION_DAYS=7
CRYSTAL_LOG_LEVEL=info
CRYSTAL_WORKERS=8crystal run src/app.crexport DATABASE_URL=postgres://localhost:5432/authority_db
crystal run src/app.crdocker run -e DATABASE_URL=... -e SECRET_KEY=... azutoolkit/authoritydocker run --env-file .env azutoolkit/authoritygit clone https://github.com/azutoolkit/authority.git
cd authorityshards installcreatedb authority_dbpsql -c "CREATE DATABASE authority_db;"cp .env.example .env.local# Server
CRYSTAL_ENV=development
PORT=4000
BASE_URL=http://localhost:4000
# Database
DATABASE_URL=postgres://localhost:5432/authority_db
# Security
SECRET_KEY=development-secret-key-change-in-production
# Token Lifetimes
ACCESS_TOKEN_TTL=3600
CODE_TTL=600crystal run src/db/migrate.crcrystal run src/db/seed.crcrystal run src/app.cr./scripts/dev.shcrystal build src/app.cr --release -o bin/authorityCRYSTAL_ENV=production ./bin/authoritycrystal speccrystal spec spec/models/user_spec.crwatchexec -e cr -r "crystal run src/app.cr"dropdb authority_db
createdb authority_db
crystal run src/db/migrate.cr
crystal run src/db/seed.crauthority/
├── src/
│ ├── app.cr # Application entry point
│ ├── db/
│ │ ├── migrate.cr # Migration runner
│ │ ├── seed.cr # Seed data
│ │ └── migrations/ # Migration files
│ ├── endpoints/ # HTTP handlers
│ ├── models/ # Data models
│ ├── services/ # Business logic
│ └── views/ # Response serializers
├── public/
│ ├── templates/ # Jinja templates
│ ├── css/ # Stylesheets
│ └── js/ # JavaScript
├── spec/ # Tests
├── shard.yml # Dependencies
└── .env.example # Environment templateexport PATH="$PATH:/usr/local/crystal/bin"# macOS
brew services start postgresql
# Linux
sudo systemctl start postgresqlrm -rf lib .shards
shards installlsof -i :4000sudo apt install certbot python3-certbot-nginxsudo certbot --nginx -d auth.example.comserver {
listen 443 ssl http2;
server_name auth.example.com;
ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
location / {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name auth.example.com;
return 301 https://$server_name$request_uri;
}BASE_URL=https://auth.example.com# traefik.yml
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: weblabels:
- "traefik.enable=true"
- "traefik.http.routers.authority.rule=Host(`auth.example.com`)"
- "traefik.http.routers.authority.entrypoints=websecure"
- "traefik.http.routers.authority.tls.certresolver=letsencrypt"openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout authority.key \
-out authority.crt \
-subj "/CN=localhost"SSL_CERT=/path/to/authority.crt
SSL_KEY=/path/to/authority.key
BASE_URL=https://localhost:4000SSL_CERT=/etc/letsencrypt/live/auth.example.com/fullchain.pem
SSL_KEY=/etc/letsencrypt/live/auth.example.com/privkey.pem
BASE_URL=https://auth.example.comsudo certbot renew --dry-run# /etc/letsencrypt/renewal-hooks/post/reload-authority.sh
#!/bin/bash
docker-compose -f /path/to/docker-compose.yml restart authority# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" always;# Check certificate
openssl s_client -connect auth.example.com:443 -servername auth.example.com
# SSL Labs test
# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=auth.example.comSSL_CERT=/path/to/fullchain.pem # Not just cert.pemchmod 644 authority.crt
chmod 600 authority.keyBASE_URL=https://auth.example.com{
"id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2024-01-15T10:30:00Z",
"event": "user.login",
"actor_id": "user-uuid",
"actor_type": "user",
"resource_type": "session",
"resource_id": "session-uuid",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"metadata": {
"method": "password",
"mfa_used": true
}
}GET /api/audit-logs?limit=100&offset=0
Authorization: Bearer {admin_token}{
"data": [
{
"id": "...",
"timestamp": "2024-01-15T10:30:00Z",
"event": "user.login",
...
}
],
"total": 1250,
"limit": 100,
"offset": 0
}GET /api/audit-logs?event=user.login_failed&from=2024-01-01&to=2024-01-31
Authorization: Bearer {admin_token}GET /api/audit-logs/{id}
Authorization: Bearer {admin_token}# Retain logs for 90 days
AUDIT_LOG_RETENTION_DAYS=90
# Run cleanup daily
AUDIT_LOG_CLEANUP_SCHEDULE=0 2 * * *crystal run src/tasks/cleanup_audit_logs.crAUDIT_LOG_SYSLOG=true
SYSLOG_HOST=logs.example.com
SYSLOG_PORT=514AUDIT_LOG_FILE=/var/log/authority/audit.log
AUDIT_LOG_FORMAT=json# Splunk
AUDIT_LOG_SPLUNK_URL=https://splunk.example.com:8088
AUDIT_LOG_SPLUNK_TOKEN=your-hec-token
# Elasticsearch
AUDIT_LOG_ELASTICSEARCH_URL=https://elasticsearch.example.com:9200
AUDIT_LOG_ELASTICSEARCH_INDEX=authority-audit# Alert after 10 failed logins in 5 minutes
grep "user.login_failed" /var/log/authority/audit.log | \
awk -v d="$(date -d '5 minutes ago' +%Y-%m-%dT%H:%M)" '$2 > d' | \
wc -l | \
xargs -I {} sh -c '[ {} -gt 10 ] && echo "Alert: High failed logins"'PASSWORD_MIN_LENGTH=12
REQUIRE_UPPERCASE=true
REQUIRE_LOWERCASE=true
REQUIRE_NUMBERS=true
REQUIRE_SPECIAL=false
PASSWORD_HISTORY_COUNT=5
PASSWORD_EXPIRY_DAYS=0PASSWORD_MIN_LENGTH=16
REQUIRE_UPPERCASE=true
REQUIRE_LOWERCASE=true
REQUIRE_NUMBERS=true
REQUIRE_SPECIAL=true
PASSWORD_HISTORY_COUNT=12
PASSWORD_EXPIRY_DAYS=90PASSWORD_MIN_LENGTH=10
REQUIRE_UPPERCASE=false
REQUIRE_LOWERCASE=false
REQUIRE_NUMBERS=false
REQUIRE_SPECIAL=false
PASSWORD_HISTORY_COUNT=3
PASSWORD_EXPIRY_DAYS=0Password must:
✗ Be at least 12 characters
✓ Contain an uppercase letter
✓ Contain a lowercase letter
✗ Contain a numberPASSWORD_EXPIRY_WARNING_DAYS=14PASSWORD_EXPIRY_GRACE_DAYS=7# Remember last 5 passwords
PASSWORD_HISTORY_COUNT=5This password was used recently. Please choose a different password.CHECK_COMMON_PASSWORDS=true
COMMON_PASSWORD_LIST=/path/to/passwords.txtwget https://github.com/danielmiessler/SecLists/raw/master/Passwords/Common-Credentials/10k-most-common.txt -O passwords.txtPOST /api/validate-password
Content-Type: application/json
{
"password": "MyNewPassword123"
}{
"valid": false,
"errors": [
"Password must contain a special character"
]
}GET /api/users/{id}/password-status
Authorization: Bearer {token}{
"last_changed": "2024-01-01T00:00:00Z",
"expires_at": "2024-04-01T00:00:00Z",
"days_until_expiry": 45,
"requires_change": false
}function generatePassword(length = 16) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
return password;
}// Generate random code verifier
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
// Create code challenge from verifier
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(hash));
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}const CLIENT_ID = 'your_client_id';
const REDIRECT_URI = 'http://localhost:3000/callback';
const AUTHORITY_URL = 'http://localhost:4000';
async function login() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateCodeVerifier(); // Random state for CSRF protection
// Store for later
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `${AUTHORITY_URL}/authorize?${params}`;
}// On your callback page
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
// Verify state matches
if (state !== sessionStorage.getItem('state')) {
throw new Error('Invalid state parameter');
}
// Get stored code verifier
const codeVerifier = sessionStorage.getItem('code_verifier');
// Exchange code for tokens
const tokens = await exchangeCode(code, codeVerifier);
console.log('Access token:', tokens.access_token);
}async function exchangeCode(code, codeVerifier) {
const response = await fetch(`${AUTHORITY_URL}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
return response.json();
}{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "openid profile email"
}async function fetchUserInfo(accessToken) {
const response = await fetch(`${AUTHORITY_URL}/userinfo`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return response.json();
}
// Result:
// {
// "sub": "user-uuid",
// "name": "John Doe",
// "email": "john@example.com"
// }<!DOCTYPE html>
<html>
<head>
<title>OAuth Demo</title>
</head>
<body>
<button id="login">Login with Authority</button>
<div id="user"></div>
<script>
const CLIENT_ID = 'your_client_id';
const REDIRECT_URI = 'http://localhost:3000/callback';
const AUTHORITY_URL = 'http://localhost:4000';
// Check if this is a callback
if (window.location.search.includes('code=')) {
handleCallback();
}
document.getElementById('login').onclick = login;
async function login() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `${AUTHORITY_URL}/authorize?${params}`;
}
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (state !== sessionStorage.getItem('state')) {
alert('Invalid state');
return;
}
const codeVerifier = sessionStorage.getItem('code_verifier');
const tokenResponse = await fetch(`${AUTHORITY_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier
})
});
const tokens = await tokenResponse.json();
const userResponse = await fetch(`${AUTHORITY_URL}/userinfo`, {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
});
const user = await userResponse.json();
document.getElementById('user').textContent = `Welcome, ${user.name}!`;
}
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(hash));
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
</script>
</body>
</html>
docker run -d -p 6379:6379 redis:7-alpinebrew install redis
brew services start redissudo apt install redis-server
sudo systemctl enable redis
sudo systemctl start redisREDIS_URL=redis://localhost:6379REDIS_URL=redis://:password@localhost:6379REDIS_URL=redis://localhost:6379/1# redis.conf
# Memory
maxmemory 256mb
maxmemory-policy allkeys-lru
# Persistence
save 900 1
save 300 10
save 60 10000
# Security
requirepass your_redis_password
bind 127.0.0.1
# Performance
tcp-keepalive 300REDIS_URL=rediss://user:password@redis.example.com:6380version: '3.8'
services:
authority:
image: azutoolkit/authority
environment:
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
redis_data:REDIS_URL=redis://sentinel1:26379,sentinel2:26379,sentinel3:26379/mymasterREDIS_URL=redis://node1:6379,node2:6379,node3:6379authority:session:{session_id} -> {user_id, created_at, ...}authority:token:{token_hash} -> {user_id, scope, exp, ...}authority:ratelimit:{ip}:{endpoint} -> countredis-cli
# Check memory usage
INFO memory
# List keys
KEYS authority:*
# Monitor commands
MONITORredis-cli -h localhost -p 6379 pingredis-cli pingredis-cli -a your_password pingredis-cli INFO memoryGOOGLE_OAUTH_ENABLED=true
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret<a href="https://your-authority-domain/auth/google" class="btn-google">
<svg><!-- Google icon --></svg>
Sign in with Google
</a><a href="https://your-authority-domain/auth/google?forward_url=BASE64_ENCODED_URL">
Sign in with Google
</a>const forwardUrl = btoa('https://your-app.com/dashboard');
const googleAuthUrl = `https://your-authority-domain/auth/google?forward_url=${forwardUrl}`;https://your-authority-domain/auth/google/callbackGITHUB_OAUTH_ENABLED=true
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret<a href="https://your-authority-domain/auth/github" class="btn-github">
<svg><!-- GitHub icon --></svg>
Sign in with GitHub
</a>const forwardUrl = btoa('https://your-app.com/dashboard');
const githubAuthUrl = `https://your-authority-domain/auth/github?forward_url=${forwardUrl}`;https://your-authority-domain/auth/github/callbackhttps://your-authority-domain/auth/linkedin/callbackLINKEDIN_OAUTH_ENABLED=true
LINKEDIN_CLIENT_ID=your-linkedin-client-id
LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret<a href="https://your-authority-domain/auth/linkedin" class="btn-linkedin">
<svg><!-- LinkedIn icon --></svg>
Sign in with LinkedIn
</a>const forwardUrl = btoa('https://your-app.com/dashboard');
const linkedinAuthUrl = `https://your-authority-domain/auth/linkedin?forward_url=${forwardUrl}`;https://your-authority-domain/auth/linkedin/callback
abc12-def34
ghi56-jkl78
mno90-pqr12
...REQUIRE_ADMIN_MFA=trueREQUIRE_MFA=trueMFA_GRACE_PERIOD_DAYS=7GET /api/users/{id}
Authorization: Bearer {admin_token}{
"id": "user-uuid",
"email": "user@example.com",
"mfa_enabled": true,
"mfa_configured_at": "2024-01-15T10:30:00Z"
}// After successful password authentication
if (user.mfa_enabled) {
// Show TOTP input form
showMFAPrompt();
} else if (settings.require_mfa) {
// Redirect to MFA setup
redirectToMFASetup();
}<a href="https://your-authority-domain/auth/google?link=true">
Connect Google Account
</a>POST /auth/{provider}/unlinkcurl -X POST https://your-authority-domain/auth/google/unlink \
-H "Cookie: session=..." \
-H "Content-Type: application/json"{
"success": true,
"message": "Google account unlinked successfully"
}{
"error": "cannot_unlink",
"message": "Cannot unlink the only login method. Set a password first."
}// Fetch user's linked providers
const response = await fetch('https://your-authority-domain/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const userInfo = await response.json();
// Check for linked providers in user profile<div class="linked-accounts">
<h3>Linked Accounts</h3>
<div class="provider">
<span class="icon">🔵</span>
<span class="name">Google</span>
<span class="email">user@gmail.com</span>
<button onclick="unlinkProvider('google')">Disconnect</button>
</div>
<div class="provider">
<span class="icon">⚫</span>
<span class="name">GitHub</span>
<span class="status">Not connected</span>
<a href="/auth/github?link=true">Connect</a>
</div>
</div>async function unlinkProvider(provider) {
if (!confirm(`Disconnect ${provider}?`)) return;
try {
const response = await fetch(`/auth/${provider}/unlink`, {
method: 'POST',
credentials: 'include'
});
const result = await response.json();
if (result.success) {
// Refresh UI
location.reload();
} else {
alert(result.message);
}
} catch (error) {
alert('Failed to unlink account');
}
}{
"error": "already_linked",
"message": "This social account is linked to another user"
}{
"event": "social_account_linked",
"user_id": "user-uuid",
"provider": "google",
"provider_user_id": "google-user-id",
"timestamp": "2024-01-15T10:30:00Z"
}
{
"event": "social_account_unlinked",
"user_id": "user-uuid",
"provider": "github",
"timestamp": "2024-01-15T11:45:00Z"
}crystal run src/tasks/create_admin.cr -- \
--email admin@example.com \
--name "Admin User" \
--password "SecurePassword123"ADMIN_EMAIL=admin@example.com \
ADMIN_PASSWORD=SecurePassword123 \
crystal run src/db/seed.crPOST /api/users
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"email": "newadmin@example.com",
"name": "New Admin",
"password": "SecurePassword123",
"role": "admin"
}PATCH /api/users/{id}
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"role": "admin"
}super_admin
└── admin
└── userREQUIRE_ADMIN_MFA=trueADMIN_PASSWORD_MIN_LENGTH=16
ADMIN_PASSWORD_EXPIRY_DAYS=30ADMIN_ALLOWED_IPS=10.0.0.0/8,192.168.1.100PATCH /api/users/{id}
Content-Type: application/json
Authorization: Bearer {super_admin_token}
{
"role": "user"
}PATCH /api/users/{id}
Content-Type: application/json
Authorization: Bearer {super_admin_token}
{
"active": false
}# Lock after 5 failed attempts
LOCKOUT_THRESHOLD=5
# Lock for 30 minutes
LOCKOUT_DURATION=30
# Auto-unlock enabled
ENABLE_AUTO_UNLOCK=true# First lockout: 15 minutes
# Second lockout: 30 minutes
# Third lockout: 60 minutes
# Fourth+: 120 minutes
PROGRESSIVE_LOCKOUT=true# Example: Send alert on multiple lockouts
grep "user.locked" /var/log/authority/audit.log | alert-scriptGET /api/users/{id}
Authorization: Bearer {admin_token}{
"id": "user-uuid",
"email": "user@example.com",
"locked": true,
"locked_at": "2024-01-15T10:30:00Z",
"failed_attempts": 5,
"unlock_at": "2024-01-15T11:00:00Z"
}POST /api/users/{id}/unlock
Authorization: Bearer {admin_token}LOCKOUT_BY_IP=true
IP_LOCKOUT_THRESHOLD=10LOCKOUT_WHITELIST=192.168.1.0/24,10.0.0.0/8Your account has been temporarily locked due to multiple failed login attempts.
Please try again in 30 minutes or contact support.Forgot your password? [Reset Password]https://your-authority-domain/auth/apple/callbackAPPLE_OAUTH_ENABLED=true
APPLE_CLIENT_ID=com.yourcompany.yourapp.auth
APPLE_TEAM_ID=XXXXXXXXXX
APPLE_KEY_ID=XXXXXXXXXX
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIGTAgEA...
-----END PRIVATE KEY-----"<a href="https://your-authority-domain/auth/apple" class="btn-apple">
<svg><!-- Apple logo --></svg>
Sign in with Apple
</a>abc123@privaterelay.appleid.comPOST /api/scopes
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"name": "documents:read",
"description": "Read access to documents",
"default": false
}GET /api/scopes
Authorization: Bearer {admin_token}{
"data": [
{
"id": "scope-uuid",
"name": "documents:read",
"description": "Read access to documents",
"default": false
}
]
}PATCH /api/scopes/{id}
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"description": "Updated description"
}DELETE /api/scopes/{id}
Authorization: Bearer {admin_token}documents:read
documents:write
documents:delete
users:read
users:writeapi:v1:read
api:v2:read{
"client_name": "My App",
"scope": "openid profile email documents:read documents:write"
}My App is requesting access to:
✓ Your profile information
✓ Your email address
✓ Read your documents
✓ Modify your documents
[Allow] [Deny]const token = jwt.verify(accessToken, publicKey);
const scopes = token.scope.split(' ');
if (!scopes.includes('documents:read')) {
throw new ForbiddenError('Insufficient scope');
}DEFAULT_SCOPES=openid profile email{
"name": "admin",
"requires": ["read", "write"]
}https://your-authority-domain/auth/apple/callback



POST /register/{client_id}/renew_secret
Authorization: Bearer {admin_token}{
"client_id": "abc123def456",
"client_secret": "new_secret_xyz789",
"client_secret_expires_at": 0
}POST /register/{client_id}/secrets
Authorization: Bearer {admin_token}
{
"action": "add"
}DELETE /register/{client_id}/secrets/{old_secret_id}
Authorization: Bearer {admin_token}POST /register/{client_id}/renew_secret
Authorization: Bearer {admin_token}# Example: Update Kubernetes secret
kubectl create secret generic oauth-secret \
--from-literal=client-secret=NEW_SECRET \
--dry-run=client -o yaml | kubectl apply -f -
# Rolling restart
kubectl rollout restart deployment/my-app#!/bin/bash
# rotate-secrets.sh
CLIENT_ID="abc123def456"
ADMIN_TOKEN="your_admin_token"
AUTHORITY_URL="https://auth.example.com"
# Rotate secret
NEW_SECRET=$(curl -s -X POST \
"${AUTHORITY_URL}/register/${CLIENT_ID}/renew_secret" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
| jq -r '.client_secret')
# Update Kubernetes secret
kubectl create secret generic oauth-secret \
--from-literal=client-secret="${NEW_SECRET}" \
--dry-run=client -o yaml | kubectl apply -f -
# Restart application
kubectl rollout restart deployment/my-app
# Notify team
echo "Client secret rotated for ${CLIENT_ID}" | \
slack-notify --channel "#security"# Rotate quarterly
0 0 1 */3 * /path/to/rotate-secrets.shCLIENT_SECRET_LIFETIME_DAYS=90GET /api/clients?secret_expires_before=2024-04-01
Authorization: Bearer {admin_token}GET /api/audit-logs?event=client.secret_rotated&client_id={client_id}
Authorization: Bearer {admin_token}# Notify on rotation
NOTIFY_ON_SECRET_ROTATION=true
NOTIFICATION_WEBHOOK=https://hooks.slack.com/...# Revoke all tokens for client
POST /api/clients/{client_id}/revoke_tokens
Authorization: Bearer {admin_token}GET /api/users/{id}/sessions
Authorization: Bearer {admin_token}{
"data": [
{
"id": "session-uuid",
"created_at": "2024-01-15T10:30:00Z",
"last_activity": "2024-01-15T14:20:00Z",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
"device": "Chrome on macOS",
"location": "San Francisco, CA"
}
]
}DELETE /api/sessions/{session_id}
Authorization: Bearer {admin_token}DELETE /api/users/{id}/sessions
Authorization: Bearer {admin_token}SINGLE_SESSION=trueIDLE_TIMEOUT_MINUTES=30NOTIFY_NEW_SESSION=trueNew login to your Authority account
Device: Chrome on macOS
Location: San Francisco, CA
Time: January 15, 2024 at 10:30 AM
If this wasn't you, secure your account immediately.crystal run src/tasks/revoke_all_sessions.cr# Revoke sessions older than 30 days
DELETE /api/sessions?older_than=30d
Authorization: Bearer {admin_token}
# Revoke sessions from specific IP
DELETE /api/sessions?ip=192.168.1.100
Authorization: Bearer {admin_token}User Session
└── Access Token 1
└── Access Token 2
└── Refresh Token 1REVOKE_TOKENS_ON_SESSION_END=trueGET /api/metrics/sessions
Authorization: Bearer {admin_token}{
"active_sessions": 1250,
"sessions_today": 342,
"unique_users": 890
}GET /authorize?response_type=code&client_id=abc123&redirect_uri=https://app.example.com/callback&scope=openid%20profile&state=xyz789{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"scope": "openid profile email"
}{
"active": true,
"client_id": "abc123",
"username": "user@example.com",
"scope": "openid profile email",
"sub": "user-uuid",
"token_type": "Bearer",
"exp": 1699999999,
"iat": 1699996399
}{
"active": false
}{
"device_code": "e2623df1-8594-47b4-b528-41ed3daecc1a",
"user_code": "56933A",
"verification_uri": "https://auth.example.com/activate",
"verification_uri_complete": "https://auth.example.com/activate?user_code=56933A",
"audience": "My Application",
"expires_in": 300,
"interval": 5
}{
"error": "authorization_pending",
"error_description": "The user has not yet completed authorization"
}{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"scope": "openid profile"
}{
"sub": "user-uuid",
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"email": "john@example.com",
"email_verified": true
}{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/oauth2/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"introspection_endpoint": "https://auth.example.com/oauth/introspect",
"revocation_endpoint": "https://auth.example.com/oauth/revoke",
"device_authorization_endpoint": "https://auth.example.com/device/code",
"scopes_supported": ["openid", "profile", "email", "read", "write"],
"response_types_supported": ["code", "token"],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"password",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["S256", "plain"],
"claims_supported": [
"sub", "iss", "aud", "exp", "iat",
"email", "email_verified", "name",
"given_name", "family_name"
]
}{
"keys": [
{
"kty": "RSA",
"kid": "authority-key-1",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}{
"client_name": "My Application",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "openid profile email",
"logo_uri": "https://app.example.com/logo.png",
"client_uri": "https://app.example.com",
"tos_uri": "https://app.example.com/tos",
"policy_uri": "https://app.example.com/privacy",
"contacts": ["admin@example.com"]
}{
"client_id": "abc123-uuid",
"client_secret": "xyz789-secret",
"client_id_issued_at": 1699996399,
"client_secret_expires_at": 0,
"client_name": "My Application",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "openid profile email"
}{
"success": true
}{
"error": "invalid_token"
}{
"status": "ok"
}POST /register
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"client_name": "My Application",
"redirect_uris": [
"https://myapp.com/callback",
"https://myapp.com/auth/callback"
],
"grant_types": [
"authorization_code",
"refresh_token"
],
"response_types": ["code"],
"scope": "openid profile email",
"token_endpoint_auth_method": "client_secret_basic"
}{
"client_id": "abc123def456",
"client_secret": "xyz789ghi012",
"client_name": "My Application",
"redirect_uris": [
"https://myapp.com/callback",
"https://myapp.com/auth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "openid profile email",
"token_endpoint_auth_method": "client_secret_basic",
"client_id_issued_at": 1705312200,
"client_secret_expires_at": 0
}GET /register/{client_id}
Authorization: Bearer {admin_token}PATCH /register/{client_id}
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"redirect_uris": [
"https://myapp.com/callback",
"https://myapp.com/auth/callback",
"https://staging.myapp.com/callback"
]
}DELETE /register/{client_id}
Authorization: Bearer {admin_token}https://myapp.com/callback
https://myapp.com/auth/callback
https://staging.myapp.com/callback
http://localhost:3000/callback (development only)https://myapp.com/* # No wildcards
https://*.myapp.com/callback # No wildcards
http://myapp.com/callback # No HTTP in production{
"scope": "openid profile email read write admin"
}{
"client_name": "My Application",
"client_uri": "https://myapp.com",
"logo_uri": "https://myapp.com/logo.png",
"tos_uri": "https://myapp.com/terms",
"policy_uri": "https://myapp.com/privacy",
"contacts": ["support@myapp.com"]
}# Open in browser
open "http://localhost:4000/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=openid"public/images/logo.png
public/images/logo-dark.png # For dark backgrounds
public/images/favicon.ico<img src="/images/logo.png" alt="{{ app_name }}" class="logo">:root {
/* Brand colors */
--primary-color: #7c3aed; /* Primary purple */
--primary-hover: #6d28d9; /* Darker purple */
--primary-light: #a78bfa; /* Lighter purple */
/* Accent colors */
--accent-color: #06b6d4; /* Cyan accent */
/* Status colors */
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
}:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-card: #1e293b;
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--border-color: #334155;
}.theme-light {
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--bg-card: #ffffff;
--text-primary: #0f172a;
--text-secondary: #64748b;
--border-color: #e2e8f0;
}APP_NAME=MyAuth
APP_TAGLINE=Secure authentication for everyone<title>{{ app_name }} - Sign In</title>
<p>{{ app_tagline }}</p><!-- In layout.html -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">:root {
--font-family: 'Inter', system-ui, sans-serif;
}
body {
font-family: var(--font-family);
}@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}.login-page {
background-image: url('/images/login-bg.jpg');
background-size: cover;
background-position: center;
}.login-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
padding: 2rem;
}<!-- emails/base.html -->
<div class="header" style="background-color: #7c3aed; padding: 20px; text-align: center;">
<img src="{{ base_url }}/images/logo-dark.png" alt="{{ app_name }}" style="max-width: 150px;">
</div><div class="footer" style="text-align: center; padding: 20px; color: #666;">
<p>© {{ year }} {{ company_name }}. All rights reserved.</p>
<p>{{ company_address }}</p>
</div><div class="client-info">
{% if client.logo_uri %}
<img src="{{ client.logo_uri }}" alt="{{ client.name }}" class="client-logo">
{% endif %}
<h2>{{ client.name }}</h2>
</div><ul class="scopes">
{% for scope in scopes %}
<li>
<i class="icon icon-{{ scope.name }}"></i>
{{ scope.description }}
</li>
{% endfor %}
</ul># Branding
APP_NAME=MyAuth
APP_TAGLINE=Secure authentication
COMPANY_NAME=My Company
COMPANY_ADDRESS=123 Main St, City
# URLs
LOGO_URL=/images/logo.png
FAVICON_URL=/images/favicon.ico
TERMS_URL=https://example.com/terms
PRIVACY_URL=https://example.com/privacy
SUPPORT_URL=https://example.com/support
# Theme
THEME=dark # dark or light
PRIMARY_COLOR=#7c3aedpublic/templates/emails/
├── base.html # Base layout
├── welcome.html # Welcome email
├── verification.html # Email verification
├── password-reset.html # Password reset
├── mfa-enabled.html # MFA confirmation
└── new-login.html # New login alert<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
max-width: 150px;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #7c3aed;
color: white;
text-decoration: none;
border-radius: 6px;
margin: 20px 0;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ base_url }}/images/logo.png" alt="Authority" class="logo">
</div>
{% block content %}{% endblock %}
<div class="footer">
<p>This email was sent by {{ app_name }}.</p>
<p>{{ company_address }}</p>
</div>
</body>
</html>{% extends "emails/base.html" %}
{% block content %}
<h1>Reset Your Password</h1>
<p>Hi {{ user.name }},</p>
<p>We received a request to reset your password. Click the button below to choose a new password:</p>
<p style="text-align: center;">
<a href="{{ reset_url }}" class="button">Reset Password</a>
</p>
<p>This link will expire in {{ expiry_minutes }} minutes.</p>
<p>If you didn't request a password reset, you can safely ignore this email. Your password won't be changed.</p>
<p>
Best regards,<br>
The {{ app_name }} Team
</p>
{% endblock %}{% extends "emails/base.html" %}
{% block content %}
<h1>Welcome to {{ app_name }}!</h1>
<p>Hi {{ user.name }},</p>
<p>Thanks for creating an account. We're excited to have you on board!</p>
<p>Here are a few things you can do:</p>
<ul>
<li>Complete your profile</li>
<li>Enable two-factor authentication</li>
<li>Connect your applications</li>
</ul>
<p style="text-align: center;">
<a href="{{ profile_url }}" class="button">View Your Profile</a>
</p>
<p>
Welcome aboard,<br>
The {{ app_name }} Team
</p>
{% endblock %}SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=notifications@example.com
SMTP_PASSWORD=your_password
SMTP_FROM=noreply@example.com
SMTP_FROM_NAME=AuthorityEMAIL_VERIFICATION_REQUIRED=true
EMAIL_VERIFICATION_TTL=86400
PASSWORD_RESET_TTL=3600# Development mode shows emails in browser
CRYSTAL_ENV=development
EMAIL_PREVIEW=truecrystal run src/tasks/send_test_email.cr -- \
--to test@example.com \
--template password-reset<!-- Use inline styles -->
<p style="color: #333; font-size: 16px;">Text</p>
<!-- Use tables for layout -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>Content</td>
</tr>
</table>
<!-- Simple buttons -->
<a href="{{ url }}" style="
display: inline-block;
padding: 12px 24px;
background-color: #7c3aed;
color: #ffffff;
text-decoration: none;
">Button</a>public/templates/emails/
├── en/
│ ├── welcome.html
│ └── password-reset.html
├── es/
│ ├── welcome.html
│ └── password-reset.html
└── fr/
├── welcome.html
└── password-reset.htmlDEFAULT_LOCALE=en



openid (required)profileemailaddressphoneiss)sub)aud)exp)public/
├── templates/
│ ├── layout.html # Base layout
│ ├── signin.html # Login page
│ ├── signup.html # Registration
│ ├── authorize.html # OAuth consent
│ ├── activate.html # Device activation
│ ├── forgot-password.html # Password reset request
│ ├── reset-password.html # Password reset form
│ ├── errors.html # Error messages
│ └── emails/ # Email templates
│ ├── verification.html
│ ├── password-reset.html
│ └── welcome.html
├── css/
│ └── styles.css # Main stylesheet
└── js/
└── app.js # Client JavaScript<h1>Welcome, {{ user.name }}</h1>
<p>Email: {{ user.email }}</p>{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
{% if user.mfa_enabled %}
<span class="badge">MFA Enabled</span>
{% endif %}<ul class="scopes">
{% for scope in scopes %}
<li>{{ scope.name }}: {{ scope.description }}</li>
{% endfor %}
</ul><!DOCTYPE html>
<html>
<head>
<title>{% block title %}Authority{% endblock %}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
{% block body %}{% endblock %}
<script src="/js/app.js"></script>
</body>
</html>{% extends "layout.html" %}
{% set title = "Sign In" %}
{% block body %}
<main class="login-form">
{% include "errors.html" %}
<form action="/signin" method="post">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Sign In</button>
</form>
</main>
{% endblock %}:root {
/* Primary colors */
--primary-color: #7c3aed;
--primary-hover: #6d28d9;
/* Background colors */
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-card: #1e293b;
/* Text colors */
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
/* Status colors */
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
/* Border radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
}/* Light theme */
.theme-light {
--bg-primary: #ffffff;
--bg-secondary: #f1f5f9;
--text-primary: #0f172a;
--text-secondary: #64748b;
}TEMPLATES_PATH=/path/to/custom/templates# Restart server to reload templates
docker-compose restart authorityGET https://auth.example.com/authorize?
response_type=code
&client_id=abc123
&redirect_uri=https://app.example.com/callback
&scope=openid%20profile%20email
&state=xyz789https://app.example.com/callback?code=AUTH_CODE_HERE&state=xyz789POST /token HTTP/1.1
Host: auth.example.com
Authorization: Basic YWJjMTIzOnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_HERE
&redirect_uri=https://app.example.com/callback{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"scope": "openid profile email"
}const express = require('express');
const crypto = require('crypto');
const app = express();
const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const REDIRECT_URI = 'http://localhost:3000/callback';
const AUTHORITY_URL = 'https://auth.example.com';
// Step 1: Redirect to authorization
app.get('/login', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.state = state;
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: state
});
res.redirect(`${AUTHORITY_URL}/authorize?${params}`);
});
// Step 2: Handle callback
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (state !== req.session.state) {
return res.status(400).send('Invalid state');
}
// Exchange code for tokens
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
const tokenResponse = await fetch(`${AUTHORITY_URL}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${credentials}`
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI
})
});
const tokens = await tokenResponse.json();
req.session.tokens = tokens;
res.redirect('/profile');
});from flask import Flask, redirect, request, session
import requests
import secrets
app = Flask(__name__)
CLIENT_ID = 'your_client_id'
CLIENT_SECRET = 'your_client_secret'
REDIRECT_URI = 'http://localhost:5000/callback'
AUTHORITY_URL = 'https://auth.example.com'
@app.route('/login')
def login():
state = secrets.token_urlsafe(16)
session['state'] = state
params = {
'response_type': 'code',
'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URI,
'scope': 'openid profile email',
'state': state
}
return redirect(f"{AUTHORITY_URL}/authorize?{urlencode(params)}")
@app.route('/callback')
def callback():
code = request.args.get('code')
state = request.args.get('state')
if state != session.get('state'):
return 'Invalid state', 400
token_response = requests.post(
f"{AUTHORITY_URL}/token",
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': REDIRECT_URI
},
auth=(CLIENT_ID, CLIENT_SECRET)
)
session['tokens'] = token_response.json()
return redirect('/profile'){
"error": "error_code",
"error_description": "Human-readable description",
"error_uri": "https://docs.example.com/errors/error_code"
}https://app.example.com/callback?error=access_denied&error_description=User%20denied%20accessHTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "authorization_pending",
"error_description": "The user has not yet completed authorization"
}HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
Content-Type: application/json
{
"error": "invalid_token",
"error_description": "The access token expired"
}{
"error": "validation_error",
"error_description": "Request validation failed",
"details": [
{
"field": "email",
"message": "Email format is invalid"
},
{
"field": "password",
"message": "Password must be at least 12 characters"
}
]
}HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"error_description": "Rate limit exceeded. Retry after 60 seconds.",
"retry_after": 60
}function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(hash));
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}import hashlib
import base64
import secrets
def generate_code_verifier():
return secrets.token_urlsafe(32)
def generate_code_challenge(verifier):
digest = hashlib.sha256(verifier.encode()).digest()
return base64.urlsafe_b64encode(digest).rstrip(b'=').decode()GET https://auth.example.com/authorize?
response_type=code
&client_id=abc123
&redirect_uri=https://app.example.com/callback
&scope=openid%20profile%20email
&state=xyz789
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_HERE
&redirect_uri=https://app.example.com/callback
&client_id=abc123
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkimport { useEffect, useState } from 'react';
const CLIENT_ID = 'your_client_id';
const REDIRECT_URI = 'http://localhost:3000/callback';
const AUTHORITY_URL = 'https://auth.example.com';
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
// Check for callback
if (window.location.search.includes('code=')) {
handleCallback();
}
}, []);
async function login() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateCodeVerifier();
// Store for callback
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `${AUTHORITY_URL}/authorize?${params}`;
}
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
// Verify state
if (state !== sessionStorage.getItem('state')) {
throw new Error('Invalid state');
}
const codeVerifier = sessionStorage.getItem('code_verifier');
// Exchange code for tokens
const response = await fetch(`${AUTHORITY_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier
})
});
const tokens = await response.json();
// Get user info
const userResponse = await fetch(`${AUTHORITY_URL}/userinfo`, {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
});
setUser(await userResponse.json());
// Clean up
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('state');
window.history.replaceState({}, '', '/');
}
return (
<div>
{user ? (
<p>Welcome, {user.name}!</p>
) : (
<button onClick={login}>Login</button>
)}
</div>
);
}import * as AuthSession from 'expo-auth-session';
import * as Crypto from 'expo-crypto';
const CLIENT_ID = 'your_client_id';
const AUTHORITY_URL = 'https://auth.example.com';
async function login() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const redirectUri = AuthSession.makeRedirectUri();
const result = await AuthSession.startAsync({
authUrl: `${AUTHORITY_URL}/authorize?` + new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: redirectUri,
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
})
});
if (result.type === 'success') {
const tokens = await exchangeCode(result.params.code, codeVerifier, redirectUri);
return tokens;
}
}{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"token_type": "Bearer",
"expires_in": 3600
}POST /token HTTP/1.1
Host: auth.example.com
Authorization: Basic YWJjMTIzOnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=dGhpcyBpcyBhIHJlZnJlc2g{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4...",
"token_type": "Bearer",
"expires_in": 3600
}class TokenManager {
constructor(tokens) {
this.tokens = tokens;
this.expiresAt = Date.now() + (tokens.expires_in * 1000);
this.scheduleRefresh();
}
scheduleRefresh() {
// Refresh 5 minutes before expiry
const refreshIn = this.expiresAt - Date.now() - (5 * 60 * 1000);
if (refreshIn > 0) {
setTimeout(() => this.refresh(), refreshIn);
}
}
async refresh() {
const response = await fetch('/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.tokens.refresh_token
})
});
this.tokens = await response.json();
this.expiresAt = Date.now() + (this.tokens.expires_in * 1000);
this.scheduleRefresh();
return this.tokens;
}
getAccessToken() {
return this.tokens.access_token;
}
}async function apiCall(url, options = {}) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${tokenManager.getAccessToken()}`
}
});
if (response.status === 401) {
// Token expired, refresh and retry
await tokenManager.refresh();
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${tokenManager.getAccessToken()}`
}
});
}
return response;
}{
"error": "invalid_grant",
"error_description": "The refresh token is invalid or expired"
}async function refresh() {
const response = await fetch('/token', {
method: 'POST',
// ...
});
if (!response.ok) {
const error = await response.json();
if (error.error === 'invalid_grant') {
// Refresh token invalid - require re-login
logout();
redirectToLogin();
return;
}
throw new Error(error.error_description);
}
return response.json();
}grant_type=refresh_token
&refresh_token=dGhpcyBpcyBhIHJlZnJlc2g
&scope=read # Originally had "read write"scope=openid profile email offline_accessPOST /token/revoke HTTP/1.1
Content-Type: application/x-www-form-urlencoded
token=dGhpcyBpcyBhIHJlZnJlc2g&token_type_hint=refresh_tokenUser → Your App → Authority → Your App Backend → TokenUser → Your App → Authority → Your App → Token (with PKCE)Your Service → Authority → TokenDevice → Authority → User on Browser → Device polls → TokenSubject: Reset your password
Click the link below to reset your password:
https://auth.example.com/reset-password?token=abc123...
This link expires in 1 hour.
If you didn't request this, ignore this email.{% extends "emails/base.html" %}
{% block content %}
<h1>Reset Your Password</h1>
<p>Hi {{ user.name }},</p>
<p>Click the button below to reset your password:</p>
<a href="{{ reset_url }}" class="button">Reset Password</a>
<p>This link expires in {{ expiry_minutes }} minutes.</p>
{% endblock %}POST /api/users/{id}/send-password-reset
Authorization: Bearer {admin_token}PATCH /api/users/{id}
Content-Type: application/json
Authorization: Bearer {admin_token}
{
"password": "NewSecurePassword123"
}PASSWORD_RESET_RATE_LIMIT=3
PASSWORD_RESET_RATE_WINDOW=3600HIDE_EMAIL_EXISTENCE=trueIf an account with that email exists, a reset link has been sent.{% extends "layout.html" %}
{% set title = "Reset Password" %}
{% block body %}
<main class="reset-form">
<h1>Choose a New Password</h1>
<form action="/reset-password" method="post">
<input type="hidden" name="token" value="{{ token }}">
<input type="password" name="password" placeholder="New Password" required>
<input type="password" name="password_confirmation" placeholder="Confirm Password" required>
<button type="submit">Reset Password</button>
</form>
</main>
{% endblock %}<h1>Password Reset Complete</h1>
<p>Your password has been updated.</p>
<a href="/signin">Sign In</a>// Request reset
const response = await fetch('/api/password-reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
// Complete reset
const response = await fetch('/api/password-reset/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: 'reset_token_from_email',
password: 'NewPassword123'
})
});GET /userinfo HTTP/1.1
Host: auth.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...POST /userinfo HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
access_token=eyJhbGciOiJSUzI1NiIs...{
"sub": "user-uuid",
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"preferred_username": "johnd",
"email": "john@example.com",
"email_verified": true,
"picture": "https://example.com/johnd/photo.jpg",
"locale": "en-US",
"updated_at": 1699996399
}{
"formatted": "123 Main St\nCity, State 12345",
"street_address": "123 Main St",
"locality": "City",
"region": "State",
"postal_code": "12345",
"country": "US"
}async function getUserInfo(accessToken) {
const response = await fetch('https://auth.example.com/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch user info');
}
return response.json();
}
// Usage
const user = await getUserInfo(tokens.access_token);
console.log(`Hello, ${user.name}!`);import requests
def get_userinfo(access_token):
response = requests.get(
'https://auth.example.com/userinfo',
headers={'Authorization': f'Bearer {access_token}'}
)
if response.status_code != 200:
raise Exception('Failed to fetch user info')
return response.json()
user = get_userinfo(tokens['access_token'])
print(f"Hello, {user['name']}!")HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token is invalid"
{
"error": "invalid_token",
"error_description": "The access token is invalid"
}HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token has expired"
{
"error": "invalid_token",
"error_description": "The access token has expired"
}HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="openid profile"
{
"error": "insufficient_scope",
"error_description": "The access token lacks the required scope"
}class UserInfoCache {
constructor(ttl = 60000) { // 1 minute
this.cache = new Map();
this.ttl = ttl;
}
async get(accessToken) {
const cached = this.cache.get(accessToken);
if (cached && Date.now() < cached.expiresAt) {
return cached.data;
}
const data = await fetchUserInfo(accessToken);
this.cache.set(accessToken, {
data,
expiresAt: Date.now() + this.ttl
});
return data;
}
}{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"registration_endpoint": "https://auth.example.com/register",
"revocation_endpoint": "https://auth.example.com/token/revoke",
"introspection_endpoint": "https://auth.example.com/token/introspect",
"device_authorization_endpoint": "https://auth.example.com/device",
"scopes_supported": [
"openid",
"profile",
"email",
"address",
"phone",
"offline_access"
],
"response_types_supported": [
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token"
],
"response_modes_supported": [
"query",
"fragment"
],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
],
"claims_supported": [
"sub",
"iss",
"aud",
"exp",
"iat",
"auth_time",
"nonce",
"name",
"given_name",
"family_name",
"email",
"email_verified",
"picture",
"locale"
],
"code_challenge_methods_supported": [
"S256",
"plain"
]
}async function discoverProvider(issuer) {
const response = await fetch(
`${issuer}/.well-known/openid-configuration`
);
return response.json();
}
// Use discovered configuration
const config = await discoverProvider('https://auth.example.com');
// Now use the endpoints
const authUrl = new URL(config.authorization_endpoint);
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('response_type', 'code');
// ...import requests
def discover_provider(issuer):
response = requests.get(
f"{issuer}/.well-known/openid-configuration"
)
return response.json()
config = discover_provider('https://auth.example.com')
print(f"Auth endpoint: {config['authorization_endpoint']}")class ProviderConfig {
constructor(issuer) {
this.issuer = issuer;
this.config = null;
this.expiresAt = null;
}
async getConfig() {
// Cache for 1 hour
if (this.config && Date.now() < this.expiresAt) {
return this.config;
}
const response = await fetch(
`${this.issuer}/.well-known/openid-configuration`
);
this.config = await response.json();
this.expiresAt = Date.now() + (60 * 60 * 1000);
return this.config;
}
}function validateConfig(config, expectedIssuer) {
if (config.issuer !== expectedIssuer) {
throw new Error('Issuer mismatch');
}
if (!config.authorization_endpoint) {
throw new Error('Missing authorization endpoint');
}
if (!config.scopes_supported.includes('openid')) {
throw new Error('OpenID scope not supported');
}
return true;
}CRYSTAL_ENV=development
PORT=4000
BASE_URL=http://localhost:4000
DATABASE_URL=postgres://localhost:5432/authority_db
SECRET_KEY=dev-secret-key
CRYSTAL_LOG_LEVEL=debugCRYSTAL_ENV=production
PORT=4000
BASE_URL=https://auth.example.com
DATABASE_URL=postgres://user:pass@db:5432/authority_db?sslmode=require
SECRET_KEY=your-production-secret-key
CRYSTAL_WORKERS=8
CRYSTAL_LOG_LEVEL=info
# Security
LOCKOUT_THRESHOLD=5
LOCKOUT_DURATION=30
PASSWORD_MIN_LENGTH=12
REQUIRE_ADMIN_MFA=true
# Tokens
ACCESS_TOKEN_TTL=3600
REFRESH_TOKEN_TTL=2592000
# Redis
REDIS_URL=redis://redis:6379
# Email
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=your-smtp-password
SMTP_FROM=noreply@example.comUser → Client → Authority → Client (with token in URL fragment)GET /authorize?
response_type=token
&client_id=abc123
&redirect_uri=https://app.example.com/callback
&scope=openid%20profile
&state=xyz789https://app.example.com/callback#
access_token=eyJhbGciOiJSUzI1NiIs...
&token_type=Bearer
&expires_in=3600
&state=xyz789// Redirect with response_type=token
window.location = '/authorize?response_type=token&client_id=...';
// Get token from URL fragment
const token = new URLSearchParams(window.location.hash.slice(1)).get('access_token');// Generate PKCE values
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
// Redirect with response_type=code
window.location = `/authorize?response_type=code&client_id=...&code_challenge=${challenge}&code_challenge_method=S256`;
// Exchange code for token
const tokenResponse = await fetch('/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
code_verifier: verifier,
client_id: CLIENT_ID
})
});POST /token HTTP/1.1
Host: auth.example.com
Authorization: Basic YWJjMTIzOnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=user@example.com
&password=secret123
&scope=openid%20profile{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"scope": "openid profile"
}ENABLE_PASSWORD_GRANT=true
PASSWORD_GRANT_ALLOWED_CLIENTS=trusted-app-1,trusted-app-2// Instead of collecting password in app
// Redirect to Authority's login page
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile',
code_challenge: challenge,
code_challenge_method: 'S256'
});
// Open in-app browser
openAuthUrl(`${AUTHORITY_URL}/authorize?${params}`);// Redirect to Authority for login
window.location = `${AUTHORITY_URL}/authorize?response_type=code&...`;https://your-authority-domain/auth/facebook/callbackFACEBOOK_OAUTH_ENABLED=true
FACEBOOK_CLIENT_ID=your-facebook-app-id
FACEBOOK_CLIENT_SECRET=your-facebook-app-secret<a href="https://your-authority-domain/auth/facebook" class="btn-facebook">
<svg><!-- Facebook icon --></svg>
Continue with Facebook
</a>const forwardUrl = btoa('https://your-app.com/dashboard');
const facebookAuthUrl = `https://your-authority-domain/auth/facebook?forward_url=${forwardUrl}`;https://your-authority-domain/auth/facebook/callbackhttps://app.example.com/callback?
code=AUTH_CODE_HERE
&state=xyz789POST /token
grant_type=authorization_code
&code=AUTH_CODE_HERE
&redirect_uri=https://app.example.com/callback
&client_id=abc123
&client_secret=secret123{
"access_token": "eyJhbG...",
"refresh_token": "dGhpc...",
"expires_in": 3600
}GET /api/user
Authorization: Bearer eyJhbG...┌──────────────────────────────────────────────────┐
│ │
│ Resource Owner Authorization Server │
│ (User) (Authority) │
│ │ │ │
│ │ authorizes │ issues tokens │
│ ▼ ▼ │
│ Client ◄─────────────────────────────────► │
│ (App) Resource Server │
│ (API) │
│ │
└──────────────────────────────────────────────────┘Full User Data
├── profile (name, picture)
├── email (email address)
├── read (read-only access)
├── write (modify data)
└── admin (administrative access)scope=profile email read"App X wants to access your profile and email"User → Client → Authority → User (login) → Client (code) → Authority → Client (tokens)Service → Authority (client_id + secret) → Service (token)Device → Authority (get code)
Device → User: "Go to URL, enter code"
User → Authority (enter code, approve)
Device → Authority (poll for token)GET /authorize?
response_type=code
&client_id=abc123
&redirect_uri=https://app.example.com/callback
&scope=profile email
&state=xyz789 Attacker
│
┌─────────┐ │ ┌─────────┐
│ Client │───code_challenge───┼───────────────────►│Authority│
│ │◄──authorization_code◄──────────────────│ │
│ │───code_verifier────┼────────────────────►│ │
│ │◄──access_token─────┼────────────────────│ │
└─────────┘ │ └─────────┘
│
Attacker has code,
but can't prove
they know verifier{
"iss": "https://auth.example.com",
"sub": "user-uuid",
"aud": "client-id",
"exp": 1699999999,
"iat": 1699996399,
"auth_time": 1699996300,
"nonce": "abc123",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true
}{
"sub": "user-uuid",
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"email": "john@example.com",
"email_verified": true,
"picture": "https://..."
}GET /.well-known/openid-configuration// 1. Request authorization with openid scope
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: state,
nonce: nonce // For ID token validation
});
window.location = `${AUTHORITY_URL}/authorize?${params}`;
// 2. Exchange code for tokens
const tokens = await fetch('/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI
})
}).then(r => r.json());
// 3. Validate and use ID token
const idToken = parseIdToken(tokens.id_token);
console.log('User:', idToken.name);
// 4. Optionally fetch more claims
const userinfo = await fetch('/userinfo', {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
}).then(r => r.json());{
"client_id": "my-client",
"access_token_ttl": 7200,
"refresh_token_ttl": 604800,
"refresh_token_rotation": true
}ACCESS_TOKEN_TTL=900 # 15 minutes
REFRESH_TOKEN_TTL=86400 # 1 day
REFRESH_TOKEN_ROTATION=true
CODE_TTL=300 # 5 minutesACCESS_TOKEN_TTL=86400 # 1 day
REFRESH_TOKEN_TTL=7776000 # 90 days
REFRESH_TOKEN_ROTATION=falseACCESS_TOKEN_TTL=3600 # 1 hour
REFRESH_TOKEN_TTL=0 # Disabled┌────────────────────────────────────────┐
│ OpenID Connect │
│ (Authentication) │
├────────────────────────────────────────┤
│ OAuth 2.0 │
│ (Authorization) │
└────────────────────────────────────────┘{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "client-abc",
"exp": 1699999999,
"iat": 1699996399,
"auth_time": 1699996300,
"nonce": "abc123",
"name": "John Doe",
"email": "john@example.com"
}scope=openid profile emailID Token: "This is John, authenticated 5 minutes ago"
→ Use for login decisions
Access Token: "John has read access to documents"
→ Use for API authorizationGET /userinfo
Authorization: Bearer access_token{
"sub": "user-123",
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"email": "john@example.com",
"picture": "https://..."
}GET /.well-known/openid-configuration{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json"
}User clicks "Login" → Redirect to Authority → User logs in →
Redirect back with tokens → User is logged inUser logged into Authority → Visits App A → Already authenticated →
Visits App B → Already authenticated (no re-login)Client → Authority (prompt=none) →
If logged in: Return tokens
If not: Return errorusers
├── id (UUID)
├── email (unique)
├── password_hash
├── name
├── mfa_secret
├── mfa_enabled
├── locked
├── locked_at
├── failed_attempts
├── role
├── created_at
└── updated_atclients
├── id (UUID)
├── client_id (unique)
├── client_secret_hash
├── name
├── redirect_uris (array)
├── grant_types (array)
├── scopes (array)
├── client_type
├── created_at
└── updated_at┌─────────────────────────────────────┐
│ Rate Limiting │
├─────────────────────────────────────┤
│ Input Validation │
├─────────────────────────────────────┤
│ Authentication │
├─────────────────────────────────────┤
│ Authorization │
├─────────────────────────────────────┤
│ Business Logic │
├─────────────────────────────────────┤
│ Data Validation │
├─────────────────────────────────────┤
│ Audit Logging │
└─────────────────────────────────────┘ ┌─────────────┐
│ Load │
│ Balancer │
└──────┬──────┘
┌──────────────┼──────────────┐
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Authority │ │ Authority │ │ Authority │
│ Instance │ │ Instance │ │ Instance │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
└──────────────┼──────────────┘
┌──────┴──────┐
┌─────────────┴─────────────┴─────────────┐
│ PostgreSQL │
│ (Primary) │
└─────────────┬─────────────┬─────────────┘
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ Replica │ │ Replica │
└───────────┘ └───────────┘eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGhvcml0eS1rZXktMSJ9.
eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLXV1aWQi...
.signature{
"alg": "RS256",
"typ": "JWT",
"kid": "authority-key-1"
}{
"iss": "https://auth.example.com",
"sub": "user-uuid",
"aud": "client-id",
"exp": 1699999999,
"iat": 1699996399,
"auth_time": 1699996300,
"nonce": "n-0S6_WzA2Mj",
"at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true
}const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json'
});
async function validateIdToken(idToken, expectedNonce) {
return new Promise((resolve, reject) => {
const getKey = (header, callback) => {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
};
jwt.verify(idToken, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'your_client_id'
}, (err, decoded) => {
if (err) {
reject(err);
return;
}
// Check nonce
if (expectedNonce && decoded.nonce !== expectedNonce) {
reject(new Error('Invalid nonce'));
return;
}
// Check auth_time if max_age was requested
// Check other claims as needed
resolve(decoded);
});
});
}import jwt
from jwt import PyJWKClient
jwks_client = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
def validate_id_token(id_token, expected_nonce=None):
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
claims = jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256"],
issuer="https://auth.example.com",
audience="your_client_id"
)
if expected_nonce and claims.get('nonce') != expected_nonce:
raise ValueError('Invalid nonce')
return claimsconst nonce = generateRandomString();
sessionStorage.setItem('oidc_nonce', nonce);
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
nonce: nonce
});const savedNonce = sessionStorage.getItem('oidc_nonce');
const claims = await validateIdToken(tokens.id_token, savedNonce);
sessionStorage.removeItem('oidc_nonce');function validateAtHash(idToken, accessToken) {
const claims = jwt.decode(idToken);
if (claims.at_hash) {
const hash = crypto.createHash('sha256').update(accessToken).digest();
const expectedHash = hash.slice(0, hash.length / 2);
const calculatedAtHash = base64url.encode(expectedHash);
if (claims.at_hash !== calculatedAtHash) {
throw new Error('at_hash mismatch');
}
}
}Your App → Authority → User Login → Your App
→ API Access# Clone and run
git clone https://github.com/azutoolkit/authority.git
cd authority
docker-compose up -d
# Visit http://localhost:4000# Strict account lockout
LOCKOUT_THRESHOLD=3
LOCKOUT_DURATION=60
PROGRESSIVE_LOCKOUT=true
# Strong passwords
PASSWORD_MIN_LENGTH=16
REQUIRE_UPPERCASE=true
REQUIRE_LOWERCASE=true
REQUIRE_NUMBERS=true
REQUIRE_SPECIAL=true
PASSWORD_EXPIRY_DAYS=90
CHECK_COMMON_PASSWORDS=true
# MFA required
REQUIRE_MFA=true
REQUIRE_ADMIN_MFA=true
# Short sessions
SESSION_DURATION_DAYS=1
IDLE_TIMEOUT_MINUTES=15
SINGLE_SESSION=true
# Notifications
NOTIFY_NEW_SESSION=true
# IP restrictions
ADMIN_ALLOWED_IPS=10.0.0.0/8# Lenient lockout
LOCKOUT_THRESHOLD=10
LOCKOUT_DURATION=15
ENABLE_AUTO_UNLOCK=true
# Reasonable passwords
PASSWORD_MIN_LENGTH=10
REQUIRE_UPPERCASE=false
REQUIRE_LOWERCASE=false
REQUIRE_NUMBERS=false
PASSWORD_EXPIRY_DAYS=0
# MFA optional except admins
REQUIRE_MFA=false
REQUIRE_ADMIN_MFA=true
# Longer sessions
SESSION_DURATION_DAYS=30
IDLE_TIMEOUT_MINUTES=60Creation: User approves consent
Lifetime: 10 minutes (configurable)
Exchange: POST /token (one-time use)
Deletion: After exchange or expirationCreation: Token endpoint (code exchange or refresh)
Lifetime: 1 hour (configurable)
Usage: Bearer token in API requests
Expiration: Rejected after TTL{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "client-abc",
"exp": 1699999999,
"scope": "read write"
}Creation: Token endpoint (with offline_access scope)
Lifetime: 30 days (configurable)
Usage: POST /token with grant_type=refresh_token
Rotation: New refresh token on each useTime T: Client has Refresh Token A
Time T+1: Client uses Refresh Token A
↓
New Access Token issued
New Refresh Token B issued
Refresh Token A invalidated
Time T+2: Attacker tries Refresh Token A
↓
Rejected (already used)
Alert: Possible token theftRefresh Token A used at T
Refresh Token A still valid for 2 seconds
After 2 seconds: Only Refresh Token B validCreation: Token endpoint (with openid scope)
Lifetime: 1 hour (typically)
Usage: Validate at login time
Storage: Client-side (for user info display)Session Revoked
├── Access Tokens invalidated
└── Refresh Tokens invalidatedPOST /token/introspect
↓
{
"active": false // Token revoked
}┌─────────────────────────────────────────┐
│ PostgreSQL │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Access │ │ Refresh │ │ Auth │ │
│ │ Tokens │ │ Tokens │ │ Codes │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Redis │
│ ┌─────────────────────────────────┐ │
│ │ Token Cache (optional) │ │
│ │ Session Storage │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘// Refresh 5 minutes before expiry
scheduleRefresh(expiresIn - 300);// Refresh on 401 response
if (response.status === 401) {
await refresh();
retry();
}async function apiCall(endpoint) {
// Check token validity
if (isExpired(accessToken)) {
accessToken = await refresh();
}
return fetch(endpoint, {
headers: { Authorization: `Bearer ${accessToken}` }
});
}