Auth Setup
Authentication in Next.js usually means picking a library (NextAuth, Clerk, Lucia), reading 40 pages of docs, configuring adapters, setting up callback URLs, and hoping the session strategy you chose doesn't bite you in six months. It's powerful and flexible, which is another way of saying there are a lot of ways to get it wrong.
Nuxt has nuxt-auth-utils. One module, zero adapters, built-in session handling, and OAuth helpers for every major provider. You add it to your config, create one server route, set three environment variables, and authentication works. It's opinionated in the way that saves you from yourself.
Outcome
Install nuxt-auth-utils, register a GitHub OAuth app, and create the server-side callback handler.
Fast Track
- Install
nuxt-auth-utilsand add it tonuxt.config.ts - Register a GitHub OAuth app and set environment variables
- Create the GitHub OAuth handler in
server/routes/auth/github.get.ts
Hands-on exercise 3.1
Wire up GitHub OAuth from scratch.
Requirements:
- Install
nuxt-auth-utilsand add it to themodulesarray innuxt.config.ts - Register a new OAuth app on GitHub with the callback URL
http://localhost:3000/auth/github - Create a
.envfile withNUXT_OAUTH_GITHUB_CLIENT_ID,NUXT_OAUTH_GITHUB_CLIENT_SECRET, andNUXT_SESSION_PASSWORD - Create
server/routes/auth/github.get.tsthat handles the OAuth callback - On success, store the user's login, avatar URL, and ID in the session
Implementation hints:
nuxt-auth-utilsauto-importsdefineOAuthGitHubEventHandler,setUserSession,getUserSession, andrequireUserSessionin server routes- It also auto-imports
useUserSessionin Vue components - The module reads
NUXT_OAUTH_GITHUB_CLIENT_IDandNUXT_OAUTH_GITHUB_CLIENT_SECRETautomatically. No config mapping needed NUXT_SESSION_PASSWORDmust be at least 32 characters. It encrypts the session cookie- Server routes in
server/routes/(notserver/api/) map directly to URLs.server/routes/auth/github.get.tsbecomes/auth/github
First, update the Nuxt config to include the module:
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
compatibilityDate: "2025-05-01",
modules: ["nuxt-auth-utils"],
css: ["~/assets/css/main.css"],
vite: {
plugins: [tailwindcss()],
},
});One line. The module handles session management, cookie encryption, OAuth flows, and auto-importing its composables. In the Next.js world, this would be the equivalent of installing NextAuth, creating an auth config file, setting up the route handler, configuring providers, and wiring up the session provider component.
Next, register a GitHub OAuth app. Go to GitHub Settings > Developer settings > OAuth Apps > New OAuth App:
- Application name: Hot Springs Finder (Dev)
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/auth/github
GitHub gives you a Client ID and lets you generate a Client Secret. Put them in a .env file:
NUXT_OAUTH_GITHUB_CLIENT_ID=your_client_id_here
NUXT_OAUTH_GITHUB_CLIENT_SECRET=your_client_secret_here
NUXT_SESSION_PASSWORD=a-random-string-at-least-32-characters-longnuxt-auth-utils picks up the NUXT_OAUTH_* variables by convention. No config object, no provider setup. The naming convention IS the configuration.
Now the OAuth handler. This is the route GitHub redirects to after the user authorizes your app:
export default defineOAuthGitHubEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
login: user.login,
avatar_url: user.avatar_url,
id: user.id,
},
});
return sendRedirect(event, "/springs");
},
onError(event, error) {
console.error("GitHub OAuth error:", error);
return sendRedirect(event, "/login?error=auth");
},
});defineOAuthGitHubEventHandler handles the entire OAuth dance: redirecting to GitHub, exchanging the code for a token, fetching the user profile. Your job is just to decide what to do with the result.
setUserSession stores whatever you pass to the user key in an encrypted, HTTP-only cookie. No database, no session store. The cookie IS the session. This is fine for our use case. For production apps with complex session needs, you'd add a database adapter, but the module supports that too.
Notice this file lives in server/routes/auth/, not server/api/auth/. The difference: server/api/ routes are prefixed with /api/. server/routes/ routes map directly to URLs. Our callback URL is /auth/github, not /api/auth/github.
Nuxt reads .env at startup. If you add or change environment variables, restart the dev server. Hot reload doesn't pick up .env changes.
Need a random 32-character string? Run openssl rand -base64 32 in your terminal. Or type whatever you want, as long as it's long enough. The password never leaves the server.
Try It
Restart the dev server (to pick up the new module and .env changes):
pnpm devVisit http://localhost:3000/auth/github. You should be redirected to GitHub's authorization page. Authorize the app, and GitHub redirects you back to /auth/github, which runs your handler, sets the session, and redirects to /springs.
Open your browser's dev tools, go to the Application/Storage tab, and look for a cookie called nuxt-session. It should be there, HTTP-only, and encrypted.
If you see an error page instead, check:
- Are the Client ID and Secret correct in
.env? - Did you restart the dev server after creating
.env? - Is the callback URL in your GitHub OAuth app settings exactly
http://localhost:3000/auth/github?
Commit
git add -A && git commit -m "feat(auth): add nuxt-auth-utils with GitHub OAuth handler"Done-When
nuxt-auth-utilsis in themodulesarray innuxt.config.ts.envcontains the three required environment variables- Visiting
/auth/githubredirects to GitHub and back - After authorization, a
nuxt-sessioncookie exists in the browser - You can explain the difference between
server/routes/andserver/api/
Solution
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
compatibilityDate: "2025-05-01",
modules: ["nuxt-auth-utils"],
css: ["~/assets/css/main.css"],
vite: {
plugins: [tailwindcss()],
},
});NUXT_OAUTH_GITHUB_CLIENT_ID=your_client_id_here
NUXT_OAUTH_GITHUB_CLIENT_SECRET=your_client_secret_here
NUXT_SESSION_PASSWORD=a-random-string-at-least-32-characters-longexport default defineOAuthGitHubEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
login: user.login,
avatar_url: user.avatar_url,
id: user.id,
},
});
return sendRedirect(event, "/springs");
},
onError(event, error) {
console.error("GitHub OAuth error:", error);
return sendRedirect(event, "/login?error=auth");
},
});Was this helpful?