JavaScript / Node.js

Sign in with NIA — React

Add “Sign in with NIA” to a React single-page app with react-oidc-context and the standard OpenID Connect flow. Follow the steps top to bottom — tick each one off as you go.

~10 minEasyreact-oidc-context
Try OAuth Flow
Integration progress
0/16

01Overview

This guide adds “Sign in with NIA” to a React single-page app using the standard OpenID Connect flow. NIA is a conformant OIDC provider, so you use react-oidc-context — a thin React wrapper around the battle-tested oidc-client-ts library — with no NIA-specific SDK.

A React SPA runs entirely in the browser, so it is a public client: there is no server to keep a secret, and none is issued. Instead, the browser proves it owns the authorization code with PKCE — a one-time cryptographic verifier/challenge pair generated on your device.

  • Flow: Authorization Code + PKCE (S256)
  • Tokens: RS256 access & ID tokens, opaque refresh token (with offline_access)
  • You write: a provider wrapper, a /callback route, and login/logout buttons

02Requirements

  • React 18+ and a build tool (Vite or Create React App)
  • A NIA account with access to the client dashboard
  • A registered OAuth client (client_id only — no secret)
  • Your app's redirect URI registered on that client
  • Node.js 18+ for local development

03Installation

react-oidc-context depends on oidc-client-ts, which does the actual PKCE, token, and JWKS work — install both.

npm install react-oidc-context oidc-client-ts

04Required OAuth library

Wrap your app in AuthProvider once, at the root. The authority points at NIA's issuer — the discovery document under it advertises every endpoint, the JWKS URL, and supported algorithms automatically.

src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { AuthProvider } from "react-oidc-context";
import App from "./App";

const oidcConfig = {
  authority: import.meta.env.VITE_NIA_ISSUER,
  client_id: import.meta.env.VITE_NIA_CLIENT_ID,
  redirect_uri: window.location.origin + "/callback",
  post_logout_redirect_uri: window.location.origin,
  response_type: "code",
  scope: "openid email profile",
  // NIA requires PKCE — oidc-client-ts generates the verifier/challenge for you.
};

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <AuthProvider {...oidcConfig}>
      <App />
    </AuthProvider>
  </React.StrictMode>
);
Add offline_access to the scope and automaticSilentRenew: true to the config only if you need refresh tokens (see step 13).

05Environment Variables

A public client has no secret to protect, so these values are safe to ship in the bundle. Vite exposes any VITE_-prefixed variable to client code via import.meta.env.

.env
VITE_NIA_ISSUER=https://dev-api.niaauth.com
VITE_NIA_CLIENT_ID=your-client-id

06Register OAuth Client

In the NIA dashboard, create the client your React app will authenticate as:

  • Open Dashboard → Clients → New client
  • Client type: Public (token endpoint auth method: none — no secret is issued)
  • Allowed scopes: openid email profile (add offline_access for refresh)
  • Add your redirect URI (next step)
  • Add your app's origin to the CORS allowlist (see step 10)
  • Copy the client_id — there is no secret to store

07Redirect URI

NIA matches redirect URIs exactly — scheme, host, port, and path must be identical, with no trailing-slash difference. Register one URI per environment.

redirect URIs
# Development (Vite default port)
http://localhost:5173/callback

# Production
https://app.example.com/callback
A mismatch is the #1 integration error. http://localhost:5173/callback/ (trailing slash) will be rejected.

08Issuer

The issuer is the single URL every OIDC library 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 signinRedirect() from a click handler to send the browser to NIA. oidc-client-ts generates the PKCE verifier and challenge (S256), stores them, and builds the authorization URL for you.

src/LoginButton.tsx
import { useAuth } from "react-oidc-context";

export function LoginButton() {
  const auth = useAuth();

  if (auth.isLoading) return <p>Loading…</p>;
  if (auth.isAuthenticated) return null;

  return <button onClick={() => auth.signinRedirect()}>Sign in with NIA</button>;
}

10Token Exchange

NIA redirects back to /callback with a code. AuthProvider detects that route, sends the code plus the stored PKCE verifier to NIA's token endpoint, and stores the resulting tokens — no route component of your own is required.

src/Profile.tsx
import { useAuth } from "react-oidc-context";

export function Profile() {
  const auth = useAuth();
  if (!auth.isAuthenticated) return null;

  const accessToken = auth.user?.access_token;
  const claims = auth.user?.profile; // validated ID-token claims

  return (
    <div>
      <p>Signed in as {claims?.email}</p>
      <p>Access token: {accessToken?.slice(0, 12)}…</p>
    </div>
  );
}
Because a React SPA calls NIA's /token endpoint directly from the browser, the app's origin must be added to NIA's CORS allowlist, or the token request is blocked by the browser.

11JWKS Validation

NIA signs ID tokens with RS256. oidc-client-ts fetches the public keys from the JWKS endpoint (discovered automatically), caches them, and verifies the signature, iss, aud, exp, and nonce before auth.user is populated. You do not verify manually.

To confirm what was validated, inspect the parsed profile claims:

verify (illustrative)
const claims = auth.user?.profile;

console.assert(claims?.iss === import.meta.env.VITE_NIA_ISSUER);      // issuer
console.assert(claims?.aud === import.meta.env.VITE_NIA_CLIENT_ID);   // audience = your client
// signature (RS256, via JWKS), exp and nonce are already verified by oidc-client-ts.
Keys rotate. Because oidc-client-ts refetches JWKS by kid, rotation needs no code change on your side.

12Session Management

Use auth.isAuthenticated and auth.user to guard rendering. Tokens live in memory (and optionally sessionStorage), scoped to the tab — there is no server session to manage.

src/App.tsx
import { useAuth } from "react-oidc-context";
import { LoginButton } from "./LoginButton";
import { Profile } from "./Profile";

export default function App() {
  const auth = useAuth();

  if (auth.isLoading) return <p>Loading…</p>;
  if (auth.error) return <p>Auth error: {auth.error.message}</p>;

  return auth.isAuthenticated ? <Profile /> : <LoginButton />;
}

13Refresh Tokens

Request offline_access and enable automaticSilentRenew so react-oidc-context refreshes the access token in the background before it expires — no manual refresh calls needed.

src/main.tsx
const oidcConfig = {
  authority: import.meta.env.VITE_NIA_ISSUER,
  client_id: import.meta.env.VITE_NIA_CLIENT_ID,
  redirect_uri: window.location.origin + "/callback",
  post_logout_redirect_uri: window.location.origin,
  response_type: "code",
  scope: "openid email profile offline_access",
  automaticSilentRenew: true, // refresh in the background using the refresh token
};
NIA rotates refresh tokens on each use. oidc-client-ts stores the replacement automatically — you never handle the raw refresh token.

14Logout

Call signoutRedirect() to clear the local session and send the user to NIA's end-session endpoint so they are signed out of NIA too. Register your post_logout_redirect_uri on the client.

src/LogoutButton.tsx
import { useAuth } from "react-oidc-context";

export function LogoutButton() {
  const auth = useAuth();
  if (!auth.isAuthenticated) return null;

  return <button onClick={() => auth.signoutRedirect()}>Sign out</button>;
}

15Deployment

Before going live:

  • Serve the app over HTTPS — required for secure redirects and cookies
  • Register the production redirect URI on the client
  • Add the production origin to NIA's CORS allowlist
  • Set VITE_NIA_ISSUER and VITE_NIA_CLIENT_ID at build time for the prod environment
  • Public client — there is no secret to protect, so nothing to keep out of the bundle
.env.production
VITE_NIA_ISSUER=https://dev-api.niaauth.com
VITE_NIA_CLIENT_ID=your-client-id

16Troubleshooting

  • CORS error on token request — your app's origin isn't in NIA's CORS allowlist; add it in the dashboard.
  • redirect_uri_mismatch — the URI must match the registered one exactly (no trailing slash, correct scheme/port).
  • invalid_client — you registered a confidential client by mistake; SPAs must use a public client (token_endpoint_auth_method: none).
  • invalid_grant / PKCE failed — the verifier stored before redirect wasn't found on return; check that sessionStorage isn't cleared between login and callback (e.g. a different tab or private-browsing restrictions).
  • Stuck on /callback — confirm redirect_uri matches exactly what the app requests, and that the route renders AuthProvider's children so it can process the response.
  • invalid_scope — the scope isn't in the client's allowed_scopes; add it in the dashboard.