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.
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
/callbackroute, 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_idonly — 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-ts04Required 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.
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>
);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.
VITE_NIA_ISSUER=https://dev-api.niaauth.com
VITE_NIA_CLIENT_ID=your-client-id06Register 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:
openidemailprofile(addoffline_accessfor 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.
# Development (Vite default port)
http://localhost:5173/callback
# Production
https://app.example.com/callbackhttp://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: 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 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.
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.
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>
);
}/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:
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.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.
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.
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
};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.
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_ISSUERandVITE_NIA_CLIENT_IDat build time for the prod environment - Public client — there is no secret to protect, so nothing to keep out of the bundle
VITE_NIA_ISSUER=https://dev-api.niaauth.com
VITE_NIA_CLIENT_ID=your-client-id16Troubleshooting
- 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
sessionStorageisn't cleared between login and callback (e.g. a different tab or private-browsing restrictions). - Stuck on
/callback— confirmredirect_urimatches exactly what the app requests, and that the route rendersAuthProvider'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.