For relying parties
Point your auth library at the discovery document and you are done. Every snippet below uses the generic OIDC option of the library in question, so there is nothing Dead Simple specific to install. Two things to decide up front:
- Open or registered client. An open client is any https origin you control:
client_idis that origin, everyredirect_urimust share it, there is no secret, and PKCE carries the security. Open clients can requestopenid email profile. A registered client (onePOST /register) gets adse_idc_id and adse_ics_secret, and can also requestowner_email,organdoffline_access. See the comparison below. - Which claims you key on. Use
subas the user id. It is the inbox id and it does not change if the agent is renamed. Theemailclaim is the inbox address and is always verified, because the inbox is the credential.
# Everything an OIDC library needs is here curl https://id.deadsimple.email/.well-known/openid-configuration # Issuer https://id.deadsimple.email # Authorization https://id.deadsimple.email/authorize (GET or POST) # Token https://id.deadsimple.email/token # Userinfo https://id.deadsimple.email/userinfo # JWKS https://id.deadsimple.email/.well-known/jwks.json # Registration https://id.deadsimple.email/register (RFC 7591) # Revocation https://id.deadsimple.email/revoke (RFC 7009) # id_token alg ES256 PKCE S256 only # token_endpoint_auth none, client_secret_basic, client_secret_post
Auth.js (NextAuth v5)
Auth.js accepts a custom provider object with type: "oidc". With issuer set it reads the discovery document itself; wellKnown is shown for clarity and for v4. The callback path Auth.js uses is /api/auth/callback/<id>, which shares your origin, so an open client works with no registration.
import NextAuth from "next-auth" export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ { id: "deadsimple", name: "Dead Simple", type: "oidc", issuer: "https://id.deadsimple.email", wellKnown: "https://id.deadsimple.email/.well-known/openid-configuration", // Open client: your https origin is the client_id, no secret. clientId: "https://app.example.com", client: { token_endpoint_auth_method: "none" }, // Registered client instead? Use the ids from /register: // clientId: "dse_idc_...", clientSecret: "dse_ics_...", // and add "owner_email org" to the scope below. checks: ["pkce", "state"], authorization: { params: { scope: "openid email profile" } }, profile(claims) { return { id: claims.sub, email: claims.email, name: claims.name } }, }, ], })
Better Auth
Better Auth's genericOAuth plugin takes a discoveryUrl and does the rest. Its callback path is /api/auth/oauth2/callback/<providerId>. The snippet uses a registered client so it can ask for owner_email; drop clientSecret and the two extra scopes for an open client.
import { betterAuth } from "better-auth" import { genericOAuth } from "better-auth/plugins" export const auth = betterAuth({ plugins: [ genericOAuth({ config: [ { providerId: "deadsimple", discoveryUrl: "https://id.deadsimple.email/.well-known/openid-configuration", clientId: process.env.DSE_ID_CLIENT_ID, // dse_idc_... clientSecret: process.env.DSE_ID_CLIENT_SECRET, // dse_ics_... scopes: ["openid", "email", "profile", "owner_email", "org"], pkce: true, }, ], }), ], }) // client side await authClient.signIn.oauth2({ providerId: "deadsimple", callbackURL: "/dashboard" })
Clerk
In the Clerk Dashboard open SSO Connections, add a connection for all users, and choose Custom OpenID Connect (OIDC) provider. Clerk requires a client secret, so register a client first and paste Clerk's redirect URL into redirect_uris.
| Clerk field | Value |
|---|---|
| Name | Dead Simple |
| Key | deadsimple |
| Discovery Endpoint | https://id.deadsimple.email/.well-known/openid-configuration |
| Client ID / Client Secret | dse_idc_... / dse_ics_... from /register |
| Scopes | openid email profile (add owner_email org if you want them) |
| Attribute mapping | User ID sub, Email email, Name name |
Supabase
Supabase Auth does not act as a relying party for arbitrary OIDC issuers: signInWithOAuth is limited to its built-in provider list and signInWithIdToken accepts a fixed set of issuers. The honest integration is to run the OIDC flow yourself (or with one of the libraries above), then verify the id_token against our JWKS in an Edge Function and create or look up the Supabase user keyed on sub.
import { createRemoteJWKSet, jwtVerify } from "npm:jose@5" import { createClient } from "npm:@supabase/supabase-js@2" const JWKS = createRemoteJWKSet(new URL("https://id.deadsimple.email/.well-known/jwks.json")) Deno.serve(async (req) => { const { id_token } = await req.json() const { payload } = await jwtVerify(id_token, JWKS, { issuer: "https://id.deadsimple.email", audience: "https://app.example.com", // your client_id algorithms: ["ES256"], }) // payload.sub is the inbox_id; payload.email is the agent's address. const admin = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!) const { data } = await admin.auth.admin.createUser({ email: payload.email as string, email_confirm: true, user_metadata: { inbox_id: payload.sub, agent: true, owner_verified: payload.owner_verified }, }) // Then issue a session for data.user (magic link via generateLink, or your own JWT). return Response.json({ user_id: data.user?.id }) })
Auth0
Use Auth0's OpenID Connect connection type (Authentication, Enterprise, OpenID Connect), which takes the issuer and discovers the endpoints. A Custom Social Connection also works if you prefer to fill in the authorize and token URLs by hand. Either way, register a client and add https://<tenant>.auth0.com/login/callback to its redirect_uris. Via the Management API:
{
"name": "deadsimple",
"strategy": "oidc",
"options": {
"type": "back_channel",
"discovery_url": "https://id.deadsimple.email/.well-known/openid-configuration",
"client_id": "dse_idc_...",
"client_secret": "dse_ics_...",
"scope": "openid email profile owner_email org"
}
}
Registering a client
One call, authenticated with a Dead Simple API key (trial keys from agent self-signup cannot register clients). The secret is shown once. The same thing is available with the API envelope at POST /v1/identity/clients, and in the dashboard.
curl -X POST https://id.deadsimple.email/register \ -H "Authorization: Bearer dse_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "client_name": "Acme SaaS", "client_uri": "https://acme.example", "redirect_uris": ["https://acme.example/auth/callback"], "scope": "openid email profile owner_email org offline_access", "token_endpoint_auth_method": "client_secret_basic" }' # 201 { "client_id": "dse_idc_3f9a1c77b2e04d5a6b8c", "client_secret": "dse_ics_...", // shown once "client_secret_expires_at": 0, "redirect_uris": ["https://acme.example/auth/callback"], "scope": "openid email profile owner_email org offline_access", "token_endpoint_auth_method": "client_secret_basic", "grant_types": ["authorization_code", "refresh_token"], "issuer": "https://id.deadsimple.email", "discovery_url": "https://id.deadsimple.email/.well-known/openid-configuration" }