Developer documentation · v1 · last updated 2026-04-13
OAuth 2.1, OIDC, and passkeys —
drop into any stack.
Standards-only integration. PKCE is mandatory. Implicit flow is not supported. The endpoints documented here are the same ones our own console uses, against the same backend, signed by the same Ed25519 key.
⌐ specification
- Protocol
- OAuth 2.1 / OIDC
- PKCE
- Mandatory · S256
- Token signing
- Ed25519 / EdDSA
- Discovery
- /.well-known/openid-configuration
- Endpoints
- 69 · documented below
Documentation
Migration Notes — Security Update 2026-04-13
Wenme's security posture was hardened on 2026-04-13. Most integrations require no changes. Review these items if your application is affected.
- 1
OAuth
refresh_tokengrant removed from discovery.This grant was advertised but never implemented; it has been removed from
/.well-known/openid-configurationto reflect actual support. If your app relied on refresh-token rotation, contact us — we will reintroduce it with proper one-time-use rotation when needed. - 2
OAuth tokens now strictly validated.
Access tokens, ID tokens, and refresh tokens are now namespaced via the
audandtoken_typeclaims. Tokens minted for one endpoint cannot be replayed at another. Compliant OAuth clients are unaffected; misuse (e.g., sending an ID token to/api/...) now returns401 Unauthorized. - 3
SAML SP integrations must sign AuthnRequests.
The IdP metadata now advertises
WantAuthnRequestsSigned="true". Service Providers must register a signing certificate and sign AuthnRequest messages. Unsigned requests will be rejected. - 4
Magic-link
redirect_urlis now allowlisted.The post-authentication redirect target must match either an
*.wenme.netHTTPS subdomain or one of your registered OAuth clientredirect_uris. Arbitrary external URLs return400 Bad Request. - 5
Webhook delivery target URLs are validated against SSRF.
Webhook endpoints pointing at loopback (
127.0.0.1), private (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), link-local (169.254.0.0/16), or CGNAT addresses are rejected. Use a public, internet-reachable HTTPS URL for your webhook receiver. - 6
Strict edge security headers.
All Wenme pages now enforce
Content-Security-Policywithframe-ancestors 'none'andCross-Origin-Opener-Policy: same-origin. If your application embeds Wenme pages via<iframe>, the embed will fail. Use the redirect-based OAuth flow instead of iframe embedding.
Questions or migration assistance? Email [email protected].
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();Authentication Methods
Wenme is 100% passwordless. Users authenticate using one of three methods, and your app receives standard OAuth tokens regardless of which method they choose.
Passkeys
WebAuthn/FIDO2 passkeys with biometric verification. Touch ID, Face ID, Windows Hello, or hardware security keys.
Authenticator (TOTP)
Time-based one-time passwords from authenticator apps like Google Authenticator, Authy, or 1Password.
Magic Links
One-click email authentication. A unique, single-use link is sent to the user's verified email address.
For OAuth integrators: You do not need to implement any of these methods. Wenme handles authentication entirely. Your app redirects to Wenme, the user chooses their preferred method, and Wenme redirects back with an authorization code. See the OAuth 2.1 Flow section for integration details.
Complete OAuth 2.1 Integration Guide
Breaking Change: JWT Signing Algorithm Updated to EdDSA
As of March 2026, Wenme has migrated JWT signing from RSA-2048 (RS256) to Ed25519 (EdDSA). This is a NIST-recommended upgrade for stronger security with smaller, faster tokens.
If your app login is broken, check for:
- Hardcoded
algorithm: "RS256"in your JWT verification config — change to"EdDSA" - Cached JWKS keys — clear your JWKS cache so your app fetches the new Ed25519 public key
- JWT libraries that don't support EdDSA — upgrade to a version that supports Ed25519 / RFC 8037
New JWKS format (Ed25519):
{
"keys": [{
"kty": "OKP", // Was "RSA"
"crv": "Ed25519", // New: curve name
"alg": "EdDSA", // Was "RS256"
"kid": "...",
"use": "sig",
"x": "..." // 32-byte public key (base64url)
// No more "n" and "e" fields (those were RSA-specific)
}]
}Apps using wellKnown auto-discovery with JWKS rotation should work automatically after clearing cached keys.
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. The signing algorithm is EdDSA (Ed25519).
Notable discovery fields:
response_modes_supported: includesjwt,query.jwt,fragment.jwt(JARM)tls_client_certificate_bound_access_tokens:true(mTLS / RFC 8705)authorization_response_iss_parameter_supported:true
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_sessionToken Validation (Updated 2026-04-13)
As of 2026-04-13, Wenme strictly validates the aud, iss, and token_type claims on every token. Tokens minted for one endpoint cannot be replayed at another.
| Claim | Expected Value | Used For |
|---|---|---|
| aud | wenme-api | Access tokens used at /api/* |
| aud | <client_id> | ID tokens (consumed by the OAuth client) |
| token_type | access_token | id_token | refresh_token | Disambiguates token use; wrong type returns 401 |
| iss | <issuer from discovery> | Must equal issuer in /.well-known/openid-configuration |
Compliant OAuth clients that send the access token to /oauth/userinfo and the API gateway are unaffected. Sending an id_token to /api/* or reusing a refresh token at the userinfo endpoint now returns 401 Unauthorized.
Refresh Token Grant
The refresh_token grant has been removed from /.well-known/openid-configuration. Refresh token grant is currently not supported. If your integration requires long-lived sessions, contact [email protected]— we will reintroduce it with proper one-time-use rotation.
Client Secret Scoping
Organization admins see OAuth client_secret values only for apps within their own tenant. Cross-tenant client secret access is restricted to users with the platform_admin role.
OAuth 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
What gets auto-configured
When you enter your App Domain in the dashboard and click Generate URIs, the following fields are pre-filled. You can add, remove, or edit any of them on the app's settings page.
Redirect URIs (5 patterns)
Covers NextAuth, Express/Passport, Django, Auth.js, and custom React out of the box.
https://yourdomain/callback
https://yourdomain/auth/callback
https://yourdomain/oauth/callback
https://yourdomain/api/auth/callback
https://yourdomain/api/auth/callback/wenmePost-Logout Redirect URIs (3 patterns)
Covers RP-Initiated Logout (OpenID Connect end_session endpoint) for most flows.
https://yourdomain/
https://yourdomain/logout
https://yourdomain/logged-outApplication URLs
Note:You can add, remove, or edit any of these on the app's settings page after creation. Only register additional URIs manually if your callback or logout target lives outside these standard paths.
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=xyz123Auto-registered as /, /logout, and /logged-out when you create an app via the dashboard. You only need to register additional URIs manually if your logout target lives outside these paths.
3. 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": "[email protected]",
"exp": 1640995200
}Response (Inactive/Invalid Token):
{
"active": false
}Session Management
Key Principle
Wenme is the authenticator— it proves who the user is. Your app manages its own session after that. Do not call Wenme on every request. Do not rely on Wenme's session cookies. Your app's session is your app's responsibility.
How It Works
User clicks "Sign in with Wenme"
Your app redirects to Wenme's OAuth authorize endpoint
Wenme authenticates the user
Passkey, authenticator app, or magic link — Wenme handles it all
Wenme redirects back with an authorization code
Your callback URL receives code and state parameters
Your backend exchanges the code for tokens
POST /oauth/token returns access_token, id_token, and refresh_token
Your app creates its OWN session
Set your own cookie or JWT. Store user info in your database. Wenme's job is done.
Example: OAuth Callback Handler
After receiving the authorization code from Wenme, exchange it for tokens, extract the user info, and create your app's session. Wenme is not involved after this point.
Node.js / Express
app.get("/auth/callback", async (req, res) => {
const { code, state } = req.query;
// Verify state matches what you stored before redirect
if (state !== req.session.oauthState) {
return res.status(400).send("Invalid state parameter");
}
// Step 4: Exchange code for tokens (server-to-server)
const tokenRes = 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",
code,
redirect_uri: "https://yourapp.com/auth/callback",
client_id: process.env.WENME_CLIENT_ID,
client_secret: process.env.WENME_CLIENT_SECRET,
code_verifier: req.session.codeVerifier, // PKCE
}),
});
const tokens = await tokenRes.json();
// Step 5: Get user info from Wenme (one-time call)
const userRes = await fetch("https://identity.wenme.net/oauth/userinfo", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const wenmeUser = await userRes.json();
// wenmeUser = { sub: "uuid", name: "Alice", email: "[email protected]", picture: "..." }
// ============================================
// STEP 6: CREATE YOUR OWN SESSION
// This is YOUR app's responsibility from here
// ============================================
// Find or create user in YOUR database
let user = await db.users.findByEmail(wenmeUser.email);
if (!user) {
user = await db.users.create({
wenmeId: wenmeUser.sub,
email: wenmeUser.email,
name: wenmeUser.name,
avatar: wenmeUser.picture,
});
}
// Create YOUR app's session (cookie, JWT, whatever you prefer)
req.session.userId = user.id;
req.session.email = user.email;
// Store tokens if you need to call Wenme userinfo later
// (optional — most apps don't need this)
req.session.wenmeAccessToken = tokens.access_token;
req.session.wenmeRefreshToken = tokens.refresh_token;
// Redirect to your app — Wenme is no longer involved
res.redirect("/dashboard");
});C# / ASP.NET Core
[HttpGet("callback")]
public async Task<IActionResult> Callback(string code, string state)
{
// Verify state
if (state != HttpContext.Session.GetString("oauth_state"))
return BadRequest("Invalid state parameter");
// Exchange code for tokens
var tokens = await _wenmeAuth.ExchangeCodeAsync(code);
// Get user info from Wenme (one-time)
var wenmeUser = await _wenmeAuth.GetUserInfoAsync(tokens.AccessToken);
// =============================================
// CREATE YOUR OWN SESSION — Wenme is done here
// =============================================
var user = await _userService.FindOrCreateAsync(new {
WenmeId = wenmeUser.Sub,
Email = wenmeUser.Email,
Name = wenmeUser.Name
});
// Set YOUR app's auth cookie
var claims = new List<Claim> {
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, user.Name),
};
var identity = new ClaimsIdentity(claims, "WenmeAuth");
await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
return Redirect("/dashboard");
}Common Mistakes
Calling Wenme on every request
Do NOT call /oauth/userinfo on every page load to check if the user is logged in. Use your own session. Call Wenme once during the callback, then manage auth yourself.
Using Wenme's session cookies
Wenme's auth_session cookie is for wenme.net only. Your app cannot read it (different domain). Create your own cookies on your own domain.
Skipping state parameter validation
Always generate a random state before redirecting to Wenme, store it in your session, and verify it in the callback. This prevents CSRF attacks.
Using access_token as your session
The Wenme access_token expires and is meant for calling Wenme APIs. Don't use it as your app's session token. Create your own session with your own expiry policy.
Setting redirect_url to your OAuth callback
When using app invitations, the redirect_url should point to your app's main page (e.g. /dashboard), NOT your OAuth callback URL. The magic link authenticates via Wenme — your app's auth middleware will then trigger the OAuth flow automatically.
Session Lifecycle
User clicks "Sign in with Wenme"
│
▼
Your app redirects to Wenme (/oauth/authorize)
│ with: client_id, redirect_uri, state, code_challenge, scope
│
▼
Wenme authenticates user (passkey / authenticator / magic link)
│
▼
Wenme redirects back to your app (/auth/callback?code=...&state=...)
│
▼
Your backend exchanges code for tokens (POST /oauth/token)
│ → access_token, id_token, refresh_token
│
▼
Your backend calls GET /oauth/userinfo (one time)
│ → { sub, name, email, picture }
│
▼
┌──────────────────────────────────────────────┐
│ YOUR APP TAKES OVER HERE │
│ │
│ • Find or create user in YOUR database │
│ • Create YOUR session (cookie / JWT) │
│ • Set YOUR session expiry policy │
│ • Redirect to YOUR dashboard │
│ │
│ Wenme is NOT involved in any subsequent │
│ requests. Your session = your responsibility│
└──────────────────────────────────────────────┘
│
▼
All subsequent requests use YOUR session only
│
▼
User clicks "Logout" in your app
│ → Clear YOUR session
│ → Optionally: call Wenme POST /oauth/end_session
│ (only if you want to log out of Wenme too)When to Call Wenme Again
| Scenario | Call Wenme? | Details |
|---|---|---|
| User loads a page | No | Check your own session |
| Check if user is logged in | No | Check your own session cookie/JWT |
| Get user's email or name | No | Stored in your database at login time |
| Refresh user profile data | Occasionally | Call /oauth/userinfo periodically (e.g. daily) if you need up-to-date profile |
| User logs in again | Yes | Full OAuth flow again — Wenme handles authentication |
| User logs out from all apps | Yes | Call POST /oauth/end_session to end Wenme session too |
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": "[email protected]",
"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: '[email protected]',
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"
}Security Enforcement Policies
Organizations can define security policies that control authentication requirements for their users. Policies are set at the organization level and can be overridden per app. This lets you enforce stricter rules for sensitive apps while keeping the default relaxed for others.
Enforcement Levels
| Level | Value | Behavior |
|---|---|---|
| None | none | No enforcement. Users can log in without the feature enabled. |
| Encouraged | encouraged | Users see a prompt to enable the feature but can dismiss it and continue. |
| Required | required | Users must enable the feature before they can access the app. Login is blocked until the requirement is met. |
Inheritance Model
The organization sets the default policy for all its apps. Each app can override individual fields or use inheritto fall through to the organization default. When evaluating a user's login, the platform merges the org policy with any app-level overrides.
If an app sets a field to inherit, the org-level value is used. If the org has no value, the platform default (none) applies.
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/org/:id/security-policy | Get the organization-level security policy |
| PUT | /api/org/:id/security-policy | Update the organization-level security policy |
| GET | /api/oauth/clients/:id/security-policy | Get the app-level security policy override |
| PUT | /api/oauth/clients/:id/security-policy | Update the app-level security policy override |
Organization Policy Example
// PUT /api/org/:id/security-policy
{
"passkey_enforcement": "required",
"mfa_enforcement": "encouraged"
}Response
// GET /api/org/:id/security-policy
{
"passkey_enforcement": "required",
"mfa_enforcement": "encouraged"
}App-Level Override Example
// PUT /api/oauth/clients/:id/security-policy
// "inherit" means use the org-level value
{
"passkey_enforcement": "inherit",
"mfa_enforcement": "required"
}
// Effective policy for this app:
// passkey_enforcement: "required" (inherited from org)
// mfa_enforcement: "required" (app override)Authentication Required
All security policy endpoints require session authentication with admin or owner role on the organization. CSRF token is required for PUT requests.
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": "[email protected]",
"role": "member" // Options: owner, admin, member, viewer, billing
}Response (Success)
{
"success": true,
"message": "Invitation sent successfully",
"invitation": {
"id": "invitation-uuid",
"email": "[email protected]",
"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.
App Invitations API
Invite users to your OAuth application using your app's client ID and client secret. The invitee receives a single email with a magic link — one click to accept the invitation, sign in, and reach the app.
POST /api/app/invite
Invite a user to your OAuth app. Authenticate with your app's client_id and client_secret.
Authentication
Pass your client_id and client_secret in the request body. You can find these in your app settings under your organization dashboard.
Request Body
{
"client_id": "your_app_client_id",
"client_secret": "your_app_client_secret",
"email": "[email protected]",
"role": "user",
"redirect_url": "https://yourapp.com/dashboard"
}Fields
| Field | Required | Description |
|---|---|---|
| client_id | Yes | Your OAuth app's client ID |
| client_secret | Yes | Your OAuth app's client secret |
| Yes | Email address of the user to invite | |
| role | No | Role within the app (default: "user") |
| redirect_url | No | URL to redirect user after accepting. Falls back to the app's website URL, then wenme.net. |
| metadata | No | JSON object with custom data to attach to the invitation |
Response (Success)
{
"success": true,
"invitation_id": "uuid",
"invitation_link": "https://identity.wenme.net/api/auth/verify-magic-link-redirect?token=...",
"message": "Invitation sent successfully"
}Example (cURL)
curl -X POST https://identity.wenme.net/api/app/invite \
-H "Content-Type: application/json" \
-d '{
"client_id": "your_app_client_id",
"client_secret": "your_app_client_secret",
"email": "[email protected]",
"role": "user",
"redirect_url": "https://yourapp.com/dashboard"
}'One-click flow:The user receives a single email and clicks once to accept the invitation, sign in, and reach the app. The invitation link is a magic link — single-use, expires in 7 days. If the user doesn't have a Wenme account, one is created automatically.
Security Best Practices
When handling the invitation callback in your app, follow these practices to prevent unauthorized role assignment.
Do NOT match invitations by email alone. If your OAuth callback links invitations purely by email, any user who signs in with a matching email will get the invited role — even if they never clicked the invitation link.
// DANGEROUS — do NOT do this
var pending = invitationService.getByEmail(user.email);
if (pending && pending.status === "pending")
invitationService.linkUser(pending.id, user.id); // anyone with this email gets the role!Pass invitationId through the redirect URL
When calling POST /api/app/invite, embed the invitation ID in the redirect_url so it flows back to your callback after Wenme authenticates the user.
// When sending the invite, include invitationId in redirect_url
const response = await fetch("https://identity.wenme.net/api/app/invite", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: "your_client_id",
client_secret: "your_client_secret",
email: "[email protected]",
role: "premium",
redirect_url: `https://yourapp.com/callback?invitationId=${invitationId}`
})
});Validate the triple in your callback
In your OAuth callback, only link the invitation when all three conditions are met:
invitationIdis explicitly present in the callback URL- The invitation exists and status is
pending - The invitation's email matches the Wenme-authenticated user's email
// In your OAuth callback handler
const invitationId = searchParams.get("invitationId");
// Exchange code for tokens, get user info from Wenme...
const userInfo = await wenme.getUserInfo(accessToken);
// Only link if invitationId was explicitly passed
if (invitationId) {
const invitation = await db.getInvitation(invitationId);
if (invitation
&& invitation.status === "pending"
&& invitation.email.toLowerCase() === userInfo.email.toLowerCase()) {
await db.linkUserAndAssignRole(invitationId, userInfo.sub);
}
// Mismatch? User still logs in — just no role upgrade
}Remove public accept endpoints
Do not expose a POST /invite/{token}/accept endpoint. The only way to complete an invitation should be clicking the Wenme magic link, which authenticates the user and redirects to your callback with the invitationId. Your public invite page should be info-only ("check your email"), not an accept button.
The secure flow: Admin creates invitation → Wenme sends magic link with invitationId in redirect URL → User clicks link → Wenme authenticates → Redirects to /callback?invitationId=abc-123 → Your backend validates the triple (ID + status + email) → Role assigned.
Multi-Tenant App Management (Enterprise)
Programmatically spawn child OAuth apps under your organization. Intended for SaaS products that onboard their own customers and need a dedicatedclient_id/client_secretper downstream tenant (typical examples: an ERP, a CMS, or a workflow platform giving each customer their own isolated SSO configuration).
Enterprise tier only. Available once KaritKarma enablesmulti_tenant_apps_enabledon your organization. Contact sales to request access.
Authentication
All endpoints require an API key with theapps:writescope. Generate one in Organization Settings → API Keys. Pass it as a bearer token:
Authorization: Bearer wm_pk_<your_api_key>
POST /api/apps
Create a child OAuth app under the calling tenant. Returns theclient_secretexactly once — store it immediately; it cannot be retrieved later.
Request
curl -X POST https://identity.wenme.net/api/apps \
-H "Authorization: Bearer wm_pk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"redirect_uris": ["https://acme.example.com/auth/callback"],
"post_logout_redirect_uris": ["https://acme.example.com/"],
"scopes": ["openid", "profile", "email"],
"client_type": "confidential",
"logo_url": "https://acme.example.com/logo.png",
"metadata": { "partner_tenant_id": "acme-uuid" }
}'Response (201 Created)
{
"id": "9f8b...",
"client_id": "wm_app_a1b2c3d4e5f6...",
"client_secret": "<48 hex chars — shown only once>",
"name": "Acme Corp",
"redirect_uris": ["https://acme.example.com/auth/callback"],
"scopes": ["openid", "profile", "email"],
"client_type": "confidential",
"metadata": { "partner_tenant_id": "acme-uuid" },
"created_at": "2026-04-13T10:00:00Z"
}Validation
name— 1–255 charactersredirect_uris— at least one URL, no wildcardsscopes— must be a subset of your organization's allowed scopesclient_type—confidentialorpublicmetadata— opaque JSON object, stored as-is for partner traceability
GET /api/apps/:id
Fetch an app's metadata. The client_secretis never returned here.
curl https://identity.wenme.net/api/apps/9f8b... \ -H "Authorization: Bearer wm_pk_..."
PATCH /api/apps/:id
Partial update. Send only the fields you want to change. All fields accepted on create are patchable except client_type.
curl -X PATCH https://identity.wenme.net/api/apps/9f8b... \
-H "Authorization: Bearer wm_pk_..." \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["https://acme.example.com/auth/cb", "https://acme.example.com/auth/cb2"],
"metadata": { "partner_tenant_id": "acme-uuid", "tier": "pro" }
}'POST /api/apps/:id/rotate-secret
Mint a new client_secret, invalidate the previous one, and revoke all active access + refresh tokens for this app. The new secret is returned exactly once.
curl -X POST https://identity.wenme.net/api/apps/9f8b.../rotate-secret \ -H "Authorization: Bearer wm_pk_..."
DELETE /api/apps/:id
Soft-delete. The app is deactivated and moved to the platform trash bucket; the full row is retained for 30 days and may be restored by a platform administrator. Returns 204 No Content.
curl -X DELETE https://identity.wenme.net/api/apps/9f8b... \ -H "Authorization: Bearer wm_pk_..."
Webhook events
Subscribe to these on any webhook registered against your organization. Theclient_secretis never included in webhook payloads.
{
"id": "<delivery_id>",
"event": "app.created",
"timestamp": "2026-04-13T10:00:00Z",
"data": {
"app_id": "9f8b...",
"client_id": "wm_app_...",
"name": "Acme Corp",
"client_type": "confidential",
"scopes": ["openid","profile","email"],
"metadata": { "partner_tenant_id": "acme-uuid" },
"created_by": "api_key:<id>",
"created_at": "2026-04-13T10:00:00Z"
}
}app.created— new child app mintedapp.updated— metadata, redirect URIs or scopes changedapp.secret_rotated— client_secret rotated (secret not included)app.deleted— app soft-deleted
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": "[email protected]",
"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
Magic Link redirect_url Allowlist (2026-04-13)
The post-authentication redirect_url passed to POST /auth/magic-link, POST /api/invitation/send-magic-link, and POST /api/app/invite is now allowlisted. Arbitrary external URLs return 400 Bad Request.
Allowed
- https://yourapp.wenme.net/dashboard
- https://yourapp.example.com/auth/callback (if registered as redirect_uri)
Rejected
- https://attacker.example.com/
- javascript:alert(1)
- data:text/html,...
Allowed targets: any HTTPS *.wenme.net subdomain, or a URL that exactly matches one of your OAuth client's registered redirect_uris. javascript: and data: schemes are always rejected.
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.
OpenID Configuration
Wenme publishes a standard OpenID Connect Discovery document. Most OAuth libraries can auto-configure themselves from this single URL.
Discovery Endpoint
GET https://wenme.net/.well-known/openid-configurationReturns a JSON document containing all OAuth and OIDC endpoints, supported algorithms, scopes, and capabilities.
Key Fields
| Field | Value |
|---|---|
| issuer | https://identity.wenme.net |
| authorization_endpoint | https://identity.wenme.net/oauth/authorize |
| token_endpoint | https://identity.wenme.net/oauth/token |
| userinfo_endpoint | https://identity.wenme.net/oauth/userinfo |
| jwks_uri | https://identity.wenme.net/.well-known/jwks.json |
| revocation_endpoint | https://identity.wenme.net/oauth/revoke |
| introspection_endpoint | https://identity.wenme.net/oauth/introspect |
| end_session_endpoint | https://identity.wenme.net/oauth/end_session |
| id_token_signing_alg_values_supported | ["EdDSA"] |
| code_challenge_methods_supported | ["S256"] |
| response_modes_supported | ["query", "fragment", "jwt", "query.jwt", "fragment.jwt"] |
| tls_client_certificate_bound_access_tokens | true |
JWKS Format (Ed25519)
The JSON Web Key Set endpoint returns Ed25519 public keys used to verify JWT signatures. Tokens are signed with EdDSA (not RS256).
{
"keys": [{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"kid": "wenme-eddsa-...",
"use": "sig",
"x": "<32-byte public key, base64url encoded>"
}]
}Migration note: If your app previously hardcoded RS256, update to EdDSA. Apps using wellKnown auto-discovery should work automatically after clearing cached JWKS keys.
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 "[email protected]" \
-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: '[email protected]' })
}
);Graceful Degradation
If Turnstile is unavailable or fails to load, authentication proceeds normally. Bot detection is an additional layer, never a blocker.
Webhooks
SSRF Protection on Webhook URLs (2026-04-13)
Webhook delivery targets are validated against Server-Side Request Forgery. Register a public, internet-reachable HTTPS endpoint for your webhook receiver. Rejected URL classes:
Loopback:
127.0.0.0/8,::1,localhostPrivate (RFC 1918):
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16Link-local (incl. cloud metadata
169.254.169.254):169.254.0.0/16CGNAT:
100.64.0.0/10
DNS rebinding defense: Wenme re-resolves the webhook hostname and re-validates the destination IP at the time of each delivery. A hostname that resolves to a public IP during registration but a private IP at delivery time will be rejected.
Receive real-time HTTP notifications when events happen on the Wenme platform. Register webhook endpoints in your Organization Settings → Webhooks tab to start receiving events. All webhook payloads are signed with HMAC-SHA256 so you can verify authenticity.
Authentication
Each webhook delivery includes a signature in the X-Webhook-Signature header, computed as sha256=<hex> using your webhook secret. Always verify this signature before processing the payload.
Available Events
| Event | Description |
|---|---|
| user.created | New user account created |
| user.updated | User profile information updated |
| user.deleted | User account deleted |
| user.login | User successfully authenticated |
| user.logout | User session ended |
| user.passkey_added | New passkey registered |
| user.passkey_removed | Passkey deleted |
| user.mfa_enabled | MFA enabled on account |
| user.mfa_disabled | MFA disabled on account |
| user.email_changed | User email address updated |
| invitation.sent | Invitation sent to a user |
| invitation.accepted | User accepted an invitation |
| consent.granted | User granted OAuth consent to an app |
| consent.revoked | User revoked OAuth consent from an app |
| session.created | New session established |
| session.ended | Session terminated or expired |
| app.user.added | User added to an OAuth app |
| app.user.removed | User removed from an OAuth app |
| 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": "[email protected]",
"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 verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expected)
);
}
// In your webhook handler:
app.post('/webhooks/wenme', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhook(
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');
});Delivery Headers
Every webhook delivery includes the following headers:
| Header | Description |
|---|---|
| X-Webhook-Signature | HMAC-SHA256 signature (sha256=<hex>) |
| X-Webhook-Timestamp | Unix timestamp of the delivery |
| X-Webhook-ID | Unique delivery ID for deduplication |
| X-Webhook-Event | Event type (e.g., user.login) |
Retry Policy
Timeout
10 seconds per delivery attempt
Retries
3 attempts with exponential backoff
Backoff Schedule
1 minute, 5 minutes, 30 minutes
Log Retention
30 days of delivery history
Setup
Register webhook endpoints in your Organization Settings → Webhooks tab, or manage them via the API.
Management API
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/org/:id/webhooks | Create webhook endpoint |
| GET | /api/org/:id/webhooks | List all webhooks |
| PUT | /api/org/:id/webhooks/:webhookId | Update webhook configuration |
| DELETE | /api/org/:id/webhooks/:webhookId | Delete webhook |
| GET | /api/org/:id/webhooks/:webhookId/deliveries | View delivery log |
| POST | /api/org/:id/webhooks/:webhookId/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": "[email protected]",
"name": {
"givenName": "Jane",
"familyName": "Doe"
},
"emails": [{
"value": "[email protected]",
"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
FAPI 2.0 Security Profile
Financial-GradeJARM — JWT Secured Authorization Response Mode
JARM wraps the authorization response in a signed JWT, preventing response tampering and mix-up attacks. Instead of returning ?code=xxx&state=yyy as plain query parameters, the server returns a single ?response=<signed-jwt> that your app verifies before extracting the authorization code.
Supported Response Modes
| Mode | Delivery | Description |
|---|---|---|
| jwt | Default (query) | JWT response in query string |
| query.jwt | Query string | Explicit query string delivery |
| fragment.jwt | URL fragment | JWT in URL fragment (for SPAs) |
Authorization Request with JARM
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
response_mode=jwt&
redirect_uri=https://your-app.com/callback&
scope=openid+profile+email&
state=RANDOM_STATE&
code_challenge=PKCE_CHALLENGE&
code_challenge_method=S256JARM Response
Instead of separate query parameters, your callback receives a single JWT:
// Callback URL:
// https://your-app.com/callback?response=eyJhbGciOiJFZERTQSIs...
// Decoded JWT payload contains:
{
"iss": "https://identity.wenme.net",
"aud": "YOUR_CLIENT_ID",
"code": "AUTHORIZATION_CODE",
"state": "RANDOM_STATE",
"exp": 1711900000,
"iat": 1711899700
}Verify Before Extracting
Always verify the JWT signature using the JWKS endpoint before extracting the authorization code. Check that iss matches the Wenme issuer, aud matches your client ID, and the token is not expired.
mTLS Certificate-Bound Tokens (RFC 8705)
Mutual TLS binds access tokens to the client's TLS certificate. Even if a token is intercepted, it cannot be used without the corresponding client certificate. This provides sender-constrained tokens for high-security environments.
How It Works
cnf.x5t#S256 claim with the thumbprintCertificate Thumbprint Header
# Option 1: Present client certificate via TLS (preferred)
curl --cert client.pem --key client-key.pem \
-X POST https://identity.wenme.net/oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "client_id=YOUR_CLIENT_ID" \
-d "code_verifier=PKCE_VERIFIER"
# Option 2: Pass thumbprint via header (when TLS terminates at proxy)
curl -X POST https://identity.wenme.net/oauth/token \
-H "X-Client-Cert-Thumbprint: SHA256_THUMBPRINT_BASE64URL" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "client_id=YOUR_CLIENT_ID" \
-d "code_verifier=PKCE_VERIFIER"Bound Token Claims
// Access token includes certificate confirmation:
{
"sub": "user-uuid",
"scope": "openid profile email",
"cnf": {
"x5t#S256": "base64url-encoded-sha256-thumbprint"
}
}mTLS is Opt-In
Clients that do not present a TLS certificate receive standard Bearer tokens without certificate binding. mTLS is advertised in the OpenID discovery document via tls_client_certificate_bound_access_tokens: true.
SAML 2.0 Identity Provider
Enterprise SSOWenme acts as a SAML 2.0 Identity Provider (IdP). Enterprise applications that only speak SAML can authenticate users through Wenme, giving your workforce a single passwordless identity across legacy and modern apps. Wenme supports SP-initiated SSO, IdP-initiated SSO, and Single Logout (SLO).
IdP Metadata
Endpoint:
GET /saml/metadataEntity ID:
https://identity.wenme.net/saml/metadataFormat:
application/xmlSP-Initiated SSO Flow
AuthnRequest to https://identity.wenme.net/saml/ssoSupported Bindings
| Binding | Direction | Description |
|---|---|---|
| HTTP-Redirect | SP → IdP | AuthnRequest sent via GET redirect |
| HTTP-POST | SP → IdP | AuthnRequest sent via POST form |
| HTTP-POST | IdP → SP | SAML Response always delivered via POST |
NameID Formats
| Format | Description |
|---|---|
| emailAddress | User's email address (default) |
| persistent | Opaque, stable identifier across sessions |
| transient | One-time identifier, different per session |
Attribute Mapping
Attributes included in the SAML assertion are configurable per SP via the admin API. The default attribute set includes:
| SAML Attribute | Wenme Field |
|---|---|
| User email address | |
| name | User display name |
| role | Organization role |
SP Registration (Admin API)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/saml/service-providers | Register a new Service Provider |
| GET | /api/admin/saml/service-providers | List all registered SPs |
| PUT | /api/admin/saml/service-providers/:id | Update SP configuration |
| DELETE | /api/admin/saml/service-providers/:id | Remove a Service Provider |
SP Configuration Example (Spring Security SAML)
<!-- Download IdP metadata from GET /saml/metadata -->
<metadata:EntityDescriptor
entityID="https://identity.wenme.net/saml/metadata"
xmlns:metadata="urn:oasis:names:tc:SAML:2.0:metadata">
<!-- SSO endpoint, signing certificate, and
supported bindings are included in the
metadata XML document -->
</metadata:EntityDescriptor>AuthnRequest Signing Required (2026-04-13)
Wenme's IdP metadata advertises WantAuthnRequestsSigned="true". All Service Providers must sign AuthnRequest messages; unsigned requests are rejected.
POST /api/admin/saml/service-providers).<samlp:AuthnRequest> with the corresponding private key using RSA-SHA256.<ds:Signature> element in either the AuthnRequest XML body (HTTP-POST binding) or the Signature query parameter (HTTP-Redirect binding).Consult the IdP metadata at GET /saml/metadata for the required KeyDescriptor use="signing" configuration and supported bindings before registering your SP.
Security
WantAuthnRequestsSigned="true" — SP-signed AuthnRequests requiredMobileTwoFactorContract — reflects passwordless MFASAML vs. OAuth 2.1
For new applications, we recommend OAuth 2.1 with PKCE. SAML 2.0 support is provided for enterprise environments that require it (e.g., legacy Java/.NET apps, on-premise software). Both protocols share the same passwordless authentication experience on the Wenme side.
LDAP / Active Directory
Directory SyncSync users from Active Directory or any LDAP v3 directory into your Wenme organization. Users are automatically provisioned with configurable role mappings, and they authenticate via Wenme's passwordless methods (passkeys, magic links) — no AD password is needed on the Wenme side.
Connection Options
| Protocol | Port | Security |
|---|---|---|
| LDAP | 389 | Plaintext (not recommended) |
| LDAPS | 636 | TLS encrypted |
| STARTTLS | 389 | Upgrade to TLS |
How It Works
Attribute Mapping
Map LDAP attributes to Wenme user fields. All mappings are configurable per connector.
| LDAP Attribute | Wenme Field | Default |
|---|---|---|
| Required | ||
| displayName | name | Required |
| sAMAccountName | username | Optional |
| telephoneNumber | phone | Optional |
| title | job_title | Optional |
Group-to-Role Mapping
Map AD security groups to Wenme organization roles. Users are assigned the highest-privilege role they match.
{
"CN=IT Admins,OU=Groups,DC=company,DC=com": "admin",
"CN=Developers,OU=Groups,DC=company,DC=com": "member",
"CN=Viewers,OU=Groups,DC=company,DC=com": "viewer"
}Management API (Organization-Scoped)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/org/:id/ldap | Create a new LDAP connector |
| GET | /api/org/:id/ldap | List LDAP connectors |
| PUT | /api/org/:id/ldap/:connectorId | Update connector configuration |
| DELETE | /api/org/:id/ldap/:connectorId | Delete a connector |
| POST | /api/org/:id/ldap/:connectorId/test | Test connection to directory |
| POST | /api/org/:id/ldap/:connectorId/sync | Trigger a manual sync |
| GET | /api/org/:id/ldap/:connectorId/status | Get sync status and last run |
Create Connector
curl -X POST https://identity.wenme.net/api/org/ORG_ID/ldap \
-H "Authorization: Bearer wm_pk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Corporate AD",
"host": "ldaps://ad.company.com:636",
"bind_dn": "CN=svc-wenme,OU=Service Accounts,DC=company,DC=com",
"bind_password": "service-account-password",
"base_dn": "OU=Users,DC=company,DC=com",
"search_filter": "(objectClass=user)",
"sync_interval_minutes": 5,
"group_mappings": {
"CN=IT Admins,OU=Groups,DC=company,DC=com": "admin",
"CN=Developers,OU=Groups,DC=company,DC=com": "member"
}
}'Security
LDAP vs. SCIM
If your identity provider supports SCIM 2.0 (e.g., Azure AD, Okta), we recommend using SCIM for provisioning as it is a modern, REST-based standard. LDAP connectors are best suited for on-premise Active Directory environments that do not expose SCIM endpoints.
GDPR Data Rights
EU RegulationData Export — Right to Portability (Article 20)
Users can download a complete export of their personal data in machine-readable JSON format. The export includes profile information, passkey metadata, OAuth consents, active sessions, organization memberships, app access records, and email aliases.
Endpoint:
GET /api/user/data-exportAuth:
Session cookie requiredResponse:
application/json (downloadable file)Example Request
curl https://identity.wenme.net/api/user/data-export \
-H "Cookie: auth_session=YOUR_SESSION_TOKEN" \
-o my-data.jsonExported Data Categories
| Category | Fields Included |
|---|---|
| Profile | Name, email, username, bio, job title, company, location, social links |
| Passkeys | Device names, creation dates (credentials are never exported) |
| OAuth Consents | Authorized apps, granted scopes, consent timestamps |
| Sessions | Active sessions with device info and last activity |
| Organizations | Memberships, roles, join dates |
| App Access | Apps the user has been invited to or authorized |
Also available in Profile Settings under "Download My Data".
Account Deletion — Right to Erasure (Article 17)
Users can request permanent deletion of their account. The process includes a 30-day grace period during which the request can be cancelled. After the grace period, all personal data is permanently removed.
Deletion Flow
POST /api/user/deletion-request| Endpoint | Method | Description |
|---|---|---|
| /api/user/deletion-request | POST | Initiate account deletion |
| /api/user/deletion-cancel | POST | Cancel within 30-day grace period |
Also available in Profile Settings under "Delete My Account".
Breach Notification (Articles 33/34)
Wenme implements automated breach detection and notification workflows compliant with GDPR timelines.
| Requirement | Implementation |
|---|---|
| 72-hour authority notification | Automated tracking from detection to supervisory authority notification |
| User notification | Automatic email notification to affected users when risk is high |
| Breach management | Admin endpoints for logging, tracking, and resolving breach incidents |
Organization Responsibility
As a data processor, Wenme notifies your organization within 24 hours of detecting a breach. Your organization remains responsible for notifying the supervisory authority within 72 hours (Article 33) and affected individuals without undue delay (Article 34).
Availability Monitoring
Wenme runs automated health checks every 60 seconds across all platform services. Uptime data is tracked for 24-hour, 7-day, and 30-day rolling windows. Incidents are classified by severity and tracked through resolution.
Monitored Services
| Service | Check Interval | What is Checked |
|---|---|---|
| Identity API | 60s | HTTP health endpoint, response time, error rate |
| OAuth / OIDC | 60s | Token issuance, JWKS availability, discovery endpoint |
| PostgreSQL | 60s | Connection pool, query latency, replication lag |
| Redis | 60s | Ping, memory usage, eviction rate |
| Object Storage | 60s | Bucket accessibility, upload/download latency |
Incident Severity Classification
| Priority | Definition | Response Target |
|---|---|---|
| P1 — Critical | Complete service outage or data breach | 15 minutes |
| P2 — High | Major feature degradation (e.g., login failures) | 1 hour |
| P3 — Medium | Minor feature impact, workaround available | 4 hours |
| P4 — Low | Cosmetic issue or minor inconvenience | Next business day |
Admin Availability Endpoint
Endpoint:
GET /api/admin/availabilityAuth:
Admin session required// Response example
{
"uptime": {
"24h": "99.98%",
"7d": "99.95%",
"30d": "99.97%"
},
"services": [
{ "name": "identity-api", "status": "healthy", "latency_ms": 12 },
{ "name": "oauth", "status": "healthy", "latency_ms": 8 },
{ "name": "postgres", "status": "healthy", "latency_ms": 3 },
{ "name": "redis", "status": "healthy", "latency_ms": 1 },
{ "name": "storage", "status": "healthy", "latency_ms": 15 }
],
"incidents": {
"open": 0,
"resolved_30d": 2
}
}Uptime Tracking Periods
Uptime percentages are calculated from health check results over rolling 24-hour, 7-day, and 30-day windows. Only full outages (service unreachable) count against uptime — degraded performance is tracked separately in latency metrics.
Custom Branding
Organizations can customize the login page experience for their OAuth apps. Configure branding in Organization Settings → Branding tab to give users a seamless, branded authentication flow.
Customizable Elements
Buttons and interactive elements on the login page
Login page background to match your brand
Text color on primary action buttons
Custom heading shown on the login page
Your logo displayed prominently on the login page
How It Works
Configure branding in Organization Settings → Branding tab
During OAuth flows, the login page fetches branding based on the app's client_id
The login page applies your organization's custom colors, logo, and welcome text
A "Powered by Wenme" footer is always displayed on branded login pages to maintain trust and transparency.
Branding API
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/oauth/branding/:client_id | Fetch branding for an app (public, no auth) |
| GET | /api/organization/:slug/branding | Get current branding config |
| PUT | /api/organization/:slug/branding | Update branding settings |
Expanded White-Label Fields
The branding config supports 14 fields that give organizations full control over the login experience. All fields are optional — unset fields use Wenme defaults.
| Field | Type | Description |
|---|---|---|
| primary_color | string | Hex color for buttons and interactive elements (e.g. #2563eb) |
| background_color | string | Hex color for the login page background |
| button_text_color | string | Hex color for text on primary buttons |
| welcome_text | string | Custom heading on the login page (max 200 characters) |
| brand_name | string | Organization name shown in the login header and page title |
| favicon_url | string | URL to a custom favicon displayed in the browser tab |
| background_image_url | string | URL to a background image (overrides background_color when set) |
| font_family | string | CSS font-family for the login page (e.g. Inter, sans-serif) |
| hide_wenme_branding | boolean | When true, hides the "Powered by Wenme" footer |
| custom_footer_text | string | Custom text shown in the login page footer |
| privacy_url | string | Link to the organization's privacy policy shown on the login page |
| terms_url | string | Link to the organization's terms of service shown on the login page |
| support_email | string | Contact email for users who need help on the login page |
| login_heading | string | Replaces the default "Sign in" heading on the login form |
Per-App Branding
Individual apps can override the organization branding. Any field left empty in the app-level config inherits from the parent organization. This allows a single org to present different brands for different products.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/oauth/clients/:id/branding | Get app-level branding overrides |
| PUT | /api/oauth/clients/:id/branding | Update app-level branding overrides |
App-Level Override Example
// PUT /api/oauth/clients/:id/branding
// Only set fields you want to override; the rest inherit from the org.
{
"primary_color": "#dc2626",
"brand_name": "SecureVault",
"welcome_text": "Welcome to SecureVault",
"login_heading": "Sign in to your vault",
"hide_wenme_branding": true
}The public endpoint GET /api/oauth/branding/:client_id automatically merges the app-level overrides with the org-level defaults, so the login page always receives a complete branding config.
App Logos
In addition to organization-level branding, each app can have its own icon and full logo. These are used in OAuth consent screens and login emails.
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 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 "[email protected]"
# 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 "[email protected]"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.
Connected Apps & Consent Management
Users can view and manage which OAuth applications have access to their data. The Connected Apps page is available at Profile → Connected Apps and provides full transparency over granted permissions.
User Experience
Users see a list of all apps they have authorized, along with the granted scopes and authorization date
Users can revoke access for any app at any time with one click
Revoking consent also invalidates all active access tokens and refresh tokens for that app
The user will need to re-authorize the app on their next login
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/user/consents | List all apps the user has authorized |
| DELETE | /api/user/consents/:clientId | Revoke app access and invalidate tokens |
Example Response
// GET /api/user/consents
{
"consents": [
{
"client_id": "newsforge_abc123",
"app_name": "NewsForge",
"logo_url": "https://storage.lonesock.pro/wenme/app/...",
"scopes": ["openid", "profile", "email"],
"granted_at": "2026-01-15T08:30:00Z"
}
]
}Revoking consent is immediate and permanent. The app will lose access to the user's data and any active sessions for that app will be terminated.
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.
Security Posture
Wenme is built passwordless-first with defense in depth: Ed25519 JWT signing, AES-256-GCM encryption at rest, mandatory PKCE on OAuth 2.1, strict audience/issuer validation on all tokens, HTTP-only session cookies, CSRF with constant-time validation, SSRF-validated webhook delivery, edge-enforced CSP with frame-ancestors 'none', and a dedicated NTTN inter-site link for on-prem deployments.
For the full security architecture and compliance program, see our Security page and Compliance page.
Need Help?
Developer Support
Our team is here to help you integrate Wenme authentication into your applications.
Wenme is a KaritKarma product.
Last updated: 2026-04-13