Wenme
Developer Documentation

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.

OAuth 2.1
PKCE Required
Passwordless
OpenID Connect

Quick Start

Prerequisites

1Create an OAuth app in Wenme Dashboard
2Get your Client ID and Secret
3Configure redirect URIs
4Implement PKCE (mandatory)

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-configuration

Returns 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/authorize
https://identity.wenme.net/oauth/token
https://identity.wenme.net/oauth/userinfo
https://identity.wenme.net/.well-known/jwks.json
https://identity.wenme.net/oauth/revoke
https://identity.wenme.net/oauth/introspect
https://identity.wenme.net/oauth/end_session

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 ID
redirect_uriMust match registered URI exactly
response_typeMust be "code" (OAuth 2.1)
scopeSpace-separated permissions
stateRandom string for CSRF protection
code_challengePKCE challenge (base64url)
code_challenge_methodMust be "S256"

📋 Available Scopes

openidOpenID Connect authentication
profileUser's profile information
emailUser's email address
offline_accessRefresh token for long-lived access

Note: PKCE is mandatory in OAuth 2.1. The implicit grant flow is not supported.

Setup Your Application

1

Register Your Application

Visit the Organization Dashboard to create your OAuth application.

Application Name:Your App Name
Redirect URIs:https://yourapp.com/auth/callback
Scopes:openid profile email
2

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:

PKCE mandatory for all clients
Authorization Code flow only
Exact redirect URI matching
Short-lived tokens (1 hour)
Refresh token rotation
No implicit or password flow
1

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();
2

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_token
3

Get 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_SECRET

2. 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=xyz123

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_SECRET

Response (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

EndpointMethodDescription
/oauth/authorizeGETOAuth authorization endpoint
/oauth/tokenPOSTExchange code for tokens
/oauth/userinfoGETGet user information (OIDC UserInfo)
/oauth/revokePOSTRevoke access token
/.well-known/openid-configurationGETOpenID Connect discovery
/.well-known/jwks.jsonGETJSON 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)

EndpointMethodDescription
/api/keysPOSTCreate new API key
/api/keys/tenant/:tenantIdGETList API keys for organization
/api/keys/:keyIdDELETERevoke 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:

ScopeDescription
invite:writeSend team invitations
invite:readList/view invitations
members:readList organization members
members:writeManage organization members
org:readRead organization details
org:writeUpdate organization settings
users:readRead user profiles
apps:readList OAuth applications
apps:writeManage 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).

EndpointMethodDescription
/api/tenant/invitePOSTSend team invitation email
/api/tenant/:tenantId/invitationsGETList pending invitations for organization
/api/tenant/invitation/:invitationIdDELETECancel pending invitation
/api/invitation/acceptPOSTAccept invitation (public endpoint)
/api/invitation/send-magic-linkPOSTSend magic link for invitation acceptance
/api/tenant/:tenantId/membersGETList organization members
/api/user/organizationsGETList 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.

EndpointMethodDescription
/api/profileGETGet current user's profile
/api/profilePUTUpdate user profile
/api/profile/avatarPOSTUpload profile picture
/api/profile/usernamePUTChange username
/api/profile/email/changePOSTRequest email change (requires passkey verification)
/api/user/:usernameGETGet 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.

EndpointMethodDescription
Passkey (WebAuthn) Endpoints
/api/passkey/register/startPOSTBegin passkey registration
/api/passkey/register/finishPOSTComplete passkey registration
/api/passkey/authenticate/startPOSTBegin passkey authentication
/api/passkey/authenticate/finishPOSTComplete passkey authentication
/api/passkey/listGETList user's registered passkeys
/api/passkey/:idDELETERemove a passkey
Cross-Device QR Authentication
/api/auth/qr/createPOSTGenerate QR code for cross-device login (desktop)
/api/auth/qr/scanPOSTMark QR session as scanned, get user email (phone)
/api/auth/qr/confirmPOSTConfirm authentication with passkey credential (phone)
/api/auth/qr/status/:tokenGETPoll QR session status; returns session cookie when confirmed (desktop)
TOTP/Authenticator Endpoints
/api/auth/totp/generatePOSTGenerate QR code and backup codes for setup
/api/auth/totp/verifyPOSTVerify and enable authenticator
/api/auth/totp/disablePOSTDisable authenticator (requires code)
/api/auth/totp/validatePOSTValidate TOTP code during login (public)
/api/auth/mfa/verifyPOSTVerify MFA code and create session
Magic Link Endpoints
/auth/magic-linkPOSTSend magic link email (body: email)
/api/auth/verify-magic-linkPOSTVerify magic link token and create session
/api/auth/verify-magic-link-redirectGETVerify magic link with redirect (from email links)
Session & Security Endpoints
/api/auth/methods/checkPOSTCheck available auth methods for user (public)
/api/auth/csrf-tokenGETGet CSRF token for authenticated requests
/api/sessionsGETList active sessions
/api/sessions/:id/revokePOSTRevoke a specific session
/auth/logoutPOST/GETLogout and clear session

Base URL: All authentication APIs are served from https://identity.wenme.net

OAuth Scopes

ScopeDescriptionClaims Returned
openidRequired for OIDCsub
profileUser profile informationname, picture, given_name, family_name, preferred_username, updated_at
emailUser email addressemail, email_verified
phoneUser phone numberphone_number, phone_number_verified
addressUser addressaddress (structured object)
offline_accessRefresh tokenEnables 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.

App Domain: newsforge.news
Automatically configures:
• 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:
# DNS Configuration
auth.newsforge.news CNAME newsforge-app.herokuapp.com
api.newsforge.news CNAME newsforge-api.aws.com
# OAuth Redirect URIs
https://auth.newsforge.news/callback ✅
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

Enterprise

Wenme 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
BrowserConditional UIVersion
ChromeSupported108+
SafariSupported16+
EdgeSupported108+
FirefoxPartial122+

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 FieldTypeDescription
allow_syncedbooleanAllow synced passkeys (iCloud, Google)
require_device_boundbooleanRequire hardware-bound credentials only
allowed_aaguidsstring[]Allowlist of authenticator AAGUIDs
blocked_aaguidsstring[]Blocklist of authenticator AAGUIDs

DPoP Token Binding

RFC 9449

Demonstration 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

1Client generates an asymmetric key pair (ES256)
2Client creates a DPoP proof JWT signed with the private key
3DPoP proof is sent alongside the token request via DPoP header
4Server binds the issued token to the public key (JWK thumbprint)
5On resource requests, client proves possession by sending a fresh DPoP proof
DPoP 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 9126

PAR 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

1Client POSTs all authorization parameters to /oauth/par
2Server validates and returns a request_uri
3Client redirects browser to authorize with only request_uri and client_id
4Server resolves the stored parameters and proceeds with the OAuth flow
Push 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 CIBA

Client-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

1Client sends backchannel auth request to /oauth/bc-authorize
2Server returns an auth_req_id and sends notification to user
3User receives email notification with approval link
4User approves with passkey authentication (Face ID / Touch ID)
5Client polls /oauth/token with the auth_req_id
6After approval, tokens are returned
Initiate 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
FactorScore ImpactDescription
New Device+30First login from unrecognized device fingerprint
New IP Address+15Login from previously unseen IP address
New Country+25Login from a country not in user history
Impossible Travel+40Geographic distance impossible within timeframe
Unusual Time+10Login outside user's normal hours
Failed Attempts+5 eachRecent failed authentication attempts
VPN / Tor+20Connection via VPN or Tor exit node
Risk Levels
0-30

Normal

31-60

Elevated

61-80

High

81-100

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

EventDescription
user.loginUser successfully authenticated
user.logoutUser session ended
user.createdNew user account created
user.mfa_enabledMFA enabled on account
user.mfa_disabledMFA disabled on account
user.passkey_addedNew passkey registered
user.passkey_removedPasskey deleted
user.email_changedUser email address updated
security.suspicious_activitySuspicious login pattern detected
security.account_lockedAccount locked due to failed attempts
security.high_risk_loginLogin 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
HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the payload
X-Webhook-IDUnique delivery ID for deduplication
X-Webhook-EventEvent type (e.g., user.login)
X-Webhook-TimestampUnix 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
MethodEndpointDescription
POST/api/admin/webhooksCreate webhook endpoint
GET/api/admin/webhooksList all webhooks
PUT/api/admin/webhooks/:idUpdate webhook configuration
DELETE/api/admin/webhooks/:idDelete webhook
GET/api/admin/webhooks/:id/deliveriesView delivery history
POST/api/admin/webhooks/:id/testSend test event

SCIM 2.0 Provisioning

RFC 7642/7643/7644

System 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/v2

Auth:

Authorization: Bearer wm_pk_...

Scopes:

scim:read, scim:write
Discovery Endpoints (Public, No Auth)
EndpointDescription
/scim/v2/ServiceProviderConfigServer capabilities and supported features
/scim/v2/SchemasSupported SCIM schemas
/scim/v2/ResourceTypesSupported resource types (Users, Groups)
User CRUD Endpoints
MethodEndpointDescription
GET/scim/v2/UsersList users (supports filtering)
GET/scim/v2/Users/:idGet a specific user
POST/scim/v2/UsersCreate a new user
PUT/scim/v2/Users/:idReplace user attributes
PATCH/scim/v2/Users/:idUpdate specific attributes
DELETE/scim/v2/Users/:idDeactivate user (soft delete)
Filtering
GET /scim/v2/Users?filter=userName eq "user@example.com"
GET /scim/v2/Users?filter=externalId eq "12345"
GET /scim/v2/Users?filter=active eq true
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)
  1. Go to Enterprise Applications > New Application
  2. Select "Non-gallery application"
  3. Navigate to Provisioning > Automatic
  4. Set Tenant URL to SCIM base URL
  5. Set Secret Token to your API key
  6. Test Connection, then Start Provisioning
Okta
  1. Go to Applications > Create App Integration
  2. Select SCIM 2.0 (with HTTP Header auth)
  3. Set SCIM Base URL and API Token
  4. Configure provisioning actions
  5. Enable "Create Users" and "Deactivate Users"
  6. 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

1

User clicks "Sign in with Wenme" on your app and enters their email

2

Wenme sends a magic link email branded with your app's full logo and name

3

User clicks the link and is authenticated, then redirected back to your app

What the Email Looks Like

Email Preview

Hello Jane,

[Your App's Full Logo]

Signing in to continue to Your App Name

Click the button below to instantly log in to your wenme account. No password needed.

Login with Magic Link

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.

wenme tenant:wenme SSO <noreply@wenme.net>
Custom tenant:YourBrand Auth <yourbrand.auth@wenme.net>

Email Types

Wenme sends the following transactional emails using a unified template system. All emails follow a consistent, professional design.

Email TypeWhen SentApp Branding
Magic LinkUser requests passwordless loginFull logo + app name
Email VerificationAfter registrationTenant branding
WelcomeAfter email verifiedTenant branding
Password SetupEmail-first registrationTenant branding
Security AlertNew device login, password changeTenant branding
App InvitationUser invited to an OAuth appApp 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 Dashboard

OAuth 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.