Skip to content

Commit

Permalink
Merge pull request AzureAD#2803 from AzureAD/guard-access-denied
Browse files Browse the repository at this point in the history
[msal-angular] Guard supports angular routes for login failure
  • Loading branch information
jo-arroyo authored Jan 11, 2021
2 parents c68ac59 + 4acb0dc commit 72b8e06
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Msal guard supports angular routes for login failure (#2803)",
"packageName": "@azure/msal-angular",
"email": "[email protected]",
"dependentChangeType": "patch"
}
10 changes: 7 additions & 3 deletions lib/msal-angular/docs/v2-docs/1.x-2.x-upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,22 @@ See the [updated sample](https://github.com/AzureAD/microsoft-authentication-lib
* The `logger` is now set through configurations for the MSAL instance, under `system.loggerOptions`, which include a `loggerCallback`, `piiLoggingEnabled` and `logLevel`, instead of an instance of a `logger`. The `logger` can also be set dynamically by using `MsalService.setLogger()`. See the [`logger documentation`](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/logging.md) for more information and [sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v2-samples/angular10-sample-app/src/app/app.module.ts) for usage.

### API changes

* The `acquireToken` and `login` methods now take different request objects as parameters. See the [msal.service.ts](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/src/msal.service.ts) for details.
* Broadcast events now emit an `EventMessage` object, instead of just strings. See the [Angular sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v2-samples/angular10-sample-app/src/app/app.component.ts) for an example of how to implement.
* Applications using `Redirect` methods must implement the `handleRedirectObservable` method (and have it run on every page load), which will capture the result of redirect operations. See the [Angular sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v2-samples/angular10-sample-app/src/app/home/home.component.ts) for an example of how to implement.

### MSAL Guard

* **Interfaces**: `MsalGuard` now implements `CanActivateChild` and `CanLoad` in addition to `CanActivate`. Example code snippets are provided in our [initialization doc](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/initialization.md#secure-the-routes-in-your-application) and examples of usage can be found in our sample application [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v2-samples/angular11-sample-app/src/app/app-routing.module.ts).
* **Redirect on failure**: `MsalGuard` configuration now has a `loginFailedRoute` that can be configured. The Guard will redirect to this page if login is required and fails. See the Angular sample for examples of implementing it in the [configuration](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/c0609f899704215515eeeac77e9885228d6d5dbb/samples/msal-angular-v2-samples/angular11-sample-app/src/app/app.module.ts#L48) and [app routing module](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/c0609f899704215515eeeac77e9885228d6d5dbb/samples/msal-angular-v2-samples/angular11-sample-app/src/app/app-routing.module.ts#L40).

### Accounts

* When getting accounts, we recommend using `getAccountByHomeId()` and `getAccountByLocalId()`, available on the MSAL instance. `getAccount()` is now `getAccountByUsername()`, but should be a secondary choice, as it may be less reliable and is for convenience only.
* `getAllAccounts()` is also available on the MSAL instance. Please see [docs](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-browser/classes/_src_app_clientapplication_.clientapplication.html) for `@azure/msal-browser` for more details on account methods.
* Additionally, you can now get and set active acccounts using `getActiveAccount()` and `setActiveAccount()`. We recommend setting the active account after logging in with popups or calling `handleRedirectObservable()`. See [our sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/513855f780aef1cb1c905944ec3ba139623addf3/samples/msal-angular-v2-samples/angular11-sample-app/src/app/app.component.ts#L48) for examples of its use.

### MSAL Guard Interfaces
* `MsalGuard` now implements `CanActivateChild` and `CanLoad` in addition to `CanActivate`. Example code snippets are provided in our [initialization doc](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/initialization.md#secure-the-routes-in-your-application) and examples of usage can be found in our sample application [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v2-samples/angular11-sample-app/src/app/app-routing.module.ts).

## Angular 9+ and rxjs@6

MSAL Angular now expects that your application is built with `@angular/core@>=9`, `@angular/common@>=9`, `rxjs@6`. As with MSAL Angular 1.x, `rxjs-compat` is not required.
Expand Down
13 changes: 11 additions & 2 deletions lib/msal-angular/docs/v2-docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ MSAL for Angular accepts three configuration objects:
2. [`MsalGuardConfiguration`](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/src/msal.guard.config.ts): A set of options specifically for the Angular guard.
3. [`MsalInterceptorConfiguration`](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/src/msal.interceptor.config.ts): A set of options specifically for the Angular interceptor.

An `authRequest` object can be specified on `MsalGuardConfiguration` and `MsalInterceptorConfiguration` to set additional options. This is an advanced featured that is not required. All possible parameters for the request object can be found here: [`PopupRequest`](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-browser/modules/_src_request_popuprequest_.html) and [`RedirectRequest`](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-browser/modules/_src_request_redirectrequest_.html).
### Angular-specific configurations

* An `interactionType` must be specified on `MsalGuardConfiguration` and `MsalInterceptorConfiguration`, and can be set to `Popup` or `Redirect`.
* An `authRequest` object can be specified on `MsalGuardConfiguration` and `MsalInterceptorConfiguration` to set additional options. This is an advanced featured that is not required. All possible parameters for the request object can be found here: [`PopupRequest`](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-browser/modules/_src_request_popuprequest_.html) and [`RedirectRequest`](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-browser/modules/_src_request_redirectrequest_.html).
* The `protectedResourceMap` object on `MsalInterceptorConfiguration` is used to protect routes. See the [upgrade guide](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/1.x-2.x-upgrade-guide.md) to see how to use wildcards, and how this differs from MSAL 1.x.
* The `loginFailedRoute` string can be set on `MsalGuardConfiguration`. Msal Guard will redirect to this route if login is required and fails.

## MsalModule.forRoot

Expand Down Expand Up @@ -46,6 +51,7 @@ import { IPublicClientApplication, PublicClientApplication, InteractionType, Bro
}
}, {
interactionType: InteractionType.Popup // MSAL Guard Configuration
loginFailedRoute: "/login-failed"
}, {
interactionType: InteractionType.Redirect, // MSAL Interceptor Configuration
protectedResourceMap
Expand Down Expand Up @@ -103,7 +109,10 @@ export function MSALInterceptorConfigFactory(): MsalInterceptorConfig {
}

export function MSALGuardConfigFactory(): MsalGuardConfiguration {
return { interactionType: InteractionType.Redirect };
return {
interactionType: InteractionType.Redirect,
loginFailedRoute: "./login-failed"
};
}

@NgModule({
Expand Down
2 changes: 1 addition & 1 deletion lib/msal-angular/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export const MSAL_INTERCEPTOR_CONFIG = new InjectionToken<string>("MSAL_INTERCEP

export const name = "@azure/msal-angular";

export const version = "2.0.0-alpha.0";
export const version = "2.0.0-alpha.1";
1 change: 1 addition & 0 deletions lib/msal-angular/src/msal.guard.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ import { PopupRequest, RedirectRequest,InteractionType } from "@azure/msal-brows
export type MsalGuardConfiguration = {
interactionType: InteractionType.Popup | InteractionType.Redirect;
authRequest?: Partial<PopupRequest> | Partial<Omit<RedirectRequest, "redirectStartPage">>;
loginFailedRoute?: string;
};
32 changes: 29 additions & 3 deletions lib/msal-angular/src/msal.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Router, UrlTree } from '@angular/router';
import { BrowserUtils, InteractionType, IPublicClientApplication, PublicClientApplication, UrlString } from '@azure/msal-browser';
import { of } from 'rxjs';
import { MsalGuardConfiguration } from './msal.guard.config';
Expand All @@ -12,6 +12,7 @@ let routeMock: any = { snapshot: {} };
let routeStateMock: any = { snapshot: {}, url: '/' };
let routerMock = { navigate: jasmine.createSpy('navigate') };
let testInteractionType: InteractionType;
let testLoginFailedRoute: string;

function MSALInstanceFactory(): IPublicClientApplication {
return new PublicClientApplication({
Expand All @@ -25,7 +26,8 @@ function MSALInstanceFactory(): IPublicClientApplication {
function MSALGuardConfigFactory(): MsalGuardConfiguration {
return {
//@ts-ignore
interactionType: testInteractionType
interactionType: testInteractionType,
loginFailedRoute: testLoginFailedRoute
}
}

Expand Down Expand Up @@ -54,6 +56,7 @@ function initializeMsal() {
describe('MsalGuard', () => {
beforeEach(() => {
testInteractionType = InteractionType.Popup;
testLoginFailedRoute = undefined;
initializeMsal();
});

Expand Down Expand Up @@ -110,7 +113,7 @@ describe('MsalGuard', () => {
done();
});

it("should return false after login with popup fails", (done) => {
it("should return false after login with popup fails and no loginFailedRoute set", (done) => {
spyOn(MsalService.prototype, "handleRedirectObservable").and.returnValue(
//@ts-ignore
of("test")
Expand All @@ -126,6 +129,29 @@ describe('MsalGuard', () => {
done();
});

it("should return loginFailedRoute after login with popup fails and loginFailedRoute set", (done) => {
testLoginFailedRoute = "failed";
initializeMsal();

spyOn(guard, "parseUrl").and.returnValue(
testLoginFailedRoute as unknown as UrlTree
)

spyOn(MsalService.prototype, "handleRedirectObservable").and.returnValue(
//@ts-ignore
of("test")
);

spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([]);

spyOn(MsalService.prototype, "loginPopup").and.throwError("login error");

const listener = jasmine.createSpy();
guard.canActivate(routeMock, routeStateMock).subscribe(listener);
expect(listener).toHaveBeenCalledWith("failed");
done();
});

it("should return false after logging in with redirect", (done) => {
testInteractionType = InteractionType.Redirect;
initializeMsal();
Expand Down
35 changes: 28 additions & 7 deletions lib/msal-angular/src/msal.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, CanLoad } from "@angular/router";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, CanLoad, UrlTree, Router } from "@angular/router";
import { MsalService } from "./msal.service";
import { Injectable, Inject } from "@angular/core";
import { Location } from "@angular/common";
Expand All @@ -15,12 +15,23 @@ import { Observable, of } from "rxjs";

@Injectable()
export class MsalGuard implements CanActivate, CanActivateChild, CanLoad {
private loginFailedRoute?: UrlTree;

constructor(
@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
private authService: MsalService,
private location: Location,
private router: Router
) { }

/**
* Parses url string to UrlTree
* @param url
*/
parseUrl(url: string): UrlTree {
return this.router.parseUrl(url);
}

/**
* Builds the absolute url for the destination page
* @param path Relative path of requested page
Expand Down Expand Up @@ -57,8 +68,7 @@ export class MsalGuard implements CanActivate, CanActivateChild, CanLoad {
this.authService.getLogger().verbose("Guard - login by popup successful, can activate, setting active account");
this.authService.instance.setActiveAccount(response.account);
return true;
}),
catchError(() => of(false))
})
);
}

Expand All @@ -71,7 +81,7 @@ export class MsalGuard implements CanActivate, CanActivateChild, CanLoad {
return of(false);
}

private activateHelper(state?: RouterStateSnapshot): Observable<boolean> {
private activateHelper(state?: RouterStateSnapshot): Observable<boolean|UrlTree> {
if (this.msalGuardConfig.interactionType !== InteractionType.Popup && this.msalGuardConfig.interactionType !== InteractionType.Redirect) {
throw new BrowserConfigurationAuthError("invalid_interaction_type", "Invalid interaction type provided to MSAL Guard. InteractionType.Popup or InteractionType.Redirect must be provided in the MsalGuardConfiguration");
}
Expand All @@ -87,6 +97,13 @@ export class MsalGuard implements CanActivate, CanActivateChild, CanLoad {
return of(false);
}

/**
* If a loginFailedRoute is set in the config, set this as the loginFailedRoute
*/
if (this.msalGuardConfig.loginFailedRoute) {
this.loginFailedRoute = this.parseUrl(this.msalGuardConfig.loginFailedRoute);
}

return this.authService.handleRedirectObservable()
.pipe(
concatMap(() => {
Expand All @@ -103,22 +120,26 @@ export class MsalGuard implements CanActivate, CanActivateChild, CanLoad {
}),
catchError(() => {
this.authService.getLogger().verbose("Guard - error while logging in, unable to activate");
if (this.loginFailedRoute) {
this.authService.getLogger().verbose("Guard - loginFailedRoute set, redirecting");
return of(this.loginFailedRoute);
}
return of(false);
})
);
}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean|UrlTree> {
this.authService.getLogger().verbose("Guard - canActivate");
return this.activateHelper(state);
}

canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean|UrlTree> {
this.authService.getLogger().verbose("Guard - canActivateChild");
return this.activateHelper(state);
}

canLoad(): Observable<boolean> {
canLoad(): Observable<boolean|UrlTree> {
this.authService.getLogger().verbose("Guard - canLoad");
return this.activateHelper();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ProfileComponent } from './profile/profile.component';
import { HomeComponent } from './home/home.component';
import { MsalGuard } from '@azure/msal-angular';
import { DetailComponent } from './detail/detail.component';
import { FailedComponent } from './failed/failed.component';

const routes: Routes = [
{
Expand Down Expand Up @@ -34,6 +35,10 @@ const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'login-failed',
component: FailedComponent
}
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
}

export function MSALGuardConfigFactory(): MsalGuardConfiguration {
return { interactionType: InteractionType.Redirect };
return {
interactionType: InteractionType.Redirect,
loginFailedRoute: '/login-failed'
};
}

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>
Login failed. Please try again.
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-orders',
templateUrl: './failed.component.html'
})
export class FailedComponent implements OnInit {

constructor() { }

ngOnInit(): void {
}

}

0 comments on commit 72b8e06

Please sign in to comment.