Skip to content

Commit 2287195

Browse files
committed
Add BetterAuth tutorial
1 parent 150b5b4 commit 2287195

1 file changed

Lines changed: 314 additions & 0 deletions

File tree

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
---
2+
title: Better Auth
3+
---
4+
5+
import { StepByStep, Step, StepText, StepCode } from "@site/src/components/Steps";
6+
import Tabs from '@theme/Tabs';
7+
import TabItem from '@theme/TabItem';
8+
9+
This guide will walk you through integrating **Better Auth** authentication with
10+
your **SpacetimeDB** React application. You will configure Better Auth as an
11+
OIDC provider, obtain an ID token from Better Auth, and pass that token to your
12+
SpacetimeDB connection.
13+
14+
## Prerequisites
15+
16+
We assume you have the following prerequisites in place:
17+
18+
- A working SpacetimeDB project. Follow our [React Quickstart Guide](../../00100-intro/00200-quickstarts/00100-react.md)
19+
if you need help setting this up.
20+
- A Better Auth application with a working sign-in flow.
21+
- A public URL for your Better Auth server. SpacetimeDB validates the token by
22+
fetching OIDC metadata from the token issuer, so the issuer URL must be
23+
reachable by the SpacetimeDB server.
24+
25+
## Getting started
26+
27+
<StepByStep>
28+
29+
<Step title="Install Better Auth OIDC packages">
30+
<StepText>
31+
Install the Better Auth OAuth provider plugin on your auth server and
32+
`react-oidc-context` in your React application.
33+
</StepText>
34+
<StepCode>
35+
<Tabs groupId="package-manager" defaultValue="NPM">
36+
<TabItem value="NPM" label="NPM">
37+
```bash
38+
npm add better-auth @better-auth/oauth-provider react-oidc-context
39+
```
40+
</TabItem>
41+
<TabItem value="Yarn" label="Yarn">
42+
```bash
43+
yarn add better-auth @better-auth/oauth-provider react-oidc-context
44+
```
45+
</TabItem>
46+
<TabItem value="PNPM" label="PNPM">
47+
```bash
48+
pnpm add better-auth @better-auth/oauth-provider react-oidc-context
49+
```
50+
</TabItem>
51+
<TabItem value="Bun" label="Bun">
52+
```bash
53+
bun add better-auth @better-auth/oauth-provider react-oidc-context
54+
```
55+
</TabItem>
56+
</Tabs>
57+
</StepCode>
58+
</Step>
59+
60+
<Step title="Configure Better Auth as an OIDC provider">
61+
<StepText>
62+
Add the Better Auth JWT and OAuth provider plugins. The OAuth provider plugin
63+
exposes OIDC metadata and authorization endpoints, while the JWT plugin signs
64+
the ID token that SpacetimeDB will validate.
65+
66+
Use the exact issuer URL in every place below. If your Better Auth routes live
67+
under `/api/auth`, your issuer is usually `https://your-domain.com/api/auth`.
68+
69+
After changing the Better Auth configuration, run the Better Auth migration or
70+
schema generation command for your project so the OAuth provider tables are
71+
created.
72+
</StepText>
73+
<StepCode>
74+
75+
```typescript
76+
// auth.ts
77+
import { betterAuth } from 'better-auth';
78+
import { jwt } from 'better-auth/plugins';
79+
import { oauthProvider } from '@better-auth/oauth-provider';
80+
81+
export const auth = betterAuth({
82+
// ... your existing Better Auth configuration
83+
84+
// OAuth Provider mode uses its own token endpoint.
85+
disabledPaths: ['/token'],
86+
87+
plugins: [
88+
jwt(),
89+
oauthProvider({
90+
loginPage: '/sign-in',
91+
consentPage: '/consent',
92+
scopes: ['openid', 'profile', 'email'],
93+
}),
94+
],
95+
});
96+
```
97+
98+
</StepCode>
99+
</Step>
100+
101+
<Step title="Expose Better Auth OIDC metadata">
102+
<StepText>
103+
SpacetimeDB validates external tokens by reading
104+
`<issuer>/.well-known/openid-configuration` and then fetching the issuer's JWKS.
105+
Expose the Better Auth metadata routes in your framework. The example below uses
106+
Next.js route handlers; use the equivalent route mechanism for your framework.
107+
</StepText>
108+
<StepCode>
109+
110+
```typescript
111+
// app/api/auth/.well-known/openid-configuration/route.ts
112+
import { oauthProviderOpenIdConfigMetadata } from '@better-auth/oauth-provider';
113+
import { auth } from '@/lib/auth';
114+
115+
export const GET = oauthProviderOpenIdConfigMetadata(auth);
116+
```
117+
118+
```typescript
119+
// app/.well-known/oauth-authorization-server/api/auth/route.ts
120+
import { oauthProviderAuthServerMetadata } from '@better-auth/oauth-provider';
121+
import { auth } from '@/lib/auth';
122+
123+
export const GET = oauthProviderAuthServerMetadata(auth);
124+
```
125+
126+
</StepCode>
127+
</Step>
128+
129+
<Step title="Create an OAuth client">
130+
<StepText>
131+
Create a public OAuth client for your React application and save its
132+
`client_id`. The redirect URI must match your local or production React app URL.
133+
134+
Run this from a trusted server script or admin route, not from browser code.
135+
</StepText>
136+
<StepCode>
137+
138+
```typescript
139+
const client = await auth.api.adminCreateOAuthClient({
140+
headers,
141+
body: {
142+
client_name: 'SpacetimeDB React App',
143+
redirect_uris: ['http://localhost:5173'],
144+
token_endpoint_auth_method: 'none',
145+
skip_consent: true,
146+
},
147+
});
148+
149+
console.log(client.client_id);
150+
```
151+
152+
</StepCode>
153+
</Step>
154+
155+
<Step title="Wrap your React app with AuthProvider">
156+
<StepText>
157+
Configure `react-oidc-context` to authenticate against your Better Auth issuer.
158+
The `authority` value must match the `iss` claim in the token.
159+
</StepText>
160+
<StepCode>
161+
162+
```tsx
163+
// main.tsx
164+
import { StrictMode } from 'react';
165+
import { createRoot } from 'react-dom/client';
166+
import { AuthProvider } from 'react-oidc-context';
167+
168+
import App from './App.tsx';
169+
170+
const oidcConfig = {
171+
authority: '<YOUR_BETTER_AUTH_ISSUER>',
172+
client_id: '<YOUR_BETTER_AUTH_CLIENT_ID>',
173+
redirect_uri: window.location.origin,
174+
post_logout_redirect_uri: window.location.origin,
175+
response_type: 'code',
176+
scope: 'openid profile email',
177+
automaticSilentRenew: true,
178+
};
179+
180+
function onSigninCallback() {
181+
window.history.replaceState({}, document.title, window.location.pathname);
182+
}
183+
184+
createRoot(document.getElementById('root')!).render(
185+
<StrictMode>
186+
<AuthProvider {...oidcConfig} onSigninCallback={onSigninCallback}>
187+
<App />
188+
</AuthProvider>
189+
</StrictMode>
190+
);
191+
```
192+
193+
</StepCode>
194+
</Step>
195+
196+
<Step title="Pass the Better Auth ID token to SpacetimeDB">
197+
<StepText>
198+
Use the ID token from `react-oidc-context` as the SpacetimeDB authentication
199+
token. When a user is not signed in yet, redirect them to Better Auth first.
200+
</StepText>
201+
<StepCode>
202+
203+
```tsx
204+
// App.tsx
205+
import { useEffect, useMemo } from 'react';
206+
import { useAuth } from 'react-oidc-context';
207+
import { Identity } from 'spacetimedb';
208+
import { SpacetimeDBProvider } from 'spacetimedb/react';
209+
import { DbConnection, ErrorContext } from './module_bindings';
210+
211+
const onConnect = (_conn: DbConnection, identity: Identity) => {
212+
console.log(
213+
'Connected to SpacetimeDB with identity:',
214+
identity.toHexString()
215+
);
216+
};
217+
218+
const onDisconnect = () => {
219+
console.log('Disconnected from SpacetimeDB');
220+
};
221+
222+
const onConnectError = (_ctx: ErrorContext, err: Error) => {
223+
console.log('Error connecting to SpacetimeDB:', err);
224+
};
225+
226+
function SpacetimeApp({ token }: { token: string }) {
227+
const connectionBuilder = useMemo(() => {
228+
return DbConnection.builder()
229+
.withUri('<YOUR SPACETIMEDB URL>')
230+
.withDatabaseName('<YOUR SPACETIMEDB MODULE NAME>')
231+
.withToken(token)
232+
.onConnect(onConnect)
233+
.onDisconnect(onDisconnect)
234+
.onConnectError(onConnectError);
235+
}, [token]);
236+
237+
return (
238+
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
239+
<div>
240+
<h1>SpacetimeDB React App</h1>
241+
<p>You can now use SpacetimeDB in your app with Better Auth.</p>
242+
</div>
243+
</SpacetimeDBProvider>
244+
);
245+
}
246+
247+
export default function App() {
248+
const auth = useAuth();
249+
250+
useEffect(() => {
251+
if (!auth.isLoading && !auth.isAuthenticated && !auth.activeNavigator) {
252+
auth.signinRedirect().catch(console.error);
253+
}
254+
}, [auth]);
255+
256+
if (auth.isLoading || auth.activeNavigator) {
257+
return <p>Loading...</p>;
258+
}
259+
260+
if (auth.error) {
261+
return <p>Authentication error: {auth.error.message}</p>;
262+
}
263+
264+
const token = auth.user?.id_token;
265+
266+
if (!auth.isAuthenticated || !token) {
267+
return <p>Redirecting to sign in...</p>;
268+
}
269+
270+
return <SpacetimeApp token={token} />;
271+
}
272+
```
273+
274+
</StepCode>
275+
</Step>
276+
277+
<Step title="Validate Better Auth claims in your module">
278+
<StepText>
279+
SpacetimeDB validates the token signature before your reducers run. Your module
280+
should still restrict which issuers and audiences it accepts.
281+
</StepText>
282+
<StepCode>
283+
284+
```typescript
285+
import { SenderError } from 'spacetimedb/server';
286+
287+
const BETTER_AUTH_ISSUER = '<YOUR_BETTER_AUTH_ISSUER>';
288+
const BETTER_AUTH_CLIENT_ID = '<YOUR_BETTER_AUTH_CLIENT_ID>';
289+
290+
export const onConnect = spacetimedb.clientConnected(ctx => {
291+
const jwt = ctx.senderAuth.jwt;
292+
293+
if (jwt == null) {
294+
throw new SenderError('Unauthorized: JWT is required to connect');
295+
}
296+
297+
if (jwt.issuer !== BETTER_AUTH_ISSUER) {
298+
throw new SenderError(`Unauthorized: Invalid issuer ${jwt.issuer}`);
299+
}
300+
301+
if (!jwt.audience.includes(BETTER_AUTH_CLIENT_ID)) {
302+
throw new SenderError(`Unauthorized: Invalid audience ${jwt.audience}`);
303+
}
304+
});
305+
```
306+
307+
</StepCode>
308+
</Step>
309+
310+
</StepByStep>
311+
312+
You are now set up to use **Better Auth** authentication with SpacetimeDB. When
313+
users open your React application, they will sign in through Better Auth, receive
314+
an OIDC ID token, and connect to SpacetimeDB using that token.

0 commit comments

Comments
 (0)