Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/actions/pack-sample/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ inputs:
version:
description: Version being released (e.g. 1.2.3), used to name the zip file
required: true
path:
description: >
Sample app directory, relative to the repo root. Defaults to samples/<sdk>/quickstart;
pass this for a sample that doesn't follow that convention (e.g. an integration sample
under samples/integrations/<sdk>/<framework>).
required: false
default: ''

outputs:
archive:
Expand All @@ -26,14 +33,15 @@ runs:
run: |
SDK="${{ inputs.sdk }}"
VERSION="${{ inputs.version }}"
SAMPLE_PATH="${{ inputs.path }}"
DEST_DIR="$(pwd)"
FOLDER="thunderid-${SDK}-sdk-sample-v${VERSION}"
ARCHIVE="${FOLDER}.zip"

PACK_DIR="$(mktemp -d)"
trap 'rm -rf "$PACK_DIR"' EXIT

cd "samples/${SDK}/quickstart"
cd "${SAMPLE_PATH:-samples/${SDK}/quickstart}"
pnpm pack --pack-destination "${PACK_DIR}"
cd "${DEST_DIR}"

Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,22 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: 📦 Pack Sample
id: pack
uses: ./.github/actions/pack-sample
with:
sdk: better-auth
version: ${{ steps.bump.outputs.version }}
path: samples/integrations/better-auth/nextjs

- name: 🚀 Create GitHub Release
env:
GH_TOKEN: ${{ secrets.THUNDERID_AUTOMATION_BOT }}
run: |
gh release create "sdk/better-auth/v${{ steps.bump.outputs.version }}" \
--title "ThunderID Better Auth SDK v${{ steps.bump.outputs.version }}" \
--generate-notes
--generate-notes \
"${{ steps.pack.outputs.archive }}"

# ── Level 1: depends on javascript ─────────────────────────────────────────
release-browser:
Expand Down
31 changes: 31 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
packages:
- packages/*
- samples/*/*
- samples/integrations/*/*
- tests/*

allowBuilds:
Expand Down
15 changes: 15 additions & 0 deletions samples/integrations/better-auth/nextjs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Secret used by Better Auth to sign/encrypt session cookies. Generate locally, not from the console.
BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
# Base URL this app is served from. Used to derive the OAuth callback URL below.
BETTER_AUTH_URL=http://localhost:3000

# ThunderID issuer URL, e.g. https://localhost:8090. The OIDC discovery URL is derived from
# this value: {THUNDERID_ISSUER}/.well-known/openid-configuration.
THUNDERID_ISSUER=https://localhost:8090

# OAuth 2.0 / OIDC client credentials from the application's Credentials tab in the console.
THUNDERID_CLIENT_ID=your-client-id-here
THUNDERID_CLIENT_SECRET=your-client-secret-here

# DANGER: Disables ALL TLS verification. Only for local development with self-signed certs. NEVER use in production.
NODE_TLS_REJECT_UNAUTHORIZED=0
4 changes: 4 additions & 0 deletions samples/integrations/better-auth/nextjs/.stackblitzrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"installDependencies": false,
"startCommand": "npm run prepare-dev && npm install && npm run dev"
}
69 changes: 69 additions & 0 deletions samples/integrations/better-auth/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Better Auth + ThunderID Sample

<a href="https://stackblitz.com/fork/github/thunder-id/javascript-sdks/tree/main/samples/integrations/better-auth/nextjs?file=.env" target="_blank"><img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" /></a>

A minimal Next.js 15 App Router application demonstrating [Better Auth](https://better-auth.com)'s
[Generic OAuth plugin](https://better-auth.com/docs/plugins/generic-oauth) configured for ThunderID via the
[`@thunderid/better-auth`](../../../../packages/better-auth) provider helper.

All OAuth 2.0 / OIDC handling is performed by Better Auth itself — this sample has no ThunderID SDK
dependency, and keeps users/sessions in an in-memory store (`better-auth/adapters/memory`) that resets on
every server restart. Swap it for a real [database adapter](https://better-auth.com/docs/adapters) in a real
app.

## Prerequisites

- Node.js 18+
- pnpm
- A ThunderID application with an OAuth 2.0 / OIDC (`authorization_code`) client

## Getting started

1. Copy the example environment file:

```bash
cp .env.example .env
```

2. Fill in your ThunderID credentials in `.env`:

```dotenv
BETTER_AUTH_SECRET=<run: openssl rand -base64 32>
BETTER_AUTH_URL=http://localhost:3000
THUNDERID_ISSUER=https://localhost:8090
THUNDERID_CLIENT_ID=<your-client-id>
THUNDERID_CLIENT_SECRET=<your-client-secret>
```

`THUNDERID_CLIENT_ID` and `THUNDERID_CLIENT_SECRET` come from the application's Credentials tab in the
ThunderID console.

3. Register the redirect URI on your ThunderID application (see the app's config notice in this sample for
the exact value to use):

```
http://localhost:3000/api/auth/callback/thunderid
```

4. Start the development server:

```bash
pnpm dev
```

The app is now running at [http://localhost:3000](http://localhost:3000). Click **Sign in with
ThunderID** to try the flow.

## How it works

- [`lib/auth.ts`](./lib/auth.ts) — the Better Auth server instance. Registers the `genericOAuth` plugin with
the `thunderid()` helper from `@thunderid/better-auth`, which supplies the ThunderID issuer's discovery URL
and default `openid profile email` scopes.
- [`lib/auth-client.ts`](./lib/auth-client.ts) — the Better Auth React client. No client plugin is needed for
ThunderID; sign-in goes through the standard social-provider API.
- [`app/api/auth/[...all]/route.ts`](./app/api/auth/%5B...all%5D/route.ts) — the catch-all route handler that
exposes Better Auth's API via `toNextJsHandler`.
- [`app/page.tsx`](./app/page.tsx) — signs in with `authClient.signIn.social({provider: 'thunderid'})`, shows
the session via `authClient.useSession()`, and signs out with `authClient.signOut()`.
- [`app/components/ConfigNotice.tsx`](./app/components/ConfigNotice.tsx) — shown instead of the app when a
required environment variable is missing, with the exact redirect URI to register on ThunderID.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { toNextJsHandler } from 'better-auth/next-js'
import { getAuth } from '../../../../lib/auth'

export const { GET, POST } = toNextJsHandler({
handler: (request) => getAuth().handler(request),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use client'
import { useState } from 'react'
import BetterAuthLogo from './icons/BetterAuthLogo'
import ThunderMark from './ThunderMark'

function MoonIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
)
}

function SunIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" /><line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
)
}

export default function ConfigNotice({ missing }: { missing: string[] }) {
const [dark, setDark] = useState(false)

const toggle = () => {
const next = !dark
setDark(next)
document.documentElement.classList.toggle('dark', next)
}

return (
<div className="app">
<nav className="nav">
<span className="nav-logo">
<BetterAuthLogo size={24} fill="#3688ff" />
<span className="wordmark-name">Sample</span>
</span>
<div style={{ flex: 1 }} />
<div className="nav-actions">
<button className="dark-toggle" onClick={toggle} aria-label={dark ? 'Light mode' : 'Dark mode'}>
{dark ? <SunIcon /> : <MoonIcon />}
</button>
</div>
</nav>

<section className="hero">
<div className="hero-inner">
<div className="hero-mark">
<ThunderMark height={40} />
</div>

<div className="hero-badge config-badge">
<span className="hero-badge-line" />
<span>Setup required</span>
<span className="hero-badge-line" />
</div>

<h1 className="hero-title">Configuration needed</h1>

<p className="hero-subtitle">
This sample can&apos;t reach ThunderID yet. Follow the steps
below, then restart the dev server.
</p>

<div className="config-step">
<div className="config-step-label">Step 1 &middot; Set environment variables</div>

<ul className="config-list">
{missing.map((key) => (
<li key={key} className="config-list-item">{key}</li>
))}
</ul>

<p className="config-hint">
Copy <code>.env.example</code> to <code>.env</code>, fill in the
values from your ThunderID application, then run <code>npm run dev</code> again.
</p>
</div>

<div className="config-step">
<div className="config-step-label">Step 2 &middot; Register the callback URL</div>

<div className="config-box">
<p className="config-box-body">
Better Auth redirects the user to ThunderID and back via this route. In the{' '}
<strong>ThunderID Console</strong>, open this application and go
to <strong>Advanced Settings &rarr; OAuth2 Configuration</strong>,
then add the exact redirect URI below.
</p>

<div className="config-value-group">
<div>
<div className="config-value-label">Authorized redirect URI</div>
<code className="config-value">http://localhost:3000/api/auth/callback/thunderid</code>
</div>
</div>
</div>
</div>

<p className="config-docs-note">
Need more info? Take a look at the{' '}
<a href="https://better-auth.com/docs/plugins/generic-oauth" target="_blank" rel="noopener noreferrer">
Better Auth Generic OAuth plugin docs.
</a>
</p>
</div>
</section>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'
import { useState } from 'react'
import { authClient } from '../../lib/auth-client'

export default function HeroCtas() {
const [isLoading, setIsLoading] = useState(false)

const signIn = async () => {
setIsLoading(true)
await authClient.signIn.social({ provider: 'thunderid', callbackURL: '/' })
}

return (
<div className="hero-ctas">
<button className="btn-primary" onClick={() => { void signIn() }} disabled={isLoading}>
{isLoading ? 'Signing in…' : 'Sign in with ThunderID'}
</button>
</div>
)
}
64 changes: 64 additions & 0 deletions samples/integrations/better-auth/nextjs/app/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client'
import { useState } from 'react'
import BetterAuthLogo from './icons/BetterAuthLogo'
import { authClient } from '../../lib/auth-client'


function MoonIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
)
}

function SunIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" /><line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
)
}

export default function Nav() {
const [dark, setDark] = useState(false)
const { data: session } = authClient.useSession()

const toggle = () => {
const next = !dark
setDark(next)
document.documentElement.classList.toggle('dark', next)
}

const signIn = () => {
void authClient.signIn.social({ provider: 'thunderid', callbackURL: '/' })
}

const signOut = () => {
void authClient.signOut()
}

return (
<nav className="nav">
<span className="nav-logo">
<BetterAuthLogo size={24} fill="#3688ff" />
<span className="wordmark-name">Sample</span>
</span>
<div style={{ flex: 1 }} />
<div className="nav-actions">
<button className="dark-toggle" onClick={toggle} aria-label={dark ? 'Light mode' : 'Dark mode'}>
{dark ? <SunIcon /> : <MoonIcon />}
</button>
{session?.user ? (
<button className="btn-outline" onClick={signOut}>Sign out</button>
) : (
<button className="btn-primary" onClick={signIn}>Sign in</button>
)}
</div>
</nav>
)
}
Loading
Loading