Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add withAppInitializerAuthCheck as a feature for provideAuth #2006

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}'`);
});
}
...
```
Comment on lines +244 to +265
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also refer to the checkAuth API, this keeps the documentation in one place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this in more detail? I'm not sure I fully understand what you mean.

Copy link
Contributor

@timdeschryver timdeschryver Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs already provide information and an example for checkAuth and checkAuthMultiple.
This means we can (in my opinion) just refer to the documentation, instead of including it here.
This prevents duplicates and makes it easier to change/update the docs later.

We can remove this section and update the text above the example, something as:

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(...)`](/docs/documentation/public-api#checkauthurl-string-configid-string) or
[`OidcSecurityService.checkAuthMultiple(...)`](/docs/documentation/public-api#checkauthmultipleurl-string) within the `app.component.ts` file (or a similar code path that is called early in your app).

As a result we don't repeat ourselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that, but @FabianGosebrink requested to add docs to have the "old" and "new" way side by side or underneath, so that we can link to it.
I'm fine with either. Should I adapt, as suggested by @timdeschryver?

Copy link
Contributor

@timdeschryver timdeschryver Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nevermind my comment then :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easy, I just wanted it to make it as easy as possible for the users to use. I am fine with both :)


## 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
Loading