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 + + + + + + + + + + + + + +
+
+ + +
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" } }