N

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.

~5 minEasy@naslabs-ai/nia-next-auth
Try OAuth Flow
Integration progress
0/16

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.ts config, 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_SECRET for Auth.js to sign its session cookies

03Installation

Install Auth.js and our NIA provider package.

npm install next-auth @naslabs-ai/nia-next-auth

Then generate a secret Auth.js uses to sign session cookies:

auth secret
npx auth secret
This writes AUTH_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,
    }),
  ],
})
Add 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.

.env.local
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-value

06Register 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: openid email profile (add offline_access for refresh)
  • Add your redirect URI (next step)
  • Copy the client_id and client_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.

redirect URIs
# Development
http://localhost:3000/api/auth/callback/nia

# Production
https://app.example.com/api/auth/callback/nia
NIA matches redirect URIs exactly — scheme, host, port, and path. Since Auth.js dictates this path, the only thing you can get wrong is the host.

08Issuer

The issuer is the single URL Auth.js needs. Its discovery document advertises all endpoints below.

issuer & endpoints
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.json

09Authorization Flow

Call signIn("nia") and signOut() as server actions from a form — no client-side OAuth code needed.

app/page.tsx
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 state and sends the PKCE code_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.

Keys rotate. Because Auth.js refetches JWKS by 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.

auth.ts
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.

app/actions.ts
"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 fresh AUTH_SECRET in your host — never commit .env.local
  • If deployed behind a reverse proxy or load balancer, set AUTH_TRUST_HOST=true (or trustHost: true in auth.ts) so Auth.js trusts the forwarded host
  • Serve over HTTPS — NIA and Auth.js both require secure cookies in production
.env (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=true

16Troubleshooting

  • redirect_uri_mismatch — the registered URI must be exactly /api/auth/callback/nia on the correct host; Auth.js does not let you change this path.
  • MissingSecret — AUTH_SECRET is not set. Run npx auth secret or 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_ISSUER points at dev-api.niaauth.com, not dev-platform or the dashboard host.
  • ID-token signature — usually server clock skew; sync time via NTP.