Skip to content

feat: connect planner via OAuth 2.1 / OIDC#32

Open
mroderick wants to merge 4 commits into
mainfrom
feature/magic-link-code-exchange
Open

feat: connect planner via OAuth 2.1 / OIDC#32
mroderick wants to merge 4 commits into
mainfrom
feature/magic-link-code-exchange

Conversation

@mroderick

@mroderick mroderick commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Connect the auth service to the planner via OAuth 2.1 / OIDC using Better Auth's built-in oauthProvider and jwt plugins. The planner acts as a first-party OAuth client.

Commits

Commit Description
chore: add @better-auth/oauth-provider and @playwright/test dependencies Add @better-auth/oauth-provider runtime dep, bump better-auth to 1.6.20, add @playwright/test dev dep
feat: add JWT and OAuth provider plugins with login flow, seeding, and tests Core feature: Better Auth config with jwt/oauthProvider/magicLink plugins, OAuth authorize login flow, PKCE seeding, test infrastructure with schema isolation, 49 new unit/integration tests
ci: add CI pipeline and Playwright e2e test for OAuth flow CI workflow with parallel test groups + e2e Playwright job testing full magic link OAuth flow
docs: add architecture documentation covering OAuth 2.1 flows Architecture doc with sequence diagrams for GitHub OAuth and Magic Link flows

Changes

Auth configuration (src/auth.js)

  • Add jwt() plugin — issues id_tokens with email and name claims for the planner (RS256)
  • Add oauthProvider() plugin — exposes OAuth 2.1 authorize/token endpoints (/api/auth/oauth2/*)
  • Add account.skipStateCookieCheck: true — cross-site OAuth callback compatibility
  • Add trustedOrigins with localhost + auth base URL

Config (src/config.js)

  • Add AUTH_DEFAULT_PORT and PLANNER_DEFAULT_PORT constants
  • Add planner_redirect_uris and allowed_redirects from PLANNER_REDIRECT_URIS env var (comma-separated, supports multiple environments)

Login flow (src/app/routes/auth.js, src/app/components/login.js)

  • Replace redirect_url-based flow with callbackURL pointing to the OAuth authorize endpoint
  • Extract named route handlers (showLogin, showMagicLinkForm, sendMagicLink, startGitHubOAuth)
  • getCallbackURL preserves OAuth query params (PKCE, state, etc.) from the authorize request
  • GitHub and magic link buttons now POST/GET with callbackURL

Database seeding

  • src/app/db/seed-client.js — inserts the planner OAuth client via raw SQL with ON CONFLICT DO UPDATE for idempotency. Accepts redirect URIs as JSON array via $1::jsonb. Validates schemaName to prevent SQL injection.
  • scripts/migrate.js — runs migrations and seeds the planner client
  • scripts/heroku-release.sh — runs node scripts/migrate.js on every deploy

Dev test helpers

  • src/dev/magic-links.js — captures sent magic links when SENDGRID_API_KEY is unset
  • src/app/app.js — exposes /api/test/magic-links GET/DELETE in non-production, magic link endpoint localhost-restricted

CI

  • Add e2e job running Playwright after unit tests
  • Use chromium-headless-shell for smaller download
  • Add permissions: contents: read at workflow level

Documentation

  • docs/architecture.md — architecture overview with sequence diagrams, environment layout, env vars, and operational details

Testing

158 unit tests, 0 failures. New tests cover:

  • OAuth provider: planner client seeded correctly (public, PKCE-required, redirect URIs as JSON array)
  • Authorize endpoint: redirects unauthenticated users to login, issues code when authenticated
  • Token endpoint: exchanges code for access token with PKCE, rejects missing/invalid PKCE, invalid code, mismatched redirect_uri
  • Integration: full flow — authenticate -> authorize -> exchange code -> verify JWT via JWKS
  • E2E (Playwright): unauthenticated authorize -> login -> magic link -> verify -> authorize -> code

@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch 3 times, most recently from 2c4d394 to 9826035 Compare June 18, 2026 09:42
Comment thread src/app/routes/auth.js Outdated
Comment thread src/auth.js
@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch 6 times, most recently from f302a42 to 5ff85f2 Compare June 21, 2026 21:16
Comment thread .github/workflows/ci.yml Fixed
Comment on lines +26 to +39
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
- run: npm ci
- run: |
npm run prettier:check
npm run lint
npm run fallow

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe pull these into another pr to fix?

Comment thread .github/workflows/ci.yml Fixed
@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch 2 times, most recently from 714f6dc to 215a38d Compare June 21, 2026 21:32
@mroderick mroderick marked this pull request as ready for review June 21, 2026 21:42
@mroderick mroderick requested a review from till June 21, 2026 21:42
@mroderick mroderick changed the title feat: add code exchange endpoint and auth callback for planner integration feat: connect planner via OAuth 2.1 / OIDC Jun 21, 2026
@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch 2 times, most recently from 82dbb02 to 8e0c04b Compare June 22, 2026 10:44
@mroderick

Copy link
Copy Markdown
Collaborator Author

@till, would you mind reviewing this one?

@till till left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started, but I need to look more.

Comment thread src/auth.js Outdated
Comment thread .github/workflows/ci.yml Fixed
Comment on lines +26 to +39
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
- run: npm ci
- run: |
npm run prettier:check
npm run lint
npm run fallow

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe pull these into another pr to fix?

Comment thread .github/workflows/ci.yml
- run: npm ci
- run: |
npm run prettier:check
npm run lint

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand these changes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was really not well implemented.
I added an e2e test, but the workflow file took a beating in the process. I've added a commit, that makes the final diff read well.
Once review is complete, I'll rebase the confusion away.

@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch from e31a213 to 326ae91 Compare June 24, 2026 09:50
@mroderick mroderick marked this pull request as draft June 24, 2026 09:55
@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch 3 times, most recently from 1c48d90 to 430959a Compare June 24, 2026 10:06
@mroderick mroderick requested a review from till June 24, 2026 10:11
@mroderick mroderick marked this pull request as ready for review June 24, 2026 10:11
@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch 2 times, most recently from 6ad9514 to 6fe91f7 Compare June 25, 2026 11:11
…d tests

Core changes:
- Configure Better Auth with jwt, oauthProvider, magicLink plugins
- Route login flow through OAuth authorize endpoint with callbackURL
- Add skipStateCookieCheck for cross-site redirect compatibility
- Add database seeding for planner OAuth client (jsonb, idempotent, multi-URI)
- Add heroku-release.sh running migrate.js on every deploy
- Add dev helper for magic link capture in non-production
- Add test helper with isolated PG schema per test

Tests cover: provider seeding, authorize, token exchange with PKCE,
JWKS verification, and full integration flow.
Adds a CI workflow with unit tests, coverage, and a Playwright e2e job.
The e2e test covers the full magic link OAuth flow end-to-end.
@mroderick mroderick force-pushed the feature/magic-link-code-exchange branch from cc6b9fe to 14c1035 Compare June 25, 2026 15:00
@mroderick

mroderick commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

I have deployed both PRs for planner and auth to Heroku, in order to verify things in staging before we merge the PRs.

I also reviewed the PRs against OWASP top 10 for 2025, and fixed the few minor things that surfaced.

This unfortunately meant that I had to make some changes since I originally marked this as ready for review. I chose to rewrite the git history, in order to make the PRs easier to follow ... i.e. without any side quests. They should read fairly linearly now.


Testing on Heroku

To verify the OAuth 2.1 flow end-to-end:

Flow A: Sign in with GitHub

  1. Open https://codebar-staging.herokuapp.com/ — confirm the app loads
  2. Navigate to https://codebar-staging.herokuapp.com/auth/codebar << this is not linked in the UI, we'll only do that, once it is proven to work in production
  3. Observe you're redirected to https://auth.codebar.io/login?... with OAuth params
    (client_id=planner, response_type=code, code_challenge=..., state=...) in the URL
  4. Click "Sign In with GitHub" — authenticate with your GitHub account
  5. Observe you're redirected back to the staging app, signed in

Flow B: Sign in with Magic Link

  1. Clear all cookies for *.codebar.io and *.herokuapp.com to start fresh
  2. Navigate to https://codebar-staging.herokuapp.com/auth/codebar again
  3. Observe you're redirected to https://auth.codebar.io/login?...
  4. Enter your email address and click "Send Magic Link"
  5. Check your inbox for the magic link email from auth-noreply@codebar.io
  6. Click the link in the email
  7. Observe you're redirected back to the staging app, signed in

If something goes wrong

Check the staging app logs:

  heroku logs --tail -a codebar-staging

OAuth errors from the planner side show as (codebar) Authentication failure! <error_type>. The auth
app is already in production — let us know if you need access to its logs too.

@till till left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot. But LMK, what you think, not hard no's on anything. I will have a look again.

Comment thread docs/architecture.md

## Overview

The auth app (`auth.codebar.io`) is an OAuth 2.1 / OIDC provider built with

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It already serves profile, logout and I think link/unlink capabilities today. Those will stay around.

Comment thread src/app/db/seed-client.js
*/

// ponytail: raw SQL because Better Auth doesn't expose a public API to
// create OAuth clients without admin auth. If the schema changes, update here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why would we not use admin-api?

Comment thread src/app/routes/auth.js
const error = c.req.query("error");
const success = c.req.query("success");
const redirectUrl = validateRedirectUrl(c.req.query("redirect_url"), "");
function getCallbackURL(c) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be somewhere else, like in the utlities. This will is pretty large already.

Comment thread src/app/app.js

app.route("/demo", demoHandler);

// Dev-only endpoint for Playwright tests to retrieve captured magic links

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar, maybe spin them out to another file.

Comment thread src/auth.js

if (!apiKey) {
console.log(`Magic Link for ${email}: ${url}`);
devMagicLinks.push({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be more obvious, e.g. not rely on apiKey to be set (which could be a mistake), but instead use the same environment guard.

Comment thread src/config.js
export const AUTH_DEFAULT_PORT = 3001;
export const PLANNER_DEFAULT_PORT = 3000;

const parseRedirectUris = () =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the auth app have a staging? Otherwise, how would this work.

const testInstance = await getTestInstance();
const app = createApp(testInstance.auth, testInstance.db);

const params = new URLSearchParams({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put this into a helper and make it more descriptive. But the params repeats itself.

Maybe generally, I haven't look at it all in detail, but it could use some test helpers for all the tests.

Comment thread playwright.config.js
@@ -0,0 +1,17 @@
import { defineConfig, devices } from "@playwright/test";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥳

Comment thread src/auth.js
// extra signed cookie check fails on Heroku/Cloudflare because the
// `__Secure-better-auth.state` cookie (SameSite=Lax) doesn't survive
// the cross-site redirect from GitHub back to the callback.
// This is a known pattern: https://github.com/better-auth/better-auth/issues/4969

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is closed and from last year, is this even applicable?

Comment thread src/app/app.js
// Dev-only endpoint for Playwright tests to retrieve captured magic links
// Guarded by NODE_ENV check AND remote address — requests from non-local
// sources are rejected even if NODE_ENV isn't production (e.g. staging).
if (process.env.NODE_ENV !== "production") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also fetch this in config and re-use it here. So the code is not using process.env everywhere?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants