v0 prompt to add JWT authentication
Auth is where generated apps quietly cut corners, tokens in localStorage, redirects that leak. This spec forces v0 into the safe pattern: jose-signed tokens in httpOnly cookies, middleware guarding /dashboard, and a refresh rotation flow. The result is a working login you can test in the preview and audit line by line.
Last updated
Add JWT authentication to an existing Next.js App Router project without pulling in a full auth provider. Route handlers: app/api/auth/login/route.ts verifies credentials against a users table in {{database}} using bcrypt comparison, then signs a short-lived access token and a longer refresh token with the jose library, both delivered as httpOnly, secure, sameSite strict cookies, never returned in the JSON body or stored in localStorage. app/api/auth/refresh/route.ts rotates the pair, and app/api/auth/logout/route.ts clears both cookies. Protection: middleware.ts guards everything under /dashboard and /api/protected, verifying signature and expiry, redirecting browsers to /login with a from query param for post-login return, and answering API calls with a JSON 401 instead of an HTML redirect. Secrets: read JWT_SECRET from env, and if it is missing show a plain configuration notice in the preview instead of crashing. UI: a /login page using shadcn/ui Form with inline field errors from server-side zod validation, a generic invalid credentials message that never reveals whether the email exists, and a disabled submit state while the request is in flight. Edge cases: an expired access token with a valid refresh token rotates silently on the next request, a tampered token clears both cookies and forces login, and the payload carries only the user id, no email or role claims. Acceptance: in the v0 preview, logging in lands on /dashboard, an incognito visit to /dashboard bounces to /login, and logout revokes access immediately.Customize it
Runs in your browser. Nothing you type here is sent anywhere.
Add JWT authentication to an existing Next.js App Router project without pulling in a full auth provider. Route handlers: app/api/auth/login/route.ts verifies credentials against a users table in PostgreSQL using bcrypt comparison, then signs a short-lived access token and a longer refresh token with the jose library, both delivered as httpOnly, secure, sameSite strict cookies, never returned in the JSON body or stored in localStorage. app/api/auth/refresh/route.ts rotates the pair, and app/api/auth/logout/route.ts clears both cookies. Protection: middleware.ts guards everything under /dashboard and /api/protected, verifying signature and expiry, redirecting browsers to /login with a from query param for post-login return, and answering API calls with a JSON 401 instead of an HTML redirect. Secrets: read JWT_SECRET from env, and if it is missing show a plain configuration notice in the preview instead of crashing. UI: a /login page using shadcn/ui Form with inline field errors from server-side zod validation, a generic invalid credentials message that never reveals whether the email exists, and a disabled submit state while the request is in flight. Edge cases: an expired access token with a valid refresh token rotates silently on the next request, a tampered token clears both cookies and forces login, and the payload carries only the user id, no email or role claims. Acceptance: in the v0 preview, logging in lands on /dashboard, an incognito visit to /dashboard bounces to /login, and logout revokes access immediately.
Same task in other tools
Questions about this prompt
Why jose and hand-rolled routes instead of an auth library?
The task is owning the token flow. jose runs in the middleware runtime, and httpOnly cookies keep tokens out of reach of injected scripts. If you later want provider logins, only the auth route handlers change, the middleware contract and cookie names stay.
How do pages and API routes fail differently when a token is bad?
The middleware branches on the path: browser routes under /dashboard get a redirect to /login carrying a from param, while /api/protected returns a JSON 401 so client fetches can handle it. Adjust the matcher in middleware.ts to move routes between the two behaviors.
Login succeeds in the preview but /dashboard still bounces me. Why?
The two usual causes are a middleware matcher that misses /dashboard subpaths and a cookie name that differs between the login handler and the middleware. Ask v0 to centralize cookie names as constants in a lib/auth.ts file both sides import.