Skip to main content

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

  1. The user goes to Connected Apps in their wallet and clicks Disconnect next to your app.

  2. The wallet removes the connection locally (always succeeds).

  3. 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>
  4. Your /revoked page:

    • Reads appIdentity and signature from 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).
  5. Your /api/revoke endpoint verifies the signature and terminates the user's session.

Using a URL fragment rather than a query parameter prevents appIdentity from appearing in server access logs or Referer headers.


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

DoesDoes not
Notify your app that the user disconnectedDelete data the app already stored
Provide the user's appIdentity and a signatureChange the user's per-app key
Allow you to terminate the server sessionAffect 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.