Skip to content

Commit

Permalink
Merge pull request #2006 from kyubisation/feat-app-initializer-auth-c…
Browse files Browse the repository at this point in the history
…heck

feat: add `withAppInitializerAuthCheck` as a feature for `provideAuth`
  • Loading branch information
FabianGosebrink authored Oct 9, 2024
2 parents cb3080a + 49a71ee commit 14f032c
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,51 @@ export const appConfig: ApplicationConfig = {
bootstrapApplication(AppComponent, appConfig);
```

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
...
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}'`);
});
}
...
```
## Config Values
### `configId`
Expand Down
39 changes: 38 additions & 1 deletion projects/angular-auth-oidc-client/src/lib/provide-auth.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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', () => {
Expand Down Expand Up @@ -55,4 +57,39 @@ describe('provideAuth', () => {
expect(configLoader instanceof StsConfigHttpLoader).toBe(true);
});
});

describe('features', () => {
let oidcSecurityServiceMock: jasmine.SpyObj<OidcSecurityService>;

beforeEach(waitForAsync(() => {
oidcSecurityServiceMock = jasmine.createSpyObj<OidcSecurityService>(
'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
);
});
});
});
43 changes: 41 additions & 2 deletions projects/angular-auth-oidc-client/src/lib/provide-auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
APP_INITIALIZER,
EnvironmentProviders,
makeEnvironmentProviders,
Provider,
Expand All @@ -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[] {
Expand All @@ -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],
},
],
};
}
4 changes: 4 additions & 0 deletions projects/sample-code-flow-azuread/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
53 changes: 53 additions & 0 deletions projects/sample-code-flow-azuread/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -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()])
),
],
};
30 changes: 0 additions & 30 deletions projects/sample-code-flow-azuread/src/app/app.module.ts

This file was deleted.

14 changes: 6 additions & 8 deletions projects/sample-code-flow-azuread/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -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);
32 changes: 0 additions & 32 deletions projects/sample-code-flow-azuread/src/app/auth-config.module.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('ForbiddenComponent', () => {

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ForbiddenComponent],
imports: [ForbiddenComponent],
}).compileComponents();
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('ProtectedComponent', () => {

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProtectedComponent],
imports: [ProtectedComponent],
}).compileComponents();
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Loading

0 comments on commit 14f032c

Please sign in to comment.