Wenme OAuth 2.1 Integration
Implement enterprise-grade passwordless authentication in minutes with our secure OAuth 2.1 platform. Full PKCE support, OpenID Connect, and zero passwords.
Documentation
Quick Start
Prerequisites
Integration Code
// 1. Redirect users to Wenme OAuth authorization
const authUrl = new URL('https://identity.wenme.net/oauth/authorize');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'YOUR_CALLBACK_URL');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'openid profile email');
authUrl.searchParams.append('state', generateRandomState());
// 2. PKCE (mandatory in OAuth 2.1)
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
window.location.href = authUrl.toString();Complete OAuth 2.1 Integration Guide
Step 1: Automatic Endpoint Discovery (Recommended)
Wenme supports OpenID Connect Discovery. Simply fetch this endpoint to get all OAuth configuration:
GET https://wenme.net/.well-known/openid-configurationReturns all endpoints, supported scopes, and algorithms in JSON. Use this for automatic configuration.
Step 2: Configure Your OAuth Client
For NextAuth.js or similar libraries:
{
id: "wenme",
name: "Wenme",
type: "oauth",
wellKnown: "https://wenme.net/.well-known/openid-configuration",
authorization: {
params: {
scope: "openid profile email"
}
},
clientId: process.env.WENME_CLIENT_ID,
clientSecret: process.env.WENME_CLIENT_SECRET,
idToken: true,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture
}
}
}OAuth Endpoints (All Return JSON)
https://identity.wenme.net/oauth/authorizehttps://identity.wenme.net/oauth/tokenhttps://identity.wenme.net/oauth/userinfohttps://identity.wenme.net/.well-known/jwks.jsonhttps://identity.wenme.net/oauth/revokehttps://identity.wenme.net/oauth/introspecthttps://identity.wenme.net/oauth/end_sessionOAuth Authorization URL Format
Complete Authorization URL Structure
All parameters are REQUIRED except where noted. Missing response_type=code will result in an error.
https://identity.wenme.net/oauth/authorize?
client_id=YOUR_CLIENT_ID&
redirect_uri=YOUR_CALLBACK_URL&
response_type=code&
scope=openid+profile+email&
state=RANDOM_STATE&
code_challenge=PKCE_CHALLENGE&
code_challenge_method=S256✅ Required Parameters
client_idYour application's client IDredirect_uriMust match registered URI exactlyresponse_typeMust be "code" (OAuth 2.1)scopeSpace-separated permissionsstateRandom string for CSRF protectioncode_challengePKCE challenge (base64url)code_challenge_methodMust be "S256"📋 Available Scopes
openidOpenID Connect authenticationprofileUser's profile informationemailUser's email addressoffline_accessRefresh token for long-lived accessNote: PKCE is mandatory in OAuth 2.1. The implicit grant flow is not supported.
Setup Your Application
Register Your Application
Visit the Organization Dashboard to create your OAuth application.
Store Credentials Securely
Never expose your Client Secret in client-side code!
# .env file (server-side only)WENME_CLIENT_ID=wenme_abc123... WENME_CLIENT_SECRET=secret_xyz789... WENME_REDIRECT_URI=https://yourapp.com/auth/callback
OAuth 2.1 Flow
Full OAuth 2.1 Compliance (RFC 9207)
Wenme implements the latest OAuth 2.1 standard with mandatory security enhancements:
Authorization Request
Redirect user to Wenme authorization endpoint with PKCE parameters.
// 1. Redirect users to Wenme OAuth authorization
const authUrl = new URL('https://identity.wenme.net/oauth/authorize');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'YOUR_CALLBACK_URL');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'openid profile email');
authUrl.searchParams.append('state', generateRandomState());
// 2. PKCE (mandatory in OAuth 2.1)
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
window.location.href = authUrl.toString();Token Exchange
Exchange authorization code for access tokens.
// 3. Exchange authorization code for tokens
const response = await fetch('https://identity.wenme.net/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
code: authorizationCode,
redirect_uri: 'YOUR_CALLBACK_URL',
code_verifier: codeVerifier // PKCE verifier (mandatory in OAuth 2.1)
})
});
const tokens = await response.json();
// tokens.access_token, tokens.id_token, tokens.refresh_tokenGet User Information
Use access token to fetch user profile.
// 4. Get user information
const userResponse = await fetch('https://identity.wenme.net/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
const user = await userResponse.json();
// user.sub, user.email, user.name, user.picture (avatar URL)PKCE Security
Important Security Notice
PKCE (Proof Key for Code Exchange) is mandatory in OAuth 2.1 for ALL clients, including confidential clients. This prevents authorization code interception attacks and is no longer optional.
PKCE Implementation
// Generate code verifier (43-128 characters)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Generate 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 btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}Code Examples
JavaScript Quick Start
// 1. Redirect users to Wenme OAuth authorization
const authUrl = new URL('https://identity.wenme.net/oauth/authorize');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'YOUR_CALLBACK_URL');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'openid profile email');
authUrl.searchParams.append('state', generateRandomState());
// 2. PKCE (mandatory in OAuth 2.1)
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
// 3. Exchange authorization code for tokens
const response = await fetch('https://identity.wenme.net/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
code: authorizationCode,
redirect_uri: 'YOUR_CALLBACK_URL',
code_verifier: codeVerifier // PKCE verifier (mandatory in OAuth 2.1)
})
});
const tokens = await response.json();
// tokens.access_token, tokens.id_token, tokens.refresh_token
// 4. Get user information
const userResponse = await fetch('https://identity.wenme.net/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
const user = await userResponse.json();
// user.sub, user.email, user.name, user.picture (avatar URL)Logout & Token Management
Three Ways to Handle Logout
1. Token Revocation (Recommended)
Immediately invalidate tokens on the authorization server. Best for security.
POST https://identity.wenme.net/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=ACCESS_TOKEN&
token_type_hint=access_token&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET2. End Session (OpenID Connect)
Logs user out from Wenme and optionally redirects back to your app.
GET https://identity.wenme.net/oauth/end_session?
id_token_hint=ID_TOKEN&
post_logout_redirect_uri=https://yourapp.com/logout-complete&
state=xyz1233. Client-Side Only
Simply delete tokens from local storage. Token remains valid until expiry.
// JavaScript
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('refresh_token');
// Redirect to login or home
window.location.href = '/';Logout Implementation Examples
NextAuth.js with Token Revocation
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
export default NextAuth({
// ... provider config
events: {
async signOut({ token }) {
// Revoke token when user signs out
if (token?.accessToken) {
await fetch('https://identity.wenme.net/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
token: token.accessToken,
token_type_hint: 'access_token',
client_id: process.env.WENME_CLIENT_ID,
client_secret: process.env.WENME_CLIENT_SECRET,
}),
});
}
},
},
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token
token.idToken = account.id_token
}
return token
},
},
})Express.js with End Session
// Express logout route
app.post('/logout', async (req, res) => {
const { id_token } = req.session;
// Clear server session
req.session.destroy();
// Build Wenme logout URL
const logoutUrl = new URL('https://identity.wenme.net/oauth/end_session');
logoutUrl.searchParams.append('id_token_hint', id_token);
logoutUrl.searchParams.append(
'post_logout_redirect_uri',
'https://yourapp.com/logout-complete'
);
logoutUrl.searchParams.append('state', generateRandomState());
// Redirect to Wenme logout
res.redirect(logoutUrl.toString());
});
// Logout complete callback
app.get('/logout-complete', (req, res) => {
// Verify state parameter
if (req.query.state !== expectedState) {
return res.status(400).send('Invalid state');
}
res.redirect('/');
});React SPA with Token Revocation
// React logout hook
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
export function useLogout() {
const navigate = useNavigate();
const logout = useCallback(async () => {
const token = localStorage.getItem('access_token');
if (token) {
// Revoke token
try {
await fetch('https://identity.wenme.net/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
token,
token_type_hint: 'access_token',
client_id: process.env.REACT_APP_WENME_CLIENT_ID,
// Note: Don't include client_secret in frontend apps
}),
});
} catch (error) {
console.error('Token revocation failed:', error);
}
}
// Clear local storage
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('refresh_token');
// Redirect to login
navigate('/login');
}, [navigate]);
return logout;
}Token Introspection
Check if a token is still valid without making an API call.
POST https://identity.wenme.net/oauth/introspect
Content-Type: application/x-www-form-urlencoded
token=ACCESS_TOKEN&
token_type_hint=access_token&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRETResponse (Active Token):
{
"active": true,
"scope": "openid profile email",
"client_id": "your_client_id",
"username": "user@example.com",
"exp": 1640995200
}Response (Inactive/Invalid Token):
{
"active": false
}API Endpoints
| Endpoint | Method | Description |
|---|---|---|
| /oauth/authorize | GET | OAuth authorization endpoint |
| /oauth/token | POST | Exchange code for tokens |
| /oauth/userinfo | GET | Get user information (OIDC UserInfo) |
| /oauth/revoke | POST | Revoke access token |
| /.well-known/openid-configuration | GET | OpenID Connect discovery |
| /.well-known/jwks.json | GET | JSON Web Key Set |
API Keys (Server-to-Server)
API keys enable autonomous server-to-server calls without user sessions. Perfect for backend services, cron jobs, and automated integrations that need to call Wenme APIs programmatically.
When to Use API Keys
- Autonomous operations: Backend services that need to send invitations, manage members, etc.
- Scheduled tasks: Cron jobs that sync users or send bulk invitations
- No user context: Operations that don't have a logged-in user session
- Service-to-service: Your server calling Wenme APIs directly
Note: API keys bypass session cookies and CSRF tokens. They authenticate the application, not a user.
API Key Format
API keys follow a consistent format for easy identification:
wm_pk_<32 random hex characters> Example: wm_pk_ab1234567890abcdef1234567890abcd
wm_pk_- Wenme API key prefix- First 2 chars after prefix are used for key lookup (prefix)
- Full key is hashed with bcrypt (never stored in plaintext)
Using API Keys
Include the API key in the Authorization header:
Request Example
curl -X POST https://identity.wenme.net/api/tenant/invite \
-H "Authorization: Bearer wm_pk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "your-organization-uuid",
"email": "newuser@example.com",
"role": "member"
}'JavaScript/TypeScript Example
const response = await fetch('https://identity.wenme.net/api/tenant/invite', {
method: 'POST',
headers: {
'Authorization': 'Bearer wm_pk_your_api_key_here',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenant_id: 'your-organization-uuid',
email: 'newuser@example.com',
role: 'member'
})
});
const result = await response.json();
console.log(result);API Key Management Endpoints
Create, list, and revoke API keys (requires session auth)
| Endpoint | Method | Description |
|---|---|---|
| /api/keys | POST | Create new API key |
| /api/keys/tenant/:tenantId | GET | List API keys for organization |
| /api/keys/:keyId | DELETE | Revoke an API key |
Creating an API Key
Create API keys via the management API or the Wenme console.
POST /api/keys
{
"name": "PV Production Integration",
"tenant_id": "your-organization-uuid",
"scopes": ["invite:write", "invite:read"],
"description": "API key for automated user invitations",
"expires_in_days": 365 // Optional, 0 = never expires
}Response (Success)
{
"success": true,
"message": "API key created successfully. Save this key - it won't be shown again!",
"api_key": {
"id": "key-uuid",
"name": "PV Production Integration",
"key": "wm_pk_ab1234567890abcdef1234567890abcd", // Only shown once!
"key_prefix": "wm_pk_ab",
"scopes": ["invite:write", "invite:read"],
"expires_at": "2026-12-04T00:00:00Z",
"created_at": "2025-12-04T00:00:00Z"
}
}Important: The full API key is only shown once at creation time. Store it securely immediately. If lost, you must create a new key.
Available Scopes
API keys use scope-based permissions to control access:
| Scope | Description |
|---|---|
| invite:write | Send team invitations |
| invite:read | List/view invitations |
| members:read | List organization members |
| members:write | Manage organization members |
| org:read | Read organization details |
| org:write | Update organization settings |
| users:read | Read user profiles |
| apps:read | List OAuth applications |
| apps:write | Manage OAuth applications |
| * | Full access (all scopes) |
Security Features
- ✓Rate Limiting: Default 60 requests/minute, 10,000 requests/day per key
- ✓IP Whitelisting: Optionally restrict keys to specific IP addresses/CIDRs
- ✓Tenant Binding: Keys are bound to a specific organization and cannot access other orgs
- ✓Audit Logging: All API key usage is logged for compliance and debugging
- ✓Key Expiration: Optional expiration dates for time-limited access
- ✓Instant Revocation: Keys can be revoked immediately if compromised
Error Responses
Invalid API Key (401)
{
"error": "invalid_api_key",
"message": "Invalid API key"
}Insufficient Scope (403)
{
"error": "insufficient_scope",
"message": "API key does not have required scope: invite:write",
"required_scope": "invite:write"
}Tenant Mismatch (403)
{
"error": "tenant_mismatch",
"message": "API key can only invite to its own organization"
}Rate Limit Exceeded (429)
{
"error": "invalid_api_key",
"message": "Rate limit exceeded"
}Organization & Team APIs
APIs for managing organizations, team members, and invitations. Supports both session-based authentication (browser) and API key authentication (server-to-server).
| Endpoint | Method | Description |
|---|---|---|
| /api/tenant/invite | POST | Send team invitation email |
| /api/tenant/:tenantId/invitations | GET | List pending invitations for organization |
| /api/tenant/invitation/:invitationId | DELETE | Cancel pending invitation |
| /api/invitation/accept | POST | Accept invitation (public endpoint) |
| /api/invitation/send-magic-link | POST | Send magic link for invitation acceptance |
| /api/tenant/:tenantId/members | GET | List organization members |
| /api/user/organizations | GET | List user's organizations |
POST /api/tenant/invite
Send an invitation to join an organization. Requires admin or owner role.
Request Headers
Content-Type: application/json Cookie: auth_session=<session_token> X-CSRF-Token: <csrf_token>
Request Body
{
"tenant_id": "uuid-of-organization",
"email": "newuser@example.com",
"role": "member" // Options: owner, admin, member, viewer, billing
}Response (Success)
{
"success": true,
"message": "Invitation sent successfully",
"invitation": {
"id": "invitation-uuid",
"email": "newuser@example.com",
"role": "member",
"status": "pending",
"expires_at": "2025-12-11T00:00:00Z"
}
}Note: Invitations expire after 7 days. The invitee receives an email with a magic link to accept the invitation.
User Profile APIs
APIs for user profile management. All endpoints require authentication.
| Endpoint | Method | Description |
|---|---|---|
| /api/profile | GET | Get current user's profile |
| /api/profile | PUT | Update user profile |
| /api/profile/avatar | POST | Upload profile picture |
| /api/profile/username | PUT | Change username |
| /api/profile/email/change | POST | Request email change (requires passkey verification) |
| /api/user/:username | GET | Get public profile by username |
GET /api/profile Response
{
"success": true,
"user": {
"id": "uuid",
"email": "user@example.com",
"username": "johndoe",
"name": "John Doe",
"avatar_url": "https://storage.wenme.net/...",
"bio": "Software developer",
"location": "Dhaka, Bangladesh",
"website": "https://example.com",
"totp_enabled": true,
"passkey_enabled": true,
"email_verified": true,
"phone_number": "+880...",
"date_of_birth": "1990-01-01",
"linkedin_url": "https://linkedin.com/in/...",
"github_url": "https://github.com/...",
"created_at": "2025-01-01T00:00:00Z"
}
}Authentication APIs
APIs for passwordless authentication including passkeys, TOTP, and magic links.
| Endpoint | Method | Description |
|---|---|---|
| Passkey (WebAuthn) Endpoints | ||
| /api/passkey/register/start | POST | Begin passkey registration |
| /api/passkey/register/finish | POST | Complete passkey registration |
| /api/passkey/authenticate/start | POST | Begin passkey authentication |
| /api/passkey/authenticate/finish | POST | Complete passkey authentication |
| /api/passkey/list | GET | List user's registered passkeys |
| /api/passkey/:id | DELETE | Remove a passkey |
| Cross-Device QR Authentication | ||
| /api/auth/qr/create | POST | Generate QR code for cross-device login (desktop) |
| /api/auth/qr/scan | POST | Mark QR session as scanned, get user email (phone) |
| /api/auth/qr/confirm | POST | Confirm authentication with passkey credential (phone) |
| /api/auth/qr/status/:token | GET | Poll QR session status; returns session cookie when confirmed (desktop) |
| TOTP/Authenticator Endpoints | ||
| /api/auth/totp/generate | POST | Generate QR code and backup codes for setup |
| /api/auth/totp/verify | POST | Verify and enable authenticator |
| /api/auth/totp/disable | POST | Disable authenticator (requires code) |
| /api/auth/totp/validate | POST | Validate TOTP code during login (public) |
| /api/auth/mfa/verify | POST | Verify MFA code and create session |
| Magic Link Endpoints | ||
| /auth/magic-link | POST | Send magic link email (body: email) |
| /api/auth/verify-magic-link | POST | Verify magic link token and create session |
| /api/auth/verify-magic-link-redirect | GET | Verify magic link with redirect (from email links) |
| Session & Security Endpoints | ||
| /api/auth/methods/check | POST | Check available auth methods for user (public) |
| /api/auth/csrf-token | GET | Get CSRF token for authenticated requests |
| /api/sessions | GET | List active sessions |
| /api/sessions/:id/revoke | POST | Revoke a specific session |
| /auth/logout | POST/GET | Logout and clear session |
Base URL: All authentication APIs are served from https://identity.wenme.net
OAuth Scopes
| Scope | Description | Claims Returned |
|---|---|---|
| openid | Required for OIDC | sub |
| profile | User profile information | name, picture, given_name, family_name, preferred_username, updated_at |
| User email address | email, email_verified | |
| phone | User phone number | phone_number, phone_number_verified |
| address | User address | address (structured object) |
| offline_access | Refresh token | Enables refresh_token in token response |
Security Note: External applications only receive standard OIDC claims. Internal platform data (roles, permissions, tenant IDs) is never exposed to third-party applications.
Domain Configuration
App Domain Feature
Configure your application domain to automatically manage redirect URIs. Set your app domain once, and all standard OAuth callback paths will be configured.
• https://newsforge.news/callback
• https://newsforge.news/auth/callback
• https://newsforge.news/oauth/callback
• https://newsforge.news/api/auth/callback
• https://newsforge.news/signin-callback
CNAME Support
Wenme OAuth fully supports CNAME records for custom domains. Use subdomains that point to your actual servers while maintaining a consistent brand.
Example CNAME Setup:
api.newsforge.news CNAME newsforge-api.aws.com
https://api.newsforge.news/oauth/callback ✅
SSL Certificate Required
Ensure valid SSL certificates for all CNAME domains
Use CNAME Domain in URIs
Register redirect URIs with the CNAME domain, not the target
Multiple Variations Supported
Register all domain variations (www, subdomains, etc.)
Security Best Practices
OAuth 2.1 Security Requirements
Always use PKCE
PKCE is mandatory for all OAuth 2.1 flows, even for confidential clients
Validate state parameter
Always verify the state parameter to prevent CSRF attacks
Use HTTPS everywhere
All redirect URIs must use HTTPS in production (localhost allowed for development)
Secure token storage
Never store tokens in localStorage or sessionStorage. Use secure HTTP-only cookies or server-side storage
Rotate refresh tokens
Always exchange refresh tokens for new ones to detect token replay attacks
Validate redirect URIs
Register all redirect URIs and use exact string matching (no wildcards)
Common Security Mistakes to Avoid
- ❌ Using implicit flow (removed in OAuth 2.1)
- ❌ Storing client secrets in frontend code
- ❌ Not validating the state parameter
- ❌ Using localStorage for token storage
- ❌ Not implementing PKCE
- ❌ Using wildcard redirect URIs
- ❌ Not using HTTPS in production
- ❌ Long-lived access tokens (use short expiry + refresh tokens)
Banking-Grade Security
EnterpriseWenme implements security controls that meet financial institution requirements:
Distributed Rate Limiting
Redis-based rate limiting works across all server instances. Automatic IP blocking on abuse detection.
Session Timeouts
30-minute idle timeout and 7-day absolute timeout. Automatic cleanup of stale sessions.
Security Event Logging
SIEM-ready JSON logs. Email and Slack alerting for critical security events.
Magic Link Protection
15-minute expiry, 3 requests per 15 minutes rate limit. Per-email and per-IP tracking.
Image Upload Hardening
Magic byte validation, dimension limits, EXIF stripping. SVG XSS protection.
CSRF Protection
Token-based validation on all state-changing endpoints. Automatic token refresh.
Cross-Device QR Authentication
WhatsApp Web-style login with desktop binding cookies, atomic state transitions, and anti-enumeration.
Open Redirect Protection
Domain allowlist prevents attackers from redirecting users to malicious sites after login.
Advanced Passkeys
Conditional UI (Passkey Autofill)
Wenme supports WebAuthn Conditional UI, allowing passkey credentials to appear in browser autofill prompts. Users see their passkey alongside saved passwords and can authenticate with a single tap.
// Enable Conditional UI in your login form
const credential = await navigator.credentials.get({
publicKey: {
challenge: serverChallenge,
rpId: 'wenme.net',
allowCredentials: [], // Empty = show all available passkeys
userVerification: 'preferred'
},
mediation: 'conditional' // Enables autofill UI
});
// HTML input with WebAuthn autofill
// <input type="text" autoComplete="username webauthn" />Browser Support
| Browser | Conditional UI | Version |
|---|---|---|
| Chrome | Supported | 108+ |
| Safari | Supported | 16+ |
| Edge | Supported | 108+ |
| Firefox | Partial | 122+ |
Passkey Type Policies
Administrators can enforce passkey type requirements per organization. Control whether synced passkeys (iCloud Keychain, Google Password Manager) or device-bound passkeys (hardware security keys) are allowed.
| Policy Field | Type | Description |
|---|---|---|
| allow_synced | boolean | Allow synced passkeys (iCloud, Google) |
| require_device_bound | boolean | Require hardware-bound credentials only |
| allowed_aaguids | string[] | Allowlist of authenticator AAGUIDs |
| blocked_aaguids | string[] | Blocklist of authenticator AAGUIDs |
DPoP Token Binding
RFC 9449Demonstration of Proof-of-Possession (DPoP) binds access tokens to a client's cryptographic key pair. Even if a token is stolen, it cannot be used without the private key.
How DPoP Works
DPoP headerDPoP Proof JWT Structure
// DPoP Proof JWT Header
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}
// DPoP Proof JWT Claims
{
"jti": "unique-token-id",
"htm": "POST",
"htu": "https://identity.wenme.net/oauth/token",
"iat": 1700000000
}Token Request with DPoP
curl -X POST https://identity.wenme.net/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "DPoP: eyJhbGciOi...signature" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "client_id=YOUR_CLIENT_ID" \
-d "code_verifier=PKCE_VERIFIER"Resource Request with DPoP-Bound Token
curl https://identity.wenme.net/oauth/userinfo \
-H "Authorization: DPoP eyJhbGciOi...access_token" \
-H "DPoP: eyJhbGciOi...fresh_proof"JavaScript Implementation
// Generate DPoP key pair using Web Crypto API
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // non-extractable private key
['sign', 'verify']
);
// Export public key as JWK for the DPoP header
const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
// Create DPoP proof
const header = btoa(JSON.stringify({
typ: 'dpop+jwt', alg: 'ES256',
jwk: { kty: publicJwk.kty, crv: publicJwk.crv,
x: publicJwk.x, y: publicJwk.y }
}));
const payload = btoa(JSON.stringify({
jti: crypto.randomUUID(),
htm: 'POST',
htu: 'https://identity.wenme.net/oauth/token',
iat: Math.floor(Date.now() / 1000)
}));DPoP is Optional
Standard Bearer tokens still work. DPoP provides an additional layer of security for high-value applications. When DPoP is used, the server returns token_type: "DPoP" instead of token_type: "Bearer".
Pushed Authorization Requests
RFC 9126PAR moves authorization parameters from the browser URL to a server-to-server request. This prevents parameter tampering and is required by FAPI 2.0 for open banking.
PAR Flow
/oauth/parrequest_urirequest_uri and client_idPush Authorization Request
curl -X POST https://identity.wenme.net/oauth/par \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "response_type=code" \
-d "redirect_uri=https://yourapp.com/callback" \
-d "scope=openid profile email" \
-d "state=random_state" \
-d "code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" \
-d "code_challenge_method=S256"
# Response:
# {
# "request_uri": "urn:ietf:params:oauth:request_uri:abc123",
# "expires_in": 60
# }Redirect with Request URI
// Redirect the user's browser
window.location.href =
'https://identity.wenme.net/oauth/authorize' +
'?client_id=YOUR_CLIENT_ID' +
'&request_uri=urn:ietf:params:oauth:request_uri:abc123';Node.js Implementation
const parResponse = await fetch(
'https://identity.wenme.net/oauth/par',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.WENME_CLIENT_ID,
client_secret: process.env.WENME_CLIENT_SECRET,
response_type: 'code',
redirect_uri: 'https://yourapp.com/callback',
scope: 'openid profile email',
state: crypto.randomUUID(),
code_challenge: pkceChallenge,
code_challenge_method: 'S256'
})
}
);
const { request_uri, expires_in } = await parResponse.json();
// Redirect with minimal parameters
res.redirect(
`https://identity.wenme.net/oauth/authorize?` +
`client_id=${clientId}&request_uri=${request_uri}`
);CIBA Backchannel Auth
OpenID CIBAClient-Initiated Backchannel Authentication enables server-to-server authorization where the user approves on a separate device. Ideal for banking transactions, healthcare approvals, and corporate sign-off workflows.
CIBA Flow
/oauth/bc-authorizeauth_req_id and sends notification to user/oauth/token with the auth_req_idInitiate Backchannel Auth
curl -X POST https://identity.wenme.net/oauth/bc-authorize \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=openid" \
-d "login_hint=user@example.com" \
-d "binding_message=Approve transfer of $500 to Account #1234"
# Response:
# {
# "auth_req_id": "1c266114-a1be-4252-8ad1-04986c5b9ac1",
# "expires_in": 300,
# "interval": 5
# }Poll for Token
curl -X POST https://identity.wenme.net/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:openid:params:grant-type:ciba" \
-d "auth_req_id=1c266114-a1be-4252-8ad1-04986c5b9ac1" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
# Pending: { "error": "authorization_pending" }
# Denied: { "error": "access_denied" }
# Expired: { "error": "expired_token" }
# Success: { "access_token": "...", "token_type": "Bearer" }Use Cases
Banking
Approve wire transfers from a separate device
Healthcare
Authorize prescription access with binding message
Corporate
Sign-off on deployments or sensitive operations
Risk Scoring & Bot Detection
Adaptive Risk Scoring
Every authentication attempt is scored across 7 risk factors. High-risk logins automatically trigger step-up authentication requirements.
Risk Factors
| Factor | Score Impact | Description |
|---|---|---|
| New Device | +30 | First login from unrecognized device fingerprint |
| New IP Address | +15 | Login from previously unseen IP address |
| New Country | +25 | Login from a country not in user history |
| Impossible Travel | +40 | Geographic distance impossible within timeframe |
| Unusual Time | +10 | Login outside user's normal hours |
| Failed Attempts | +5 each | Recent failed authentication attempts |
| VPN / Tor | +20 | Connection via VPN or Tor exit node |
Risk Levels
Normal
Elevated
High
Critical
Submit Device Fingerprint
// Submit device info for risk scoring
const response = await fetch(
'https://identity.wenme.net/api/auth/risk/device-info',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
screen_resolution: `${screen.width}x${screen.height}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: navigator.language,
platform: navigator.platform,
color_depth: screen.colorDepth,
touch_support: navigator.maxTouchPoints > 0
})
}
);Bot Detection (Cloudflare Turnstile)
Wenme integrates Cloudflare Turnstile for invisible bot detection. No CAPTCHAs required — Turnstile runs silently and provides a verification token.
// Include Turnstile token in auth requests
const response = await fetch(
'https://identity.wenme.net/api/auth/passkey/login/start',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Turnstile-Token': turnstileToken // From Turnstile widget
},
body: JSON.stringify({ email: 'user@example.com' })
}
);Graceful Degradation
If Turnstile is unavailable or fails to load, authentication proceeds normally. Bot detection is an additional layer, never a blocker.
Webhooks
Receive real-time notifications when events occur in your Wenme organization. Webhooks are signed with HMAC-SHA256 for security.
Event Types
| Event | Description |
|---|---|
| user.login | User successfully authenticated |
| user.logout | User session ended |
| user.created | New user account created |
| user.mfa_enabled | MFA enabled on account |
| user.mfa_disabled | MFA disabled on account |
| user.passkey_added | New passkey registered |
| user.passkey_removed | Passkey deleted |
| user.email_changed | User email address updated |
| security.suspicious_activity | Suspicious login pattern detected |
| security.account_locked | Account locked due to failed attempts |
| security.high_risk_login | Login with elevated risk score |
Webhook Payload
{
"id": "evt_abc123",
"event": "user.login",
"timestamp": "2026-02-11T10:30:00Z",
"data": {
"user_id": "7fafe5c2-01ea-42e6-adb3-11c56e65fe17",
"email": "user@example.com",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0...",
"method": "passkey",
"risk_score": 15
}
}Signature Verification (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
app.post('/webhooks/wenme', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET
);
if (!isValid) return res.status(401).send('Invalid signature');
// Process the event
const { event, data } = req.body;
console.log('Received:', event, data);
res.status(200).send('OK');
});Webhook Headers
| Header | Description |
|---|---|
| X-Webhook-Signature | HMAC-SHA256 signature of the payload |
| X-Webhook-ID | Unique delivery ID for deduplication |
| X-Webhook-Event | Event type (e.g., user.login) |
| X-Webhook-Timestamp | Unix timestamp of the event |
Delivery Policy
Timeout
10 seconds per delivery attempt
Retries
3 attempts with exponential backoff
Backoff Schedule
60 seconds, then 5 minutes
Log Retention
30 days of delivery history
Management API
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/webhooks | Create webhook endpoint |
| GET | /api/admin/webhooks | List all webhooks |
| PUT | /api/admin/webhooks/:id | Update webhook configuration |
| DELETE | /api/admin/webhooks/:id | Delete webhook |
| GET | /api/admin/webhooks/:id/deliveries | View delivery history |
| POST | /api/admin/webhooks/:id/test | Send test event |
SCIM 2.0 Provisioning
RFC 7642/7643/7644System for Cross-domain Identity Management (SCIM) enables automatic user provisioning from identity providers like Azure AD and Okta. Automate user lifecycle management — create, update, and deactivate users without manual intervention.
Base URL & Authentication
Base URL:
https://identity.wenme.net/scim/v2Auth:
Authorization: Bearer wm_pk_...Scopes:
scim:read, scim:writeDiscovery Endpoints (Public, No Auth)
| Endpoint | Description |
|---|---|
| /scim/v2/ServiceProviderConfig | Server capabilities and supported features |
| /scim/v2/Schemas | Supported SCIM schemas |
| /scim/v2/ResourceTypes | Supported resource types (Users, Groups) |
User CRUD Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /scim/v2/Users | List users (supports filtering) |
| GET | /scim/v2/Users/:id | Get a specific user |
| POST | /scim/v2/Users | Create a new user |
| PUT | /scim/v2/Users/:id | Replace user attributes |
| PATCH | /scim/v2/Users/:id | Update specific attributes |
| DELETE | /scim/v2/Users/:id | Deactivate user (soft delete) |
Filtering
Create User (Azure AD Compatible)
curl -X POST https://identity.wenme.net/scim/v2/Users \
-H "Authorization: Bearer wm_pk_your_api_key" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "user@example.com",
"name": {
"givenName": "Jane",
"familyName": "Doe"
},
"emails": [{
"value": "user@example.com",
"primary": true
}],
"active": true,
"externalId": "azure-ad-object-id-123"
}'Update User (PATCH)
curl -X PATCH https://identity.wenme.net/scim/v2/Users/USER_ID \
-H "Authorization: Bearer wm_pk_your_api_key" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "replace",
"path": "active",
"value": false
}]
}'Deprovisioning
DELETE requests deactivate the user account (sets active: false) rather than permanently destroying data. This allows re-provisioning and meets audit trail requirements.
IdP Setup Guides
Azure AD (Entra ID)
- Go to Enterprise Applications > New Application
- Select "Non-gallery application"
- Navigate to Provisioning > Automatic
- Set Tenant URL to SCIM base URL
- Set Secret Token to your API key
- Test Connection, then Start Provisioning
Okta
- Go to Applications > Create App Integration
- Select SCIM 2.0 (with HTTP Header auth)
- Set SCIM Base URL and API Token
- Configure provisioning actions
- Enable "Create Users" and "Deactivate Users"
- Assign users/groups to start syncing
App Branding
Customize how your application appears across the Wenme platform by uploading brand assets. These are used in OAuth consent screens and login emails sent to your users.
Two Logo Types
Square, 200x200px
Shown on the OAuth consent screen when users authorize your app. Accepts PNG, JPEG, or SVG. Automatically cropped to a square.
Wide/wordmark, max 400px
Shown in magic link login emails when users sign in to your app via Wenme. Preserves aspect ratio. Accepts PNG, JPEG, or SVG.
Upload via Dashboard
Navigate to your app's edit page and find the Branding section at the top of the form. Upload both logo types from there.
Upload via API
# Upload app icon (square, 200x200)
curl -X POST https://identity.wenme.net/api/apps/{app_id}/logo \
-H "Cookie: auth_session=..." \
-H "X-CSRF-Token: ..." \
-F "logo=@icon.png"
# Upload full logo (wide/wordmark, max 400px wide)
curl -X POST https://identity.wenme.net/api/apps/{app_id}/full-logo \
-H "Cookie: auth_session=..." \
-H "X-CSRF-Token: ..." \
-F "logo=@full-logo.png"API Response
// App icon upload response
{
"success": true,
"logo_url": "https://storage.lonesock.pro/wenme/app/...",
"message": "App logo uploaded successfully"
}
// Full logo upload response
{
"success": true,
"full_logo_url": "https://storage.lonesock.pro/wenme/app-full/...",
"message": "App full logo uploaded successfully"
}Both logo types are optimized on upload: images are resized, EXIF data is stripped, and the file is re-encoded for security. Max upload size is 2MB.
Reading Logo URLs
Both logo_url and full_logo_url are returned in the OAuth client API responses.
# Public client info (no auth required)
GET /api/oauth/client/{client_id}
{
"name": "My App",
"logo_url": "https://storage.lonesock.pro/wenme/app/...",
"full_logo_url": "https://storage.lonesock.pro/wenme/app-full/...",
"website_url": "https://myapp.com"
}
# Authenticated client details
GET /api/oauth/clients/{id}
{
"success": true,
"client": {
"id": "...",
"name": "My App",
"logo_url": "...",
"full_logo_url": "...",
...
}
}Email Integration
Wenme automatically sends branded emails on behalf of your application during authentication flows. When a user signs in to your OAuth app via magic link, the email includes your app's branding for a seamless, native-feeling experience.
How It Works
User clicks "Sign in with Wenme" on your app and enters their email
Wenme sends a magic link email branded with your app's full logo and name
User clicks the link and is authenticated, then redirected back to your app
What the Email Looks Like
Email Preview
Hello Jane,
Signing in to continue to Your App Name
Click the button below to instantly log in to your wenme account. No password needed.
This link expires in 15 minutes. If you didn't request this, you can safely ignore this email.
If no full logo is uploaded, the email shows only the app name as text. Upload a full logo for the best user experience.
Tenant-Specific Sender Addresses
Emails are sent from a tenant-specific sender address to build trust with your users. Each organization gets a recognizable "From" name and address.
Email Types
Wenme sends the following transactional emails using a unified template system. All emails follow a consistent, professional design.
| Email Type | When Sent | App Branding |
|---|---|---|
| Magic Link | User requests passwordless login | Full logo + app name |
| Email Verification | After registration | Tenant branding |
| Welcome | After email verified | Tenant branding |
| Password Setup | Email-first registration | Tenant branding |
| Security Alert | New device login, password change | Tenant branding |
| App Invitation | User invited to an OAuth app | App icon + name |
No Configuration Required
Email delivery is handled automatically by Wenme's infrastructure. You do not need to configure SMTP, email templates, or sending domains. Simply upload your app's logos and Wenme takes care of the rest. All emails are sent via Wenme's transactional email service with proper SPF, DKIM, and DMARC authentication.
Testing Your Integration
Test Credentials
You can create a test application in the dashboard for development purposes.
Go to Organization DashboardOAuth Flow Tester
Use our built-in OAuth flow tester to validate your configuration.
Need Help?
Developer Support
Our team is here to help you integrate Wenme authentication into your applications.
Wenme is a KaritKarma product.