Implementing OpenID Connect Authentication in Angular 19 Without NgModules

Published: (February 26, 2026 at 06:48 AM EST)
4 min read
Source: Dev.to

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

IssueRoot CauseFix
Login appeared to do nothingActive SSO sessionSet prompt=login
Tokens were returned but Angular did not recognize authenticationMissing checkAuth on startupAdded provideAppInitializer
After login it kept returning to the login pageIncorrect guard observable typingCorrected the guard implementation
Redirect URL landed on login instead of dashboardRedirect 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 checkAuth on 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.

👉 GitHub Repository

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!

👉 For More Blogs

0 views
Back to Blog

Related posts

Read more »