Skip to content

fix: add NextjsGrader check for getSignInUrl in Server Components#110

Open
nicknisi wants to merge 5 commits intomainfrom
fix/nextjs-auth-hardening
Open

fix: add NextjsGrader check for getSignInUrl in Server Components#110
nicknisi wants to merge 5 commits intomainfrom
fix/nextjs-auth-hardening

Conversation

@nicknisi
Copy link
Copy Markdown
Member

@nicknisi nicknisi commented Mar 30, 2026

Summary

When running workos install on a Next.js B2B app, the installer agent could generate nav-auth.tsx as a Server Component calling getSignInUrl(). This adds a structural check to the NextjsGrader that catches this anti-pattern during end-to-end evals.

Changes

nextjs.grader.ts

findUnsafeGetSignInUrlUsage(workDir) — exported helper that scans .tsx files under app/ and src/app/ for getSignInUrl() invocations in server-rendered components. The detection is layered:

  • Invocation regex (/\bgetSignInUrl\s*\(/) — matches actual calls, not imports, variable names, or prose mentions
  • Comment stripping (stripComments()) — removes // and /* */ comments before testing the regex, preventing false positives on commented-out calls, TODOs, or remediation notes
  • Top-level directive detection (hasTopLevelDirective()) — checks the module prologue only (strips leading comments/whitespace, then checks if file starts with directive). Inline 'use server' in function bodies does NOT count as safe — this catches the case where a page calls getSignInUrl() at render level but also has an inline server action
  • .tsx-only glob — route handlers are .ts by convention and naturally excluded

The check is wired into grade() after the AuthKitProvider check, producing a grade check named "No getSignInUrl in Server Components". It passes when no unsafe usage is found (including when the agent uses the preferred useAuth() + refreshAuth client-side pattern and getSignInUrl doesn't appear at all).

Unit tests (8 cases)

Case Expected Why
getSignInUrl() in app/page.tsx Fail Core bug — server-rendered page
getSignInUrl() in shared nav-auth.tsx Fail Shared component without directive
Inline 'use server' with render-level getSignInUrl() Fail Prologue detection must reject inline directives
'use client' component with getSignInUrl() call Pass Client components are safe
Top-level 'use server' file Pass Server Actions run in request context
No getSignInUrl() anywhere Pass Agent used refreshAuth client-side only
Comment mention without () Pass Regex requires invocation parens
Commented-out getSignInUrl() calls Pass Comment stripping prevents false flag

Companion PR: workos/skills#13 (reference guide + eval cases)

- Add exported findUnsafeGetSignInUrlUsage() helper that scans .tsx
  files for getSignInUrl() invocations without top-level 'use client'
  or 'use server' directives
- Add hasTopLevelDirective() with module prologue detection (strips
  comments/whitespace, ignores inline 'use server' in function bodies)
- Wire check into grade() after AuthKitProvider check
- Add 7 unit tests covering: server page, shared component, client
  component, top-level server action, inline server action trap,
  no usage, and comment mention edge case

Addresses Alexander Southgate's friction log where the agent generated
nav-auth.tsx as a Server Component calling getSignInUrl().
The invocation regex matched getSignInUrl() inside comments, causing
false positives on files with commented-out examples or TODOs like
"// don't call getSignInUrl() here". Strip single-line and multi-line
comments before testing the regex. Add test case for this edge case.
The test fixture previously only imported getSignInUrl without calling
it. Now includes an actual getSignInUrl() call inside an onClick
handler to properly exercise the directive detection logic.
- Combine two-pass comment stripping into single regex
- Use path.relative() instead of fragile string replacement
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant