-
Notifications
You must be signed in to change notification settings - Fork 3
fix(workos): mandate POST server-action sign-out in Next.js skill #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -200,6 +200,40 @@ export function NavAuth() { | |||||
|
|
||||||
| Call `getSignInUrl()` **only** inside a Server Action or Route Handler. It is the safe wrapper for AuthKit sign-in/sign-up flows. | ||||||
|
|
||||||
| ### Sign out with a POST server action, never a GET route | ||||||
|
|
||||||
| Sign-out **mutates state** — it clears the session — so it must never be a `GET` route handler. A `GET /auth/signout` is unsafe: Next.js `<Link>` prefetch can trigger it on hover (logging users out unexpectedly), and it is CSRF-exposable via `<img src="/auth/signout">`. `workos doctor` flags this as `SIGNOUT_GET_HANDLER`. | ||||||
|
|
||||||
| Use a **POST server action**. In a Server Component, an inline action is fine: | ||||||
|
|
||||||
| ```tsx | ||||||
| <form action={async () => { 'use server'; await signOut(); }}> | ||||||
| <button type="submit">Sign out</button> | ||||||
| </form> | ||||||
| ``` | ||||||
|
|
||||||
| A **client** component (for example a nav that needs `useAuth()`) cannot define an inline `'use server'` action — put it in a separate server-action module and import it: | ||||||
|
|
||||||
| ```tsx | ||||||
| // app/auth/actions.ts | ||||||
| 'use server'; | ||||||
| import { signOut } from '@workos-inc/authkit-nextjs'; // verify export path in README | ||||||
| export async function signOutAction() { | ||||||
| await signOut(); | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ```tsx | ||||||
| 'use client'; | ||||||
| import { signOutAction } from '@/app/auth/actions'; | ||||||
| // ... | ||||||
| <form action={signOutAction}> | ||||||
| <button type="submit">Sign out</button> | ||||||
| </form> | ||||||
| ``` | ||||||
|
|
||||||
| `signOut()` accepts an optional `{ returnTo }`; with none, it redirects to the Logout URI configured in your WorkOS dashboard. If a generated `GET` sign-out route exists, **delete it** rather than switching it to `POST` — that removes the extra logout surface entirely. | ||||||
|
|
||||||
| ### Critical auth URL gotchas | ||||||
|
|
||||||
| - **Never** call `getSignInUrl()` / `getSignUpUrl()` inside a Server Component render (`page.tsx`, `layout.tsx`, async `nav-auth.tsx`, etc.). | ||||||
|
|
@@ -230,7 +264,10 @@ rg -n "getAuthorizationUrl|window\.location\.href\s*=\s*auth\.signInUrl" app src | |||||
| # 5. Audit getSignInUrl() usage — safe in Server Actions/Route Handlers, unsafe in page/layout/component render | ||||||
| rg -n "getSignInUrl\(" app src/app 2>/dev/null || true | ||||||
|
|
||||||
| # 6. Build succeeds | ||||||
| # 6. CRITICAL: Audit for an unsafe GET sign-out route — sign-out mutates state, so it must be a POST server action, never a GET handler | ||||||
| rg -n -g '**/{signout,sign-out,logout}/route.*' "export (async )?function GET|export const GET" app src/app 2>/dev/null && echo "FAIL: sign-out is a GET route — convert to a POST server action and delete the GET route (see 'Sign out with a POST server action, never a GET route')" || echo "OK: no GET sign-out route" | ||||||
|
|
||||||
| # 7. Build succeeds | ||||||
| npm run build | ||||||
| ``` | ||||||
|
|
||||||
|
|
@@ -289,6 +326,17 @@ This error causes OAuth codes to expire ("invalid_grant"), so fix the handler fi | |||||
| 2. For client-side sign-in buttons, use `refreshAuth({ ensureSignedIn: true })` | ||||||
| 3. Do not hand-roll the sign-in action with raw `getAuthorizationUrl()` unless you also persist `sealedState` exactly as the SDK expects | ||||||
|
|
||||||
| ### `SIGNOUT_GET_HANDLER` (flagged by `workos doctor`) | ||||||
|
|
||||||
| **Cause:** The sign-out route is a `GET` handler (e.g. `export async function GET() { return signOut(); }`), often paired with a `<form method="GET">`. A `GET` with a side effect is unsafe — Next.js prefetch and CSRF (`<img src="/auth/signout">`) can trigger logout. | ||||||
|
|
||||||
| **Fix:** | ||||||
|
|
||||||
| 1. Move sign-out to a POST server action (see "Sign out with a POST server action, never a GET route" above). | ||||||
| 2. Delete the `GET` sign-out route entirely. | ||||||
| 3. Ensure the sign-out `<form>` uses a server-action `action={...}` (or `method="POST"`), not `method="GET"`. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 4. Re-run `workos doctor` to confirm the finding clears. | ||||||
|
|
||||||
| ### "middleware.ts not found" | ||||||
|
|
||||||
| - Check: File at project root or `src/`, not inside `app/` | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline-action code block calls
signOut()without showing its import. An LLM installer agent (or a developer) copying only this block would produceReferenceError: signOut is not defined. The import is shown later in the client-component section (app/auth/actions.ts) but not here. Addingimport { signOut } from '@workos-inc/authkit-nextjs';at the top of the snippet (or in a preceding comment) would make the example self-contained.