Handling Revocation
When a user disconnects your app from their mwen.io wallet, the extension sends a signed revocation notification to your app. This page explains the mechanics and how to implement the required endpoints.
How revocation works
-
The user goes to Connected Apps in their wallet and clicks Disconnect next to your app.
-
The wallet removes the connection locally (always succeeds).
-
The extension opens a new browser tab pointed at your
revocationPath(default:/revoked) with the following URL fragment:https://yourapp.com/revoked#appIdentity=<did:jwk>&signature=<jws> -
Your
/revokedpage:- Reads
appIdentityandsignaturefrom the URL fragment (never from the query string — fragments are not sent to the server). - Strips the fragment from browser history.
- POSTs the values to your API (
/api/revoke).
- Reads
-
Your
/api/revokeendpoint verifies the signature and terminates the user's session.
Using a URL fragment rather than a query parameter prevents
appIdentityfrom appearing in server access logs orRefererheaders.
The revocation page (/revoked)
// app/revoked/page.tsx (Next.js App Router, client component)
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default function RevokedPage() {
const router = useRouter();
useEffect(() => {
const fragment = window.location.hash.slice(1); // remove leading '#'
if (!fragment) return;
// Immediately clear the fragment from browser history
window.history.replaceState(null, '', window.location.pathname);
const params = new URLSearchParams(fragment);
const appIdentity = params.get('appIdentity');
const signature = params.get('signature');
if (!appIdentity) return;
// Notify your API
fetch('/api/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appIdentity, signature }),
}).finally(() => {
// Redirect to home or a goodbye page
router.replace('/');
});
}, [router]);
return <p>Disconnecting...</p>;
}
The revoke API endpoint (/api/revoke)
// app/api/revoke/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { appIdentity, signature } = await req.json() as {
appIdentity?: string;
signature?: string;
};
if (!appIdentity) {
return NextResponse.json({ error: 'Missing appIdentity' }, { status: 400 });
}
// 1. Find the user in your database by appIdentity (did:jwk)
const user = await db.users.findByDid(appIdentity);
if (!user) {
// Unknown user — nothing to revoke; respond 200 to avoid leaking info
return NextResponse.json({ ok: true });
}
// 2. Optionally verify the JWS signature
// The signature is an ES256 JWS signed with the user's per-app private key.
// Verify using the public key embedded in appIdentity (did:jwk self-resolution).
// This step is recommended but not required — the appIdentity alone is sufficient
// to identify and invalidate the session.
// 3. Terminate the session
await db.sessions.deleteByDid(appIdentity);
return NextResponse.json({ ok: true });
}
The revocation signature
The signature parameter is an ES256 JWS signed with the user's per-app private key. Its payload is:
{
"appIdentity": "did:jwk:eyJrdH...",
"revokedAt": 1708444800000
}
To verify the signature, self-resolve the did:jwk to extract the P-256 public key (same process as in Verifying Credentials):
import { importJWK, compactVerify } from 'jose';
async function verifyRevocationSignature(
appIdentity: string,
signature: string
): Promise<boolean> {
try {
const b64 = appIdentity.replace('did:jwk:', '');
const jwk = JSON.parse(Buffer.from(b64, 'base64url').toString());
const publicKey = await importJWK(jwk, 'ES256');
await compactVerify(signature, publicKey);
return true;
} catch {
return false;
}
}
Signature verification is recommended but optional. The appIdentity value is your stable user identifier — even without a valid signature, receiving a revocation for a known appIdentity is sufficient justification to clear that user's session.
Revocation configuration
Register your revocationPath in MwenClientConfig. The origin is determined automatically from window.location.origin:
const mwenConfig: MwenClientConfig = {
clientId: 'myapp.com',
appName: 'My App',
revocationPath: '/revoked', // default
};
The extension will open https://myapp.com/revoked#appIdentity=...&signature=....
What revocation does and does not do
| Does | Does not |
|---|---|
| Notify your app that the user disconnected | Delete data the app already stored |
Provide the user's appIdentity and a signature | Change the user's per-app key |
| Allow you to terminate the server session | Affect other apps the user has connected |
If the user reconnects later, they will sign in again and receive the same appIdentity — their per-app key is deterministic. Treat a re-authentication after revocation the same as a new sign-in.