Implementing OpenID Connect Authentication in Angular 19 Without NgModules
Source: Dev.to
Tech stack
- Node v20.18.3
- Angular 19.2.20
angular-auth-oidc-client- Identity Server as the OpenID provider
- No NgModules – everything is stand‑alone
Step 1 – Install the OIDC client
npm install angular-auth-oidc-client
The library handles the heavy lifting: token exchange, storage, refresh, and state handling. Do not manage tokens manually.
Step 2 – Configure authentication in app.config.ts
Because this is a stand‑alone Angular app, everything is configured via ApplicationConfig.
import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { provideRouter, Router } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { AuthModule, StsConfigLoader, OidcSecurityService } from 'angular-auth-oidc-client';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
// OIDC module
importProvidersFrom(
AuthModule.forRoot({
loader: {
provide: StsConfigLoader,
useFactory: OidcConfigLoaderFactory,
},
})
),
// Run checkAuth on app start
provideAppInitializer(() => {
const oidcSecurityService = inject(OidcSecurityService);
const router = inject(Router);
return oidcSecurityService.checkAuth().toPromise().then(result => {
if (result?.isAuthenticated) {
router.navigate(['/dashboard']);
}
});
})
]
};
Important: Calling checkAuth on app startup is mandatory. Without it the redirect from Identity Server isn’t processed, and login appears broken. This was the first major issue I faced—tokens were returned, but Angular never picked them up.
Step 3 – OIDC configuration (config loader)
{
authority: 'https://your-identity-server',
redirectUrl: window.location.origin,
postLogoutRedirectUri: window.location.origin,
clientId: 'your-client-id',
scope: 'openid profile api',
responseType: 'code',
silentRenew: true,
useRefreshToken: true
}
Tip: Always use
responseType: 'code'with PKCE.
Step 4 – Implement login
A minimal login service:
login(): void {
this.oidcSecurityService.authorize(undefined, {
customParams: { prompt: 'login' }
});
}
Why prompt=login?
I initially saw the app silently authenticating and returning tokens because an SSO session already existed on the Identity Server. Adding prompt=login forces the login page to appear, ensuring the user explicitly authenticates.
Step 5 – Guard your routes
Routes
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: '',
component: LayoutComponent,
canActivate: [AuthorizationGuard],
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'management', component: LicenceManagementComponent }
]
},
{ path: '**', redirectTo: '/login' }
];
Guard
canActivate(): Observable {
return this.oidcSecurityService.checkAuth().pipe(
map(({ isAuthenticated }) => {
return isAuthenticated ? true : this.router.parseUrl('/login');
})
);
}
Gotcha: I originally destructured a boolean as an object ({ isAuthenticated }), which broke navigation and caused a redirect loop. Correctly handling the observable fixed the issue.
Step 6 – Redirect after login
If redirectUrl points to /login, the user lands back on the login page after a successful sign‑in. Fix this by navigating away once authentication is confirmed.
ngOnInit(): void {
this.securityService.isAuthenticated().subscribe(isAuth => {
if (isAuth) {
this.router.navigate(['/dashboard']);
}
});
}
Now authenticated users never stay on the login screen.
Step 7 – Do not store tokens manually
The library already stores tokens in localStorage via DefaultLocalStorageService. If you need profile information, read the token through the service:
this.oidcSecurityService.getAccessToken().subscribe(token => {
const decoded = jwtDecode(token);
console.log(decoded.sub);
});
Keep token handling to the library—avoid duplicate storage or manual decoding.
Bonus – Lazy‑load the authenticated area
{
path: '',
component: LayoutComponent,
canActivate: [AuthorizationGuard],
loadChildren: () =>
import('./features/app.routes').then(m => m.APP_ROUTES)
}
Lazy loading the protected module improves initial load performance.
Challenges I Faced
| Issue | Root Cause | Fix |
|---|---|---|
| Login appeared to do nothing | Active SSO session | Set prompt=login |
| Tokens were returned but Angular did not recognize authentication | Missing checkAuth on startup | Added provideAppInitializer |
| After login it kept returning to the login page | Incorrect guard observable typing | Corrected the guard implementation |
| Redirect URL landed on login instead of dashboard | – | Redirect inside LoginComponent when authenticated |
Final Thoughts
Implementing OpenID Connect in Angular 19 without NgModules is actually clean once you understand the lifecycle.
Key ideas
- Always call
checkAuthon startup. - Let the library manage tokens.
- Keep guards simple.
- Understand how redirect URLs affect routing.
- Force the login prompt only when needed.
Authentication is less about writing code and more about understanding flow. Once you get the flow right, everything becomes predictable.
If you are implementing this on Angular 19 with Node 20 and standalone APIs, this structure should save you a few hours of debugging—and probably a bit of frustration too.
Happy coding!