Skip to main content

Integrating ConsentKeys OIDC with Better Auth

This guide is for ConsentKeys (CK) OIDC integrators who want to add ConsentKeys as an identity provider in an app using the Better Auth library. It walks through the exact setup used in this starter so you can replicate or adapt it in your own project.

Clone and get started: pseudoidc-starter — clone the repo, add your ConsentKeys and database credentials, and run.


Overview

Better Auth does not ship a dedicated “ConsentKeys” provider. ConsentKeys is integrated as a generic OIDC provider via the genericOAuth plugin. This gives you:

  • Standard OIDC authorization code flow
  • Configurable discovery and userinfo endpoints
  • User creation/linking in your database after ConsentKeys authentication

What you need from ConsentKeys:

  • Issuer URL (discovery document URL)
  • Client ID and Client Secret (from your CK OIDC client registration)
  • UserInfo URL (often {issuer}/userinfo or as in your CK docs)

What you need in your app:

  • Next.js (or another framework with a Better Auth–compatible handler)
  • PostgreSQL (or another DB supported by Better Auth)
  • Better Auth with the Drizzle adapter and genericOAuth plugin

1. Dependencies

yarn add better-auth
yarn add drizzle-orm pg
yarn add -D drizzle-kit

Use the Better Auth docs for the exact recommended versions and any peer dependencies (e.g. Next.js).


2. Environment Variables

Create a .env.local (or your app’s env file) with at least:

# Required by Better Auth
AUTH_SECRET="your-secret-key" # e.g. openssl rand -base64 32
NEXT_PUBLIC_URL="http://localhost:3000"

# Database (Better Auth stores users, sessions, accounts)
DATABASE_URL="postgresql://user:password@localhost:5432/your_db"

# ConsentKeys OIDC
CONSENTKEYS_CLIENT_ID="your-ck-client-id"
CONSENTKEYS_CLIENT_SECRET="your-ck-client-secret"
CONSENTKEYS_ISSUER_URL="https://api.consentkeys.com"
CONSENTKEYS_USERINFO_URL="https://api.consentkeys.com/userinfo"
  • Issuer URL: Base URL of your ConsentKeys OIDC issuer (e.g. where .well-known/openid-configuration is served).
  • UserInfo URL: Endpoint that returns the authenticated user’s claims (e.g. sub, name, email). Use the value from your CK docs or discovery document.

3. Database Schema (Drizzle)

Better Auth expects specific tables. This project uses Drizzle with PostgreSQL.

Path: src/lib/db/schema.ts

The tables below are required for Better Auth. Add verification only if you use the magic link plugin.

Click to expand full schema
import { timestamp, pgTable, text, boolean } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").$defaultFn(() => false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").$defaultFn(() => new Date()).notNull(),
updatedAt: timestamp("updated_at").$defaultFn(() => new Date()).notNull(),
});

export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
});

export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});

// Only needed for magic link plugin
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").$defaultFn(() => new Date()),
updatedAt: timestamp("updated_at").$defaultFn(() => new Date()),
});

Run migrations so these tables exist before using Better Auth.


4. Server-Side Auth Config (ConsentKeys as OIDC)

Path: src/lib/auth/auth.ts

  • Use the Drizzle adapter with the tables above.
  • Use the genericOAuth plugin with one provider: consentkeys-oidc.
  • Point that provider at your ConsentKeys issuer and userinfo URL.
  • Map ConsentKeys userinfo (e.g. sub, name, email, picture) to Better Auth’s expected user shape.

Example (adapt imports and env names to your project):

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { toNextJsHandler } from "better-auth/next-js";
import { genericOAuth } from "better-auth/plugins";
import { db } from "@/lib/db";
import { user, session, account } from "@/lib/db/schema";

export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: { user, session, account },
}),
baseURL: process.env.NEXT_PUBLIC_URL || "http://localhost:3000",
trustedOrigins: [process.env.NEXT_PUBLIC_URL || "http://localhost:3000"],
plugins: [
genericOAuth({
config: [
{
providerId: "consentkeys-oidc",
clientId: process.env.CONSENTKEYS_CLIENT_ID!,
clientSecret: process.env.CONSENTKEYS_CLIENT_SECRET!,
discoveryUrl: process.env.CONSENTKEYS_ISSUER_URL!,
authorizationUrlParams: {
scope: "openid profile email",
response_mode: "query",
nonce: crypto.randomUUID(),
},
getUserInfo: async (tokens) => {
const res = await fetch(process.env.CONSENTKEYS_USERINFO_URL!, {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
if (!res.ok) throw new Error(`UserInfo failed: ${res.status}`);
const userData = await res.json();
return {
id: userData.sub,
name: userData.name,
email: userData.email,
emailVerified: Boolean(userData.email_verified),
image: userData.picture || null,
createdAt: new Date(),
updatedAt: new Date(),
};
},
},
],
}),
],
secret: process.env.AUTH_SECRET!,
});

export const { POST, GET } = toNextJsHandler(auth);

Notes:

  • providerId "consentkeys-oidc" is used in the client when starting the OIDC flow and in callback URLs.
  • getUserInfo must return an object compatible with Better Auth’s user model; adjust field names if your CK userinfo uses different claims (e.g. email_verified, picture).

5. Expose the Auth API Route

In Next.js App Router, forward all Better Auth requests to the handler:

Path: src/app/api/auth/[...auth]/route.ts

import { POST, GET } from "@/lib/auth/auth";
export { POST, GET };

This gives you URLs like:

  • GET /api/auth/oauth2/consentkeys-oidc — start ConsentKeys OIDC login
  • GET /api/auth/oauth2/callback/consentkeys-oidc — OIDC callback (must be registered in CK)

6. Client-Side Auth (React)

Path: src/lib/auth/authClient.ts

import { createAuthClient } from "better-auth/react";
import { genericOAuthClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_URL || "http://localhost:3000",
plugins: [genericOAuthClient()],
});

export const { signIn, signOut, useSession } = authClient;

To trigger ConsentKeys login and then send the user to a specific page:

authClient.signIn.oauth2({
providerId: "consentkeys-oidc",
callbackURL: "/protected",
});

Use the same providerId as in the server config.


7. ConsentKeys OIDC Provider Configuration

In your ConsentKeys OIDC dashboard (or wherever you register the client):

  1. Redirect URI / Callback URL
    Add the Better Auth callback for this provider:

    • Local: http://localhost:3000/api/auth/oauth2/callback/consentkeys-oidc
    • Production: https://your-domain.com/api/auth/oauth2/callback/consentkeys-oidc
  2. Scopes
    Ensure the client is allowed to request openid profile email (or whatever you pass in authorizationUrlParams.scope).

  3. UserInfo
    Confirm the UserInfo URL and that it returns at least sub; map other claims (name, email, picture, etc.) in getUserInfo as above.


8. Login UI (Example)

Minimal example: a button that starts the ConsentKeys OIDC flow and redirects to /protected after success:

"use client";

import { authClient } from "@/lib/auth/authClient";

export function LoginWithConsentKeys() {
const handleLogin = () => {
authClient.signIn.oauth2({
providerId: "consentkeys-oidc",
callbackURL: "/protected",
});
};

return (
<button type="button" onClick={handleLogin}>
Login with ConsentKeys
</button>
);
}

You can add loading state, error handling, and branding (e.g. ConsentKeys logo) as in the starter’s login-form.tsx.


9. Protecting Routes (Server-Side)

To require ConsentKeys (or any Better Auth) login on a page:

import { auth } from "@/lib/auth/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function ProtectedPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) redirect("/login");

return (
<div>
<p>Welcome, {session.user.name}</p>
</div>
);
}

This repo also uses the magicLink plugin for passwordless email login. If you want both:

  • Add the magicLink plugin and a verification table (see schema.ts and AUTH_SETUP.md in the repo).
  • Configure sendMagicLink (e.g. with nodemailer) in auth.ts.
  • On the client, use magicLinkClient() and your custom email API (e.g. POST /api/auth/email) as in the starter.

ConsentKeys OIDC and magic link can coexist; users can choose either path.


11. Quick Checklist for CK Integrators

StepAction
1Install better-auth, DB driver, and Drizzle (or your adapter).
2Set AUTH_SECRET, NEXT_PUBLIC_URL, DATABASE_URL, and all CONSENTKEYS_* env vars.
3Add Better Auth schema (user, session, account; optional verification for magic link) and run migrations.
4Create auth.ts with Drizzle adapter and genericOAuth → one provider consentkeys-oidc with discovery + getUserInfo.
5Mount handler at /api/auth/[...auth] and export GET/POST.
6Create auth client with genericOAuthClient() and use signIn.oauth2({ providerId: "consentkeys-oidc", callbackURL }) for login.
7In ConsentKeys dashboard, add redirect URI .../api/auth/oauth2/callback/consentkeys-oidc for each environment.
8Protect routes with auth.api.getSession({ headers }) and redirect when unauthenticated.

12. References in This Repo

  • Server auth: src/lib/auth/auth.ts
  • Client auth: src/lib/auth/authClient.ts
  • Auth API route: src/app/api/auth/[...auth]/route.ts
  • Login form (ConsentKeys + magic link): src/components/forms/login-form.tsx
  • Protected page example: src/app/protected/page.tsx
  • DB schema: src/lib/db/schema.ts
  • Detailed env and callbacks: AUTH_SETUP.md

For general Better Auth options (session duration, cookies, etc.), see the Better Auth documentation.