diff --git a/.github/workflows/msal-angular-e2e.yml b/.github/workflows/msal-angular-e2e.yml
index ef78829206..a3350f38fd 100644
--- a/.github/workflows/msal-angular-e2e.yml
+++ b/.github/workflows/msal-angular-e2e.yml
@@ -36,6 +36,7 @@ jobs:
matrix:
sample:
- 'angular15-sample-app'
+ - 'angular16-sample-app'
name: ${{ matrix.sample }}
diff --git a/lib/msal-angular/README.md b/lib/msal-angular/README.md
index 5a4b832ddc..863786941b 100644
--- a/lib/msal-angular/README.md
+++ b/lib/msal-angular/README.md
@@ -45,7 +45,7 @@ At a minimum, `@azure/msal-angular` will follow the [support schedule of the mai
| MSAL Angular version | MSAL support status | Supported Angular versions |
|----------------------|-------------------------|----------------------------|
-| MSAL Angular v3 | Active development | 15 |
+| MSAL Angular v3-alpha | Active development | 15, 16 |
| MSAL Angular v2 | In maintenance | 9, 10, 11, 12, 13, 14 |
| MSAL Angular v1 | In maintenance | 6, 7, 8, 9 |
| MSAL Angular v0 | Out of support | 4, 5 |
@@ -86,6 +86,7 @@ All documentation for MSAL Angular v1 can be found [here](https://github.com/Azu
### MSAL Angular v3 Samples
* [Angular v15](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-angular-v3-samples/angular15-sample-app)
+* [Angular v16](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-angular-v3-samples/angular16-sample-app)
### MSAL Angular v2 Samples
* [Angular v9](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-angular-v2-samples/angular9-v2-sample-app)
diff --git a/lib/msal-angular/docs/FAQ.md b/lib/msal-angular/docs/FAQ.md
index 20c261bdf4..12db8b2a35 100644
--- a/lib/msal-angular/docs/FAQ.md
+++ b/lib/msal-angular/docs/FAQ.md
@@ -42,7 +42,9 @@ Please see [here](https://github.com/AzureAD/microsoft-authentication-library-fo
### What versions of Angular are supported?
-Msal Angular currently supports Angular 9, 10, 11, 12, 13 and 14.
+Msal Angular v3 is in alpha and currently supports Angular 15 and 16.
+
+Msal Angular v2 supports Angular 9, 10, 11, 12, 13 and 14.
### Does `@azure/msal-angular` support Server Side Rendering?
diff --git a/samples/msal-angular-v3-samples/README.md b/samples/msal-angular-v3-samples/README.md
index 70d1cdf5a3..35c2ad9825 100644
--- a/samples/msal-angular-v3-samples/README.md
+++ b/samples/msal-angular-v3-samples/README.md
@@ -7,4 +7,9 @@
* `MsalRedirectComponent`: This sample uses the `MsalRedirectComponent` to handle redirects. See our doc on [redirects](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/redirects.md) for more information.
* `PathLocationStrategy`: This sample uses the `PathLocationStrategy` for routing. See [Angular docs](https://angular.io/guide/router#locationstrategy-and-browser-url-styles) for more details.
+* [Angular 16](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-angular-v3-samples/angular16-sample-app)
+ * Consenting to scopes: This sample consents to scopes upfront. See our [configuration doc](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-angular/docs/v2-docs/configuration.md) for more information.
+ * `MsalRedirectComponent`: This sample uses the `MsalRedirectComponent` to handle redirects. See our doc on [redirects](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/redirects.md) for more information.
+ * `PathLocationStrategy`: This sample uses the `PathLocationStrategy` for routing. See [Angular docs](https://angular.io/guide/router#locationstrategy-and-browser-url-styles) for more details.
+
Additional samples will continue to be added at a later date.
diff --git a/samples/msal-angular-v3-samples/angular15-sample-app/package.json b/samples/msal-angular-v3-samples/angular15-sample-app/package.json
index 1131caeac1..422f105f03 100644
--- a/samples/msal-angular-v3-samples/angular15-sample-app/package.json
+++ b/samples/msal-angular-v3-samples/angular15-sample-app/package.json
@@ -23,8 +23,8 @@
"@angular/platform-browser": "^15.1.0",
"@angular/platform-browser-dynamic": "^15.1.0",
"@angular/router": "^15.1.0",
- "@azure/msal-angular": "file:../../../lib/msal-angular/dist",
- "@azure/msal-browser": "file:../../../lib/msal-browser",
+ "@azure/msal-angular": "^3.0.0-alpha.2",
+ "@azure/msal-browser": "^3.0.0-alpha.2",
"rxjs": "~7.8.0",
"ts-node": "^10.9.1",
"tslib": "^2.3.0",
@@ -44,4 +44,4 @@
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.9.4"
}
-}
+}
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/.editorconfig b/samples/msal-angular-v3-samples/angular16-sample-app/.editorconfig
new file mode 100644
index 0000000000..59d9a3a3e7
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/.editorconfig
@@ -0,0 +1,16 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/.gitignore b/samples/msal-angular-v3-samples/angular16-sample-app/.gitignore
new file mode 100644
index 0000000000..3ae9c7c759
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/.gitignore
@@ -0,0 +1,44 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
+
+package-lock.json
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/extensions.json b/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/extensions.json
new file mode 100644
index 0000000000..77b374577d
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
+ "recommendations": ["angular.ng-template"]
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/launch.json b/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/launch.json
new file mode 100644
index 0000000000..740e35a0c0
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/launch.json
@@ -0,0 +1,20 @@
+{
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "ng serve",
+ "type": "pwa-chrome",
+ "request": "launch",
+ "preLaunchTask": "npm: start",
+ "url": "http://localhost:4200/"
+ },
+ {
+ "name": "ng test",
+ "type": "chrome",
+ "request": "launch",
+ "preLaunchTask": "npm: test",
+ "url": "http://localhost:9876/debug.html"
+ }
+ ]
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/tasks.json b/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/tasks.json
new file mode 100644
index 0000000000..a298b5bd87
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/.vscode/tasks.json
@@ -0,0 +1,42 @@
+{
+ // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "npm",
+ "script": "start",
+ "isBackground": true,
+ "problemMatcher": {
+ "owner": "typescript",
+ "pattern": "$tsc",
+ "background": {
+ "activeOnStart": true,
+ "beginsPattern": {
+ "regexp": "(.*?)"
+ },
+ "endsPattern": {
+ "regexp": "bundle generation complete"
+ }
+ }
+ }
+ },
+ {
+ "type": "npm",
+ "script": "test",
+ "isBackground": true,
+ "problemMatcher": {
+ "owner": "typescript",
+ "pattern": "$tsc",
+ "background": {
+ "activeOnStart": true,
+ "beginsPattern": {
+ "regexp": "(.*?)"
+ },
+ "endsPattern": {
+ "regexp": "bundle generation complete"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/README.md b/samples/msal-angular-v3-samples/angular16-sample-app/README.md
new file mode 100644
index 0000000000..7999ae7603
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/README.md
@@ -0,0 +1,23 @@
+# Angular 16 MSAL Angular v3 Sample
+
+This developer sample is used to demonstrate how to use `@azure/msal-angular`.
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.4 and then upgraded to version 16.0.0-rc.3.
+
+## How to run the sample
+
+### Pre-requisites
+- Ensure [all pre-requisites](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/README.md) have been completed to run msal-angular.
+
+### Configure the application
+- Open `./src/app/app.modules.ts` in an editor.
+- Replace client id with the Application (client) ID from the portal registration, or use the currently configured lab registration.
+ - Optionally, you may replace any of the other parameters, or you can remove them and use the default values.
+
+### Running the sample
+- In a command prompt, run `npm start`.
+- Navigate to [http://localhost:4200](http://localhost:4200)
+- In the web page, click on the "Login" button. The app will automatically reload if you change any of the source files.
+
+## Additional notes
+- The default interaction type for the sample is redirects. The sample can be configured to use redirects by changing the `interactionType` in `app.module.ts` to `InteractionType.Popup`.
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/angular.json b/samples/msal-angular-v3-samples/angular16-sample-app/angular.json
new file mode 100644
index 0000000000..ad9d1adf3e
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/angular.json
@@ -0,0 +1,121 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "angular16-sample-app": {
+ "projectType": "application",
+ "schematics": {},
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/angular16-sample-app",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": [
+ "zone.js"
+ ],
+ "tsConfig": "tsconfig.app.json",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "e2e": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.e2e.ts"
+ }
+ ]
+ },
+ "dev": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true,
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.dev.ts"
+ }
+ ]
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "browserTarget": "angular16-sample-app:build:production"
+ },
+ "e2e": {
+ "browserTarget": "angular16-sample-app:build:e2e"
+ },
+ "dev": {
+ "browserTarget": "angular16-sample-app:build:dev"
+ }
+ },
+ "defaultConfiguration": "dev"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "angular16-sample-app:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ],
+ "tsConfig": "tsconfig.spec.json",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/jest.config.js b/samples/msal-angular-v3-samples/angular16-sample-app/jest.config.js
new file mode 100644
index 0000000000..c467d67b08
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ displayName: "angular16-sample-app",
+ globals: {
+ __PORT__: 4216,
+ __STARTCMD__: "npm start -- --port 4216",
+ __TIMEOUT__: 90000
+ },
+ preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset.js"
+};
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/package.json b/samples/msal-angular-v3-samples/angular16-sample-app/package.json
new file mode 100644
index 0000000000..cf28b3b552
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "angular16-sample-app",
+ "version": "0.0.0",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "ng test",
+ "lint": "ng lint",
+ "e2e": "jest",
+ "install:local": "npm install ../../../lib/msal-angular/dist ../../../lib/msal-browser",
+ "install:published": "npm install @azure/msal-angular@latest @azure/msal-browser@latest"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "^16.0.4",
+ "@angular/common": "^16.0.4",
+ "@angular/compiler": "^16.0.4",
+ "@angular/core": "^16.0.4",
+ "@angular/forms": "^16.0.4",
+ "@angular/material": "^16.0.3",
+ "@angular/platform-browser": "^16.0.4",
+ "@angular/platform-browser-dynamic": "^16.0.4",
+ "@angular/router": "^16.0.4",
+ "@azure/msal-angular": "^3.0.0-alpha.2",
+ "@azure/msal-browser": "^3.0.0-alpha.2",
+ "rxjs": "~7.8.0",
+ "ts-node": "^10.9.1",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.13.0"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^16.0.4",
+ "@angular/cli": "~16.0.4",
+ "@angular/compiler-cli": "^16.0.4",
+ "@types/jasmine": "~4.3.0",
+ "jasmine-core": "~4.5.0",
+ "jest": "^29.5.0",
+ "karma": "~6.4.0",
+ "karma-chrome-launcher": "~3.1.0",
+ "karma-coverage": "~2.2.0",
+ "karma-jasmine": "~5.1.0",
+ "karma-jasmine-html-reporter": "~2.0.0",
+ "typescript": "~4.9.4"
+ }
+}
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app-routing.module.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app-routing.module.ts
new file mode 100644
index 0000000000..0cec40dbf7
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app-routing.module.ts
@@ -0,0 +1,32 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { MsalGuard } from '@azure/msal-angular';
+import { BrowserUtils } from '@azure/msal-browser';
+import { ProfileComponent } from './profile/profile.component';
+import { HomeComponent } from './home/home.component';
+import { FailedComponent } from './failed/failed.component';
+
+const routes: Routes = [
+ {
+ path: 'profile',
+ component: ProfileComponent,
+ canActivate: [MsalGuard]
+ },
+ {
+ path: '',
+ component: HomeComponent
+ },
+ {
+ path: 'login-failed',
+ component: FailedComponent
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes, {
+ // Don't perform initial navigation in iframes or popups
+ initialNavigation: !BrowserUtils.isInIframe() && !BrowserUtils.isInPopup() ? 'enabledNonBlocking' : 'disabled' // Set to enabledBlocking to use Angular Universal
+ })],
+ exports: [RouterModule]
+})
+export class AppRoutingModule { }
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.css b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.css
new file mode 100644
index 0000000000..63fe80dfda
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.css
@@ -0,0 +1,7 @@
+.toolbar-spacer {
+ flex: 1 1 auto;
+ }
+
+ a.title {
+ color: white;
+ }
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.html b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.html
new file mode 100644
index 0000000000..281f11c3f5
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.html
@@ -0,0 +1,24 @@
+
+ {{ title }}
+
+
+
+ Profile
+
+ Login
+
+ Login using Redirect
+ Login using Popup
+
+
+ Logout
+
+ Logout using Redirect
+ Logout using Popup
+
+
+
+
+
+
+
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.spec.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.spec.ts
new file mode 100644
index 0000000000..02bef36ec9
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.spec.ts
@@ -0,0 +1,35 @@
+import { TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { AppComponent } from './app.component';
+
+describe('AppComponent', () => {
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule
+ ],
+ declarations: [
+ AppComponent
+ ],
+ }).compileComponents();
+ });
+
+ it('should create the app', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.componentInstance;
+ expect(app).toBeTruthy();
+ });
+
+ it(`should have as title 'angular16-sample-app'`, () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.componentInstance;
+ expect(app.title).toEqual('angular16-sample-app');
+ });
+
+ it('should render title', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
+ const compiled = fixture.nativeElement as HTMLElement;
+ expect(compiled.querySelector('.content span')?.textContent).toContain('angular16-sample-app app is running!');
+ });
+});
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.ts
new file mode 100644
index 0000000000..5a84361ade
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.component.ts
@@ -0,0 +1,108 @@
+import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
+import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
+import { AuthenticationResult, InteractionStatus, PopupRequest, RedirectRequest, EventMessage, EventType } from '@azure/msal-browser';
+import { Subject } from 'rxjs';
+import { filter, takeUntil } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.css']
+})
+export class AppComponent implements OnInit, OnDestroy {
+ title = 'Angular 16 - MSAL Angular v3 Sample';
+ isIframe = false;
+ loginDisplay = false;
+ private readonly _destroying$ = new Subject();
+
+ constructor(
+ @Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
+ private authService: MsalService,
+ private msalBroadcastService: MsalBroadcastService
+ ) {
+
+ }
+
+ ngOnInit(): void {
+ this.isIframe = window !== window.parent && !window.opener; // Remove this line to use Angular Universal
+ this.setLoginDisplay();
+
+ this.authService.instance.enableAccountStorageEvents(); // Optional - This will enable ACCOUNT_ADDED and ACCOUNT_REMOVED events emitted when a user logs in or out of another tab or window
+ this.msalBroadcastService.msalSubject$
+ .pipe(
+ filter((msg: EventMessage) => msg.eventType === EventType.ACCOUNT_ADDED || msg.eventType === EventType.ACCOUNT_REMOVED),
+ )
+ .subscribe((result: EventMessage) => {
+ if (this.authService.instance.getAllAccounts().length === 0) {
+ window.location.pathname = "/";
+ } else {
+ this.setLoginDisplay();
+ }
+ });
+
+ this.msalBroadcastService.inProgress$
+ .pipe(
+ filter((status: InteractionStatus) => status === InteractionStatus.None),
+ takeUntil(this._destroying$)
+ )
+ .subscribe(() => {
+ this.setLoginDisplay();
+ this.checkAndSetActiveAccount();
+ })
+ }
+
+ setLoginDisplay() {
+ this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
+ }
+
+ checkAndSetActiveAccount(){
+ /**
+ * If no active account set but there are accounts signed in, sets first account to active account
+ * To use active account set here, subscribe to inProgress$ first in your component
+ * Note: Basic usage demonstrated. Your app may require more complicated account selection logic
+ */
+ let activeAccount = this.authService.instance.getActiveAccount();
+
+ if (!activeAccount && this.authService.instance.getAllAccounts().length > 0) {
+ let accounts = this.authService.instance.getAllAccounts();
+ this.authService.instance.setActiveAccount(accounts[0]);
+ }
+ }
+
+ loginRedirect() {
+ if (this.msalGuardConfig.authRequest){
+ this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
+ } else {
+ this.authService.loginRedirect();
+ }
+ }
+
+ loginPopup() {
+ if (this.msalGuardConfig.authRequest){
+ this.authService.loginPopup({...this.msalGuardConfig.authRequest} as PopupRequest)
+ .subscribe((response: AuthenticationResult) => {
+ this.authService.instance.setActiveAccount(response.account);
+ });
+ } else {
+ this.authService.loginPopup()
+ .subscribe((response: AuthenticationResult) => {
+ this.authService.instance.setActiveAccount(response.account);
+ });
+ }
+ }
+
+ logout(popup?: boolean) {
+ if (popup) {
+ this.authService.logoutPopup({
+ mainWindowRedirectUri: "/"
+ });
+ } else {
+ this.authService.logoutRedirect();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this._destroying$.next(undefined);
+ this._destroying$.complete();
+ }
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.module.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.module.ts
new file mode 100644
index 0000000000..608b132e48
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/app.module.ts
@@ -0,0 +1,112 @@
+import { BrowserModule } from '@angular/platform-browser';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { NgModule } from '@angular/core';
+
+import { MatButtonModule } from '@angular/material/button';
+import { MatToolbarModule } from '@angular/material/toolbar';
+import { MatListModule } from '@angular/material/list';
+import { MatMenuModule } from '@angular/material/menu';
+
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { HomeComponent } from './home/home.component';
+import { ProfileComponent } from './profile/profile.component';
+
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
+import { IPublicClientApplication, PublicClientApplication, InteractionType, BrowserCacheLocation, LogLevel } from '@azure/msal-browser';
+import { MsalGuard, MsalInterceptor, MsalBroadcastService, MsalInterceptorConfiguration, MsalModule, MsalService, MSAL_GUARD_CONFIG, MSAL_INSTANCE, MSAL_INTERCEPTOR_CONFIG, MsalGuardConfiguration, MsalRedirectComponent } from '@azure/msal-angular';
+import { FailedComponent } from './failed/failed.component';
+import { environment } from 'src/environments/environment';
+
+const isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigator.userAgent.indexOf("Trident/") > -1; // Remove this line to use Angular Universal
+
+export function loggerCallback(logLevel: LogLevel, message: string) {
+ console.log(message);
+}
+
+export function MSALInstanceFactory(): IPublicClientApplication {
+ return new PublicClientApplication({
+ auth: {
+ clientId: environment.msalConfig.auth.clientId,
+ authority: environment.msalConfig.auth.authority,
+ redirectUri: '/',
+ postLogoutRedirectUri: '/'
+ },
+ cache: {
+ cacheLocation: BrowserCacheLocation.LocalStorage,
+ storeAuthStateInCookie: isIE, // set to true for IE 11. Remove this line to use Angular Universal
+ },
+ system: {
+ allowNativeBroker: false, // Disables WAM Broker
+ loggerOptions: {
+ loggerCallback,
+ logLevel: LogLevel.Info,
+ piiLoggingEnabled: false
+ }
+ }
+ });
+}
+
+export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
+ const protectedResourceMap = new Map>();
+ protectedResourceMap.set(environment.apiConfig.uri, environment.apiConfig.scopes);
+
+ return {
+ interactionType: InteractionType.Redirect,
+ protectedResourceMap
+ };
+}
+
+export function MSALGuardConfigFactory(): MsalGuardConfiguration {
+ return {
+ interactionType: InteractionType.Redirect,
+ authRequest: {
+ scopes: [...environment.apiConfig.scopes]
+ },
+ loginFailedRoute: '/login-failed'
+ };
+}
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ HomeComponent,
+ ProfileComponent,
+ FailedComponent
+ ],
+ imports: [
+ BrowserModule,
+ NoopAnimationsModule, // Animations cause delay which interfere with E2E tests
+ AppRoutingModule,
+ MatButtonModule,
+ MatToolbarModule,
+ MatListModule,
+ MatMenuModule,
+ HttpClientModule,
+ MsalModule
+ ],
+ providers: [
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: MsalInterceptor,
+ multi: true
+ },
+ {
+ provide: MSAL_INSTANCE,
+ useFactory: MSALInstanceFactory
+ },
+ {
+ provide: MSAL_GUARD_CONFIG,
+ useFactory: MSALGuardConfigFactory
+ },
+ {
+ provide: MSAL_INTERCEPTOR_CONFIG,
+ useFactory: MSALInterceptorConfigFactory
+ },
+ MsalService,
+ MsalGuard,
+ MsalBroadcastService
+ ],
+ bootstrap: [AppComponent, MsalRedirectComponent]
+})
+export class AppModule { }
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.css b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.html b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.html
new file mode 100644
index 0000000000..61effd3a25
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.html
@@ -0,0 +1,3 @@
+
+ Login failed. Please try again.
+
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.ts
new file mode 100644
index 0000000000..ae5f7a6ce6
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/failed/failed.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-orders',
+ templateUrl: './failed.component.html',
+ styleUrls: ['./failed.component.css']
+})
+export class FailedComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.css b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.html b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.html
new file mode 100644
index 0000000000..8d6562c020
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.html
@@ -0,0 +1,11 @@
+
+
Welcome to the MSAL.js v3 Angular Quickstart!
+
This sample demonstrates how to configure MSAL Angular to login, logout, protect a route, and acquire an access
+ token for a protected resource such as the Microsoft Graph.
+
Please sign-in to see your profile information.
+
+
+
+
Login successful!
+
Request your profile information by clicking Profile above.
+
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.ts
new file mode 100644
index 0000000000..2a7a33b755
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/home/home.component.ts
@@ -0,0 +1,41 @@
+import { Component, OnInit } from '@angular/core';
+import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
+import { AuthenticationResult, EventMessage, EventType, InteractionStatus } from '@azure/msal-browser';
+import { filter } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-home',
+ templateUrl: './home.component.html',
+ styleUrls: ['./home.component.css']
+})
+export class HomeComponent implements OnInit {
+ loginDisplay = false;
+
+ constructor(private authService: MsalService, private msalBroadcastService: MsalBroadcastService) { }
+
+ ngOnInit(): void {
+ this.msalBroadcastService.msalSubject$
+ .pipe(
+ filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
+ )
+ .subscribe((result: EventMessage) => {
+ console.log(result);
+ const payload = result.payload as AuthenticationResult;
+ this.authService.instance.setActiveAccount(payload.account);
+ });
+
+ this.msalBroadcastService.inProgress$
+ .pipe(
+ filter((status: InteractionStatus) => status === InteractionStatus.None)
+ )
+ .subscribe(() => {
+ this.setLoginDisplay();
+ })
+
+ }
+
+ setLoginDisplay() {
+ this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
+ }
+
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.css b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.html b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.html
new file mode 100644
index 0000000000..57522236e9
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.html
@@ -0,0 +1,6 @@
+
+
First Name: {{profile?.givenName}}
+
Last Name: {{profile?.surname}}
+
Email: {{profile?.userPrincipalName}}
+
Id: {{profile?.id}}
+
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.ts
new file mode 100644
index 0000000000..4bae93d2a4
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/app/profile/profile.component.ts
@@ -0,0 +1,34 @@
+import { Component, OnInit } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { environment } from 'src/environments/environment';
+
+type ProfileType = {
+ givenName?: string,
+ surname?: string,
+ userPrincipalName?: string,
+ id?: string
+};
+
+@Component({
+ selector: 'app-profile',
+ templateUrl: './profile.component.html',
+ styleUrls: ['./profile.component.css']
+})
+export class ProfileComponent implements OnInit {
+ profile: ProfileType | undefined;
+
+ constructor(
+ private http: HttpClient
+ ) { }
+
+ ngOnInit() {
+ this.getProfile(environment.apiConfig.uri);
+ }
+
+ getProfile(url: string) {
+ this.http.get(url)
+ .subscribe(profile => {
+ this.profile = profile;
+ });
+ }
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/assets/.gitkeep b/samples/msal-angular-v3-samples/angular16-sample-app/src/assets/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.dev.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.dev.ts
new file mode 100644
index 0000000000..c4067b2f9b
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.dev.ts
@@ -0,0 +1,13 @@
+export const environment = {
+ production: false,
+ msalConfig: {
+ auth: {
+ clientId: 'b5c2e510-4a17-4feb-b219-e55aa5b74144',
+ authority: 'https://login.microsoftonline.com/common'
+ }
+ },
+ apiConfig: {
+ scopes: ['user.read'],
+ uri: 'https://graph.microsoft.com/v1.0/me'
+ }
+};
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.e2e.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.e2e.ts
new file mode 100644
index 0000000000..23eee933e4
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.e2e.ts
@@ -0,0 +1,13 @@
+export const environment = {
+ production: false,
+ msalConfig: {
+ auth: {
+ clientId: '3fba556e-5d4a-48e3-8e1a-fd57c12cb82e',
+ authority: 'https://login.windows-ppe.net/common'
+ }
+ },
+ apiConfig: {
+ scopes: ['user.read'],
+ uri: 'https://graph.microsoft-ppe.com/v1.0/me'
+ }
+};
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.prod.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.prod.ts
new file mode 100644
index 0000000000..94e20771e3
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.prod.ts
@@ -0,0 +1,13 @@
+export const environment = {
+ production: true,
+ msalConfig: {
+ auth: {
+ clientId: 'ENTER_CLIENT_ID',
+ authority: 'ENTER_AUTHORITY'
+ }
+ },
+ apiConfig: {
+ scopes: ['ENTER_SCOPE'],
+ uri: 'ENTER_URI'
+ }
+};
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.ts
new file mode 100644
index 0000000000..9cb7b76334
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/environments/environment.ts
@@ -0,0 +1,26 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false,
+ msalConfig: {
+ auth: {
+ clientId: 'ENTER_CLIENT_ID',
+ authority: 'ENTER_AUTHORITY'
+ }
+ },
+ apiConfig: {
+ scopes: ['ENTER_SCOPE'],
+ uri: 'ENTER_URI'
+ }
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/favicon.ico b/samples/msal-angular-v3-samples/angular16-sample-app/src/favicon.ico
new file mode 100644
index 0000000000..997406ad22
Binary files /dev/null and b/samples/msal-angular-v3-samples/angular16-sample-app/src/favicon.ico differ
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/index.html b/samples/msal-angular-v3-samples/angular16-sample-app/src/index.html
new file mode 100644
index 0000000000..f094e9cc3f
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Angular 16 Sample App
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/main.ts b/samples/msal-angular-v3-samples/angular16-sample-app/src/main.ts
new file mode 100644
index 0000000000..c7b673cf44
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.error(err));
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/src/styles.css b/samples/msal-angular-v3-samples/angular16-sample-app/src/styles.css
new file mode 100644
index 0000000000..ca13b51e3a
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/src/styles.css
@@ -0,0 +1,16 @@
+/* You can add global styles to this file, and also import other style files */
+@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
+
+html,
+body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ font-family: Roboto, "Helvetica Neue", sans-serif;
+}
+
+.container {
+ margin: 1%;
+}
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/test/home.spec.ts b/samples/msal-angular-v3-samples/angular16-sample-app/test/home.spec.ts
new file mode 100644
index 0000000000..220f939213
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/test/home.spec.ts
@@ -0,0 +1,158 @@
+import * as puppeteer from "puppeteer";
+import {Screenshot, setupCredentials, enterCredentials, RETRY_TIMES} from "../../../e2eTestUtils/TestUtils";
+import { LabClient } from "../../../e2eTestUtils/LabClient";
+import { LabApiQueryParams } from "../../../e2eTestUtils/LabApiQueryParams";
+import { AzureEnvironments, AppTypes } from "../../../e2eTestUtils/Constants";
+import { BrowserCacheUtils } from "../../../e2eTestUtils/BrowserCacheTestUtils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/home-tests`;
+
+async function verifyTokenStore(BrowserCache: BrowserCacheUtils, scopes: string[]): Promise {
+ const tokenStore = await BrowserCache.getTokens();
+ expect(tokenStore.idTokens.length).toBe(1);
+ expect(tokenStore.accessTokens.length).toBe(1);
+ expect(tokenStore.refreshTokens.length).toBe(1);
+ expect(await BrowserCache.getAccountFromCache(tokenStore.idTokens[0])).not.toBeNull();
+ expect(await BrowserCache.accessTokenForScopesExists(tokenStore.accessTokens, scopes)).toBeTruthy;
+ const storage = await BrowserCache.getWindowStorage();
+ expect(Object.keys(storage).length).toBe(8);
+}
+
+describe('/ (Home Page)', () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(labApiParams);
+
+ [username, accountPwd] = await setupCredentials(envResponse[0], labClient);
+ });
+
+ beforeEach(async () => {
+ context = await browser.createIncognitoBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ await page.goto(`http://localhost:${port}`);
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("Home page - children are rendered after logging in with loginRedirect", async (): Promise => {
+ const testName = "redirectBaseCase";
+ const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`);
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector("xpath=//button[contains(., 'Login')]");
+ if (signInButton) {
+ await signInButton.click();
+ }
+
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginRedirectButton = await page.waitForSelector("xpath=//button[contains(., 'Login using Redirect')]");
+ if (loginRedirectButton) {
+ await loginRedirectButton.click();
+ }
+
+ await enterCredentials(page, screenshot, username, accountPwd);
+
+ // Verify UI now displays logged in content
+ await page.waitForXPath("//p[contains(., 'Login successful!')]");
+ const logoutButton = await page.waitForSelector("xpath=//button[contains(., 'Logout')]");
+ if (logoutButton) {
+ await logoutButton.click();
+ }
+ await page.waitForXPath("//button[contains(., 'Logout using')]");
+ const logoutButtons = await page.$x("//button[contains(., 'Logout using')]");
+ expect(logoutButtons.length).toBe(2);
+ if (logoutButton) {
+ await logoutButton.click();
+ }
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+
+ // Navigate to profile page
+ const profileButton = await page.waitForSelector("xpath=//span[contains(., 'Profile')]");
+ if (profileButton) {
+ await profileButton.click();
+ }
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+
+ // Verify displays profile page without activating MsalGuard
+ await page.waitForXPath("//strong[contains(., 'First Name: ')]");
+ });
+
+ it("Home page - children are rendered after logging in with loginPopup", async (): Promise => {
+ const testName = "popupBaseCase";
+ const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`);
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login
+ const signInButton = await page.waitForSelector("xpath=//button[contains(., 'Login')]");
+ await signInButton?.click();
+ await screenshot.takeScreenshot(page, "Login button clicked");
+ const loginPopupButton = await page.waitForSelector("xpath=//button[contains(., 'Login using Popup')]");
+ const newPopupWindowPromise = new Promise(resolve => page.once("popup", resolve));
+ if (loginPopupButton) {
+ await loginPopupButton.click();
+ }
+ const popupPage = await newPopupWindowPromise;
+ const popupWindowClosed = new Promise(resolve => popupPage.once("close", resolve));
+
+ await enterCredentials(popupPage, screenshot, username, accountPwd);
+ await popupWindowClosed;
+
+ await page.waitForXPath("//p[contains(., 'Login successful!')]", {timeout: 3000});
+ await screenshot.takeScreenshot(page, "Popup closed");
+
+ // Verify UI now displays logged in content
+ await page.waitForXPath("//p[contains(., 'Login successful!')]");
+ const logoutButton = await page.waitForSelector("xpath=//button[contains(., 'Logout')]");
+ if (logoutButton) {
+ await logoutButton.click();
+ }
+ const logoutButtons = await page.$x("//button[contains(., 'Logout using')]");
+ expect(logoutButtons.length).toBe(2);
+ if (logoutButton) {
+ await logoutButton.click();
+ }
+ await screenshot.takeScreenshot(page, "App signed in");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+
+ // Navigate to profile page
+ const profileButton = await page.waitForSelector("xpath=//span[contains(., 'Profile')]");
+ if (profileButton) {
+ await profileButton.click();
+ }
+ await screenshot.takeScreenshot(page, "Profile page loaded");
+
+ // Verify displays profile page without activating MsalGuard
+ await page.waitForXPath("//strong[contains(., 'First Name: ')]");
+ });
+ }
+);
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/test/profile.spec.ts b/samples/msal-angular-v3-samples/angular16-sample-app/test/profile.spec.ts
new file mode 100644
index 0000000000..8bfa60c370
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/test/profile.spec.ts
@@ -0,0 +1,107 @@
+import * as puppeteer from "puppeteer";
+import {Screenshot, setupCredentials, enterCredentials, RETRY_TIMES} from "../../../e2eTestUtils/TestUtils";
+import { LabClient } from "../../../e2eTestUtils/LabClient";
+import { LabApiQueryParams } from "../../../e2eTestUtils/LabApiQueryParams";
+import { AzureEnvironments, AppTypes } from "../../../e2eTestUtils/Constants";
+import { BrowserCacheUtils } from "../../../e2eTestUtils/BrowserCacheTestUtils";
+
+const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/profile-tests`;
+
+async function verifyTokenStore(BrowserCache: BrowserCacheUtils, scopes: string[]): Promise {
+ const tokenStore = await BrowserCache.getTokens();
+ expect(tokenStore.idTokens.length).toBe(1);
+ expect(tokenStore.accessTokens.length).toBe(1);
+ expect(tokenStore.refreshTokens.length).toBe(1);
+ expect(await BrowserCache.getAccountFromCache(tokenStore.idTokens[0])).not.toBeNull();
+ expect(await BrowserCache.accessTokenForScopesExists(tokenStore.accessTokens, scopes)).toBeTruthy;
+ const storage = await BrowserCache.getWindowStorage();
+ expect(Object.keys(storage).length).toBe(9);
+}
+
+describe('/ (Profile Page)', () => {
+ jest.retryTimes(RETRY_TIMES);
+ let browser: puppeteer.Browser;
+ let context: puppeteer.BrowserContext;
+ let page: puppeteer.Page;
+ let port: number;
+ let username: string;
+ let accountPwd: string;
+ let BrowserCache: BrowserCacheUtils;
+
+ beforeAll(async () => {
+ // @ts-ignore
+ browser = await global.__BROWSER__;
+ // @ts-ignore
+ port = global.__PORT__;
+
+ const labApiParams: LabApiQueryParams = {
+ azureEnvironment: AzureEnvironments.CLOUD,
+ appType: AppTypes.CLOUD
+ };
+
+ const labClient = new LabClient();
+ const envResponse = await labClient.getVarsByCloudEnvironment(labApiParams);
+
+ [username, accountPwd] = await setupCredentials(envResponse[0], labClient);
+ });
+
+ beforeEach(async () => {
+ context = await browser.createIncognitoBrowserContext();
+ page = await context.newPage();
+ page.setDefaultTimeout(5000);
+ BrowserCache = new BrowserCacheUtils(page, "localStorage");
+ });
+
+ afterEach(async () => {
+ await page.close();
+ await context.close();
+ });
+
+ it("Profile page - children are rendered after profile button clicked and logging in with loginRedirect", async (): Promise => {
+ await page.goto(`http://localhost:${port}`);
+
+ const testName = "profileButtonRedirectCase";
+ const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`);
+ await screenshot.takeScreenshot(page, "Page loaded");
+
+ // Initiate Login via MsalGuard by clicking Profile
+ const profileButton = await page.waitForSelector("xpath=//span[contains(., 'Profile')]");
+ if (profileButton) {
+ await profileButton.click();
+ }
+
+ await enterCredentials(page, screenshot, username, accountPwd);
+
+ // Verify UI now displays logged in content
+ await page.waitForXPath("//button[contains(., 'Logout')]");
+ await screenshot.takeScreenshot(page, "Profile page signed in");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+
+ // Verify displays profile page without activating MsalGuard
+ await page.waitForXPath("//strong[contains(., 'First Name: ')]");
+ });
+
+ it("Profile page - children are rendered after initial navigation to profile before login ", async () => {
+ // Initiate login via MsalGuard by navigating directly to profile route
+ await page.goto(`http://localhost:${port}/profile`);
+
+ const testName = "profileNavigationRedirectCase";
+ const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`);
+ await screenshot.takeScreenshot(page, "No home page load");
+
+ await enterCredentials(page, screenshot, username, accountPwd);
+
+ // Verify UI now displays logged in content
+ await page.waitForXPath("//button[contains(., 'Logout')]");
+ await screenshot.takeScreenshot(page, "Profile page signed in directly");
+
+ // Verify tokens are in cache
+ await verifyTokenStore(BrowserCache, ["User.Read"]);
+
+ // Verify displays profile page without activating MsalGuard
+ await page.waitForXPath("//strong[contains(., 'First Name: ')]");
+ });
+ }
+);
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.app.json b/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.app.json
new file mode 100644
index 0000000000..374cc9d294
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.app.json
@@ -0,0 +1,14 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.json b/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.json
new file mode 100644
index 0000000000..8dcf910477
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.json
@@ -0,0 +1,39 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "useDefineForClassFields": false,
+ "esModuleInterop": true,
+ "lib": [
+ "ES2022",
+ "dom"
+ ],
+ "paths": {
+ "@angular/*": [
+ "./node_modules/@angular/*"
+ ]
+ }
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
\ No newline at end of file
diff --git a/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.spec.json b/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.spec.json
new file mode 100644
index 0000000000..be7e9da76f
--- /dev/null
+++ b/samples/msal-angular-v3-samples/angular16-sample-app/tsconfig.spec.json
@@ -0,0 +1,14 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/samples/msal-angular-v3-samples/package.json b/samples/msal-angular-v3-samples/package.json
index 9623e8776f..539cd6c1dc 100644
--- a/samples/msal-angular-v3-samples/package.json
+++ b/samples/msal-angular-v3-samples/package.json
@@ -7,6 +7,6 @@
"preinstall": "cd .. && npm install"
},
"devDependencies": {
- "jest": "^27.0.4"
+ "jest": "^29.5.0"
}
}