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.
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_idonly — 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-clientprovideAuth 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.
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,
},
}),
],
};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",
};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:
openidemailprofile(addoffline_accessfor refresh) - Add your redirect URI (next step)
- Add your app's origin to the CORS allowlist — the browser calls
/oauth/tokendirectly - 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.
# Development
http://localhost:4200/callback
# Production
https://app.example.com/callbackhttp://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: 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
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.
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);
}
}
);/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.
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);
});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.
this.oidcSecurityService.forceRefreshSession().subscribe(({ isAuthenticated, accessToken }) => {
console.log("refreshed:", isAuthenticated, accessToken);
});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.
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.tsvalues, 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 —
silentRenewand secure cookies require it
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_iddoesn't match the registered SPA client. - invalid_grant / PKCE failed — the code verifier didn't survive the redirect; check that
redirectUrlmatches 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.