JavaScript / Node.js

Sign in with NIA — Vue

Add “Sign in with NIA” to a Vue 3 single-page app with oidc-client-ts and the standard OpenID Connect Authorization Code + PKCE flow.

~10 minEasyoidc-client-ts
Try OAuth Flow
Integration progress
0/16

01Overview

This guide adds “Sign in with NIA” to a Vue 3 single-page app using the standard OpenID Connect Authorization Code flow with PKCE. NIA is a conformant OIDC provider, so you use oidc-client-ts — a framework-agnostic OIDC client that works cleanly with Vue — with no NIA-specific SDK.

Vue runs as a public client: there is no server to hold a secret, so the browser exchanges the authorization code for tokens directly with NIA's token endpoint, protected by PKCE instead of a client secret.

  • Flow: Authorization Code + PKCE (S256)
  • Tokens: RS256 access & ID tokens, rotating refresh token
  • You write: a userManager module and a /callback route

02Requirements

  • Node.js 18+ and a Vue 3 app scaffolded with Vite
  • 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
  • Vue Router installed if you want route guards (recommended)

03Installation

oidc-client-ts is a dependency-free OIDC/OAuth2 client for browser apps. It handles PKCE, token storage, and silent renewal for you.

npm install oidc-client-ts

04Required OAuth library

Create a single shared UserManager instance and import it wherever you need auth state. It is configured once, from your env vars.

src/auth/userManager.ts
import { UserManager, WebStorageStateStore } from "oidc-client-ts";

export const userManager = new UserManager({
  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,
  userStore: new WebStorageStateStore({ store: window.localStorage }),
});
Drop offline_access from the scope if you don't need refresh tokens (see step 13).

05Environment Variables

Vite exposes only VITE_-prefixed variables to client code — there are no secrets to protect since this is a public client.

.env
VITE_NIA_ISSUER=https://dev-api.niaauth.com
VITE_NIA_CLIENT_ID=your-client-id
This is a public client — the SPA calls NIA's /token endpoint directly from the browser, so your app's origin must be added to NIA's CORS allowlist in the dashboard, or the token exchange will fail.

06Register OAuth Client

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

  • Open Dashboard → Clients → New client
  • Client type: Single-page app (public — token_endpoint_auth_method = none)
  • Allowed scopes: openid email profile (add offline_access for refresh)
  • Add your redirect URI (next step) and your app's origin to the CORS allowlist
  • Copy the client_id — there is no secret for public clients

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 login button to start the flow. oidc-client-ts generates the PKCE code verifier/challenge and state automatically and stores them in sessionStorage before redirecting.

import { userManager } from "@/auth/userManager";

async function login() {
  await userManager.signinRedirect();
}

10Token Exchange

NIA redirects back to /callback?code=...&state=.... signinRedirectCallback verifies state, sends the stored PKCE verifier, exchanges the code, and validates the ID token — all in one call.

src/views/Callback.vue
<script setup lang="ts">
import { onMounted } from "vue";
import { useRouter } from "vue-router";
import { userManager } from "@/auth/userManager";

const router = useRouter();

onMounted(async () => {
  try {
    await userManager.signinRedirectCallback();
    router.replace("/");
  } catch (err) {
    console.error("Sign-in failed", err);
    router.replace("/");
  }
});
</script>

<template>
  <p>Signing you in…</p>
</template>

After the callback resolves, read the tokens and profile with getUser():

src/auth/getUser.ts
import { userManager } from "./userManager";

const user = await userManager.getUser();
// user.access_token — send as "Authorization: Bearer <token>" to your API
// user.id_token      — validated OIDC identity token
// user.profile       — { sub, email, name, ... }

11JWKS Validation

NIA signs ID tokens with RS256. During signinRedirectCallback, oidc-client-ts fetches NIA's public keys from the JWKS endpoint (discovered automatically), caches them, and verifies the signature, iss, aud, exp, and nonce. You do not verify manually, and no key material ever leaves the library.

verify (illustrative)
const user = await userManager.getUser();
const claims = user?.profile;

console.log(claims?.iss); // "https://dev-api.niaauth.com"
console.log(claims?.aud); // your client_id
// signature (RS256, via JWKS), exp and nonce are already verified by oidc-client-ts.
Keys rotate. Because the library refetches JWKS by kid, rotation needs no code change on your side.

12Session Management

Subscribe to userManager.events to keep app state in sync with the session, and use a small composable to expose the current user reactively.

import { ref } from "vue";
import type { User } from "oidc-client-ts";
import { userManager } from "./userManager";

export const currentUser = ref<User | null>(null);

userManager.getUser().then((u) => (currentUser.value = u));
userManager.events.addUserLoaded((u) => (currentUser.value = u));
userManager.events.addUserUnloaded(() => (currentUser.value = null));
userManager.events.addAccessTokenExpired(() => (currentUser.value = null));

13Refresh Tokens

With offline_access in scope and automaticSilentRenew: true set on the UserManager, oidc-client-ts refreshes the access token in the background before it expires — no code needed beyond the initial config. NIA rotates the refresh token on every use, and the library persists the replacement for you.

src/auth/useAuth.ts (add-on)
userManager.events.addSilentRenewError((err) => {
  console.error("Silent renew failed", err);
  // Fall back to an interactive sign-in.
  userManager.signinRedirect();
});

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/components/LogoutButton.vue (script)
import { userManager } from "@/auth/userManager";

async function logout() {
  await userManager.signoutRedirect();
}

15Deployment

Before going live:

  • Register the production redirect URI (HTTPS) on the client
  • Add the production origin to NIA's CORS allowlist — required since the token exchange happens in the browser
  • Set VITE_NIA_ISSUER / VITE_NIA_CLIENT_ID as build-time env vars on your host
  • Serve the app over HTTPS — browsers block third-party redirects and storage access on plain HTTP
  • Confirm the client stays a public client (no secret embedded in the bundle)
.env.production
VITE_NIA_ISSUER=https://dev-api.niaauth.com
VITE_NIA_CLIENT_ID=your-production-client-id

16Troubleshooting

  • redirect_uri_mismatch — the URI must match the registered one exactly (no trailing slash, correct scheme/port).
  • CORS error on /oauth/token — your app's origin isn't in NIA's CORS allowlist; add it in the dashboard.
  • invalid_client — the client is configured as confidential in NIA but your app sends no secret; make sure it's registered as a public / SPA client.
  • invalid_grant / PKCE failed — sessionStorage was cleared between login and callback (e.g. a different tab or a hard reload mid-flow).
  • mismatching_state — same root cause as above: the stored PKCE/state entry didn't survive the redirect.
  • invalid_scope — the scope isn't in the client's allowed_scopes; add it in the dashboard.
  • Silent renew fails silently — check that a hidden iframe from your origin is not blocked by CSP frame-src/frame-ancestors rules.