Sign in with Google
By the end you will have GET /auth/oauth/google, a callback that creates or finds the user, and a
protected route that knows who they are. About twenty minutes, most of it in Google’s console.
-
Install
Section titled “Install”Terminal window bun add @theoven/auth @theoven/auth-basic drizzle-orm -
Get credentials from Google
Section titled “Get credentials from Google”In the Google Cloud console: Create credentials → OAuth client ID → Web application.
Add one Authorised redirect URI, and it must match exactly what your app will send — scheme, host, port, path, no trailing slash:
http://localhost:3000/auth/oauth/google/callbackCopy the client ID and secret into
.env:.env GOOGLE_CLIENT_ID=...apps.googleusercontent.comGOOGLE_CLIENT_SECRET=...APP_URL=http://localhost:3000AUTH_SECRET=a-long-random-stringCOOKIE_SECRET=another-long-random-string -
Add the accounts table
Section titled “Add the accounts table”Linked provider accounts live in their own table, on a separate export path — so an app that never uses social sign-in never gets it:
src/schema.ts export * from '@theoven/auth-basic/schema'export * from '@theoven/auth-basic/schema/accounts'Terminal window bun run db:generate && bun run db:migrate -
Configure the provider
Section titled “Configure the provider”src/app.ts import { createApp, env } from '@theoven/core'import { auth, google } from '@theoven/auth'import { basicAuth } from '@theoven/auth-basic'import { db } from '@theoven/db'import { drizzleSqlite } from '@theoven/db-drizzle'import { Database } from 'bun:sqlite'import { drizzle } from 'drizzle-orm/bun-sqlite'import * as schema from './schema'const sqlite = new Database('./data.db')const client = drizzle(sqlite, { schema })export const app = createApp({ cookies: { secret: env.string('COOKIE_SECRET') } }).use(db(drizzleSqlite({ client: sqlite, schema }))).use(auth(basicAuth({db: client,secret: env.string('AUTH_SECRET'),callbackUrl: (provider) =>`${env.string('APP_URL')}/auth/oauth/${provider}/callback`,oauth: {google: {provider: google,clientId: env.string('GOOGLE_CLIENT_ID'),clientSecret: env.string('GOOGLE_CLIENT_SECRET'),},},}),),)cookies.secretis not optional here — the sign-in handshake is carried in a signed cookie, and signing without a secret throws rather than quietly not signing. -
Try it
Section titled “Try it”Terminal window bun run devopen http://localhost:3000/auth/oauth/googleYou land back on the callback with a user and an access token:
{"accessToken": "eyJhbGciOi…","expiresIn": 900,"created": true} -
Use it on a route
Section titled “Use it on a route”src/routes/me.get.ts export const auth = trueexport default async ({ user }) => ({ id: user.id, email: user.email, name: user.name })Terminal window curl localhost:3000/me -H "Authorization: Bearer <accessToken>"
Sending the browser somewhere afterwards
Section titled “Sending the browser somewhere afterwards”Returning JSON is right for an API. A web app wants a redirect:
basicAuth({ // … afterOAuth: '/dashboard',})Or per sign-in, which is how you send someone back to the page they were on:
/auth/oauth/google?redirect=/settings/billingThe refresh token is set as an httpOnly cookie either way, so the browser can call
POST /auth/refresh for a new access token without your JavaScript ever holding it.
What just happened
Section titled “What just happened”| A first sign-in | created the user, linked the Google account, issued a session |
| A returning sign-in | found them by the Google account id — not the email |
| Their password | an unusable sentinel; they have none, and change-password answers 409 |
| The session | the same access JWT a password login issues, so logout and sign-out-everywhere work |
Adding GitHub too
Section titled “Adding GitHub too”import { github, google } from '@theoven/auth'
const providers = { google: { provider: google, clientId: env.string('GOOGLE_CLIENT_ID'), clientSecret: env.string('GOOGLE_CLIENT_SECRET'), }, github: { provider: github, clientId: env.string('GITHUB_CLIENT_ID'), clientSecret: env.string('GITHUB_CLIENT_SECRET'), },}GitHub’s callback URL is set under Settings → Developer settings → OAuth Apps. Its default
scopes include user:email, which is required: GitHub omits the address from the profile whenever
the user has kept it private, and without that scope the sign-in fails rather than creating an
account with no email.
Keeping password sign-in, or not
Section titled “Keeping password sign-in, or not”Both work side by side by default — someone can sign up with a password and attach Google, and they are one user.
For a Google-only application, turn the password endpoints off:
basicAuth({ db: client, secret, password: false, callbackUrl, oauth: { google: googleConfig } })That mounts no signup, login, forgot-password, reset-password or change-password.
refresh, logout and me stay — a session is a session however it began.
What this does not do
Section titled “What this does not do”- No “link Google” button for someone already signed in. Linking happens as a side effect of signing in, so a password user has to sign out first. An endpoint for the settings-page case is not built yet.
- No Apple. Its client secret is a JWT you sign yourself and its callback is a form POST; it needs its own work.
- Provider tokens are not stored, so you cannot call Google’s API on the user’s behalf. Pass
storeTokens: trueon the provider if you need to — and only then, because stored tokens are a liability. - Someone signing in with Google at an address that already has a password account is linked only because Google says it verified the address. A provider that does not verify is refused, with a message telling them to sign in and link from settings.