Integrating OpenID Connect (OIDC) Authentication in Angular and React
This article shows how to integrate OIDC using Authorization Code Flow with PKCE — the recommended approach for SPAs — in Angular and React.
Join the DZone community and get the full member experience.
Join For FreeOpenID Connect (OIDC) is an identity layer on top of OAuth 2.0. If you’ve used “Sign in with Google/Microsoft/Okta/Auth0”, you’ve already used OIDC. In modern single-page apps (SPAs), the best practice is:
- Authorization Code Flow + PKCE
- Store tokens in memory (avoid
localStoragewhen possible) - Use the provider’s well-known discovery document
- Protect routes and attach access tokens to API calls
This guide shows an end-to-end setup for both Angular and React.
Why Authorization Code Flow + PKCE?
For SPAs, Authorization Code Flow with PKCE is the most secure and widely recommended option because:
- No client secrets are exposed in the browser
- Protects against authorization code interception
- Works with modern identity providers (Okta, Auth0, Azure AD, Keycloak, Google)
- Aligns with OAuth 2.1 and security best practices
Prerequisites from Your Identity Provider
Before integrating OIDC, configure an SPA client in your Identity Provider (IdP) and collect:
- Issuer / Authority URL
- Client ID
- Redirect URI
- Angular:
http://localhost:4200/auth/callback - React:
http://localhost:3000/auth/callback - Post-logout Redirect URI
- Scopes:
openid profile email(+ API scopes if required)
Ensure the client:
- Uses PKCE
- Is marked as a public / SPA client
- Does not use a client secret
Part 1: OpenID Connect in Angular
Recommended Library
angular-oauth2-oidc
A mature and production-tested OIDC library for Angular.
1) Install Dependencies
npm i angular-oauth2-oidc
2. Configure OIDC Settings
Create auth.config.ts:
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'https://YOUR_ISSUER', // e.g. https://idp.example.com/realms/myrealm
clientId: 'YOUR_CLIENT_ID',
redirectUri: window.location.origin + '/auth/callback',
postLogoutRedirectUri: window.location.origin + '/',
responseType: 'code',
scope: 'openid profile email',
strictDiscoveryDocumentValidation: false, // set true if issuer matches exactly and uses https in prod
showDebugInformation: true, // disable in production
requireHttps: false, // only for local dev on http
};
3. Create an Authentication Service
Now create auth.service.ts:
import { Injectable } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { authConfig } from './auth.config';
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private oauthService: OAuthService) {}
async init(): Promise<void> {
this.oauthService.configure(authConfig);
this.oauthService.setupAutomaticSilentRefresh();
// Loads discovery document and tries login via redirect callback
await this.oauthService.loadDiscoveryDocumentAndTryLogin();
}
login(): void {
this.oauthService.initCodeFlow();
}
logout(): void {
this.oauthService.logOut();
}
get isLoggedIn(): boolean {
return this.oauthService.hasValidAccessToken();
}
get accessToken(): string {
return this.oauthService.getAccessToken();
}
get idTokenClaims(): object | null {
return this.oauthService.getIdentityClaims() || null;
}
}
4) Initialize Auth on App Startup
In app.module.ts:
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { OAuthModule } from 'angular-oauth2-oidc';
import { AppComponent } from './app.component';
import { AuthService } from './auth/auth.service';
export function initAuth(auth: AuthService) {
return () => auth.init();
}
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
OAuthModule.forRoot({
resourceServer: {
allowedUrls: ['http://localhost:8080/api'], // your backend API
sendAccessToken: true,
},
}),
],
providers: [
{ provide: APP_INITIALIZER, useFactory: initAuth, deps: [AuthService], multi: true },
],
bootstrap: [AppComponent],
})
export class AppModule {}
5) Add a Callback Route
Create a route that your redirect URI points to:
// app-routing.module.ts
const routes: Routes = [
{ path: 'auth/callback', component: EmptyCallbackComponent },
// ...
];
EmptyCallbackComponent can simply show a spinner.
6) Protect Routes with a Guard
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(): boolean {
if (!this.auth.isLoggedIn) {
this.auth.login();
return false;
}
return true;
}
}
Use it:
{ path: 'dashboard', canActivate: [AuthGuard], component: DashboardComponent }
Part 2: OpenID Connect in React
Recommended Library
react-oidc-context
Built on oidc-client-ts, lightweight and idiomatic for React.
1) Install Dependencies
npm i react-oidc-context
2) Wrap Your App with AuthProvider
// main.tsx or index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { AuthProvider } from "react-oidc-context";
import App from "./App";
const oidcConfig = {
authority: "https://YOUR_ISSUER",
client_id: "YOUR_CLIENT_ID",
redirect_uri: window.location.origin + "/auth/callback",
post_logout_redirect_uri: window.location.origin + "/",
response_type: "code",
scope: "openid profile email",
};
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<AuthProvider {...oidcConfig}>
<App />
</AuthProvider>
</React.StrictMode>
);
3) Add a Callback Page
// AuthCallback.tsx
import { useEffect } from "react";
import { useAuth } from "react-oidc-context";
export default function AuthCallback() {
const auth = useAuth();
useEffect(() => {
// react-oidc-context automatically processes the callback on this route
}, []);
if (auth.isLoading) return <div>Signing you in…</div>;
if (auth.error) return <div>Error: {auth.error.message}</div>;
// Once user is loaded, route away (your router logic here)
return <div>Signed in. You can close this page.</div>;
}
4) Create a Simple Route Guard Pattern
import { useAuth } from "react-oidc-context";
export function RequireAuth({ children }: { children: React.ReactNode }) {
const auth = useAuth();
if (auth.isLoading) return <div>Loading…</div>;
if (!auth.isAuthenticated) {
auth.signinRedirect();
return null;
}
return <>{children}</>;
}
Usage:
<RequireAuth>
<Dashboard />
</RequireAuth>
5) Call APIs with the Access Token
import { useAuth } from "react-oidc-context";
export function useApi() {
const auth = useAuth();
return async (url: string) => {
const token = auth.user?.access_token;
const res = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
return res.json();
};
}
Token Storage: What to Do (And What to Avoid)
For SPAs, storing tokens in localStorage is convenient but increases risk if an XSS bug exists.
Preferred options:
- In-memory storage (default in many libs)
- Use httpOnly secure cookies (requires a backend “BFF” pattern)
If your app is high-security (healthcare/finance), strongly consider the BFF pattern.
Common Pitfalls (And Quick Fixes)
“Invalid redirect_uri”
The redirect URI must match exactly what you configured in the IdP.
CORS issues calling your API
Your API must allow your SPA origin and accept Authorization header.
Logout doesn’t fully log out
Some IdPs require an id_token_hint or specific end-session endpoint (the OIDC library usually handles this if discovery is correct).
Silent refresh fails
Check allowed iframe origins / third-party cookies, and consider refresh-token rotation or short sessions.
Production Checklist
- Use https in production (no exceptions)
- Turn off debug logging
- Validate issuer and discovery doc
- Use least-privilege scopes
- Protect routes + secure APIs with JWT validation
- Consider BFF for sensitive apps
Opinions expressed by DZone contributors are their own.
Comments