@dgonee/id-client
v0.3.0
Published
Shared client for id.daegun.dev — OIDC sign-in plugin, session policy, MCP token verification
Readme
@dgonee/id-client
The one way apps talk to id.daegun.dev — the central
identity hub. A thin, config-only adapter over better-auth's OIDC/OAuth: browser
sign-in for app users, and JWT access-token verification for MCP resource servers.
Security settings live here, so a fix lands in every app with a version bump.
npm install @dgonee/id-client better-auth[email protected] is a peer dependency. Requires Node ≥ 20.19 (the
package is CJS; its deps are ESM-only, loaded via require(esm)).
Two entry points:
@dgonee/id-client— server config (idProvider,appSessionConfig,verifyIdAccessToken) plus, for back-compat, everything below.@dgonee/id-client/client— browser-safe subset (idClientPlugin,signInWithId, constants)."use client"files import this subpath: its module graph never touches the server-side better-auth plugin orjose, so sign-in pages don't drag server code into their bundle.
Browser sign-in (an app that delegates login to the hub)
Server — register the hub as an OAuth provider:
import { betterAuth } from "better-auth";
import { idProvider, appSessionConfig } from "@dgonee/id-client";
export const auth = betterAuth({
...appSessionConfig, // spread the WHOLE object — see the warning below
plugins: [
idProvider({
clientId: process.env.ID_CLIENT_ID!,
clientSecret: process.env.ID_CLIENT_SECRET!,
// Optional: force re-authentication on every explicit sign-in (shared
// devices) instead of silently resuming the current hub identity.
prompt: "login",
}),
],
});Client — add the plugin and trigger sign-in (note the /client subpath):
import { createAuthClient } from "better-auth/client";
import { idClientPlugin, signInWithId } from "@dgonee/id-client/client";
const authClient = createAuthClient({ plugins: [idClientPlugin()] });
signInWithId(authClient, "/dashboard"); // callbackURL after login
// or, to land failures (e.g. an app-side invite rejection) on your own page:
signInWithId(authClient, { callbackURL: "/dashboard", errorCallbackURL: "/" });Spread the whole
appSessionConfig(and itsadvancedobject) — never cherry-pick keys.advanced.useSecureCookiesandadvanced.defaultCookieAttributestogether carry the__Secure-cookie prefix and the Secure/HttpOnly attributes. Dropping any of them silently produces a weaker, non-__Secure-session cookie in production — no error, no warning. To override one attribute, spread first:advanced: { ...appSessionConfig.advanced, /* override */ }.
MCP resource server (verify a hub-issued JWT)
import { verifyIdAccessToken } from "@dgonee/id-client";
const { sub, scopes } = await verifyIdAccessToken(bearerToken, {
audience: "https://your-app.example.com", // this server's registered audience
});Verification checks the signature against the hub's rotating JWKS, the iss
claim, and — critically — the audience: a token minted for app A is rejected
at app B. Tolerates ≤60s clock skew; JWKS is cached per issuer.
Exports
| Export | Entry | Purpose |
|---|---|---|
| idProvider(opts) | root | better-auth genericOAuth config for the hub (server) |
| idClientPlugin | /client | better-auth client plugin (genericOAuthClient) |
| signInWithId(client, opts?) | /client | start the hub sign-in redirect; opts is a callbackURL string or { callbackURL?, errorCallbackURL? } |
| appSessionConfig | root | session + secure-cookie config (spread whole; server) |
| verifyIdAccessToken(token, opts) | root | validate a hub JWT access token (MCP) |
| ID_ISSUER, ID_SCOPES | /client | constants (https://id.daegun.dev, openid profile email) |
The root entry re-exports everything from /client, so 0.1.0-era root
imports keep working — the subpath exists so browser bundles can avoid the
root's server-only imports, not because the root lost anything.
License
MIT © Daegun Lee
