Angular 19에서 NgModules 없이 OpenID Connect 인증 구현
Source: Dev.to
Authentication looks simple from the outside, but it quickly humbles you once you start wiring it properly.
In the past day I implemented OpenID Connect (OIDC) authentication in an Angular 19 application (Node v20.18.3) using only stand‑alone APIs—no NgModules. This post walks through the setup, the structure, the issues I ran into, and how I fixed them.
기술 스택
- 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 – OIDC 클라이언트 설치
npm install angular-auth-oidc-client
이 라이브러리는 토큰 교환, 저장, 갱신 및 상태 관리를 포함한 복잡한 작업을 처리합니다. 토큰을 직접 관리하지 마세요.
Step 2 – 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: 앱 시작 시 checkAuth를 호출하는 것은 필수입니다. 이를 수행하지 않으면 Identity Server에서의 리다이렉트가 처리되지 않아 로그인 기능이 깨진 것처럼 보입니다. 이것이 제가 처음 마주한 주요 문제였으며, 토큰은 반환되었지만 Angular가 이를 전혀 인식하지 못했습니다.
Step 3 – OIDC 구성 (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: 항상 PKCE와 함께
responseType: 'code'를 사용하세요.
Step 4 – 로그인 구현
A minimal login service:
login(): void {
this.oidcSecurityService.authorize(undefined, {
customParams: { prompt: 'login' }
});
}
왜 prompt=login인가?
처음에는 SSO 세션이 Identity Server에 이미 존재해서 앱이 조용히 인증하고 토큰을 반환하는 것을 보았습니다. prompt=login을 추가하면 로그인 페이지가 강제로 표시되어 사용자가 명시적으로 인증하도록 합니다.
Step 5 – 라우트를 보호하세요
라우트
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' }
];
가드
canActivate(): Observable {
return this.oidcSecurityService.checkAuth().pipe(
map(({ isAuthenticated }) => {
return isAuthenticated ? true : this.router.parseUrl('/login');
})
);
}
Gotcha: 나는 원래 불리언을 객체({ isAuthenticated })로 구조분해했는데, 이 때문에 네비게이션이 깨지고 리다이렉트 루프가 발생했습니다. 옵저버블을 올바르게 처리하면서 문제가 해결되었습니다.
Step 6 – 로그인 후 리다이렉트
redirectUrl이 /login을 가리키면, 사용자는 성공적인 로그인 후 다시 로그인 페이지로 이동합니다. 인증이 확인되면 다른 페이지로 이동하도록 수정하세요.
ngOnInit(): void {
this.securityService.isAuthenticated().subscribe(isAuth => {
if (isAuth) {
this.router.navigate(['/dashboard']);
}
});
}
이제 인증된 사용자는 로그인 화면에 머무르지 않습니다.
Step 7 – 토큰을 수동으로 저장하지 마세요
라이브러리는 이미 DefaultLocalStorageService를 통해 localStorage에 토큰을 저장합니다. 프로필 정보가 필요하면 서비스에서 토큰을 읽어오세요:
this.oidcSecurityService.getAccessToken().subscribe(token => {
const decoded = jwtDecode(token);
console.log(decoded.sub);
});
토큰 처리는 라이브러리에 맡기세요—중복 저장이나 수동 디코딩을 피하십시오.
보너스 – 인증된 영역을 지연 로드하기
{
path: '',
component: LayoutComponent,
canActivate: [AuthorizationGuard],
loadChildren: () =>
import('./features/app.routes').then(m => m.APP_ROUTES)
}
보호된 모듈을 지연 로드하면 초기 로드 성능이 향상됩니다.
직면한 과제
| 문제 | 근본 원인 | 해결책 |
|---|---|---|
| 로그인 시 아무 일도 일어나지 않음 | 활성 SSO 세션 | prompt=login 설정 |
| 토큰은 반환되었지만 Angular가 인증을 인식하지 못함 | 시작 시 checkAuth 누락 | provideAppInitializer 추가 |
| 로그인 후에도 계속 로그인 페이지로 돌아감 | 잘못된 guard observable 타입 지정 | guard 구현 수정 |
| 리다이렉트 URL이 대시보드가 아닌 로그인 페이지에 도착함 | – | 인증 시 LoginComponent 내부에서 리다이렉트 |
최종 생각
Angular 19에서 NgModules 없이 OpenID Connect를 구현하는 것은 라이프사이클을 이해하면 실제로 깔끔합니다.
핵심 아이디어
- 시작 시 항상
checkAuth를 호출합니다. - 라이브러리가 토큰을 관리하도록 합니다.
- 가드를 간단하게 유지합니다.
- 리디렉션 URL이 라우팅에 어떤 영향을 미치는지 이해합니다.
- 필요할 때만 로그인 프롬프트를 강제합니다.
인증은 코드를 작성하는 것보다 흐름을 이해하는 것이 더 중요합니다. 흐름을 올바르게 잡으면 모든 것이 예측 가능해집니다.
아래는 샘플 Angular 앱 저장소입니다:
Angular 19와 Node 20, 스탠드얼론 API로 구현한다면, 이 구조가 디버깅에 몇 시간을 절약하고—아마 약간의 좌절감도 줄여줄 것입니다.
행복한 코딩 되세요!