Skip to content

workos-auth/core: Replace Clerk with WorkOS AuthKit#51

Open
x0ba wants to merge 1 commit into
stagingfrom
workos-auth/core
Open

workos-auth/core: Replace Clerk with WorkOS AuthKit#51
x0ba wants to merge 1 commit into
stagingfrom
workos-auth/core

Conversation

@x0ba
Copy link
Copy Markdown
Owner

@x0ba x0ba commented May 29, 2026

Stack Context

This 3-PR stack migrates FlightLog from Clerk to WorkOS AuthKit while preserving cookie sessions for the dashboard and Bearer tokens for the SDK/API.

Stack: workos-auth/coreworkos-auth/ui-shellworkos-auth/docs-tests

Why?

Clerk is being replaced with WorkOS AuthKit. This PR lands the server-side auth plumbing so login, callback, sign-out, and protected routes work without Clerk before any UI polish or docs updates land in the follow-ups.

What?

  • Swap @workos/authkit-sveltekit + jose for Clerk deps
  • authKitHandle with bearer verification and protected-path guards
  • OAuth callback (/callback) and POST sign-out routes
  • Hosted WorkOS sign-in/sign-up redirect interstitials
  • Minimal auth corner (email + POST sign-out) wired from layout server data

Test plan

  • bun run check
  • bun run lint (src/)
  • bunx vitest run src/lib/server/auth.test.ts src/lib/auth-redirect.test.ts
  • Manual: WorkOS dashboard configured; visit /runs → sign in → land on /runs → sign out

…auth.

Swap server hooks, auth helpers, OAuth callback/sign-out routes, and sign-in/up
redirect flows so dashboard login works end-to-end without Clerk.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flightlog Error Error May 29, 2026 8:24pm

@x0ba x0ba changed the title Replace Clerk with WorkOS AuthKit for cookie sessions and bearer API auth. workos-auth/core: Replace Clerk with WorkOS AuthKit May 29, 2026
Copy link
Copy Markdown
Owner Author

x0ba commented May 29, 2026

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR replaces Clerk with WorkOS AuthKit for server-side authentication, swapping clerk-sveltekit for @workos/authkit-sveltekit + jose and wiring up the OAuth callback, sign-out, and protected-route guards.

  • Cookie sessions are now handled by authKitHandle with iron-webcrypto-backed encryption; bearer tokens for the SDK/API path are verified via jose/JWKS using the correct WorkOS endpoint (/sso/jwks/{clientId}).
  • The marketing sign-in/sign-up pages become thin interstitials that redirect to the WorkOS-hosted UI via onMount, and the auth-corner component is simplified to show the user's email and a POST sign-out form.
  • configureAuthKit is called at module load with ?? '' fallbacks, and authKitHandle() is instantiated inside the per-request handler rather than once at module scope — both are low-risk but worth addressing before this lands in production.

Confidence Score: 4/5

The auth migration is structurally correct and the JWKS URL matches WorkOS documentation; no user data or session tokens are exposed by the changes.

The core auth flow — callback, session cookies, bearer verification, and protected-path guarding — is wired correctly. The configureAuthKit empty-string fallbacks mean a misconfigured deployment will produce a crypto failure on the first authenticated request rather than a clear startup error, which could be hard to diagnose. The authKitHandle() factory pattern also creates a new closure per request rather than reusing a module-level instance.

src/hooks.server.ts — the configureAuthKit call and the authKitHandle() instantiation pattern both warrant a second look before merging.

Important Files Changed

Filename Overview
src/hooks.server.ts Replaces Clerk handle with authKitHandle; configureAuthKit called at module load with empty-string fallbacks; authKitHandle() factory called on every request rather than cached once.
src/lib/server/auth.ts Core auth module: replaces Clerk token verification with jose/JWKS for bearer tokens, adds JWKS singleton cache, updated userId helpers. JWKS URL confirmed correct per WorkOS docs.
src/routes/callback/+server.ts New OAuth callback handler; delegates entirely to authKit.handleCallback(). Minimal and correct.
src/routes/sign-out/+server.ts New POST sign-out endpoint; delegates to authKit.signOut(event). Correct implementation.
src/routes/(marketing)/sign-in/+page.svelte JS-only redirect via onMount; no non-JS fallback so users without JavaScript see an indefinite loading state.
src/routes/(marketing)/sign-up/+page.svelte Same JS-only redirect pattern as sign-in page.
src/app.d.ts Locals updated: auth typed as non-optional AuthKitAuth (only valid after authKitHandle runs) + new optional bearerUserId. All access sites use optional chaining so no runtime risk.
src/lib/components/auth-corner.svelte Simplified to email + plain-form POST sign-out; removed Clerk component refs. Correct implementation.
src/routes/(app)/+layout.server.ts Passes userId and email from AuthKit user object to layout; correct and minimal.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant SvelteKit as SvelteKit (hooks.server.ts)
    participant AuthKitHandle as authKitHandle
    participant Auth as auth.ts
    participant WorkOS as WorkOS AuthKit
    participant Route as Route Handler

    Note over Browser,Route: Web session flow
    Browser->>SvelteKit: GET /runs (unauthenticated)
    SvelteKit->>AuthKitHandle: "authKitHandle()({event, resolve})"
    AuthKitHandle->>Auth: resolve(authEvent) guardProtectedPath
    Auth-->>SvelteKit: "redirect 303 /sign-in?redirect_url=/runs"
    SvelteKit-->>Browser: 303 /sign-in

    Browser->>SvelteKit: "GET /sign-in?redirect_url=/runs"
    SvelteKit->>Auth: "authKit.getSignInUrl({returnTo: '/runs'})"
    Auth->>WorkOS: build redirect URL
    WorkOS-->>SvelteKit: signInUrl
    SvelteKit-->>Browser: render page (JS redirects to WorkOS)
    Browser->>WorkOS: authenticate
    WorkOS-->>Browser: "redirect to /callback?code=..."

    Browser->>SvelteKit: "GET /callback?code=..."
    SvelteKit->>WorkOS: authKit.handleCallback() exchanges code
    WorkOS-->>SvelteKit: session + access token
    SvelteKit-->>Browser: set session cookie, redirect to /runs

    Note over Browser,Route: API/SDK bearer token flow
    Browser->>SvelteKit: GET /api/runs (Bearer token)
    SvelteKit->>AuthKitHandle: "authKitHandle()({event, resolve})"
    AuthKitHandle->>Auth: authenticateBearer(event)
    Auth->>WorkOS: "jwtVerify(token, JWKS at /sso/jwks/{clientId})"
    WorkOS-->>Auth: payload.sub
    Auth-->>SvelteKit: "locals.bearerUserId = sub"
    SvelteKit->>Route: resolve guarded route handler
    Route-->>Browser: 200 OK

    Note over Browser,Route: Sign-out flow
    Browser->>SvelteKit: POST /sign-out (form submit)
    SvelteKit->>WorkOS: authKit.signOut(event)
    WorkOS-->>SvelteKit: clear cookie + redirect URL
    SvelteKit-->>Browser: redirect to WorkOS sign-out
Loading

Comments Outside Diff (1)

  1. src/hooks.server.ts, line 50-64 (link)

    P2 authKitHandle() is a factory — calling it inside the async handler creates a new Handle closure on every single request. It should be called once at module scope and the resulting Handle reused, the same way ingestProxy is defined as a constant.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Cursor Fix in Codex

Fix All in Cursor Fix All in Codex

Reviews (1): Last reviewed commit: "Replace Clerk with WorkOS AuthKit for co..." | Re-trigger Greptile

Comment thread src/hooks.server.ts
Comment on lines +9 to 14
configureAuthKit({
clientId: env.WORKOS_CLIENT_ID ?? '',
apiKey: env.WORKOS_API_KEY ?? '',
redirectUri: env.WORKOS_REDIRECT_URI ?? '',
cookiePassword: env.WORKOS_COOKIE_PASSWORD ?? ''
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 All four configureAuthKit parameters fall back to an empty string when the corresponding env var is absent. A missing cookiePassword in particular will cause iron-webcrypto to throw a crypto error at request time (it requires a password of at least 32 characters), making the failure happen silently on the first authenticated request rather than at startup. Asserting these values are present at boot makes misconfiguration immediately visible.

Suggested change
configureAuthKit({
clientId: env.WORKOS_CLIENT_ID ?? '',
apiKey: env.WORKOS_API_KEY ?? '',
redirectUri: env.WORKOS_REDIRECT_URI ?? '',
cookiePassword: env.WORKOS_COOKIE_PASSWORD ?? ''
});
const missingVars = ['WORKOS_CLIENT_ID', 'WORKOS_API_KEY', 'WORKOS_REDIRECT_URI', 'WORKOS_COOKIE_PASSWORD'].filter(
(key) => !env[key as keyof typeof env]
);
if (missingVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
}
configureAuthKit({
clientId: env.WORKOS_CLIENT_ID!,
apiKey: env.WORKOS_API_KEY!,
redirectUri: env.WORKOS_REDIRECT_URI!,
cookiePassword: env.WORKOS_COOKIE_PASSWORD!
});

Fix in Cursor Fix in Codex

});
</script>

<svelte:head><title>Sign in | FlightLog</title></svelte:head>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The redirect to the WorkOS sign-in URL is wired only through onMount, so a user with JavaScript disabled will land on a loading spinner that never resolves. Adding a <noscript> meta-refresh provides a functional fallback without changing the JS path.

Suggested change
<svelte:head><title>Sign in | FlightLog</title></svelte:head>
<svelte:head>
<title>Sign in | FlightLog</title>
<noscript><meta http-equiv="refresh" content="0; url={data.signInUrl}" /></noscript>
</svelte:head>

Fix in Cursor Fix in Codex

});
</script>

<svelte:head><title>Sign up | FlightLog</title></svelte:head>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Same JS-only redirect issue as sign-in/+page.svelte — without a <noscript> fallback, users without JavaScript see an infinite loading state.

Suggested change
<svelte:head><title>Sign up | FlightLog</title></svelte:head>
<svelte:head>
<title>Sign up | FlightLog</title>
<noscript><meta http-equiv="refresh" content="0; url={data.signUpUrl}" /></noscript>
</svelte:head>

Fix in Cursor Fix in Codex

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.

1 participant