Integrate NoForm
NoForm is a reference implementation of the open auth.md agent-registration protocol.
Three moving parts, all plain HTTP — no SDK lock-in:
- Host the protocol on your domain — reverse-proxy
/auth.md+/.well-known/*+/agent/auth/*tonoform.dev/a/<slug>/…(or use a Pro custom domain). - Validate the agent-issued credential with
/agent/auth/introspectand issue your own session. - Sync users from signed webhooks (
claim.confirmed,verified.accepted).
The auth.md signup skill
You host auth.md; the companion auth.md-signup skill lets any agent complete the flow. It discovers your discovery files, registers anonymously, has the human confirm the emailed code (the consent gate — the agent never auto-confirms), and stores a scoped, revocable credential per service. Primary credentials are never touched. Works against any auth.md service — NoForm is the reference target.
# install the skill, then just ask your agent openclaw skills install auth-md-signup > "Sign me up for NoForm"
The helper
Copy this one file (it's also in the Acme example):
// lib/noform.ts — dependency-free, copy this in (Node runtime)
import crypto from "node:crypto";
export async function introspect(base, slug, apiKey, credential) {
const r = await fetch(`${base}/a/${slug}/agent/auth/introspect`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ credential }), cache: "no-store",
});
return r.ok ? r.json() : { active: false };
}
export function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signatureHeader ?? ""), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Next.js
Host the protocol on your own domain with rewrites, then validate the credential in a server component.
// next.config.ts — serve auth.md on your domain
export default {
async rewrites() {
const base = "https://noform.dev/a/<slug>";
return [
{ source: "/auth.md", destination: `${base}/auth.md` },
{ source: "/agent/auth", destination: `${base}/agent/auth` },
{ source: "/agent/auth/:p*", destination: `${base}/agent/auth/:p*` },
{ source: "/.well-known/:p*", destination: `${base}/.well-known/:p*` },
];
},
};
// app/welcome/page.tsx
const user = await introspect(BASE, SLUG, API_KEY, credential);
if (user.active) /* set your session cookie */ ;React (SPA)
Keep the API key server-side. The agent sends the user to /welcome?credential=… ; a tiny backend route introspects and sets a session, then your SPA reads it normally.
// server route (never expose the API key to the browser)
app.get("/welcome", async (req, res) => {
const u = await introspect(BASE, SLUG, API_KEY, req.query.credential);
if (u.active) { setSessionCookie(res, u.email); return res.redirect("/"); }
res.status(401).send("invalid credential");
});Node / Express
A signed webhook receiver to sync users, and introspection to log them in.
import crypto from "node:crypto";
app.post("/noform-webhook", express.raw({ type: "*/*" }), (req, res) => {
if (!verifyWebhook(req.body.toString(), req.get("x-noform-signature"), SECRET))
return res.status(401).end();
const evt = JSON.parse(req.body.toString());
// upsert on evt.type === "claim.confirmed" / "verified.accepted"
res.json({ ok: true });
});Python / FastAPI
Verify the webhook signature, then create/sync the user.
import hmac, hashlib, json, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/noform-webhook")
async def hook(req: Request):
raw = await req.body()
expected = "sha256=" + hmac.new(os.environ["NOFORM_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(req.headers.get("x-noform-signature", ""), expected):
raise HTTPException(401)
evt = json.loads(raw) # upsert on evt["type"]
return {"ok": True}Ruby on Rails
HMAC-verify the delivery, then upsert.
post "/noform-webhook" do
raw = request.body.read
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", ENV["NOFORM_WEBHOOK_SECRET"], raw)
halt 401 unless Rack::Utils.secure_compare(request.env["HTTP_X_NOFORM_SIGNATURE"].to_s, expected)
evt = JSON.parse(raw) # upsert on evt["type"]
endPHP
Introspect a credential and verify a webhook with the standard library.
function nf_introspect($base,$slug,$apiKey,$credential){
$ch=curl_init("$base/a/$slug/agent/auth/introspect");
curl_setopt_array($ch,[CURLOPT_POST=>1,CURLOPT_RETURNTRANSFER=>1,
CURLOPT_HTTPHEADER=>["Content-Type: application/json","Authorization: Bearer $apiKey"],
CURLOPT_POSTFIELDS=>json_encode(["credential"=>$credential])]);
$r=json_decode(curl_exec($ch),true); curl_close($ch); return $r;
}
function nf_verify($raw,$sig,$secret){
return hash_equals("sha256=".hash_hmac("sha256",$raw,$secret),(string)$sig);
}WordPress
An mu-plugin registers a REST webhook that creates the user; a /welcome template introspects and logs them in.
add_action('rest_api_init', function () {
register_rest_route('noform/v1', '/webhook', [
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => function ($req) {
$raw = $req->get_body();
$sig = $req->get_header('x-noform-signature');
$ok = hash_equals('sha256=' . hash_hmac('sha256', $raw, NOFORM_SECRET), (string) $sig);
if (!$ok) return new WP_REST_Response(['error' => 'bad_sig'], 401);
$evt = json_decode($raw, true);
// wp_insert_user(...) on claim.confirmed
return ['ok' => true];
},
]);
});Supabase
On the webhook, create the user with the service role; on /welcome introspect, then mint a session. Keep both keys server-side.
// webhook receiver (service role)
await supabase.auth.admin.createUser({ email, email_confirm: true });
// /welcome
const u = await introspect(BASE, SLUG, NOFORM_API_KEY, credential);
if (u.active) {
const { data } = await supabase.auth.admin.generateLink({ type: "magiclink", email: u.email });
// redirect the user through data.properties.action_link
}WorkOS AuthKit
Create the WorkOS user on provisioning, then start an AuthKit session on /welcome.
import { WorkOS } from "@workos-inc/node";
const workos = new WorkOS(process.env.WORKOS_API_KEY);
// webhook: on claim.confirmed / verified.accepted
await workos.userManagement.createUser({ email, emailVerified: true });
// /welcome: introspect → then start an AuthKit session for that userAny HTTP backend
There's no SDK requirement — it's plain HTTP. Serve the three files (reverse-proxy to noform.dev/a/<slug>/… or use a Pro custom domain), POST to /agent/auth/introspect with your API key to validate a credential, and verify webhook deliveries with an HMAC-SHA256 check of the raw body against the X-NoForm-Signature header.
See it working
acme.noform.dev is a real app built with every piece above — host, introspect, welcome.