|
| 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