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.