From 5f7673451314bfca2cefdb854c0ac996e7bab7b2 Mon Sep 17 00:00:00 2001 From: Lukas Spirig Date: Wed, 18 Sep 2024 15:44:03 +0200 Subject: [PATCH 1/3] feat: add `withAppInitializerAuthCheck` as a feature for `provideAuth` --- .../docs/documentation/configuration.md | 18 ++++++++ .../src/lib/provide-auth.spec.ts | 39 ++++++++++++++++- .../src/lib/provide-auth.ts | 43 ++++++++++++++++++- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md b/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md index 9d0b454e..d693921e 100644 --- a/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md +++ b/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md @@ -219,6 +219,24 @@ export const appConfig: ApplicationConfig = { bootstrapApplication(AppComponent, appConfig); ``` +The function `withAppInitializerAuthCheck` is provided to handle OAuth callbacks during app initialization +phase. This replaces the need to manually call `OidcSecurityService.checkAuth(...)` or +`OidcSecurityService.checkAuthMultiple(...)`. + +```ts +... +import { provideAuth, withAppInitializerAuthCheck } from 'angular-auth-oidc-client'; + +... + provideAuth({ + config: { + /* Your config here */ + }, + }, + withAppInitializerAuthCheck()), +... +``` + ## Config Values ### `configId` diff --git a/projects/angular-auth-oidc-client/src/lib/provide-auth.spec.ts b/projects/angular-auth-oidc-client/src/lib/provide-auth.spec.ts index 034bd9ec..af0a4e00 100644 --- a/projects/angular-auth-oidc-client/src/lib/provide-auth.spec.ts +++ b/projects/angular-auth-oidc-client/src/lib/provide-auth.spec.ts @@ -1,3 +1,4 @@ +import { APP_INITIALIZER } from '@angular/core'; import { TestBed, waitForAsync } from '@angular/core/testing'; import { of } from 'rxjs'; import { mockProvider } from '../test/auto-mock'; @@ -8,7 +9,8 @@ import { StsConfigLoader, StsConfigStaticLoader, } from './config/loader/config-loader'; -import { provideAuth } from './provide-auth'; +import { OidcSecurityService } from './oidc.security.service'; +import { provideAuth, withAppInitializerAuthCheck } from './provide-auth'; describe('provideAuth', () => { describe('APP_CONFIG', () => { @@ -55,4 +57,39 @@ describe('provideAuth', () => { expect(configLoader instanceof StsConfigHttpLoader).toBe(true); }); }); + + describe('features', () => { + let oidcSecurityServiceMock: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + oidcSecurityServiceMock = jasmine.createSpyObj( + 'OidcSecurityService', + ['checkAuthMultiple'] + ); + TestBed.configureTestingModule({ + providers: [ + provideAuth( + { config: { authority: 'something' } }, + withAppInitializerAuthCheck() + ), + mockProvider(ConfigurationService), + { + provide: OidcSecurityService, + useValue: oidcSecurityServiceMock, + }, + ], + }).compileComponents(); + })); + + it('should provide APP_INITIALIZER config', () => { + const config = TestBed.inject(APP_INITIALIZER); + + expect(config.length) + .withContext('Expected an APP_INITIALIZER to be registered') + .toBe(1); + expect(oidcSecurityServiceMock.checkAuthMultiple).toHaveBeenCalledTimes( + 1 + ); + }); + }); }); diff --git a/projects/angular-auth-oidc-client/src/lib/provide-auth.ts b/projects/angular-auth-oidc-client/src/lib/provide-auth.ts index ce559c2f..cd3851c0 100644 --- a/projects/angular-auth-oidc-client/src/lib/provide-auth.ts +++ b/projects/angular-auth-oidc-client/src/lib/provide-auth.ts @@ -1,4 +1,5 @@ import { + APP_INITIALIZER, EnvironmentProviders, makeEnvironmentProviders, Provider, @@ -11,13 +12,28 @@ import { import { StsConfigLoader } from './config/loader/config-loader'; import { AbstractLoggerService } from './logging/abstract-logger.service'; import { ConsoleLoggerService } from './logging/console-logger.service'; +import { OidcSecurityService } from './oidc.security.service'; import { AbstractSecurityStorage } from './storage/abstract-security-storage'; import { DefaultSessionStorageService } from './storage/default-sessionstorage.service'; +/** + * A feature to be used with `provideAuth`. + */ +export interface AuthFeature { + ɵproviders: Provider[]; +} + export function provideAuth( - passedConfig: PassedInitialConfig + passedConfig: PassedInitialConfig, + ...features: AuthFeature[] ): EnvironmentProviders { - return makeEnvironmentProviders([..._provideAuth(passedConfig)]); + const providers = _provideAuth(passedConfig); + + for (const feature of features) { + providers.push(...feature.ɵproviders); + } + + return makeEnvironmentProviders(providers); } export function _provideAuth(passedConfig: PassedInitialConfig): Provider[] { @@ -38,3 +54,26 @@ export function _provideAuth(passedConfig: PassedInitialConfig): Provider[] { { provide: AbstractLoggerService, useClass: ConsoleLoggerService }, ]; } + +/** + * Configures an app initializer, which is called before the app starts, and + * resolves any OAuth callback variables. + * When used, it replaces the need to manually call + * `OidcSecurityService.checkAuth(...)` or + * `OidcSecurityService.checkAuthMultiple(...)`. + * + * @see https://angular.dev/api/core/APP_INITIALIZER + */ +export function withAppInitializerAuthCheck(): AuthFeature { + return { + ɵproviders: [ + { + provide: APP_INITIALIZER, + useFactory: (oidcSecurityService: OidcSecurityService) => () => + oidcSecurityService.checkAuthMultiple(), + multi: true, + deps: [OidcSecurityService], + }, + ], + }; +} From 496425ba0b0f94c7809dad905cb9bf1dadb5c216 Mon Sep 17 00:00:00 2001 From: Lukas Spirig Date: Fri, 27 Sep 2024 17:37:22 +0200 Subject: [PATCH 2/3] refactor: migrate azuread example to standalone and use new feature --- .../src/app/app.component.ts | 4 ++ .../src/app/app.config.ts | 53 +++++++++++++++++++ .../src/app/app.module.ts | 30 ----------- .../src/app/app.routes.ts | 14 +++-- .../src/app/auth-config.module.ts | 32 ----------- .../app/forbidden/forbidden.component.spec.ts | 2 +- .../src/app/forbidden/forbidden.component.ts | 1 + .../src/app/home/home.component.ts | 3 ++ .../src/app/nav-menu/nav-menu.component.ts | 4 ++ .../app/protected/protected.component.spec.ts | 2 +- .../src/app/protected/protected.component.ts | 1 + .../unauthorized.component.spec.ts | 2 +- .../unauthorized/unauthorized.component.ts | 1 + projects/sample-code-flow-azuread/src/main.ts | 17 +++--- 14 files changed, 82 insertions(+), 84 deletions(-) create mode 100644 projects/sample-code-flow-azuread/src/app/app.config.ts delete mode 100644 projects/sample-code-flow-azuread/src/app/app.module.ts delete mode 100644 projects/sample-code-flow-azuread/src/app/auth-config.module.ts diff --git a/projects/sample-code-flow-azuread/src/app/app.component.ts b/projects/sample-code-flow-azuread/src/app/app.component.ts index 883b9ce6..29ad55d7 100644 --- a/projects/sample-code-flow-azuread/src/app/app.component.ts +++ b/projects/sample-code-flow-azuread/src/app/app.component.ts @@ -1,9 +1,13 @@ import { Component, inject } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; import { OidcSecurityService } from 'angular-auth-oidc-client'; +import { NavMenuComponent } from './nav-menu/nav-menu.component'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', + imports: [RouterOutlet, NavMenuComponent], + standalone: true, }) export class AppComponent { private readonly oidcSecurityService = inject(OidcSecurityService); diff --git a/projects/sample-code-flow-azuread/src/app/app.config.ts b/projects/sample-code-flow-azuread/src/app/app.config.ts new file mode 100644 index 00000000..c9370ed0 --- /dev/null +++ b/projects/sample-code-flow-azuread/src/app/app.config.ts @@ -0,0 +1,53 @@ +import { + provideHttpClient, + withInterceptors, + withInterceptorsFromDi, +} from '@angular/common/http'; +import { ApplicationConfig } from '@angular/core'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { provideRouter } from '@angular/router'; +import { + authInterceptor, + LogLevel, + provideAuth, + withAppInitializerAuthCheck, +} from 'angular-auth-oidc-client'; + +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + provideAnimationsAsync(), + provideAuth( + { + config: { + authority: + 'https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0', + authWellknownEndpointUrl: + 'https://login.microsoftonline.com/common/v2.0', + redirectUrl: window.location.origin, + clientId: 'e38ea64a-2962-4cde-bfe7-dd2822fdab32', + scope: + 'openid profile offline_access email api://e38ea64a-2962-4cde-bfe7-dd2822fdab32/access_as_user', + responseType: 'code', + silentRenew: true, + maxIdTokenIatOffsetAllowedInSeconds: 600, + issValidationOff: true, + autoUserInfo: false, + // silentRenewUrl: window.location.origin + '/silent-renew.html', + useRefreshToken: true, + logLevel: LogLevel.Debug, + customParamsAuthRequest: { + prompt: 'select_account', // login, consent + }, + }, + }, + withAppInitializerAuthCheck() + ), + provideHttpClient( + withInterceptorsFromDi(), + withInterceptors([authInterceptor()]) + ), + ], +}; diff --git a/projects/sample-code-flow-azuread/src/app/app.module.ts b/projects/sample-code-flow-azuread/src/app/app.module.ts deleted file mode 100644 index 48034d4e..00000000 --- a/projects/sample-code-flow-azuread/src/app/app.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - provideHttpClient, - withInterceptorsFromDi, -} from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; -import { AppComponent } from './app.component'; -import { routing } from './app.routes'; -import { AuthConfigModule } from './auth-config.module'; -import { ForbiddenComponent } from './forbidden/forbidden.component'; -import { HomeComponent } from './home/home.component'; -import { NavMenuComponent } from './nav-menu/nav-menu.component'; -import { ProtectedComponent } from './protected/protected.component'; -import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; - -@NgModule({ - declarations: [ - AppComponent, - NavMenuComponent, - HomeComponent, - ForbiddenComponent, - UnauthorizedComponent, - ProtectedComponent, - ], - bootstrap: [AppComponent], - imports: [BrowserModule, FormsModule, routing, AuthConfigModule], - providers: [provideHttpClient(withInterceptorsFromDi())], -}) -export class AppModule {} diff --git a/projects/sample-code-flow-azuread/src/app/app.routes.ts b/projects/sample-code-flow-azuread/src/app/app.routes.ts index b6abf3ff..a828ee5f 100644 --- a/projects/sample-code-flow-azuread/src/app/app.routes.ts +++ b/projects/sample-code-flow-azuread/src/app/app.routes.ts @@ -1,28 +1,26 @@ -import { RouterModule, Routes } from '@angular/router'; -import { AutoLoginAllRoutesGuard } from 'angular-auth-oidc-client'; +import { Routes } from '@angular/router'; +import { autoLoginPartialRoutesGuard } from 'angular-auth-oidc-client'; import { ForbiddenComponent } from './forbidden/forbidden.component'; import { HomeComponent } from './home/home.component'; import { ProtectedComponent } from './protected/protected.component'; import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; -const appRoutes: Routes = [ +export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'home' }, { path: 'home', component: HomeComponent, - canActivate: [AutoLoginAllRoutesGuard], + canActivate: [autoLoginPartialRoutesGuard], }, { path: 'forbidden', component: ForbiddenComponent, - canActivate: [AutoLoginAllRoutesGuard], + canActivate: [autoLoginPartialRoutesGuard], }, { path: 'protected', component: ProtectedComponent, - canActivate: [AutoLoginAllRoutesGuard], + canActivate: [autoLoginPartialRoutesGuard], }, { path: 'unauthorized', component: UnauthorizedComponent }, ]; - -export const routing = RouterModule.forRoot(appRoutes); diff --git a/projects/sample-code-flow-azuread/src/app/auth-config.module.ts b/projects/sample-code-flow-azuread/src/app/auth-config.module.ts deleted file mode 100644 index 9e871164..00000000 --- a/projects/sample-code-flow-azuread/src/app/auth-config.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NgModule } from '@angular/core'; -import { AuthModule, LogLevel } from 'angular-auth-oidc-client'; - -@NgModule({ - imports: [ - AuthModule.forRoot({ - config: { - authority: - 'https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0', - authWellknownEndpointUrl: - 'https://login.microsoftonline.com/common/v2.0', - redirectUrl: window.location.origin, - clientId: 'e38ea64a-2962-4cde-bfe7-dd2822fdab32', - scope: - 'openid profile offline_access email api://e38ea64a-2962-4cde-bfe7-dd2822fdab32/access_as_user', - responseType: 'code', - silentRenew: true, - maxIdTokenIatOffsetAllowedInSeconds: 600, - issValidationOff: true, - autoUserInfo: false, - // silentRenewUrl: window.location.origin + '/silent-renew.html', - useRefreshToken: true, - logLevel: LogLevel.Debug, - customParamsAuthRequest: { - prompt: 'select_account', // login, consent - }, - }, - }), - ], - exports: [AuthModule], -}) -export class AuthConfigModule {} diff --git a/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.spec.ts b/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.spec.ts index 42c77030..1207e3a9 100644 --- a/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.spec.ts +++ b/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.spec.ts @@ -7,7 +7,7 @@ describe('ForbiddenComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ForbiddenComponent], + imports: [ForbiddenComponent], }).compileComponents(); })); diff --git a/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.ts b/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.ts index df59c551..85b35411 100644 --- a/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.ts +++ b/projects/sample-code-flow-azuread/src/app/forbidden/forbidden.component.ts @@ -4,5 +4,6 @@ import { Component } from '@angular/core'; selector: 'app-forbidden', templateUrl: './forbidden.component.html', styleUrls: ['./forbidden.component.css'], + standalone: true, }) export class ForbiddenComponent {} diff --git a/projects/sample-code-flow-azuread/src/app/home/home.component.ts b/projects/sample-code-flow-azuread/src/app/home/home.component.ts index b8b84ffd..9889915a 100644 --- a/projects/sample-code-flow-azuread/src/app/home/home.component.ts +++ b/projects/sample-code-flow-azuread/src/app/home/home.component.ts @@ -1,9 +1,12 @@ +import { AsyncPipe, JsonPipe } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { OidcSecurityService } from 'angular-auth-oidc-client'; @Component({ selector: 'app-home', templateUrl: 'home.component.html', + imports: [AsyncPipe, JsonPipe], + standalone: true, }) export class HomeComponent implements OnInit { private readonly oidcSecurityService = inject(OidcSecurityService); diff --git a/projects/sample-code-flow-azuread/src/app/nav-menu/nav-menu.component.ts b/projects/sample-code-flow-azuread/src/app/nav-menu/nav-menu.component.ts index ee058efe..3d19a80d 100644 --- a/projects/sample-code-flow-azuread/src/app/nav-menu/nav-menu.component.ts +++ b/projects/sample-code-flow-azuread/src/app/nav-menu/nav-menu.component.ts @@ -1,10 +1,14 @@ +import { NgIf } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; +import { RouterLink } from '@angular/router'; import { OidcSecurityService } from 'angular-auth-oidc-client'; @Component({ selector: 'app-nav-menu', templateUrl: './nav-menu.component.html', styleUrls: ['./nav-menu.component.css'], + imports: [RouterLink, NgIf], + standalone: true, }) export class NavMenuComponent implements OnInit { private readonly oidcSecurityService = inject(OidcSecurityService); diff --git a/projects/sample-code-flow-azuread/src/app/protected/protected.component.spec.ts b/projects/sample-code-flow-azuread/src/app/protected/protected.component.spec.ts index b3c3f44d..3f909051 100644 --- a/projects/sample-code-flow-azuread/src/app/protected/protected.component.spec.ts +++ b/projects/sample-code-flow-azuread/src/app/protected/protected.component.spec.ts @@ -7,7 +7,7 @@ describe('ProtectedComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProtectedComponent], + imports: [ProtectedComponent], }).compileComponents(); })); diff --git a/projects/sample-code-flow-azuread/src/app/protected/protected.component.ts b/projects/sample-code-flow-azuread/src/app/protected/protected.component.ts index cf8dec94..720e0ca0 100644 --- a/projects/sample-code-flow-azuread/src/app/protected/protected.component.ts +++ b/projects/sample-code-flow-azuread/src/app/protected/protected.component.ts @@ -4,5 +4,6 @@ import { Component } from '@angular/core'; selector: 'app-protected', templateUrl: './protected.component.html', styleUrls: ['./protected.component.css'], + standalone: true, }) export class ProtectedComponent {} diff --git a/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.spec.ts b/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.spec.ts index f26af279..713f7877 100644 --- a/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.spec.ts +++ b/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.spec.ts @@ -7,7 +7,7 @@ describe('UnauthorizedComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [UnauthorizedComponent], + imports: [UnauthorizedComponent], }).compileComponents(); })); diff --git a/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.ts b/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.ts index c5c1482b..60c476c3 100644 --- a/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.ts +++ b/projects/sample-code-flow-azuread/src/app/unauthorized/unauthorized.component.ts @@ -4,5 +4,6 @@ import { Component } from '@angular/core'; selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.css'], + standalone: true, }) export class UnauthorizedComponent {} diff --git a/projects/sample-code-flow-azuread/src/main.ts b/projects/sample-code-flow-azuread/src/main.ts index d9a2e7e4..0b27bbbc 100644 --- a/projects/sample-code-flow-azuread/src/main.ts +++ b/projects/sample-code-flow-azuread/src/main.ts @@ -1,13 +1,8 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { bootstrapApplication } from '@angular/platform-browser'; -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; -if (environment.production) { - enableProdMode(); -} - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.error(err)); +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); From 49a71ee8c5bfe6252dc6a7bb269618a44e8ab232 Mon Sep 17 00:00:00 2001 From: Lukas Spirig Date: Mon, 30 Sep 2024 08:04:11 +0200 Subject: [PATCH 3/3] docs: extend documentation for standalone config --- .../docs/documentation/configuration.md | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md b/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md index d693921e..dccc4465 100644 --- a/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md +++ b/docs/site/angular-auth-oidc-client/docs/documentation/configuration.md @@ -219,21 +219,48 @@ export const appConfig: ApplicationConfig = { bootstrapApplication(AppComponent, appConfig); ``` -The function `withAppInitializerAuthCheck` is provided to handle OAuth callbacks during app initialization -phase. This replaces the need to manually call `OidcSecurityService.checkAuth(...)` or +Additionally, you can use the feature function `withAppInitializerAuthCheck` +to handle OAuth callbacks during app initialization phase. This replaces the +need to manually call `OidcSecurityService.checkAuth(...)` or `OidcSecurityService.checkAuthMultiple(...)`. ```ts -... +import { ApplicationConfig } from '@angular/core'; import { provideAuth, withAppInitializerAuthCheck } from 'angular-auth-oidc-client'; +export const appConfig: ApplicationConfig = { + providers: [ + provideAuth( + { + config: { + /* Your config here */ + }, + }, + withAppInitializerAuthCheck() + ), + ], +}; +``` +If you prefer to manually check OAuth callback state, you can omit +`withAppInitializerAuthCheck`. However, you then need to call +`OidcSecurityService.checkAuth(...)` or +`OidcSecurityService.checkAuthMultiple(...)` manually in your +`app.component.ts` (or a similar code path that is called early in your app). + +```ts +// Shortened for brevity ... - provideAuth({ - config: { - /* Your config here */ - }, - }, - withAppInitializerAuthCheck()), +export class AppComponent implements OnInit { + private readonly oidcSecurityService = inject(OidcSecurityService); + + ngOnInit(): void { + this.oidcSecurityService + .checkAuth() + .subscribe(({ isAuthenticated, accessToken }) => { + console.log('app authenticated', isAuthenticated); + console.log(`Current access token is '${accessToken}'`); + }); + } ... ```