JavaScript / Node.js
Sign in with NIA — Next.js
Add “Sign in with NIA” to a Next.js App Router app in minutes using Auth.js and our thin NIA provider — no OAuth code to write yourself.
01Overview
This guide adds “Sign in with NIA” to a Next.js App Router app using Auth.js (NextAuth v5) with our thin provider package, @naslabs-ai/nia-next-auth. The provider just points Auth.js at NIA's OIDC endpoints — Auth.js itself handles PKCE, state, the token exchange, and ID-token verification.
Next.js runs as a confidential client: the code exchange happens on your server, inside the Auth.js route handler, using your client secret.
- Flow: Authorization Code + PKCE (S256)
- Tokens: RS256 access & ID tokens, opaque refresh token
- You write: an
auth.tsconfig, a route handler, and a sign-in/sign-out UI
02Requirements
- Node.js 18+ and Next.js 14+ using the App Router
- A NIA account with access to the client dashboard
- A registered OAuth client (
client_id+client_secret) - Your app's redirect URI registered on that client
- An
AUTH_SECRETfor Auth.js to sign its session cookies
03Installation
Install Auth.js and our NIA provider package.
npm install next-auth @naslabs-ai/nia-next-authThen generate a secret Auth.js uses to sign session cookies:
npx auth secretAUTH_SECRET to your .env.local automatically. Regenerate a separate one for production.04Required OAuth library
Create auth.ts at your project root and configure Auth.js with the Nia provider. It resolves every endpoint, the JWKS URL, and supported algorithms from your issuer's discovery document.
import NextAuth from "next-auth"
import Nia from "@naslabs-ai/nia-next-auth"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nia({
clientId: process.env.NIA_CLIENT_ID!,
clientSecret: process.env.NIA_CLIENT_SECRET!,
issuer: process.env.NIA_ISSUER,
}),
],
})authorization: { params: { scope: "openid email profile offline_access" } } to the provider config only if you need refresh tokens (see step 14).05Environment Variables
Never hard-code secrets. Keep them in .env.local (git-ignored) — Next.js loads it automatically.
NIA_ISSUER=https://dev-api.niaauth.com
NIA_CLIENT_ID=your-client-id
NIA_CLIENT_SECRET=your-client-secret
AUTH_SECRET=replace-with-a-long-random-value06Register OAuth Client
In the NIA dashboard, create the client your Next.js app will authenticate as:
- Open Dashboard → Clients → New client
- Client type: Web (confidential)
- Allowed scopes:
openidemailprofile(addoffline_accessfor refresh) - Add your redirect URI (next step)
- Copy the
client_idandclient_secret— the secret is shown once
07Redirect URI
Auth.js fixes the callback path to /api/auth/callback/<provider id> — for the Nia provider that's always /api/auth/callback/nia. This exact path cannot be changed, so register it as-is per environment.
# Development
http://localhost:3000/api/auth/callback/nia
# Production
https://app.example.com/api/auth/callback/nia08Issuer
The issuer is the single URL Auth.js needs. Its discovery document advertises all endpoints below.
Issuer: https://dev-api.niaauth.com
Discovery: https://dev-api.niaauth.com/.well-known/openid-configuration
Authorization: https://dev-api.niaauth.com/oauth/authorize
Token: https://dev-api.niaauth.com/oauth/token
UserInfo: https://dev-api.niaauth.com/oauth/userinfo
JWKS: https://dev-api.niaauth.com/oauth/jwks.json09Authorization Flow
Call signIn("nia") and signOut() as server actions from a form — no client-side OAuth code needed.
import { auth, signIn, signOut } from "@/auth"
export default async function Home() {
const session = await auth()
if (!session) {
return (
<form action={async () => { "use server"; await signIn("nia") }}>
<button type="submit">Sign in with NIA</button>
</form>
)
}
return (
<>
<p>Signed in as {session.user?.email}</p>
<form action={async () => { "use server"; await signOut() }}>
<button type="submit">Sign out</button>
</form>
</>
)
}10Token Exchange
You don't write this step. When NIA redirects back to /api/auth/callback/nia, the route handler from step 3 hands the request to Auth.js, which:
- Verifies
stateand sends the PKCEcode_verifier - Exchanges the authorization code at NIA's token endpoint using your
client_secret - Validates the returned ID token and stores the session
There is no callback code for you to write — it is entirely handled inside @naslabs-ai/nia-next-auth and Auth.js core.
11JWKS Validation
NIA signs ID tokens with RS256. Auth.js fetches the public keys from the JWKS endpoint (discovered automatically), caches them by kid, and verifies the signature, iss, aud, exp, and nonce during the callback. You do not verify manually.
kid, rotation needs no code change on your side.12Session Management
Call auth() anywhere on the server — server components, route handlers, or server actions — to read the current session.
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function Dashboard() {
const session = await auth()
if (!session) redirect("/")
return <p>Welcome, {session.user?.name}</p>
}13Refresh Tokens
If you requested offline_access, NIA returns a refresh token. Persist it on the JWT in the jwt callback, and refresh it there once the access token is near expiry — Auth.js calls this callback on every request.
import NextAuth from "next-auth"
import Nia from "@naslabs-ai/nia-next-auth"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nia({
clientId: process.env.NIA_CLIENT_ID!,
clientSecret: process.env.NIA_CLIENT_SECRET!,
issuer: process.env.NIA_ISSUER,
authorization: { params: { scope: "openid email profile offline_access" } },
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
// First sign-in: persist tokens from the provider.
token.accessToken = account.access_token
token.refreshToken = account.refresh_token
token.expiresAt = account.expires_at
return token
}
if (Date.now() < (token.expiresAt as number) * 1000) return token
// Access token expired — use the refresh token.
const res = await fetch(`${process.env.NIA_ISSUER}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: token.refreshToken as string,
client_id: process.env.NIA_CLIENT_ID!,
client_secret: process.env.NIA_CLIENT_SECRET!,
}),
})
const refreshed = await res.json()
if (!res.ok) return { ...token, error: "RefreshTokenError" as const }
return {
...token,
accessToken: refreshed.access_token,
// NIA rotates the refresh token — persist the replacement.
refreshToken: refreshed.refresh_token ?? token.refreshToken,
expiresAt: Math.floor(Date.now() / 1000) + refreshed.expires_in,
}
},
async session({ session, token }) {
session.accessToken = token.accessToken as string
session.error = token.error as string | undefined
return session
},
},
})14Logout
signOut() clears the Auth.js session cookie. To also sign the user out of NIA (single logout), redirect to NIA's end-session endpoint afterward.
"use server"
import { signOut } from "@/auth"
export async function fullSignOut() {
await signOut({ redirect: false })
const params = new URLSearchParams({
post_logout_redirect_uri: process.env.NEXTAUTH_URL ?? "http://localhost:3000",
})
redirect(`https://dev-api.niaauth.com/oauth/logout?${params}`)
}Register your post_logout_redirect_uri on the client in the dashboard.
15Deployment
Before going live:
- Register the production redirect URI (HTTPS) on the client
- Set
NIA_CLIENT_ID,NIA_CLIENT_SECRET,NIA_ISSUER, and a freshAUTH_SECRETin your host — never commit.env.local - If deployed behind a reverse proxy or load balancer, set
AUTH_TRUST_HOST=true(ortrustHost: trueinauth.ts) so Auth.js trusts the forwarded host - Serve over HTTPS — NIA and Auth.js both require secure cookies in production
NIA_ISSUER=https://dev-api.niaauth.com
NIA_CLIENT_ID=your-prod-client-id
NIA_CLIENT_SECRET=your-prod-client-secret
AUTH_SECRET=a-different-long-random-value
AUTH_TRUST_HOST=true16Troubleshooting
- redirect_uri_mismatch — the registered URI must be exactly
/api/auth/callback/niaon the correct host; Auth.js does not let you change this path. - MissingSecret —
AUTH_SECRETis not set. Runnpx auth secretor set it manually in.env.local. - invalid_client — wrong
NIA_CLIENT_SECRET, or the client's token-auth method doesn't match what Auth.js sends. - UntrustedHost — behind a proxy or on a platform that rewrites the host header, set
AUTH_TRUST_HOST=true. - Discovery / connection errors — confirm
NIA_ISSUERpoints atdev-api.niaauth.com, notdev-platformor the dashboard host. - ID-token signature — usually server clock skew; sync time via NTP.