Wenme

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

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

    OAuth refresh_token grant 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. 2

    OAuth tokens now strictly validated.

    Access tokens, ID tokens, and refresh tokens are now namespaced via the aud and token_type claims. 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 returns 401 Unauthorized.

  3. 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. 4

    Magic-link redirect_url is now allowlisted.

    The post-authentication redirect target must match either an *.wenme.net HTTPS subdomain or one of your registered OAuth client redirect_uris. Arbitrary external URLs return 400 Bad Request.

  5. 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. 6

    Strict edge security headers.

    All Wenme pages now enforce Content-Security-Policy with frame-ancestors 'none' and Cross-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

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

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.

Phishing-resistant
Cross-device via QR code
Fastest authentication method

Authenticator (TOTP)

Time-based one-time passwords from authenticator apps like Google Authenticator, Authy, or 1Password.

Works on any device
6-digit rotating codes
Backup codes for recovery

Magic Links

One-click email authentication. A unique, single-use link is sent to the user's verified email address.

Always available as fallback
No app or device required
Single-use, time-limited

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

Returns 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: includes jwt, 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/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

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

ClaimExpected ValueUsed For
audwenme-apiAccess tokens used at /api/*
aud<client_id>ID tokens (consumed by the OAuth client)
token_typeaccess_token | id_token | refresh_tokenDisambiguates 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 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

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/wenme

Post-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-out

Application URLs

website_urlhttps://yourdomain
privacy_policy_urlhttps://yourdomain/privacy
terms_of_service_urlhttps://yourdomain/terms

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:

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

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

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

1

User clicks "Sign in with Wenme"

Your app redirects to Wenme's OAuth authorize endpoint

2

Wenme authenticates the user

Passkey, authenticator app, or magic link — Wenme handles it all

3

Wenme redirects back with an authorization code

Your callback URL receives code and state parameters

4

Your backend exchanges the code for tokens

POST /oauth/token returns access_token, id_token, and refresh_token

5

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

ScenarioCall Wenme?Details
User loads a pageNoCheck your own session
Check if user is logged inNoCheck your own session cookie/JWT
Get user's email or nameNoStored in your database at login time
Refresh user profile dataOccasionallyCall /oauth/userinfo periodically (e.g. daily) if you need up-to-date profile
User logs in againYesFull OAuth flow again — Wenme handles authentication
User logs out from all appsYesCall POST /oauth/end_session to end Wenme session too

Authorization with Darwan

Wenme + Darwan = Complete Security Stack

Wenme answers "Who is this user?" (authentication). Darwananswers "What can this user do?" (authorization). Use both together for a complete identity and access management solution.

The Complete Flow

User clicks "Sign in with Wenme"
    │
    ▼
Wenme authenticates → returns user identity
    │  sub: "user-uuid-123"
    │  email: "[email protected]"
    │  name: "Alice"
    │
    ▼
Your app creates its own session (see Session Management above)
    │
    ▼
User tries to perform an action (e.g. delete an invoice)
    │
    ▼
Your app calls Darwan:
    POST https://darwan.net/v1/authorize
    {
      "tenantId":     "your-org-uuid",
      "principalId":  "user-uuid-123",    ← same ID from Wenme's "sub"
      "action":       "delete",
      "resourceType": "invoice",
      "resourceId":   "inv-001"
    }
    │
    ▼
Darwan responds: { "allowed": true, "reason": "RBAC grant matched" }
    │
    ▼
Your app allows or denies the action

Key: Use Wenme's sub (user ID from the id_token or userinfo) as Darwan's principalId. This links authentication to authorization with a single identifier.

Integration Example

Step 1: After Wenme Login — Assign Role in Darwan

In your OAuth callback, after creating the user session, assign their role in Darwan. This only needs to happen once (on first login or when roles change).

// In your OAuth callback, after getting user info from Wenme
const wenmeUser = await wenme.getUserInfo(accessToken);
// wenmeUser.sub = "user-uuid-123"

// Assign role in Darwan (first login or role change)
await fetch("https://darwan.net/v1/assignments/principal-role", {
  method: "POST",
  headers: {
    "Authorization": "Bearer dk_live_YOUR_DARWAN_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    tenantId: "your-org-uuid",
    principalId: wenmeUser.sub,   // Wenme user ID = Darwan principal
    roleId: "role-viewer-uuid",   // Role you defined in Darwan
  }),
});
Step 2: Check Permissions on Every Action

Before executing any protected action, ask Darwan if the user is allowed.

// Middleware or helper function
async function checkPermission(userId, action, resourceType, resourceId) {
  const res = await fetch("https://darwan.net/v1/authorize", {
    method: "POST",
    headers: {
      "Authorization": "Bearer dk_live_YOUR_DARWAN_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      tenantId: "your-org-uuid",
      principalId: userId,     // From your session (originally from Wenme's sub)
      action,
      resourceType,
      resourceId,
    }),
  });
  const decision = await res.json();
  return decision.allowed;
}

// Usage in your API route
app.delete("/api/invoices/:id", async (req, res) => {
  const allowed = await checkPermission(
    req.session.userId,  // Wenme's sub, stored in your session
    "delete",
    "invoice",
    req.params.id
  );

  if (!allowed) return res.status(403).json({ error: "Forbidden" });

  await db.invoices.delete(req.params.id);
  res.json({ success: true });
});

With App Invitations

When using Wenme's app invitation API with Darwan, assign the Darwan role in your OAuth callback based on the invitation.

// 1. Admin sends invitation via Wenme
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: "editor",
    redirect_url: `https://yourapp.com/dashboard?invitationId=${invitationId}`,
  }),
});

// 2. In your OAuth callback, after Wenme authenticates the user
app.get("/auth/callback", async (req, res) => {
  const { code, state } = req.query;
  const invitationId = req.query.invitationId;  // From redirect URL

  // ... exchange code, get Wenme user info ...

  // Map invitation role to Darwan role
  if (invitationId) {
    const invitation = await db.invitations.findById(invitationId);
    if (invitation && invitation.email === wenmeUser.email) {
      const darwanRoleId = mapRoleToDarwan(invitation.role);
      // e.g. "editor" → "role-editor-uuid"

      await assignDarwanRole(wenmeUser.sub, darwanRoleId);
      await db.invitations.markAccepted(invitationId);
    }
  }

  // Create session and redirect
  req.session.userId = wenmeUser.sub;
  res.redirect("/dashboard");
});

Platform Responsibilities

ResponsibilityWenmeDarwanYour App
User login (passkey, MFA, magic link)Handles it
User identity (email, name, avatar)Provides itStores locally
Session managementManages it
Roles & permissions (RBAC/ABAC)Handles itCalls Darwan
Permission checks (can user do X?)DecidesEnforces
Compliance & audit trailsRecords it
App invitationsSends & authenticatesAssigns roleLinks both

Darwan SDKs

Official SDKs are available for C#, Go, Node.js, and Rust. See darwan.net/docs for installation and setup.

C#

ASP.NET Core

Go

Gin, Fiber, etc.

Node.js

Express, Next.js

Rust

Actix, Axum

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": "[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)

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"
}

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

LevelValueBehavior
NonenoneNo enforcement. Users can log in without the feature enabled.
EncouragedencouragedUsers see a prompt to enable the feature but can dismiss it and continue.
RequiredrequiredUsers 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

MethodEndpointDescription
GET/api/org/:id/security-policyGet the organization-level security policy
PUT/api/org/:id/security-policyUpdate the organization-level security policy
GET/api/oauth/clients/:id/security-policyGet the app-level security policy override
PUT/api/oauth/clients/:id/security-policyUpdate 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).

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": "[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
FieldRequiredDescription
client_idYesYour OAuth app's client ID
client_secretYesYour OAuth app's client secret
emailYesEmail address of the user to invite
roleNoRole within the app (default: "user")
redirect_urlNoURL to redirect user after accepting. Falls back to the app's website URL, then wenme.net.
metadataNoJSON 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:

  • invitationId is 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 characters
  • redirect_uris — at least one URL, no wildcards
  • scopes — must be a subset of your organization's allowed scopes
  • client_typeconfidential or public
  • metadata — 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 minted
  • app.updated — metadata, redirect URIs or scopes changed
  • app.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.

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": "[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.

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

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

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.

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

Returns a JSON document containing all OAuth and OIDC endpoints, supported algorithms, scopes, and capabilities.

Key Fields

FieldValue
issuerhttps://identity.wenme.net
authorization_endpointhttps://identity.wenme.net/oauth/authorize
token_endpointhttps://identity.wenme.net/oauth/token
userinfo_endpointhttps://identity.wenme.net/oauth/userinfo
jwks_urihttps://identity.wenme.net/.well-known/jwks.json
revocation_endpointhttps://identity.wenme.net/oauth/revoke
introspection_endpointhttps://identity.wenme.net/oauth/introspect
end_session_endpointhttps://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_tokenstrue

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.

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 "[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
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: '[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, localhost

  • Private (RFC 1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16

  • Link-local (incl. cloud metadata 169.254.169.254): 169.254.0.0/16

  • CGNAT: 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

EventDescription
user.createdNew user account created
user.updatedUser profile information updated
user.deletedUser account deleted
user.loginUser successfully authenticated
user.logoutUser session ended
user.passkey_addedNew passkey registered
user.passkey_removedPasskey deleted
user.mfa_enabledMFA enabled on account
user.mfa_disabledMFA disabled on account
user.email_changedUser email address updated
invitation.sentInvitation sent to a user
invitation.acceptedUser accepted an invitation
consent.grantedUser granted OAuth consent to an app
consent.revokedUser revoked OAuth consent from an app
session.createdNew session established
session.endedSession terminated or expired
app.user.addedUser added to an OAuth app
app.user.removedUser removed from an OAuth app
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": "[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:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature (sha256=<hex>)
X-Webhook-TimestampUnix timestamp of the delivery
X-Webhook-IDUnique delivery ID for deduplication
X-Webhook-EventEvent 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
MethodEndpointDescription
POST/api/org/:id/webhooksCreate webhook endpoint
GET/api/org/:id/webhooksList all webhooks
PUT/api/org/:id/webhooks/:webhookIdUpdate webhook configuration
DELETE/api/org/:id/webhooks/:webhookIdDelete webhook
GET/api/org/:id/webhooks/:webhookId/deliveriesView delivery log
POST/api/org/:id/webhooks/:webhookId/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 "[email protected]"
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": "[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)
  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

FAPI 2.0 Security Profile

Financial-Grade

JARM — 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
ModeDeliveryDescription
jwtDefault (query)JWT response in query string
query.jwtQuery stringExplicit query string delivery
fragment.jwtURL fragmentJWT 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=S256
JARM 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
1Client presents its TLS certificate during the token request
2Server computes the certificate thumbprint (SHA-256) and binds it to the access token
3The issued token includes a cnf.x5t#S256 claim with the thumbprint
4On resource requests, the server verifies the same certificate is presented
Certificate 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 SSO

Wenme 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/metadata

Entity ID:

https://identity.wenme.net/saml/metadata

Format:

application/xml

SP-Initiated SSO Flow

1User clicks “Login with Wenme” on the Service Provider (SP) application
2SP sends an AuthnRequest to https://identity.wenme.net/saml/sso
3User authenticates via passkey or MFA on Wenme
4Wenme POSTs a signed SAML assertion to the SP's Assertion Consumer Service (ACS) URL
5SP validates the assertion and creates a local session for the user

Supported Bindings

BindingDirectionDescription
HTTP-RedirectSP → IdPAuthnRequest sent via GET redirect
HTTP-POSTSP → IdPAuthnRequest sent via POST form
HTTP-POSTIdP → SPSAML Response always delivered via POST

NameID Formats

FormatDescription
emailAddressUser's email address (default)
persistentOpaque, stable identifier across sessions
transientOne-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 AttributeWenme Field
emailUser email address
nameUser display name
roleOrganization role

SP Registration (Admin API)

MethodEndpointDescription
POST/api/admin/saml/service-providersRegister a new Service Provider
GET/api/admin/saml/service-providersList all registered SPs
PUT/api/admin/saml/service-providers/:idUpdate SP configuration
DELETE/api/admin/saml/service-providers/:idRemove 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.

1Register a signing certificate with Wenme. Provide the X.509 cert in PEM format during SP registration (POST /api/admin/saml/service-providers).
2Sign every <samlp:AuthnRequest> with the corresponding private key using RSA-SHA256.
3Include the <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

IdP metadata advertises WantAuthnRequestsSigned="true" — SP-signed AuthnRequests required
Assertions signed with RSA-SHA256 — X.509 certificate available in metadata
AuthnContext: MobileTwoFactorContract — reflects passwordless MFA
Optional assertion encryption with SP's public key
Single Logout (SLO) supported via front-channel and back-channel bindings
SAML 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 Sync

Sync 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

ProtocolPortSecurity
LDAP389Plaintext (not recommended)
LDAPS636TLS encrypted
STARTTLS389Upgrade to TLS

How It Works

1Configure an LDAP connector in your Organization Settings
2Wenme connects to your AD using a service account (bind DN)
3Users are synced based on a configurable search filter and base DN
4AD groups are mapped to Wenme roles (owner, admin, member, viewer)
5Synced users log in via passkeys or magic links — no AD password needed on Wenme

Attribute Mapping

Map LDAP attributes to Wenme user fields. All mappings are configurable per connector.

LDAP AttributeWenme FieldDefault
mailemailRequired
displayNamenameRequired
sAMAccountNameusernameOptional
telephoneNumberphoneOptional
titlejob_titleOptional

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)

MethodEndpointDescription
POST/api/org/:id/ldapCreate a new LDAP connector
GET/api/org/:id/ldapList LDAP connectors
PUT/api/org/:id/ldap/:connectorIdUpdate connector configuration
DELETE/api/org/:id/ldap/:connectorIdDelete a connector
POST/api/org/:id/ldap/:connectorId/testTest connection to directory
POST/api/org/:id/ldap/:connectorId/syncTrigger a manual sync
GET/api/org/:id/ldap/:connectorId/statusGet 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

Bind password encrypted with AES-256-GCM at rest
TLS or STARTTLS required for production connections
Background sync runs every 5 minutes (configurable per connector)
Sync changes are logged in the organization audit trail
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 Regulation

Data 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-export

Auth:

Session cookie required

Response:

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.json
Exported Data Categories
CategoryFields Included
ProfileName, email, username, bio, job title, company, location, social links
PasskeysDevice names, creation dates (credentials are never exported)
OAuth ConsentsAuthorized apps, granted scopes, consent timestamps
SessionsActive sessions with device info and last activity
OrganizationsMemberships, roles, join dates
App AccessApps 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
1User initiates deletion via POST /api/user/deletion-request
2Confirmation email sent to the user's registered address
330-day grace period begins — account remains accessible
4After 30 days, all personal data is permanently deleted
EndpointMethodDescription
/api/user/deletion-requestPOSTInitiate account deletion
/api/user/deletion-cancelPOSTCancel 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.

RequirementImplementation
72-hour authority notificationAutomated tracking from detection to supervisory authority notification
User notificationAutomatic email notification to affected users when risk is high
Breach managementAdmin 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
ServiceCheck IntervalWhat is Checked
Identity API60sHTTP health endpoint, response time, error rate
OAuth / OIDC60sToken issuance, JWKS availability, discovery endpoint
PostgreSQL60sConnection pool, query latency, replication lag
Redis60sPing, memory usage, eviction rate
Object Storage60sBucket accessibility, upload/download latency
Incident Severity Classification
PriorityDefinitionResponse Target
P1 — CriticalComplete service outage or data breach15 minutes
P2 — HighMajor feature degradation (e.g., login failures)1 hour
P3 — MediumMinor feature impact, workaround available4 hours
P4 — LowCosmetic issue or minor inconvenienceNext business day
Admin Availability Endpoint

Endpoint:

GET /api/admin/availability

Auth:

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

1

Configure branding in Organization Settings → Branding tab

2

During OAuth flows, the login page fetches branding based on the app's client_id

3

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

MethodEndpointDescription
GET/api/oauth/branding/:client_idFetch branding for an app (public, no auth)
GET/api/organization/:slug/brandingGet current branding config
PUT/api/organization/:slug/brandingUpdate 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.

FieldTypeDescription
primary_colorstringHex color for buttons and interactive elements (e.g. #2563eb)
background_colorstringHex color for the login page background
button_text_colorstringHex color for text on primary buttons
welcome_textstringCustom heading on the login page (max 200 characters)
brand_namestringOrganization name shown in the login header and page title
favicon_urlstringURL to a custom favicon displayed in the browser tab
background_image_urlstringURL to a background image (overrides background_color when set)
font_familystringCSS font-family for the login page (e.g. Inter, sans-serif)
hide_wenme_brandingbooleanWhen true, hides the "Powered by Wenme" footer
custom_footer_textstringCustom text shown in the login page footer
privacy_urlstringLink to the organization's privacy policy shown on the login page
terms_urlstringLink to the organization's terms of service shown on the login page
support_emailstringContact email for users who need help on the login page
login_headingstringReplaces 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.

MethodEndpointDescription
GET/api/oauth/clients/:id/brandingGet app-level branding overrides
PUT/api/oauth/clients/:id/brandingUpdate 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

1

Users see a list of all apps they have authorized, along with the granted scopes and authorization date

2

Users can revoke access for any app at any time with one click

3

Revoking consent also invalidates all active access tokens and refresh tokens for that app

4

The user will need to re-authorize the app on their next login

API Endpoints

MethodEndpointDescription
GET/api/user/consentsList all apps the user has authorized
DELETE/api/user/consents/:clientIdRevoke 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

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 <[email protected]>
Custom tenant:YourBrand Auth <[email protected]>

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.

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