A

JavaScript / Node.js

Sign in with NIA — Angular

Add “Sign in with NIA” to an Angular SPA with angular-auth-oidc-client and the standard OpenID Connect Authorization Code + PKCE flow.

~10 minEasyangular-auth-oidc-client
Try OAuth Flow
Integration progress
0/16

01Overview

This guide adds “Sign in with NIA” to an Angular single-page app using the standard OpenID Connect flow. NIA is a conformant OIDC provider, so you use angular-auth-oidc-client — Angular's most popular OIDC client — with no NIA-specific SDK.

Angular runs as a public client: there is no client secret. The browser redirects to NIA to sign in, and the library exchanges the returned code for tokens using PKCE, entirely from the browser.

  • Flow: Authorization Code + PKCE (S256)
  • Tokens: RS256 access & ID tokens, opaque refresh token
  • You write: provider config, a callback route, and a route guard

02Requirements

  • Node.js 18+ and Angular 17+ (standalone APIs)
  • A NIA account with access to the client dashboard
  • A registered OAuth client (client_id only — public client, no secret)
  • Your app's redirect URI registered on that client
  • The Angular CLI (@angular/cli) for scaffolding routes, if you don't already have a project

03Installation

Install angular-auth-oidc-client — it handles discovery, PKCE, token storage, silent renew, and ID-token validation for you.

npm install angular-auth-oidc-client
Requires Angular 17+. This guide uses standalone components and the provideAuth functional provider — no NgModule needed.

04Required OAuth library

Register NIA once with provideAuth in your app config. Pointing authority at NIA's issuer lets the library fetch the discovery document, JWKS URL, and supported algorithms automatically.

src/app/app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideAuth } from "angular-auth-oidc-client";
import { provideRouter } from "@angular/router";
import { routes } from "./app.routes";

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideAuth({
      config: {
        authority: "https://dev-api.niaauth.com",
        redirectUrl: window.location.origin + "/callback",
        postLogoutRedirectUri: window.location.origin,
        clientId: "your-client-id",
        scope: "openid email profile offline_access",
        responseType: "code",
        silentRenew: true,
        useRefreshToken: true,
      },
    }),
  ],
};
Drop offline_access from the scope if you don't need refresh tokens — see the Refresh Tokens step.

05Environment Variables

Angular has no runtime .env support — configuration is baked in at build time via environment.ts files. Keep the authority and client ID there and reference them from app.config.ts.

export const environment = {
  production: false,
  niaAuthority: "https://dev-api.niaauth.com",
  niaClientId: "your-client-id",
};
There is no client secret to store — Angular is a public client. Never add a client_secret to a frontend build; it would ship to every browser.

06Register OAuth Client

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

  • Open Dashboard → Clients → New client
  • Client type: Single-page app (public — 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 — the browser calls /oauth/token directly
  • Copy the client_id — public clients have no secret

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
http://localhost:4200/callback

# Production
https://app.example.com/callback
A mismatch is the #1 integration error. http://localhost:4200/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

Run checkAuth() once when the app starts to process the callback (or restore an existing session), and inject OidcSecurityService wherever you need to start a login.

import { Component, OnInit, inject } from "@angular/core";
import { OidcSecurityService } from "angular-auth-oidc-client";

@Component({
  selector: "app-root",
  standalone: true,
  template: `<router-outlet />`,
})
export class AppComponent implements OnInit {
  private readonly oidcSecurityService = inject(OidcSecurityService);

  ngOnInit(): void {
    this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated }) => {
      console.log("authenticated:", isAuthenticated);
    });
  }
}

10Token Exchange

You don't write a callback handler by hand — /callback is just a route the app already serves. When the library's checkAuth() (or checkAuthMultiple()) runs on that route, it detects the code/state query params, sends the PKCE verifier it generated before the redirect, exchanges the code at /oauth/token, and validates the returned ID token — all in one call.

src/app/app.component.ts
this.oidcSecurityService.checkAuth().subscribe(
  ({ isAuthenticated, userData, accessToken, errorMessage }) => {
    if (isAuthenticated) {
      console.log("access token:", accessToken);
      console.log("claims:", userData);
    } else {
      console.warn("not authenticated:", errorMessage);
    }
  }
);
No manual routing is required for /callback beyond registering it as a normal Angular route — checkAuth() inspects the current URL for you.

11JWKS Validation

NIA signs ID tokens with RS256. angular-auth-oidc-client fetches the public keys from the JWKS endpoint (discovered automatically), caches them, and verifies the signature, iss, aud, exp, and nonce during checkAuth(). You never verify manually in application code.

verify (illustrative)
this.oidcSecurityService.userData$.subscribe(({ userData }) => {
  // signature (RS256, via JWKS), iss, aud, exp and nonce
  // are already verified by the library before this fires.
  console.log("sub:", userData?.sub);
});
Keys rotate. Because the library refetches JWKS by kid, rotation needs no code change on your side.

12Session Management

Subscribe to isAuthenticated$ and userData$ to react to auth state anywhere in the app. Protect routes with a guard built on checkAuth().

import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { OidcSecurityService } from "angular-auth-oidc-client";
import { map } from "rxjs/operators";

export const niaAuthGuard: CanActivateFn = () => {
  const oidcSecurityService = inject(OidcSecurityService);
  const router = inject(Router);

  return oidcSecurityService.isAuthenticated$.pipe(
    map(({ isAuthenticated }) => isAuthenticated || router.parseUrl("/login"))
  );
};

13Refresh Tokens

With offline_access in scope and useRefreshToken: true set, the library refreshes access tokens automatically in the background (silentRenew: true) — you rarely call this yourself. NIA rotates refresh tokens on every use; the library stores the replacement for you.

manual refresh (rarely needed)
this.oidcSecurityService.forceRefreshSession().subscribe(({ isAuthenticated, accessToken }) => {
  console.log("refreshed:", isAuthenticated, accessToken);
});
If silentRenew and useRefreshToken are both set, you generally never need to call forceRefreshSession() manually — it's there for edge cases like an explicit "retry" button.

14Logout

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

src/app/logout/logout.component.ts
import { Component, inject } from "@angular/core";
import { OidcSecurityService } from "angular-auth-oidc-client";

@Component({
  selector: "app-logout",
  standalone: true,
  template: `<button (click)="logout()">Sign out</button>`,
})
export class LogoutComponent {
  private readonly oidcSecurityService = inject(OidcSecurityService);

  logout(): void {
    this.oidcSecurityService.logoff().subscribe();
  }
}

15Deployment

Before going live:

  • Register the production redirect URI (HTTPS) on the client
  • Add the production origin to NIA's CORS allowlist — required for the browser to call /oauth/token
  • Build with environment.prod.ts values, never commit real client IDs to a public repo if avoidable
  • Confirm the client is configured as a public client — a confidential-client secret has nowhere safe to live in a SPA
  • Serve over HTTPS — silentRenew and secure cookies require it
src/environments/environment.prod.ts
export const environment = {
  production: true,
  niaAuthority: "https://dev-api.niaauth.com",
  niaClientId: "your-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 — the SPA calls NIA's token endpoint from the browser; add your app's exact origin to NIA's CORS allowlist.
  • invalid_client — a secret was sent for a public client, or the client_id doesn't match the registered SPA client.
  • invalid_grant / PKCE failed — the code verifier didn't survive the redirect; check that redirectUrl matches the registered URI and that storage isn't being cleared mid-flow.
  • silent renew fails silently — third-party cookie blocking can break iframe-based silent renew; prefer useRefreshToken: true (refresh-token based renew) as configured in this guide.
  • invalid_scope — the scope isn't in the client's allowed_scopes; add it in the dashboard.
  • ID-token signature — usually system clock skew on the device; rare on modern browsers/OSes.