├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── codeql.yml │ ├── docs.yml │ └── issues.yml ├── .gitignore ├── 1-Authentication ├── 1-sign-in │ ├── App │ │ ├── app.js │ │ ├── authConfig.js │ │ ├── controllers │ │ │ └── mainController.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ └── style.css │ │ ├── sample.test.js │ │ ├── server.js │ │ └── views │ │ │ ├── home.ejs │ │ │ ├── id.ejs │ │ │ └── includes │ │ │ ├── footer.ejs │ │ │ └── navbar.ejs │ ├── AppCreationScripts │ │ ├── AppCreationScripts.md │ │ ├── Cleanup-WithCertificates.ps1 │ │ ├── Cleanup.ps1 │ │ ├── Configure-WithCertificates.ps1 │ │ ├── Configure.ps1 │ │ └── sample.json │ ├── README-use-certificate.md │ ├── README.md │ └── ReadmeFiles │ │ ├── screenshot.png │ │ └── topology.png └── 2-sign-in-b2c │ ├── App │ ├── app.js │ ├── authConfig.js │ ├── controllers │ │ └── mainController.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── style.css │ ├── sample.test.js │ ├── server.js │ └── views │ │ ├── home.ejs │ │ ├── id.ejs │ │ └── includes │ │ ├── footer.ejs │ │ └── navbar.ejs │ ├── AppCreationScripts │ ├── AppCreationScripts.md │ ├── Cleanup-WithCertificates.ps1 │ ├── Cleanup.ps1 │ ├── Configure-WithCertificates.ps1 │ ├── Configure.ps1 │ └── sample.json │ ├── README-use-certificate.md │ ├── README.md │ └── ReadmeFiles │ ├── screenshot.png │ └── topology.png ├── 2-Authorization └── 1-call-graph │ ├── App │ ├── app.js │ ├── authConfig.js │ ├── controllers │ │ └── mainController.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── style.css │ ├── routes │ │ └── mainRoutes.js │ ├── sample.test.js │ ├── server.js │ ├── utils │ │ ├── fetchManager.js │ │ └── graphManager.js │ └── views │ │ ├── home.ejs │ │ ├── id.ejs │ │ ├── includes │ │ ├── footer.ejs │ │ └── navbar.ejs │ │ ├── profile.ejs │ │ └── tenant.ejs │ ├── AppCreationScripts │ ├── AppCreationScripts.md │ ├── Cleanup-WithCertificates.ps1 │ ├── Cleanup.ps1 │ ├── Configure-WithCertificates.ps1 │ ├── Configure.ps1 │ └── sample.json │ ├── README-use-certificate.md │ ├── README.md │ └── ReadmeFiles │ ├── screenshot.png │ └── topology.png ├── 3-Deployment ├── App │ ├── .env.example │ ├── app.js │ ├── authConfig.js │ ├── controllers │ │ └── mainController.js │ ├── msal-node-wrapper-1.0.0-beta.tgz │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── style.css │ ├── routes │ │ └── mainRoutes.js │ ├── sample.test.js │ ├── server.js │ ├── utils │ │ ├── fetchManager.js │ │ ├── graphManager.js │ │ └── keyVaultManager.js │ └── views │ │ ├── home.ejs │ │ ├── id.ejs │ │ ├── includes │ │ ├── footer.ejs │ │ └── navbar.ejs │ │ ├── profile.ejs │ │ └── tenant.ejs ├── README.md └── ReadmeFiles │ ├── disable_easy_auth.png │ ├── screenshot.png │ ├── step1.png │ ├── step2.png │ ├── step3.png │ └── topology.png ├── 4-AccessControl ├── 1-app-roles │ ├── App │ │ ├── app.js │ │ ├── authConfig.js │ │ ├── controllers │ │ │ ├── dashboardController.js │ │ │ ├── mainController.js │ │ │ └── todolistController.js │ │ ├── data │ │ │ └── db.json │ │ ├── model │ │ │ └── todo.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ └── style.css │ │ ├── routes │ │ │ ├── dashboardRoutes.js │ │ │ ├── mainRoutes.js │ │ │ └── todolistRoutes.js │ │ ├── sample.test.js │ │ ├── server.js │ │ └── views │ │ │ ├── dashboard.ejs │ │ │ ├── home.ejs │ │ │ ├── id.ejs │ │ │ ├── includes │ │ │ ├── footer.ejs │ │ │ └── navbar.ejs │ │ │ └── todolist.ejs │ ├── AppCreationScripts │ │ ├── AppCreationScripts.md │ │ ├── Cleanup-WithCertificates.ps1 │ │ ├── Cleanup.ps1 │ │ ├── CleanupUsersAndAssignRoles.ps1 │ │ ├── Configure-WithCertificates.ps1 │ │ ├── Configure.ps1 │ │ ├── CreateUsersAndAssignRoles.ps1 │ │ └── sample.json │ ├── README-use-certificate.md │ ├── README.md │ └── ReadmeFiles │ │ ├── screenshot.png │ │ └── topology.png └── 2-security-groups │ ├── App │ ├── app.js │ ├── authConfig.js │ ├── controllers │ │ ├── dashboardController.js │ │ ├── mainController.js │ │ └── todolistController.js │ ├── data │ │ └── db.json │ ├── model │ │ └── todo.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── style.css │ ├── routes │ │ ├── dashboardRoutes.js │ │ ├── mainRoutes.js │ │ └── todolistRoutes.js │ ├── sample.test.js │ ├── server.js │ └── views │ │ ├── dashboard.ejs │ │ ├── home.ejs │ │ ├── id.ejs │ │ ├── includes │ │ ├── footer.ejs │ │ └── navbar.ejs │ │ └── todolist.ejs │ ├── AppCreationScripts │ ├── AppCreationScripts.md │ ├── BulkCreateGroups.ps1 │ ├── BulkRemoveGroups.ps1 │ ├── Cleanup-WithCertificates.ps1 │ ├── Cleanup.ps1 │ ├── Configure-WithCertificates.ps1 │ ├── Configure.ps1 │ └── sample.json │ ├── README-use-certificate.md │ ├── README.md │ └── ReadmeFiles │ ├── screenshot.png │ └── topology.png ├── 5-AdvancedScenarios └── 1-call-graph-bff │ ├── App │ ├── app.js │ ├── auth │ │ └── AuthProvider.js │ ├── authConfig.js │ ├── client │ │ ├── .gitignore │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.svg │ │ │ ├── index.html │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ └── src │ │ │ ├── App.jsx │ │ │ ├── components │ │ │ ├── DataDisplay.jsx │ │ │ ├── NavigationBar.jsx │ │ │ └── PageLayout.jsx │ │ │ ├── context │ │ │ └── AuthContext.js │ │ │ ├── index.js │ │ │ ├── pages │ │ │ ├── Home.jsx │ │ │ └── Profile.jsx │ │ │ ├── styles │ │ │ ├── App.css │ │ │ └── index.css │ │ │ └── utils │ │ │ └── claimUtils.js │ ├── controllers │ │ └── authController.js │ ├── package-lock.json │ ├── package.json │ ├── routes │ │ └── mainRoutes.js │ └── utils │ │ ├── claimUtils.js │ │ └── graphClient.js │ ├── AppCreationScripts │ ├── AppCreationScripts.md │ ├── Cleanup-WithCertificates.ps1 │ ├── Cleanup.ps1 │ ├── Configure-WithCertificates.ps1 │ ├── Configure.ps1 │ └── sample.json │ ├── README-use-certificate.md │ ├── README.md │ └── ReadmeFiles │ ├── screenshot.png │ └── sequence.png ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Common └── msal-node-wrapper │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── config │ │ ├── ConfigurationHelper.ts │ │ └── ConfigurationTypes.ts │ ├── error │ │ ├── AccessDeniedError.ts │ │ └── InteractionRequiredError.ts │ ├── index.ts │ ├── middleware │ │ ├── MiddlewareOptions.ts │ │ ├── authenticateMiddleware.ts │ │ ├── context │ │ │ └── AuthContext.ts │ │ ├── errorMiddleware.ts │ │ ├── guardMiddleware.ts │ │ └── handlers │ │ │ ├── acquireTokenHandler.ts │ │ │ ├── loginHandler.ts │ │ │ ├── logoutHandler.ts │ │ │ └── redirectHandler.ts │ ├── network │ │ └── FetchManager.ts │ ├── packageMetadata.ts │ ├── provider │ │ ├── BaseAuthProvider.ts │ │ └── WebAppAuthProvider.ts │ └── utils │ │ ├── Constants.ts │ │ └── UrlUtils.ts │ ├── test │ ├── TestConstants.ts │ ├── config │ │ └── ConfigurationHelper.spec.ts │ └── utils │ │ └── UrlUtils.spec.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── typedoc.json ├── LICENSE.md └── README.md /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Issue 4 | 5 | > Please provide us with the following information: 6 | 7 | ## This issue is for the sample 8 | 9 | 10 | 11 | ```console 12 | - [ ] 1-1) Sign-in with Microsoft Entra ID 13 | - [ ] 1-2) Sign-in with Azure Active Directory B2C 14 | - [ ] 2-1) Acquire a Token and call Microsoft Graph 15 | - [ ] 3) Deploy to Azure Storage and App Service 16 | - [ ] 4-1) Use App Roles for Role-based Access Control 17 | - [ ] 4-2) Use Security Groups for Role-based Access Control 18 | ``` 19 | 20 | ## This issue is for a 21 | 22 | 23 | 24 | ```console 25 | - [ ] bug report -> please search issues before submitting 26 | - [ ] question 27 | - [ ] feature request 28 | - [ ] documentation issue or request 29 | ``` 30 | 31 | ### Minimal steps to reproduce 32 | 33 | > 34 | 35 | ### Any log messages given by the failure 36 | 37 | > 38 | 39 | ### Expected/desired behavior 40 | 41 | > 42 | 43 | ### Library version 44 | 45 | > 46 | 47 | ### Browser and version 48 | 49 | > Chrome, Edge, Firefox, Safari? 50 | 51 | ### Mention any other details that might be useful 52 | 53 | > 54 | 55 | Thanks! We'll be in touch soon. 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: untriaged 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Which version of Microsoft.Identity.Client are you using?** 11 | 12 | Note that to get help, you need to run the latest version. 13 | 14 | **Repro** 15 | 16 | ```js 17 | // code 18 | ``` 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen (or code). 22 | 23 | **Actual behavior** 24 | A clear and concise description of what happens, e.g. an exception is thrown, UI freezes. 25 | 26 | **Possible solution** 27 | 28 | 29 | **Additional context / logs / screenshots / links to code** 30 | 31 | Add any other context about the problem here, such as logs and screenshots or links to code. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | ## Purpose 4 | 5 | 6 | 7 | * ... 8 | 9 | ## Does this introduce a breaking change 10 | 11 | 12 | 13 | ```console 14 | [ ] Yes 15 | [ ] No 16 | ``` 17 | 18 | ## Pull request type 19 | 20 | What kind of change does this Pull Request introduce? 21 | 22 | 23 | 24 | ```console 25 | [ ] Bugfix 26 | [ ] Feature 27 | [ ] Code style update (formatting, local variables) 28 | [ ] Documentation content changes 29 | [ ] Other... Please describe: 30 | ``` 31 | 32 | ## How to test 33 | 34 | * Get the code 35 | 36 | ```console 37 | git clone [repo-address] 38 | cd [repo-name] 39 | git checkout [branch-name] 40 | npm install 41 | ``` 42 | 43 | ## What to check 44 | 45 | ex: verify that the following are valid: 46 | 47 | * ... 48 | 49 | ## Other Information 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build & Test 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [16.x, 18.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - run: | 32 | cd Common/msal-node-wrapper 33 | npm ci 34 | npm audit --production 35 | npm run build --if-present 36 | npm run test 37 | 38 | - run: | 39 | cd 1-Authentication/1-sign-in/App 40 | npm install 41 | npm audit --production 42 | npm run test 43 | 44 | - run: | 45 | cd 1-Authentication/2-sign-in-b2c/App 46 | npm install 47 | npm audit --production 48 | npm run test 49 | 50 | - run: | 51 | cd 2-Authorization/1-call-graph/App 52 | npm install 53 | npm audit --production 54 | npm run test 55 | 56 | - run: | 57 | cd 4-AccessControl/1-app-roles/App 58 | npm install 59 | npm audit --production 60 | npm run test 61 | 62 | - run: | 63 | cd 4-AccessControl/2-security-groups/App 64 | npm install 65 | npm audit --production 66 | npm run test -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - Common/msal-node-wrapper/** 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - Common/msal-node-wrapper/** 14 | 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | submodules: true # Fetch Hugo themes (true OR recursive) 22 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 23 | 24 | - name: Use Node.js 25 | uses: actions/setup-node@v2 26 | env: 27 | RUNNING_NODE_CI: 1 28 | - run: | 29 | cd Common/msal-node-wrapper 30 | npm ci 31 | npm run build 32 | npm run docs 33 | 34 | - name: Deploy 35 | uses: peaceiris/actions-gh-pages@v3 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./docs 39 | destination_dir: ./docs 40 | keep_files: true -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/stale 2 | name: Mark stale issues and pull requests 3 | 4 | on: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | stale: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/stale@v3 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | operations-per-run: 60 18 | stale-issue-message: 'This issue has not seen activity in 14 days. If your issue has not been resolved please leave a comment to keep this open. It will be closed in 7 days if it remains stale.' 19 | close-issue-message: 'This issue has been closed due to inactivity. If this has not been resolved please open a new issue. Thanks!' 20 | stale-pr-message: 'This PR has not seen activity in 30 days. It may be closed if it remains stale.' 21 | stale-issue-label: 'no-issue-activity' 22 | stale-pr-label: 'no-pr-activity' 23 | days-before-issue-stale: 14 24 | days-before-pr-stale: 30 25 | days-before-close: 7 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | !msal-node-wrapper-1.0.0-beta.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | # VS Code cache 106 | .vscode/ 107 | 108 | /.vs 109 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For enhanced security, consider using client certificates instead of secrets. 3 | * See README-use-certificate.md for more. 4 | */ 5 | const authConfig = { 6 | auth: { 7 | authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here", 8 | clientId: "Enter_the_Application_Id_Here", 9 | clientSecret: "Enter_the_Client_Secret_Here", 10 | // clientCertificate: { 11 | // thumbprint: "YOUR_CERT_THUMBPRINT", 12 | // privateKey: fs.readFileSync('PATH_TO_YOUR_PRIVATE_KEY_FILE'), 13 | // } 14 | redirectUri: "/redirect", 15 | }, 16 | system: { 17 | loggerOptions: { 18 | loggerCallback: (logLevel, message, containsPii) => { 19 | if (containsPii) { 20 | return; 21 | } 22 | console.log(message); 23 | }, 24 | piiLoggingEnabled: false, 25 | logLevel: 3, 26 | }, 27 | } 28 | }; 29 | 30 | module.exports = authConfig; -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/controllers/mainController.js: -------------------------------------------------------------------------------- 1 | exports.getHomePage = (req, res, next) => { 2 | const username = req.authContext.getAccount() ? req.authContext.getAccount().username : ''; 3 | res.render('home', { isAuthenticated: req.authContext.isAuthenticated(), username: username }); 4 | } 5 | 6 | exports.getIdPage = (req, res, next) => { 7 | const account = req.authContext.getAccount(); 8 | 9 | const claims = { 10 | name: account.idTokenClaims.name, 11 | preferred_username: account.idTokenClaims.preferred_username, 12 | oid: account.idTokenClaims.oid, 13 | sub: account.idTokenClaims.sub 14 | }; 15 | 16 | res.render('id', {isAuthenticated: req.authContext.isAuthenticated(), claims: claims}); 17 | } 18 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-node-tutorial-signin", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web app authenticating users against Azure AD with MSAL Node", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit", 10 | "postinstall": "cd ../../../Common/msal-node-wrapper && npm install" 11 | }, 12 | "author": "derisen", 13 | "license": "MIT", 14 | "dependencies": { 15 | "bootstrap": "^5.3.3", 16 | "ejs": "^3.1.10", 17 | "express": "^4.19.2", 18 | "express-session": "^1.18.0", 19 | "msal-node-wrapper": "file:../../../Common/msal-node-wrapper" 20 | }, 21 | "devDependencies": { 22 | "jest": "^29.7.0", 23 | "nodemon": "^3.1.0", 24 | "supertest": "^7.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/public/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | #card-div { 6 | min-width: 40%; 7 | margin: 5% auto 5% auto; 8 | } 9 | 10 | #form-area-div { 11 | width: 55%; 12 | margin: 5% auto 5% auto; 13 | } 14 | 15 | .table-area-div { 16 | width: 70%; 17 | margin: 5% auto 5% auto; 18 | } 19 | 20 | .footer { 21 | position: fixed; 22 | left: 0; 23 | bottom: 0; 24 | width: 100%; 25 | text-align: center; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | describe('Sanitize configuration object', () => { 4 | let authConfig; 5 | 6 | beforeAll(() => { 7 | authConfig = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(authConfig).toBeDefined(); 12 | }); 13 | 14 | it('should not contain client Id', () => { 15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 16 | expect(regexGuid.test(authConfig.auth.clientId)).toBe(false); 17 | }); 18 | 19 | it('should not contain tenant Id', () => { 20 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 21 | expect(regexGuid.test(authConfig.auth.tenantId)).toBe(false); 22 | }); 23 | 24 | it('should not contain client secret', () => { 25 | const regexSecret = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{34,}$/; 26 | expect(regexSecret.test(authConfig.auth.clientSecret)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('Ensure pages served', () => { 31 | let app; 32 | 33 | beforeAll(async () => { 34 | process.env.NODE_ENV = 'test'; 35 | 36 | const authConfig = require('./authConfig.js'); 37 | const main = require('./app.js'); 38 | 39 | authConfig.auth.authority = `https://login.microsoftonline.com/common`; 40 | authConfig.auth.clientId = "11111111-2222-3333-4444-111111111111"; 41 | authConfig.auth.clientSecret = "11111111222233334444111111111111"; 42 | 43 | app = await main(); 44 | const SERVER_PORT = process.env.PORT || 4000; 45 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 46 | }) 47 | 48 | it('should serve home page', async () => { 49 | const res = await request(app) 50 | .get('/'); 51 | 52 | expect(res.statusCode).toEqual(200); 53 | }); 54 | 55 | it('should protect id page', async () => { 56 | const res = await request(app) 57 | .get('/id'); 58 | 59 | expect(res.statusCode).not.toEqual(200); 60 | }); 61 | }); -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const main = require("./app"); 4 | 5 | (async () => { 6 | const app = await main(); 7 | const SERVER_PORT = process.env.PORT || 4000; 8 | 9 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 10 | })(); -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |
17 |
18 |
19 | <% if (isAuthenticated) { %> 20 |
Welcome <%= username %>
21 | <% } else { %> 22 |
Sign-in to get an ID Token
23 | <% } %> 24 |
25 |
26 |
27 |
28 | 29 | <%- include('includes/footer'); %> 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/views/id.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ID 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Some of the important claims in your ID token are shown below:

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (const [key, value] of Object.entries(claims)) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
ClaimValue
<%= key %><%= value %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "A Node.js & Express web app authenticating users against Azure AD with MSAL Node", 4 | "Level": 100, 5 | "Client": "Node.js & Express web app", 6 | "RepositoryUrl": "ms-identity-javascript-nodejs-tutorial", 7 | "Endpoint": "AAD v2.0" 8 | }, 9 | "AADApps": [ 10 | { 11 | "Id": "client", 12 | "Name": "msal-node-webapp", 13 | "Kind": "WebApp", 14 | "Audience": "AzureADMyOrg", 15 | "HomePage": "http://localhost:4000", 16 | "ReplyUrls": "http://localhost:4000/redirect", 17 | "Certificate": "Auto", 18 | "PasswordCredentials": "Auto", 19 | "Sdk": "MsalNode", 20 | "SampleSubPath": "1-Authentication\\1-sign-in\\App", 21 | "OptionalClaims": { 22 | "AccessTokenClaims": ["acct"] 23 | } 24 | } 25 | ], 26 | "CodeConfiguration": [ 27 | { 28 | "App": "client", 29 | "SettingKind": "Replace", 30 | "SettingFile": "\\..\\App\\authConfig.js", 31 | "Mappings": [ 32 | { 33 | "key": "Enter_the_Application_Id_Here", 34 | "value": ".AppId" 35 | }, 36 | { 37 | "key": "Enter_the_Tenant_Info_Here", 38 | "value": "$tenantId" 39 | }, 40 | { 41 | "key": "Enter_the_Client_Secret_Here", 42 | "value": ".AppKey" 43 | } 44 | ] 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/1-Authentication/1-sign-in/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/ReadmeFiles/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/1-Authentication/1-sign-in/ReadmeFiles/topology.png -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For enhanced security, consider using client certificates instead of secrets. 3 | * See README-use-certificate.md for more. 4 | */ 5 | const authConfig = { 6 | auth: { 7 | authority: 'https://Enter_the_B2C_Tenant_Subdomain_Here.b2clogin.com/Enter_the_B2C_Tenant_Subdomain_Here.onmicrosoft.com/Enter_the_Policy_Name_Here', 8 | clientId: "Enter_the_Application_Id_Here", 9 | clientSecret: "Enter_the_Client_Secret_Here", 10 | // clientCertificate: { 11 | // thumbprint: "YOUR_CERT_THUMBPRINT", 12 | // privateKey: fs.readFileSync('PATH_TO_YOUR_PRIVATE_KEY_FILE'), 13 | // } 14 | redirectUri: "/redirect", 15 | knownAuthorities: ["Enter_the_B2C_Tenant_Subdomain_Here.b2clogin.com"], 16 | }, 17 | system: { 18 | loggerOptions: { 19 | loggerCallback: (logLevel, message, containsPii) => { 20 | if (containsPii) { 21 | return; 22 | } 23 | console.log(message); 24 | }, 25 | piiLoggingEnabled: false, 26 | logLevel: 3, 27 | }, 28 | } 29 | }; 30 | 31 | module.exports = authConfig; -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/controllers/mainController.js: -------------------------------------------------------------------------------- 1 | exports.getHomePage = (req, res, next) => { 2 | const username = req.authContext.getAccount() ? req.authContext.getAccount().username : ''; 3 | res.render('home', { isAuthenticated: req.authContext.isAuthenticated(), username: username }); 4 | } 5 | 6 | exports.getIdPage = (req, res, next) => { 7 | const account = req.authContext.getAccount(); 8 | 9 | const claims = { 10 | name: account.idTokenClaims.name, 11 | sub: account.idTokenClaims.sub 12 | }; 13 | 14 | res.render('id', {isAuthenticated: req.authContext.isAuthenticated(), claims: claims}); 15 | } 16 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-node-tutorial-signin-b2c", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web app authenticating users against Azure AD B2C using MSAL Node", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit", 10 | "postinstall": "cd ../../../Common/msal-node-wrapper && npm install" 11 | }, 12 | "author": "derisen", 13 | "license": "MIT", 14 | "dependencies": { 15 | "bootstrap": "^5.3.3", 16 | "ejs": "^3.1.10", 17 | "express": "^4.19.2", 18 | "express-session": "^1.18.0", 19 | "msal-node-wrapper": "file:../../../Common/msal-node-wrapper" 20 | }, 21 | "devDependencies": { 22 | "jest": "^29.7.0", 23 | "nodemon": "^3.1.0", 24 | "supertest": "^7.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/public/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | #card-div { 6 | min-width: 40%; 7 | margin: 5% auto 5% auto; 8 | } 9 | 10 | #form-area-div { 11 | width: 55%; 12 | margin: 5% auto 5% auto; 13 | } 14 | 15 | .table-area-div { 16 | width: 70%; 17 | margin: 5% auto 5% auto; 18 | } 19 | 20 | #editButton { 21 | margin-top: 20px; 22 | margin-bottom: -25px; 23 | } 24 | 25 | .footer { 26 | position: fixed; 27 | left: 0; 28 | bottom: 0; 29 | width: 100%; 30 | text-align: center; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | describe('Sanitize configuration object', () => { 4 | let authConfig; 5 | 6 | beforeAll(() => { 7 | authConfig = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(authConfig).toBeDefined(); 12 | }); 13 | 14 | it('should not contain client Id', () => { 15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 16 | expect(regexGuid.test(authConfig.auth.clientId)).toBe(false); 17 | }); 18 | 19 | it('should not contain tenant Id', () => { 20 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 21 | expect(regexGuid.test(authConfig.auth.tenantId)).toBe(false); 22 | }); 23 | 24 | it('should not contain client secret', () => { 25 | const regexSecret = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{34,}$/; 26 | expect(regexSecret.test(authConfig.auth.clientSecret)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('Ensure pages served', () => { 31 | let app; 32 | 33 | beforeAll(async () => { 34 | process.env.NODE_ENV = 'test'; 35 | 36 | const authConfig = require('./authConfig.js'); 37 | const main = require('./app.js'); 38 | 39 | authConfig.auth.authority = `https://login.microsoftonline.com/common`; 40 | authConfig.auth.clientId = "11111111-2222-3333-4444-111111111111"; 41 | authConfig.auth.clientSecret = "11111111222233334444111111111111"; 42 | 43 | app = await main(); 44 | const SERVER_PORT = process.env.PORT || 4000; 45 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 46 | }) 47 | 48 | it('should serve home page', async () => { 49 | const res = await request(app) 50 | .get('/'); 51 | 52 | expect(res.statusCode).toEqual(200); 53 | }); 54 | 55 | it('should protect id page', async () => { 56 | const res = await request(app) 57 | .get('/id'); 58 | 59 | expect(res.statusCode).not.toEqual(200); 60 | }); 61 | }); -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const main = require("./app"); 4 | 5 | (async () => { 6 | const app = await main(); 7 | const SERVER_PORT = process.env.PORT || 4000; 8 | 9 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 10 | })(); -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |
17 |
18 |
19 | <% if (isAuthenticated) { %> 20 |
Welcome <%= username %>
21 | <% } else { %> 22 |
Sign-in to get an ID Token
23 | <% } %> 24 |
25 |
26 |
27 |
28 | 29 | <%- include('includes/footer'); %> 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/views/id.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ID 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Some of the important claims in your ID token are shown below:

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (const [key, value] of Object.entries(claims)) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
ClaimValue
<%= key %><%= value %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "A Node.js & Express web app authenticating users against Azure AD B2C with MSAL Node", 4 | "Level": 100, 5 | "Client": "Node.js & Express web app", 6 | "RepositoryUrl": "ms-identity-javascript-nodejs-tutorial", 7 | "Endpoint": "AAD v2.0", 8 | "Provider": "B2C" 9 | }, 10 | "AADApps": [ 11 | { 12 | "Id": "client", 13 | "Name": "msal-node-webapp", 14 | "Kind": "WebApp", 15 | "Audience": "AzureADandPersonalMicrosoftAccount", 16 | "HomePage": "http://localhost:4000", 17 | "ReplyUrls": "http://localhost:4000/redirect", 18 | "PasswordCredentials": "Auto", 19 | "Certificate": "Auto", 20 | "Sdk": "MsalNode", 21 | "SampleSubPath": "1-Authentication\\2-sign-in-b2c\\App" 22 | } 23 | ], 24 | "CodeConfiguration": [ 25 | { 26 | "App": "client", 27 | "SettingKind": "Replace", 28 | "SettingFile": "\\..\\App\\authConfig.js", 29 | "Mappings": [ 30 | { 31 | "key": "Enter_the_Application_Id_Here", 32 | "value": ".AppId" 33 | }, 34 | { 35 | "key": "Enter_the_Tenant_Info_Here", 36 | "value": "$tenantId" 37 | }, 38 | { 39 | "key": "Enter_the_Client_Secret_Here", 40 | "value": ".AppKey" 41 | } 42 | ] 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/1-Authentication/2-sign-in-b2c/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/ReadmeFiles/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/1-Authentication/2-sign-in-b2c/ReadmeFiles/topology.png -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | const path = require('path'); 7 | const express = require('express'); 8 | const session = require('express-session'); 9 | const { WebAppAuthProvider } = require('msal-node-wrapper'); 10 | 11 | const authConfig = require('./authConfig.js'); 12 | const mainRouter = require('./routes/mainRoutes'); 13 | 14 | async function main() { 15 | 16 | // initialize express 17 | const app = express(); 18 | 19 | /** 20 | * Using express-session middleware. Be sure to familiarize yourself with available options 21 | * and set them as desired. Visit: https://www.npmjs.com/package/express-session 22 | */ 23 | app.use(session({ 24 | secret: 'ENTER_YOUR_SECRET_HERE', 25 | resave: false, 26 | saveUninitialized: false, 27 | cookie: { 28 | httpOnly: true, 29 | secure: process.env.NODE_ENV === "production", // set this to true on production 30 | } 31 | })); 32 | 33 | app.use(express.urlencoded({ extended: false })); 34 | app.use(express.json()); 35 | 36 | app.set('views', path.join(__dirname, './views')); 37 | app.set('view engine', 'ejs'); 38 | 39 | app.use('/css', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/css'))); 40 | app.use('/js', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/js'))); 41 | 42 | app.use(express.static(path.join(__dirname, './public'))); 43 | 44 | try { 45 | // initialize the wrapper 46 | const authProvider = await WebAppAuthProvider.initialize(authConfig); 47 | 48 | // initialize the auth middleware before any route handlers 49 | app.use(authProvider.authenticate({ 50 | protectAllRoutes: true, // this will force login for all routes if the user is not already 51 | acquireTokenForResources: { 52 | "graph.microsoft.com": { // you can specify the resource name as you wish 53 | scopes: ["User.Read"], 54 | routes: ["/profile"] // this will acquire a token for the graph on these routes 55 | }, 56 | } 57 | })); 58 | 59 | app.use(mainRouter); 60 | 61 | /** 62 | * This error handler is needed to catch interaction_required errors thrown by MSAL. 63 | * Make sure to add it to your middleware chain after all your routers, but before any other 64 | * error handlers. 65 | */ 66 | app.use(authProvider.interactionErrorHandler()); 67 | 68 | return app; 69 | } catch (error) { 70 | console.log(error); 71 | process.exit(1); 72 | } 73 | } 74 | 75 | module.exports = main; -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For enhanced security, consider using client certificates instead of secrets. 3 | * See README-use-certificate.md for more. 4 | */ 5 | const authConfig = { 6 | auth: { 7 | authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here", 8 | clientId: "Enter_the_Application_Id_Here", 9 | clientSecret: "Enter_the_Client_Secret_Here", 10 | // clientCertificate: { 11 | // thumbprint: "YOUR_CERT_THUMBPRINT", 12 | // privateKey: fs.readFileSync('PATH_TO_YOUR_PRIVATE_KEY_FILE'), 13 | // } 14 | redirectUri: "/redirect", 15 | }, 16 | system: { 17 | loggerOptions: { 18 | loggerCallback: (logLevel, message, containsPii) => { 19 | if (containsPii) { 20 | return; 21 | } 22 | console.log(message); 23 | }, 24 | piiLoggingEnabled: false, 25 | logLevel: 3, 26 | }, 27 | } 28 | }; 29 | 30 | module.exports = authConfig; -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/controllers/mainController.js: -------------------------------------------------------------------------------- 1 | const fetchManager = require('../utils/fetchManager'); 2 | const graphManager = require('../utils/graphManager'); 3 | 4 | exports.getHomePage = (req, res, next) => { 5 | const username = req.authContext.getAccount() ? req.authContext.getAccount().username : ''; 6 | res.render('home', { isAuthenticated: req.authContext.isAuthenticated(), username: username }); 7 | } 8 | 9 | exports.getIdPage = (req, res, next) => { 10 | const account = req.authContext.getAccount(); 11 | 12 | const claims = { 13 | name: account.idTokenClaims.name, 14 | preferred_username: account.idTokenClaims.preferred_username, 15 | oid: account.idTokenClaims.oid, 16 | sub: account.idTokenClaims.sub 17 | }; 18 | 19 | res.render('id', {isAuthenticated: req.authContext.isAuthenticated(), claims: claims}); 20 | } 21 | 22 | exports.getProfilePage = async (req, res, next) => { 23 | try { 24 | /** 25 | * If you have configured your authenticate middleware accordingly, you should be 26 | * able to get the access token for the Microsoft Graph API from the cache. If not, 27 | * you will need to acquire and cache the token yourself. 28 | */ 29 | let accessToken = req.authContext.getCachedTokenForResource("graph.microsoft.com"); 30 | 31 | if (!accessToken) { 32 | 33 | /** 34 | * You can acquire a token for the Microsoft Graph API as shown below. Note that 35 | * if an interaction required error is thrown, you need to catch it and pass it 36 | * to the interactionErrorHandler middleware. 37 | */ 38 | const tokenResponse = await req.authContext.acquireToken({ 39 | scopes: ["User.Read"], 40 | account: req.authContext.getAccount(), 41 | })(req, res, next); 42 | 43 | accessToken = tokenResponse.accessToken; 44 | } 45 | 46 | const graphClient = graphManager.getAuthenticatedClient(accessToken); 47 | 48 | const profile = await graphClient 49 | .api('/me') 50 | .get(); 51 | 52 | res.render('profile', { isAuthenticated: req.authContext.isAuthenticated(), profile: profile }); 53 | } catch (error) { 54 | // pass error to error middleware for handling 55 | next(error); 56 | } 57 | } 58 | 59 | exports.getTenantPage = async (req, res, next) => { 60 | try { 61 | const tokenResponse = await req.authContext.acquireToken({ 62 | scopes: ["https://management.azure.com/user_impersonation"], 63 | account: req.authContext.getAccount(), 64 | })(req, res, next); 65 | 66 | const tenant = await fetchManager.callAPI("https://management.azure.com/tenants?api-version=2020-01-01", tokenResponse.accessToken); 67 | res.render('tenant', { isAuthenticated: req.authContext.isAuthenticated(), tenant: tenant.value[0] }); 68 | } catch (error) { 69 | next(error); 70 | } 71 | } -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-node-tutorial-call-graph", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web app calling Microsoft Graph using MSAL Node and MS Graph SDK", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit", 10 | "postinstall": "cd ../../../Common/msal-node-wrapper && npm install" 11 | }, 12 | "author": "derisen", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@microsoft/microsoft-graph-client": "^3.0.7", 16 | "bootstrap": "^5.3.3", 17 | "ejs": "^3.1.10", 18 | "express": "^4.19.2", 19 | "express-session": "^1.18.0", 20 | "isomorphic-fetch": "^3.0.0", 21 | "msal-node-wrapper": "file:../../../Common/msal-node-wrapper" 22 | }, 23 | "devDependencies": { 24 | "jest": "^29.7.0", 25 | "nodemon": "^3.1.0", 26 | "supertest": "^7.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/public/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | #card-div { 6 | min-width: 40%; 7 | margin: 5% auto 5% auto; 8 | } 9 | 10 | #form-area-div { 11 | width: 55%; 12 | margin: 5% auto 5% auto; 13 | } 14 | 15 | .table-area-div { 16 | width: 70%; 17 | margin: 5% auto 5% auto; 18 | } 19 | 20 | .footer { 21 | position: fixed; 22 | left: 0; 23 | bottom: 0; 24 | width: 100%; 25 | text-align: center; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/routes/mainRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const mainController = require('../controllers/mainController'); 4 | 5 | // initialize router 6 | const router = express.Router(); 7 | 8 | // app routes 9 | router.get('/', mainController.getHomePage); 10 | 11 | // secure routes 12 | router.get('/id', mainController.getIdPage); 13 | 14 | // auth routes 15 | router.get( 16 | '/signout', 17 | (req, res, next) => { 18 | return req.authContext.logout({ 19 | postLogoutRedirectUri: "/", 20 | })(req, res, next); 21 | } 22 | ); 23 | 24 | router.get( 25 | '/profile', 26 | mainController.getProfilePage 27 | ); 28 | 29 | router.get( 30 | '/tenant', 31 | mainController.getTenantPage 32 | ); 33 | 34 | module.exports = router; -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | describe('Sanitize configuration object', () => { 4 | let authConfig; 5 | 6 | beforeAll(() => { 7 | authConfig = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(authConfig).toBeDefined(); 12 | }); 13 | 14 | it('should not contain client Id', () => { 15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 16 | expect(regexGuid.test(authConfig.auth.clientId)).toBe(false); 17 | }); 18 | 19 | it('should not contain tenant Id', () => { 20 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 21 | expect(regexGuid.test(authConfig.auth.tenantId)).toBe(false); 22 | }); 23 | 24 | it('should not contain client secret', () => { 25 | const regexSecret = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{34,}$/; 26 | expect(regexSecret.test(authConfig.auth.clientSecret)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('Ensure pages served', () => { 31 | let app; 32 | 33 | beforeAll(async () => { 34 | process.env.NODE_ENV = 'test'; 35 | 36 | const authConfig = require('./authConfig.js'); 37 | const main = require('./app.js'); 38 | 39 | authConfig.auth.authority = `https://login.microsoftonline.com/common`; 40 | authConfig.auth.clientId = "11111111-2222-3333-4444-111111111111"; 41 | authConfig.auth.clientSecret = "11111111222233334444111111111111"; 42 | 43 | app = await main(); 44 | const SERVER_PORT = process.env.PORT || 4000; 45 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 46 | }) 47 | 48 | it('should serve home page', async () => { 49 | const res = await request(app) 50 | .get('/'); 51 | 52 | expect(res.statusCode).toEqual(302); 53 | }); 54 | 55 | it('should protect id page', async () => { 56 | const res = await request(app) 57 | .get('/id'); 58 | 59 | expect(res.statusCode).not.toEqual(200); 60 | }); 61 | }); -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const main = require("./app"); 4 | 5 | (async () => { 6 | const app = await main(); 7 | const SERVER_PORT = process.env.PORT || 4000; 8 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 9 | })(); -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/utils/fetchManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | const fetch = require('isomorphic-fetch'); 7 | 8 | /** 9 | * Simple function to call an Azure AD protected resource 10 | */ 11 | callAPI = async(endpoint, accessToken) => { 12 | 13 | if (!accessToken || accessToken === "") { 14 | throw new Error('No tokens found') 15 | } 16 | 17 | const options = { 18 | headers: { 19 | Authorization: `Bearer ${accessToken}` 20 | } 21 | }; 22 | 23 | console.log('request made to web API at: ' + new Date().toString()); 24 | 25 | try { 26 | const response = await fetch(endpoint, options); 27 | return response.json(); 28 | } catch(error) { 29 | console.log(error) 30 | return error; 31 | } 32 | } 33 | 34 | module.exports = { 35 | callAPI 36 | }; 37 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/utils/graphManager.js: -------------------------------------------------------------------------------- 1 | const graph = require('@microsoft/microsoft-graph-client'); 2 | require('isomorphic-fetch'); 3 | 4 | /** 5 | * Creating a Graph client instance via options method. For more information, visit: 6 | * https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options 7 | */ 8 | getAuthenticatedClient = (accessToken) => { 9 | // Initialize Graph client 10 | const client = graph.Client.init({ 11 | // Use the provided access token to authenticate requests 12 | authProvider: (done) => { 13 | done(null, accessToken); 14 | } 15 | }); 16 | 17 | return client; 18 | } 19 | 20 | module.exports = { 21 | getAuthenticatedClient, 22 | } -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |
17 |
18 |
19 | <% if (isAuthenticated) { %> 20 |
Welcome <%= username %>
21 | Get my profile 22 | Get my tenant 23 | <% } else { %> 24 |
Sign-in to access your resources
25 | <% } %> 26 |
27 |
28 |
29 |
30 | 31 | <%- include('includes/footer'); %> 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/views/id.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ID 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Some of the important claims in your ID token are shown below:

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (const [key, value] of Object.entries(claims)) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
ClaimValue
<%= key %><%= value %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/views/profile.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Profile 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Calling Microsoft Graph API...

17 | 22 |

Contents of the response is below:

23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% for (const [key, value] of Object.entries(profile)) { %> 35 | 36 | 37 | 38 | 39 | <% } %> 40 | 41 |
ClaimValue
<%= key %><%= value %>
42 |
43 | 44 | <%- include('includes/footer'); %> 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/App/views/tenant.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Tenant 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Calling Azure Resource Manager API...

17 | 22 |

Contents of the response is below:

23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% for (const [key, value] of Object.entries(tenant)) { %> 35 | 36 | 37 | 38 | 39 | <% } %> 40 | 41 |
ClaimValue
<%= key %><%= value %>
42 |
43 | 44 | <%- include('includes/footer'); %> 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "A Node.js & Express web app calling Microsoft Graph API using MSAL Node and Graph SDK", 4 | "Level": 200, 5 | "Client": "Node.js & Express web app", 6 | "RepositoryUrl": "ms-identity-javascript-nodejs-tutorial", 7 | "Endpoint": "AAD v2.0" 8 | }, 9 | "AADApps": [ 10 | { 11 | "Id": "client", 12 | "Name": "msal-node-webapp", 13 | "Kind": "WebApp", 14 | "Audience": "AzureADMyOrg", 15 | "HomePage": "http://localhost:4000", 16 | "ReplyUrls": "http://localhost:4000/redirect", 17 | "PasswordCredentials": "Auto", 18 | "Certificate": "Auto", 19 | "Sdk": "MsalNode", 20 | "SampleSubPath": "2-Authorization\\1-call-graph\\App", 21 | "RequiredResourcesAccess": [ 22 | { 23 | "Resource": "Microsoft Graph", 24 | "DelegatedPermissions": [ 25 | "User.Read" 26 | ] 27 | }, 28 | { 29 | "Resource": "Windows Azure Service Management API", 30 | "DelegatedPermissions": [ 31 | "user_impersonation" 32 | ] 33 | } 34 | ], 35 | "OptionalClaims": { 36 | "AccessTokenClaims": ["acct"] 37 | } 38 | } 39 | ], 40 | "CodeConfiguration": [ 41 | { 42 | "App": "client", 43 | "SettingKind": "Replace", 44 | "SettingFile": "\\..\\App\\authConfig.js", 45 | "Mappings": [ 46 | { 47 | "key": "Enter_the_Application_Id_Here", 48 | "value": ".AppId" 49 | }, 50 | { 51 | "key": "Enter_the_Tenant_Info_Here", 52 | "value": "$tenantId" 53 | }, 54 | { 55 | "key": "Enter_the_Client_Secret_Here", 56 | "value": ".AppKey" 57 | } 58 | ] 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/2-Authorization/1-call-graph/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /2-Authorization/1-call-graph/ReadmeFiles/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/2-Authorization/1-call-graph/ReadmeFiles/topology.png -------------------------------------------------------------------------------- /3-Deployment/App/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | KEY_VAULT_URI=ENTER_KEY_VAULT_URI 3 | SECRET_NAME=ENTER_SECRET_NAME 4 | -------------------------------------------------------------------------------- /3-Deployment/App/authConfig.js: -------------------------------------------------------------------------------- 1 | const authConfig = { 2 | auth: { 3 | authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here", 4 | clientId: "Enter_the_Application_Id_Here", 5 | redirectUri: "/redirect", 6 | }, 7 | system: { 8 | loggerOptions: { 9 | loggerCallback: (logLevel, message, containsPii) => { 10 | if (containsPii) { 11 | return; 12 | } 13 | console.log(message); 14 | }, 15 | piiLoggingEnabled: false, 16 | logLevel: 3, 17 | }, 18 | } 19 | }; 20 | 21 | module.exports = authConfig; -------------------------------------------------------------------------------- /3-Deployment/App/controllers/mainController.js: -------------------------------------------------------------------------------- 1 | const fetchManager = require('../utils/fetchManager'); 2 | const graphManager = require('../utils/graphManager'); 3 | 4 | exports.getHomePage = (req, res, next) => { 5 | const username = req.authContext.getAccount() ? req.authContext.getAccount().username : ''; 6 | res.render('home', { isAuthenticated: req.authContext.isAuthenticated(), username: username }); 7 | } 8 | 9 | exports.getIdPage = (req, res, next) => { 10 | const account = req.authContext.getAccount(); 11 | 12 | const claims = { 13 | name: account.idTokenClaims.name, 14 | preferred_username: account.idTokenClaims.preferred_username, 15 | oid: account.idTokenClaims.oid, 16 | sub: account.idTokenClaims.sub 17 | }; 18 | 19 | res.render('id', {isAuthenticated: req.authContext.isAuthenticated(), claims: claims}); 20 | } 21 | 22 | exports.getProfilePage = async (req, res, next) => { 23 | try { 24 | /** 25 | * If you have configured your authenticate middleware accordingly, you should be 26 | * able to get the access token for the Microsoft Graph API from the cache. If not, 27 | * you will need to acquire and cache the token yourself. 28 | */ 29 | let accessToken = req.authContext.getCachedTokenForResource("graph.microsoft.com"); 30 | 31 | if (!accessToken) { 32 | 33 | /** 34 | * You can acquire a token for the Microsoft Graph API as shown below. Note that 35 | * if an interaction required error is thrown, you need to catch it and pass it 36 | * to the interactionErrorHandler middleware. 37 | */ 38 | const tokenResponse = await req.authContext.acquireToken({ 39 | scopes: ["User.Read"], 40 | account: req.authContext.getAccount(), 41 | })(req, res, next); 42 | 43 | accessToken = tokenResponse.accessToken; 44 | } 45 | 46 | const graphClient = graphManager.getAuthenticatedClient(accessToken); 47 | 48 | const profile = await graphClient 49 | .api('/me') 50 | .get(); 51 | 52 | res.render('profile', { isAuthenticated: req.authContext.isAuthenticated(), profile: profile }); 53 | } catch (error) { 54 | // pass error to error middleware for handling 55 | next(error); 56 | } 57 | } 58 | 59 | exports.getTenantPage = async (req, res, next) => { 60 | try { 61 | const tokenResponse = await req.authContext.acquireToken({ 62 | scopes: ["https://management.azure.com/user_impersonation"], 63 | account: req.authContext.getAccount(), 64 | })(req, res, next); 65 | 66 | const tenant = await fetchManager.callAPI("https://management.azure.com/tenants?api-version=2020-01-01", tokenResponse.accessToken); 67 | res.render('tenant', { isAuthenticated: req.authContext.isAuthenticated(), tenant: tenant.value[0] }); 68 | } catch (error) { 69 | next(error); 70 | } 71 | } -------------------------------------------------------------------------------- /3-Deployment/App/msal-node-wrapper-1.0.0-beta.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/3-Deployment/App/msal-node-wrapper-1.0.0-beta.tgz -------------------------------------------------------------------------------- /3-Deployment/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-node-tutorial-deployment", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web app calling a custom web API using MSAL Node", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit" 10 | }, 11 | "author": "derisen", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@azure/identity": "^4.2.0", 15 | "@azure/keyvault-secrets": "^4.8.0", 16 | "@microsoft/microsoft-graph-client": "^3.0.7", 17 | "bootstrap": "^5.3.3", 18 | "dotenv": "^16.4.5", 19 | "ejs": "^3.1.10", 20 | "express": "^4.19.2", 21 | "express-session": "^1.18.0", 22 | "isomorphic-fetch": "^3.0.0", 23 | "msal-node-wrapper": "file:./msal-node-wrapper-1.0.0-beta.tgz" 24 | }, 25 | "devDependencies": { 26 | "jest": "^29.7.0", 27 | "nodemon": "^3.1.0", 28 | "supertest": "^7.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /3-Deployment/App/public/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | #card-div { 6 | min-width: 40%; 7 | margin: 5% auto 5% auto; 8 | } 9 | 10 | #form-area-div { 11 | width: 55%; 12 | margin: 5% auto 5% auto; 13 | } 14 | 15 | .table-area-div { 16 | width: 70%; 17 | margin: 5% auto 5% auto; 18 | } 19 | 20 | .footer { 21 | position: fixed; 22 | left: 0; 23 | bottom: 0; 24 | width: 100%; 25 | text-align: center; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /3-Deployment/App/routes/mainRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const mainController = require('../controllers/mainController'); 4 | 5 | // initialize router 6 | const router = express.Router(); 7 | 8 | // app routes 9 | router.get('/', mainController.getHomePage); 10 | router.get('/id', mainController.getIdPage); 11 | 12 | // auth routes 13 | router.get( 14 | '/signout', 15 | (req, res, next) => { 16 | return req.authContext.logout({ 17 | postLogoutRedirectUri: "/", 18 | })(req, res, next); 19 | } 20 | ); 21 | 22 | router.get( 23 | '/profile', 24 | mainController.getProfilePage 25 | ); 26 | 27 | router.get( 28 | '/tenant', 29 | mainController.getTenantPage 30 | ); 31 | 32 | module.exports = router; -------------------------------------------------------------------------------- /3-Deployment/App/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | describe('Sanitize configuration object', () => { 4 | let authConfig; 5 | 6 | beforeAll(() => { 7 | authConfig = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(authConfig).toBeDefined(); 12 | }); 13 | 14 | it('should not contain client Id', () => { 15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 16 | expect(regexGuid.test(authConfig.auth.clientId)).toBe(false); 17 | }); 18 | 19 | it('should not contain tenant Id', () => { 20 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 21 | expect(regexGuid.test(authConfig.auth.tenantId)).toBe(false); 22 | }); 23 | 24 | it('should not contain client secret', () => { 25 | const regexSecret = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{34,}$/; 26 | expect(regexSecret.test(authConfig.auth.clientSecret)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('Ensure pages served', () => { 31 | let app; 32 | 33 | beforeAll(async () => { 34 | process.env.NODE_ENV = 'test'; 35 | 36 | const authConfig = require('./authConfig.js'); 37 | const main = require('./app.js'); 38 | 39 | authConfig.auth.authority = `https://login.microsoftonline.com/common`; 40 | authConfig.auth.clientId = "11111111-2222-3333-4444-111111111111"; 41 | authConfig.auth.clientSecret = "11111111222233334444111111111111"; 42 | 43 | app = await main(); 44 | const SERVER_PORT = process.env.PORT || 4000; 45 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 46 | }) 47 | 48 | it('should serve home page', async () => { 49 | const res = await request(app) 50 | .get('/'); 51 | 52 | console.log(res); 53 | expect(res.statusCode).toEqual(200); 54 | }); 55 | 56 | it('should protect id page', async () => { 57 | const res = await request(app) 58 | .get('/id'); 59 | 60 | expect(res.statusCode).not.toEqual(200); 61 | }); 62 | }); -------------------------------------------------------------------------------- /3-Deployment/App/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const main = require("./app"); 4 | 5 | (async () => { 6 | const app = await main(); 7 | const SERVER_PORT = process.env.PORT || 4000; 8 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 9 | })(); -------------------------------------------------------------------------------- /3-Deployment/App/utils/fetchManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | const fetch = require('isomorphic-fetch'); 7 | 8 | /** 9 | * Simple function to call an Azure AD protected resource 10 | */ 11 | callAPI = async(endpoint, accessToken) => { 12 | 13 | if (!accessToken || accessToken === "") { 14 | throw new Error('No tokens found') 15 | } 16 | 17 | const options = { 18 | headers: { 19 | Authorization: `Bearer ${accessToken}` 20 | } 21 | }; 22 | 23 | console.log('request made to web API at: ' + new Date().toString()); 24 | 25 | try { 26 | const response = await fetch(endpoint, options); 27 | return response.json(); 28 | } catch(error) { 29 | console.log(error) 30 | return error; 31 | } 32 | } 33 | 34 | module.exports = { 35 | callAPI 36 | }; 37 | -------------------------------------------------------------------------------- /3-Deployment/App/utils/graphManager.js: -------------------------------------------------------------------------------- 1 | const graph = require('@microsoft/microsoft-graph-client'); 2 | require('isomorphic-fetch'); 3 | 4 | /** 5 | * Creating a Graph client instance via options method. For more information, visit: 6 | * https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options 7 | */ 8 | getAuthenticatedClient = (accessToken) => { 9 | // Initialize Graph client 10 | const client = graph.Client.init({ 11 | // Use the provided access token to authenticate requests 12 | authProvider: (done) => { 13 | done(null, accessToken); 14 | } 15 | }); 16 | 17 | return client; 18 | } 19 | 20 | module.exports = { 21 | getAuthenticatedClient, 22 | } -------------------------------------------------------------------------------- /3-Deployment/App/utils/keyVaultManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | const { DefaultAzureCredential } = require( "@azure/identity"); 7 | const { SecretClient } = require("@azure/keyvault-secrets"); 8 | 9 | async function getCredentialFromKeyVault(keyVaultUrl, credentialName) { 10 | const credential = new DefaultAzureCredential(); 11 | const secretClient = new SecretClient(keyVaultUrl, credential); 12 | 13 | const keyVaultSecret = await secretClient.getSecret(credentialName); 14 | 15 | return keyVaultSecret.value; 16 | } 17 | 18 | module.exports = { 19 | getCredentialFromKeyVault 20 | }; -------------------------------------------------------------------------------- /3-Deployment/App/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |
17 |
18 |
19 | <% if (isAuthenticated) { %> 20 |
Welcome <%= username %>
21 | Get my profile 22 | Get my tenant 23 | <% } else { %> 24 |
Sign-in to access your resources
25 | <% } %> 26 |
27 |
28 |
29 |
30 | 31 | <%- include('includes/footer'); %> 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /3-Deployment/App/views/id.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ID 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Some of the important claims in your ID token are shown below:

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (const [key, value] of Object.entries(claims)) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
ClaimValue
<%= key %><%= value %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /3-Deployment/App/views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3-Deployment/App/views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3-Deployment/App/views/profile.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Profile 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Calling Microsoft Graph API...

17 | 22 |

Contents of the response is below:

23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% for (const [key, value] of Object.entries(profile)) { %> 35 | 36 | 37 | 38 | 39 | <% } %> 40 | 41 |
ClaimValue
<%= key %><%= value %>
42 |
43 | 44 | <%- include('includes/footer'); %> 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /3-Deployment/App/views/tenant.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Tenant 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Calling Azure Resource Manager API...

17 | 22 |

Contents of the response is below:

23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% for (const [key, value] of Object.entries(tenant)) { %> 35 | 36 | 37 | 38 | 39 | <% } %> 40 | 41 |
ClaimValue
<%= key %><%= value %>
42 |
43 | 44 | <%- include('includes/footer'); %> 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /3-Deployment/ReadmeFiles/disable_easy_auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/3-Deployment/ReadmeFiles/disable_easy_auth.png -------------------------------------------------------------------------------- /3-Deployment/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/3-Deployment/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /3-Deployment/ReadmeFiles/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/3-Deployment/ReadmeFiles/step1.png -------------------------------------------------------------------------------- /3-Deployment/ReadmeFiles/step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/3-Deployment/ReadmeFiles/step2.png -------------------------------------------------------------------------------- /3-Deployment/ReadmeFiles/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/3-Deployment/ReadmeFiles/step3.png -------------------------------------------------------------------------------- /3-Deployment/ReadmeFiles/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/3-Deployment/ReadmeFiles/topology.png -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | const path = require('path'); 7 | const express = require('express'); 8 | const session = require('express-session'); 9 | const { WebAppAuthProvider } = require('msal-node-wrapper'); 10 | 11 | const authConfig = require('./authConfig.js'); 12 | const mainRouter = require('./routes/mainRoutes'); 13 | 14 | async function main() { 15 | 16 | // initialize express 17 | const app = express(); 18 | 19 | /** 20 | * Using express-session middleware. Be sure to familiarize yourself with available options 21 | * and set them as desired. Visit: https://www.npmjs.com/package/express-session 22 | */ 23 | app.use(session({ 24 | secret: 'ENTER_YOUR_SECRET_HERE', 25 | resave: false, 26 | saveUninitialized: false, 27 | cookie: { 28 | httpOnly: true, 29 | secure: process.env.NODE_ENV === "production", // set this to true on production 30 | } 31 | })); 32 | 33 | app.use(express.urlencoded({ extended: false })); 34 | app.use(express.json()); 35 | 36 | app.set('views', path.join(__dirname, './views')); 37 | app.set('view engine', 'ejs'); 38 | 39 | app.use('/css', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/css'))); 40 | app.use('/js', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/js'))); 41 | 42 | app.use(express.static(path.join(__dirname, './public'))); 43 | 44 | try { 45 | // initialize the auth middleware before any route handlers 46 | const authProvider = await WebAppAuthProvider.initialize(authConfig); 47 | 48 | app.use(authProvider.authenticate({ 49 | protectAllRoutes: true, // enforce login for all routes 50 | })); 51 | 52 | app.get( 53 | '/todolist', 54 | authProvider.guard({ 55 | idTokenClaims: { 56 | roles: ["TaskUser", "TaskAdmin"], // require the user's ID token to have either of these role claims 57 | }, 58 | }), 59 | ); 60 | 61 | app.get( 62 | '/dashboard', 63 | authProvider.guard({ 64 | idTokenClaims: { 65 | roles: ["TaskAdmin"], // require the user's ID token to have this role claim 66 | }, 67 | }) 68 | ); 69 | 70 | app.use(mainRouter); 71 | 72 | /** 73 | * This error handler is needed to catch interaction_required errors thrown by MSAL. 74 | * Make sure to add it to your middleware chain after all your routers, but before any other 75 | * error handlers. 76 | */ 77 | app.use(authProvider.interactionErrorHandler()); 78 | 79 | return app; 80 | } catch (error) { 81 | console.log(error); 82 | process.exit(1); 83 | } 84 | } 85 | 86 | module.exports = main; -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For enhanced security, consider using client certificates instead of secrets. 3 | * See README-use-certificate.md for more. 4 | */ 5 | const authConfig = { 6 | auth: { 7 | authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here", 8 | clientId: "Enter_the_Application_Id_Here", 9 | clientSecret: "Enter_the_Client_Secret_Here", 10 | // clientCertificate: { 11 | // thumbprint: "YOUR_CERT_THUMBPRINT", 12 | // privateKey: fs.readFileSync('PATH_TO_YOUR_PRIVATE_KEY_FILE'), 13 | // } 14 | redirectUri: "/redirect", 15 | }, 16 | system: { 17 | loggerOptions: { 18 | loggerCallback: (logLevel, message, containsPii) => { 19 | if (containsPii) { 20 | return; 21 | } 22 | console.log(message); 23 | }, 24 | piiLoggingEnabled: false, 25 | logLevel: 3, 26 | }, 27 | } 28 | }; 29 | 30 | module.exports = authConfig; -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/controllers/dashboardController.js: -------------------------------------------------------------------------------- 1 | const Todo = require('../model/todo'); 2 | 3 | exports.getAllTodos = (req, res) => { 4 | const todos = Todo.getAllTodos(); 5 | 6 | res.render('dashboard', { isAuthenticated: req.authContext.isAuthenticated(), todos: todos }); 7 | } -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/controllers/mainController.js: -------------------------------------------------------------------------------- 1 | exports.getHomePage = (req, res, next) => { 2 | const username = req.authContext.getAccount() ? req.authContext.getAccount().username : ''; 3 | res.render('home', { isAuthenticated: req.authContext.isAuthenticated(), username: username }); 4 | } 5 | 6 | exports.getIdPage = (req, res, next) => { 7 | const account = req.authContext.getAccount(); 8 | 9 | const claims = { 10 | name: account.idTokenClaims.name, 11 | preferred_username: account.idTokenClaims.preferred_username, 12 | oid: account.idTokenClaims.oid, 13 | roles: account.idTokenClaims.roles ? account.idTokenClaims.roles.join(' ') : null 14 | }; 15 | 16 | res.render('id', {isAuthenticated: req.authContext.isAuthenticated(), claims: claims}); 17 | } 18 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/controllers/todolistController.js: -------------------------------------------------------------------------------- 1 | const Todo = require('../model/todo'); 2 | const { nanoid } = require('nanoid'); 3 | 4 | exports.getTodos = (req, res) => { 5 | /** 6 | * The 'oid' (object id) is the only claim that should be used to uniquely identify 7 | * a user in an Azure AD tenant. The token might have one or more of the following claim, 8 | * that might seem like a unique identifier, but is not and should not be used as such, 9 | * especially for systems which act as system of record (SOR): 10 | * 11 | * - upn (user principal name): might be unique amongst the active set of users in a tenant but 12 | * tend to get reassigned to new employees as employees leave the organization and 13 | * others take their place or might change to reflect a personal change like marriage. 14 | * 15 | * - email: might be unique amongst the active set of users in a tenant but tend to get 16 | * reassigned to new employees as employees leave the organization and others take their place. 17 | */ 18 | const owner = req.authContext.getAccount().idTokenClaims['oid']; 19 | const todos = Todo.getTodosByOwner(owner) 20 | 21 | res.render('todolist', { isAuthenticated: req.authContext.isAuthenticated(), todos: todos }); 22 | } 23 | 24 | exports.postTodo = (req, res) => { 25 | const owner = req.authContext.getAccount().idTokenClaims['oid']; 26 | req.body._method === "DELETE" ? deleteTodo(req, owner) : addTodo(req, owner); 27 | 28 | res.redirect('/todolist'); 29 | } 30 | 31 | const addTodo = (req, owner) => { 32 | const id = nanoid(); 33 | const name = req.body.name; 34 | 35 | const newTodo = new Todo(id, name, owner) 36 | 37 | Todo.postTodo(newTodo); 38 | }; 39 | 40 | const deleteTodo = (req, owner) => { 41 | const id = req.body.id; 42 | 43 | Todo.deleteTodo(id, owner); 44 | }; -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/data/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/model/todo.js: -------------------------------------------------------------------------------- 1 | const lowdb = require('lowdb'); 2 | const FileSync = require('lowdb/adapters/FileSync'); 3 | const adapter = new FileSync('./data/db.json'); 4 | const db = lowdb(adapter); 5 | 6 | class Todo { 7 | 8 | id; 9 | name; 10 | owner; 11 | 12 | constructor(id, name, owner) { 13 | this.id = id; 14 | this.name = name; 15 | this.owner = owner; 16 | } 17 | 18 | static getAllTodos() { 19 | return db.get('todos') 20 | .value(); 21 | } 22 | 23 | static getTodosByOwner(owner) { 24 | return db.get('todos') 25 | .filter({ owner: owner }) 26 | .value(); 27 | } 28 | 29 | static postTodo(newTodo) { 30 | db.get('todos').push(newTodo).write(); 31 | } 32 | 33 | static deleteTodo(id, owner) { 34 | db.get('todos') 35 | .remove({ owner: owner, id: id }) 36 | .write(); 37 | } 38 | } 39 | 40 | module.exports = Todo; -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-node-tutorial-app-roles", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web app using Azure AD App Roles to implement Role-based Access Control (RBAC)", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit", 10 | "postinstall": "cd ../../../Common/msal-node-wrapper && npm install" 11 | }, 12 | "author": "derisen", 13 | "license": "MIT", 14 | "dependencies": { 15 | "bootstrap": "^5.3.3", 16 | "ejs": "^3.1.10", 17 | "express": "^4.19.2", 18 | "express-session": "^1.18.0", 19 | "lowdb": "^1.0.0", 20 | "method-override": "^3.0.0", 21 | "msal-node-wrapper": "file:../../../Common/msal-node-wrapper", 22 | "nanoid": "^3.3.2" 23 | }, 24 | "devDependencies": { 25 | "jest": "^29.7.0", 26 | "nodemon": "^3.1.0", 27 | "supertest": "^7.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/public/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | #card-div { 6 | min-width: 40%; 7 | margin: 5% auto 5% auto; 8 | } 9 | 10 | #form-area-div { 11 | width: 55%; 12 | margin: 5% auto 5% auto; 13 | } 14 | 15 | .table-area-div { 16 | width: 70%; 17 | margin: 5% auto 5% auto; 18 | } 19 | 20 | .footer { 21 | position: fixed; 22 | left: 0; 23 | bottom: 0; 24 | width: 100%; 25 | text-align: center; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/routes/dashboardRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const dashboardController = require('../controllers/dashboardController'); 3 | 4 | // initialize router 5 | const router = express.Router(); 6 | 7 | // admin route 8 | router.get('/', dashboardController.getAllTodos); 9 | 10 | module.exports = router; -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/routes/mainRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const mainController = require('../controllers/mainController'); 4 | const todolistRouter = require('./todolistRoutes'); 5 | const dashboardRouter = require('./dashboardRoutes'); 6 | 7 | // initialize router 8 | const router = express.Router(); 9 | 10 | // app routes 11 | router.get('/', mainController.getHomePage); 12 | router.get('/id', mainController.getIdPage); 13 | 14 | // auth routes 15 | router.get( 16 | '/signout', 17 | (req, res, next) => { 18 | return req.authContext.logout({ 19 | postLogoutRedirectUri: "/", 20 | })(req, res, next); 21 | } 22 | ); 23 | 24 | router.get( 25 | '/signin', 26 | (req, res, next) => { 27 | return req.authContext.login({ 28 | postLoginRedirectUri: "/", 29 | postFailureRedirectUri: "/signin" 30 | })(req, res, next); 31 | } 32 | ); 33 | 34 | // nested routes 35 | router.use( 36 | '/todolist', 37 | todolistRouter 38 | ); 39 | 40 | router.use( 41 | '/dashboard', 42 | dashboardRouter 43 | ); 44 | 45 | module.exports = router; -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/routes/todolistRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const todolistController = require('../controllers/todolistController'); 3 | 4 | // initialize router 5 | const router = express.Router(); 6 | 7 | // user routes 8 | router.get('/', todolistController.getTodos); 9 | router.post('/', todolistController.postTodo); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | describe('Sanitize configuration object', () => { 4 | let authConfig; 5 | 6 | beforeAll(() => { 7 | authConfig = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(authConfig).toBeDefined(); 12 | }); 13 | 14 | it('should not contain client Id', () => { 15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 16 | expect(regexGuid.test(authConfig.auth.clientId)).toBe(false); 17 | }); 18 | 19 | it('should not contain tenant Id', () => { 20 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 21 | expect(regexGuid.test(authConfig.auth.tenantId)).toBe(false); 22 | }); 23 | 24 | it('should not contain client secret', () => { 25 | const regexSecret = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{34,}$/; 26 | expect(regexSecret.test(authConfig.auth.clientSecret)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('Ensure pages served', () => { 31 | let app; 32 | 33 | beforeAll(async () => { 34 | process.env.NODE_ENV = 'test'; 35 | 36 | const authConfig = require('./authConfig.js'); 37 | const main = require('./app.js'); 38 | 39 | authConfig.auth.authority = `https://login.microsoftonline.com/common`; 40 | authConfig.auth.clientId = "11111111-2222-3333-4444-111111111111"; 41 | authConfig.auth.clientSecret = "11111111222233334444111111111111"; 42 | 43 | app = await main(); 44 | const SERVER_PORT = process.env.PORT || 4000; 45 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 46 | }) 47 | 48 | it('should serve home page', async () => { 49 | const res = await request(app) 50 | .get('/'); 51 | 52 | expect(res.statusCode).toEqual(302); 53 | }); 54 | 55 | it('should protect id page', async () => { 56 | const res = await request(app) 57 | .get('/id'); 58 | 59 | expect(res.statusCode).not.toEqual(200); 60 | }); 61 | }); -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const main = require("./app"); 4 | 5 | (async () => { 6 | const app = await main(); 7 | const SERVER_PORT = process.env.PORT || 4000; 8 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 9 | })(); -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/views/dashboard.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Dashboard 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

You can only see this page if you are in TaskAdmin role

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (let i=0; i < todos.length; i++) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
TaskOwner
<%= todos[i]['name'] %><%= todos[i]['owner'] %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |
17 |
18 |
19 | <% if (isAuthenticated) { %> 20 |
Welcome <%= username %>
21 | Todolist 22 | Dashboard 23 | <% } else { %> 24 |
Sign-in to access your resources
25 | <% } %> 26 |
27 |
28 |
29 |
30 | 31 | <%- include('includes/footer'); %> 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/views/id.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ID 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Some of the important claims in your ID token are shown below:

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (const [key, value] of Object.entries(claims)) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
ClaimValue
<%= key %><%= value %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/App/views/todolist.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Todolist 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

You can see this page if you are in TaskUser or TaskAdmin role

17 |
18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 | 28 | 29 | <% for (let i=0; i < todos.length; i++) { %> 30 | 31 | 32 | 39 | 40 | <% } %> 41 | 42 |
<%= todos[i]['name'] %> 33 |
34 | 35 | 36 | 37 |
38 |
43 |
44 | 45 | <%- include('includes/footer'); %> 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "A Node.js & Express web app using App Roles to implement Role-Based Access Control", 4 | "Level": 300, 5 | "Client": "Node.js & Express web app", 6 | "RepositoryUrl": "ms-identity-javascript-nodejs-tutorial", 7 | "Endpoint": "AAD v2.0" 8 | }, 9 | "AADApps": [ 10 | { 11 | "Id": "client", 12 | "Name": "msal-node-webapp", 13 | "Kind": "WebApp", 14 | "Audience": "AzureADMyOrg", 15 | "HomePage": "http://localhost:4000", 16 | "ReplyUrls": "http://localhost:4000/redirect", 17 | "PasswordCredentials": "Auto", 18 | "Certificate": "Auto", 19 | "Sdk": "MsalNode", 20 | "SampleSubPath": "4-AccessControl\\1-app-roles\\App", 21 | "AppRoles": [ 22 | { 23 | "AllowedMemberTypes": [ 24 | "User" 25 | ], 26 | "Name": "TaskAdmin", 27 | "Description": "Admins can read any user's todo list" 28 | }, 29 | { 30 | "AllowedMemberTypes": [ 31 | "User" 32 | ], 33 | "Name": "TaskUser", 34 | "Description": "Users can read and modify their todo lists" 35 | } 36 | ], 37 | "OptionalClaims": { 38 | "AccessTokenClaims": ["acct"] 39 | } 40 | } 41 | ], 42 | "CodeConfiguration": [ 43 | { 44 | "App": "client", 45 | "SettingKind": "Replace", 46 | "SettingFile": "\\..\\App\\authConfig.js", 47 | "Mappings": [ 48 | { 49 | "key": "Enter_the_Application_Id_Here", 50 | "value": ".AppId" 51 | }, 52 | { 53 | "key": "Enter_the_Tenant_Info_Here", 54 | "value": "$tenantId" 55 | }, 56 | { 57 | "key": "Enter_the_Client_Secret_Here", 58 | "value": ".AppKey" 59 | } 60 | ] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/4-AccessControl/1-app-roles/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /4-AccessControl/1-app-roles/ReadmeFiles/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/4-AccessControl/1-app-roles/ReadmeFiles/topology.png -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | const path = require('path'); 7 | const express = require('express'); 8 | const session = require('express-session'); 9 | const { WebAppAuthProvider } = require('msal-node-wrapper'); 10 | 11 | const authConfig = require('./authConfig.js'); 12 | const mainRouter = require('./routes/mainRoutes'); 13 | 14 | async function main() { 15 | 16 | // initialize express 17 | const app = express(); 18 | 19 | /** 20 | * Using express-session middleware. Be sure to familiarize yourself with available options 21 | * and set them as desired. Visit: https://www.npmjs.com/package/express-session 22 | */ 23 | app.use(session({ 24 | secret: 'ENTER_YOUR_SECRET_HERE', 25 | resave: false, 26 | saveUninitialized: false, 27 | cookie: { 28 | httpOnly: true, 29 | secure: process.env.NODE_ENV === "production", // set this to true on production 30 | } 31 | })); 32 | 33 | app.use(express.urlencoded({ extended: false })); 34 | app.use(express.json()); 35 | 36 | app.set('views', path.join(__dirname, './views')); 37 | app.set('view engine', 'ejs'); 38 | 39 | app.use('/css', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/css'))); 40 | app.use('/js', express.static(path.join(__dirname, 'node_modules/bootstrap/dist/js'))); 41 | 42 | app.use(express.static(path.join(__dirname, './public'))); 43 | 44 | try { 45 | // initialize the wrapper 46 | const authProvider = await WebAppAuthProvider.initialize(authConfig); 47 | 48 | // initialize the auth middleware before any route handlers 49 | app.use(authProvider.authenticate({ 50 | protectAllRoutes: true, // enforce login for all routes 51 | })); 52 | 53 | app.get( 54 | '/todolist', 55 | authProvider.guard({ 56 | idTokenClaims: { 57 | groups: ["Enter_the_ObjectId_of_GroupAdmin", "Enter_the_ObjectId_of_GroupMember"], // require the user's ID token to have either of these group claims 58 | }, 59 | }) 60 | ); 61 | 62 | app.get( 63 | '/dashboard', 64 | authProvider.guard({ 65 | idTokenClaims: { 66 | groups: ["Enter_the_ObjectId_of_GroupAdmin"] // require the user's ID token to have this group claim 67 | }, 68 | }) 69 | ); 70 | 71 | app.use(mainRouter); 72 | 73 | /** 74 | * This error handler is needed to catch interaction_required errors thrown by MSAL. 75 | * Make sure to add it to your middleware chain after all your routers, but before any other 76 | * error handlers. 77 | */ 78 | app.use(authProvider.interactionErrorHandler()); 79 | 80 | return app; 81 | } catch (error) { 82 | console.log(error); 83 | process.exit(1); 84 | } 85 | } 86 | 87 | module.exports = main; -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For enhanced security, consider using client certificates instead of secrets. 3 | * See README-use-certificate.md for more. 4 | */ 5 | const authConfig = { 6 | auth: { 7 | authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here", 8 | clientId: "Enter_the_Application_Id_Here", 9 | clientSecret: "Enter_the_Client_Secret_Here", 10 | // clientCertificate: { 11 | // thumbprint: "YOUR_CERT_THUMBPRINT", 12 | // privateKey: fs.readFileSync('PATH_TO_YOUR_PRIVATE_KEY_FILE'), 13 | // } 14 | redirectUri: "/redirect", 15 | }, 16 | system: { 17 | loggerOptions: { 18 | loggerCallback: (logLevel, message, containsPii) => { 19 | if (containsPii) { 20 | return; 21 | } 22 | console.log(message); 23 | }, 24 | piiLoggingEnabled: false, 25 | logLevel: 3, 26 | }, 27 | } 28 | }; 29 | 30 | module.exports = authConfig; -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/controllers/dashboardController.js: -------------------------------------------------------------------------------- 1 | const Todo = require('../model/todo'); 2 | 3 | exports.getAllTodos = (req, res) => { 4 | const todos = Todo.getAllTodos(); 5 | 6 | res.render('dashboard', { isAuthenticated: req.authContext.isAuthenticated(), todos: todos }); 7 | } -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/controllers/mainController.js: -------------------------------------------------------------------------------- 1 | exports.getHomePage = (req, res, next) => { 2 | const username = req.authContext.getAccount() ? req.authContext.getAccount().username : ''; 3 | res.render('home', { isAuthenticated: req.authContext.isAuthenticated(), username: username }); 4 | } 5 | 6 | exports.getIdPage = (req, res, next) => { 7 | const account = req.authContext.getAccount(); 8 | 9 | const claims = { 10 | name: account.idTokenClaims.name, 11 | preferred_username: account.idTokenClaims.preferred_username, 12 | oid: account.idTokenClaims.oid, 13 | groups: account.idTokenClaims.groups ? account.idTokenClaims.groups.join(' ') : "A groups overage has occurred. To learn more about how to handle group overages, please visit https://learn.microsoft.com/azure/active-directory/develop/id-token-claims-reference#groups-overage-claim" 14 | }; 15 | 16 | res.render('id', {isAuthenticated: req.authContext.isAuthenticated(), claims: claims}); 17 | } 18 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/controllers/todolistController.js: -------------------------------------------------------------------------------- 1 | const Todo = require('../model/todo'); 2 | const { nanoid } = require('nanoid'); 3 | 4 | exports.getTodos = (req, res) => { 5 | /** 6 | * The 'oid' (object id) is the only claim that should be used to uniquely identify 7 | * a user in an Azure AD tenant. The token might have one or more of the following claim, 8 | * that might seem like a unique identifier, but is not and should not be used as such, 9 | * especially for systems which act as system of record (SOR): 10 | * 11 | * - upn (user principal name): might be unique amongst the active set of users in a tenant but 12 | * tend to get reassigned to new employees as employees leave the organization and 13 | * others take their place or might change to reflect a personal change like marriage. 14 | * 15 | * - email: might be unique amongst the active set of users in a tenant but tend to get 16 | * reassigned to new employees as employees leave the organization and others take their place. 17 | */ 18 | const owner = req.authContext.getAccount().idTokenClaims['oid']; 19 | const todos = Todo.getTodosByOwner(owner) 20 | 21 | res.render('todolist', { isAuthenticated: req.authContext.isAuthenticated(), todos: todos }); 22 | } 23 | 24 | exports.postTodo = (req, res) => { 25 | const owner = req.authContext.getAccount().idTokenClaims['oid']; 26 | req.body._method === "DELETE" ? deleteTodo(req, owner) : addTodo(req, owner); 27 | 28 | res.redirect('/todolist'); 29 | } 30 | 31 | const addTodo = (req, owner) => { 32 | const id = nanoid(); 33 | const name = req.body.name; 34 | 35 | const newTodo = new Todo(id, name, owner) 36 | 37 | Todo.postTodo(newTodo); 38 | }; 39 | 40 | const deleteTodo = (req, owner) => { 41 | const id = req.body.id; 42 | 43 | Todo.deleteTodo(id, owner); 44 | }; -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/data/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/model/todo.js: -------------------------------------------------------------------------------- 1 | const lowdb = require('lowdb'); 2 | const FileSync = require('lowdb/adapters/FileSync'); 3 | const adapter = new FileSync('./data/db.json'); 4 | const db = lowdb(adapter); 5 | 6 | class Todo { 7 | 8 | id; 9 | name; 10 | owner; 11 | 12 | constructor(id, name, owner) { 13 | this.id = id; 14 | this.name = name; 15 | this.owner = owner; 16 | } 17 | 18 | static getAllTodos() { 19 | return db.get('todos') 20 | .value(); 21 | } 22 | 23 | static getTodosByOwner(owner) { 24 | return db.get('todos') 25 | .filter({ owner: owner }) 26 | .value(); 27 | } 28 | 29 | static postTodo(newTodo) { 30 | db.get('todos').push(newTodo).write(); 31 | } 32 | 33 | static deleteTodo(id, owner) { 34 | db.get('todos') 35 | .remove({ owner: owner, id: id }) 36 | .write(); 37 | } 38 | } 39 | 40 | module.exports = Todo; -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-node-tutorial-security-groups", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web app using Azure AD Security Groups to implement Role-based Access Control (RBAC)", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit", 10 | "postinstall": "cd ../../../Common/msal-node-wrapper && npm install" 11 | }, 12 | "author": "derisen", 13 | "license": "MIT", 14 | "dependencies": { 15 | "bootstrap": "^5.3.3", 16 | "ejs": "^3.1.10", 17 | "express": "^4.19.2", 18 | "express-session": "^1.18.0", 19 | "lowdb": "^1.0.0", 20 | "method-override": "^3.0.0", 21 | "msal-node-wrapper": "file:../../../Common/msal-node-wrapper", 22 | "nanoid": "^3.3.2" 23 | }, 24 | "devDependencies": { 25 | "jest": "^29.7.0", 26 | "nodemon": "^3.1.0", 27 | "supertest": "^7.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/public/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | #card-div { 6 | min-width: 40%; 7 | margin: 5% auto 5% auto; 8 | } 9 | 10 | #form-area-div { 11 | width: 55%; 12 | margin: 5% auto 5% auto; 13 | } 14 | 15 | .table-area-div { 16 | width: 70%; 17 | margin: 5% auto 5% auto; 18 | } 19 | 20 | .footer { 21 | position: fixed; 22 | left: 0; 23 | bottom: 0; 24 | width: 100%; 25 | text-align: center; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/routes/dashboardRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const dashboardController = require('../controllers/dashboardController'); 4 | 5 | // initialize router 6 | const router = express.Router(); 7 | 8 | // admin route 9 | router.get('/', dashboardController.getAllTodos); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/routes/mainRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const mainController = require('../controllers/mainController'); 4 | const todolistRouter = require('./todolistRoutes'); 5 | const dashboardRouter = require('./dashboardRoutes'); 6 | 7 | // initialize router 8 | const router = express.Router(); 9 | 10 | // app routes 11 | router.get('/', mainController.getHomePage); 12 | router.get('/id', mainController.getIdPage); 13 | 14 | // auth routes 15 | router.get( 16 | '/signout', 17 | (req, res, next) => { 18 | return req.authContext.logout({ 19 | postLogoutRedirectUri: "/", 20 | })(req, res, next); 21 | } 22 | ); 23 | 24 | // nested routes 25 | router.use( 26 | '/todolist', 27 | todolistRouter 28 | ); 29 | 30 | router.use( 31 | '/dashboard', 32 | dashboardRouter 33 | ); 34 | 35 | module.exports = router; -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/routes/todolistRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const todolistController = require('../controllers/todolistController'); 3 | 4 | // initialize router 5 | const router = express.Router(); 6 | 7 | // user routes 8 | router.get('/', todolistController.getTodos); 9 | router.post('/', todolistController.postTodo); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | describe('Sanitize configuration object', () => { 4 | let authConfig; 5 | 6 | beforeAll(() => { 7 | authConfig = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(authConfig).toBeDefined(); 12 | }); 13 | 14 | it('should not contain client Id', () => { 15 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 16 | expect(regexGuid.test(authConfig.auth.clientId)).toBe(false); 17 | }); 18 | 19 | it('should not contain tenant Id', () => { 20 | const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 21 | expect(regexGuid.test(authConfig.auth.tenantId)).toBe(false); 22 | }); 23 | 24 | it('should not contain client secret', () => { 25 | const regexSecret = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{34,}$/; 26 | expect(regexSecret.test(authConfig.auth.clientSecret)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('Ensure pages served', () => { 31 | let app; 32 | 33 | beforeAll(async () => { 34 | process.env.NODE_ENV = 'test'; 35 | 36 | const authConfig = require('./authConfig.js'); 37 | const main = require('./app.js'); 38 | 39 | authConfig.auth.authority = `https://login.microsoftonline.com/common`; 40 | authConfig.auth.clientId = "11111111-2222-3333-4444-111111111111"; 41 | authConfig.auth.clientSecret = "11111111222233334444111111111111"; 42 | 43 | app = await main(); 44 | const SERVER_PORT = process.env.PORT || 4000; 45 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 46 | }) 47 | 48 | it('should serve home page', async () => { 49 | const res = await request(app) 50 | .get('/'); 51 | 52 | expect(res.statusCode).toEqual(302); 53 | }); 54 | 55 | it('should protect id page', async () => { 56 | const res = await request(app) 57 | .get('/id'); 58 | 59 | expect(res.statusCode).not.toEqual(200); 60 | }); 61 | }); -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const main = require("./app"); 4 | 5 | (async () => { 6 | const app = await main(); 7 | const SERVER_PORT = process.env.PORT || 4000; 8 | app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)); 9 | })(); -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/views/dashboard.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Dashboard 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

You can only see this page if you are in TaskAdmin role

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (let i=0; i < todos.length; i++) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
TaskOwner
<%= todos[i]['name'] %><%= todos[i]['owner'] %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/views/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |
17 |
18 |
19 | <% if (isAuthenticated) { %> 20 |
Welcome <%= username %>
21 | Todolist 22 | Dashboard 23 | <% } else { %> 24 |
Sign-in to access your resources
25 | <% } %> 26 |
27 |
28 |
29 |
30 | 31 | <%- include('includes/footer'); %> 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/views/id.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ID 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

Some of the important claims in your ID token are shown below:

17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% for (const [key, value] of Object.entries(claims)) { %> 29 | 30 | 31 | 32 | 33 | <% } %> 34 | 35 |
ClaimValue
<%= key %><%= value %>
36 |
37 | 38 | <%- include('includes/footer'); %> 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/App/views/todolist.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Todolist 10 | 11 | 12 | 13 | <%- include('includes/navbar', {isAuthenticated: isAuthenticated}); %> 14 | 15 |
16 |

You can see this page if you are in GroupMember or GroupAdmin group

17 |
18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 | 28 | 29 | <% for (let i=0; i < todos.length; i++) { %> 30 | 31 | 32 | 39 | 40 | <% } %> 41 | 42 |
<%= todos[i]['name'] %> 33 |
34 | 35 | 36 | 37 |
38 |
43 |
44 | 45 | <%- include('includes/footer'); %> 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/4-AccessControl/2-security-groups/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /4-AccessControl/2-security-groups/ReadmeFiles/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/4-AccessControl/2-security-groups/ReadmeFiles/topology.png -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/app.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | const express = require('express'); 4 | const session = require('express-session'); 5 | const cookieParser = require('cookie-parser'); 6 | const helmet = require('helmet'); 7 | const csrf = require('lusca').csrf; 8 | 9 | const mainRouter = require("./routes/mainRoutes"); 10 | 11 | const { 12 | SESSION_COOKIE_NAME, 13 | REDIRECT_URI 14 | } = require('./authConfig'); 15 | 16 | const PORT = process.env.PORT || 4000; 17 | 18 | const app = express(); 19 | 20 | app.use(helmet()); 21 | app.use(express.json()); 22 | app.use(cookieParser()); 23 | app.use(express.urlencoded({ extended: false })); 24 | app.use(express.static(path.join(__dirname, 'client/build'))); 25 | 26 | const sessionConfig = { 27 | name: SESSION_COOKIE_NAME, 28 | secret: 'ENTER_YOUR_SECRET_HERE', // replace with your own secret 29 | resave: false, 30 | saveUninitialized: false, 31 | cookie: { 32 | sameSite: 'strict', 33 | httpOnly: true, 34 | secure: false, // set this to true on production 35 | }, 36 | }; 37 | 38 | if (app.get('env') === 'production') { 39 | app.set('trust proxy', 1); // trust first proxy e.g. App Service 40 | sessionConfig.cookie.secure = true; // serve secure cookies on production 41 | } 42 | 43 | app.use(session(sessionConfig)); 44 | app.use(csrf({ 45 | blocklist: [new URL(REDIRECT_URI).pathname], 46 | })); 47 | 48 | app.use(function (err, req, res, next) { 49 | // set locals, only providing error in development 50 | res.locals.message = err.message; 51 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 52 | 53 | // render the error page 54 | res.status(err.status || 500); 55 | res.render('error'); 56 | }) 57 | 58 | app.use(mainRouter); 59 | 60 | app.listen(PORT, () => { 61 | console.log(`Server listening on ${PORT}`); 62 | }); -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | const msal = require('@azure/msal-node'); 6 | const fs = require("fs"); 7 | 8 | const REDIRECT_URI = "http://localhost:4000/auth/redirect"; 9 | const POST_LOGOUT_REDIRECT_URI = "http://localhost:4000"; 10 | const GRAPH_ME_ENDPOINT = "https://graph.microsoft.com/v1.0/me"; 11 | 12 | const SESSION_COOKIE_NAME = "msid.sample.session"; 13 | const STATE_COOKIE_NAME = "msid.sample.state"; 14 | 15 | const msalConfig = { 16 | auth: { 17 | clientId: 'Enter_the_Application_Id_Here', 18 | authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Id_Here', 19 | clientSecret: 'Enter_the_Client_Secret_Here', 20 | // clientCertificate: { 21 | // thumbprint: 'YOUR_CERT_THUMBPRINT', // replace with thumbprint obtained during step 2 above 22 | // privateKey: fs.readFileSync('PATH_TO_YOUR_PRIVATE_KEY_FILE'), // e.g. c:/Users/diego/Desktop/example.key 23 | // }, 24 | clientCapabilities: ['CP1'], // this let's the resource know this client is capable of handling claims challenges 25 | }, 26 | system: { 27 | loggerOptions: { 28 | loggerCallback(loglevel, message, containsPii) { 29 | console.log(message); 30 | }, 31 | piiLoggingEnabled: false, 32 | logLevel: msal.LogLevel.Verbose, 33 | }, 34 | }, 35 | }; 36 | 37 | module.exports = { 38 | msalConfig, 39 | REDIRECT_URI, 40 | POST_LOGOUT_REDIRECT_URI, 41 | GRAPH_ME_ENDPOINT, 42 | SESSION_COOKIE_NAME, 43 | STATE_COOKIE_NAME, 44 | }; 45 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-identity-bff", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "bootstrap": "^5.2.3", 10 | "react": "^18.2.0", 11 | "react-bootstrap": "^2.7.0", 12 | "react-dom": "^18.2.0", 13 | "react-router-dom": "^6.7.0", 14 | "react-scripts": "5.0.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Icon-identity-221 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | MSAL Express Webapp | Azure 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.svg", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { PageLayout } from './components/PageLayout'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | 4 | import { AuthProvider } from './context/AuthContext'; 5 | import { Home } from './pages/Home'; 6 | import { Profile } from './pages/Profile'; 7 | 8 | import './styles/App.css'; 9 | 10 | const Pages = () => { 11 | return ( 12 | 13 | } /> 14 | } /> 15 | 16 | ); 17 | }; 18 | 19 | const App = () => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default App; -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/components/NavigationBar.jsx: -------------------------------------------------------------------------------- 1 | import { Nav, Navbar, Button } from 'react-bootstrap'; 2 | 3 | export const NavigationBar = ({ account, login, logout }) => { 4 | return ( 5 | <> 6 | 7 | 8 | Microsoft identity platform 9 | 10 | {account ? ( 11 | <> 12 | 13 | Profile 14 | 15 |
16 | 25 |
26 | 27 | ) : ( 28 | <> 29 |
30 | 39 |
40 | 41 | )} 42 |
43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/components/PageLayout.jsx: -------------------------------------------------------------------------------- 1 | import { NavigationBar } from "./NavigationBar"; 2 | import { useAuth } from '../context/AuthContext'; 3 | 4 | 5 | export const PageLayout = (props) => { 6 | /** 7 | * Most applications will need to conditionally render certain components based on whether a user is signed in or not. 8 | * msal-react provides 2 easy ways to do this. AuthenticatedTemplate and UnauthenticatedTemplate components will 9 | * only render their children if a user is authenticated or unauthenticated, respectively. 10 | */ 11 | const { account, login, logout } = useAuth(); 12 | 13 | return ( 14 | <> 15 | 16 |
17 |
18 |
Microsoft Authentication Library For React - Tutorial
19 |
20 |
21 | {props.children} 22 |
23 | {account && account != null ? ( 24 | 37 | ) : null} 38 | 39 | ); 40 | }; -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react'; 2 | 3 | export const AuthContext = React.createContext(); 4 | export const useAuth = () => useContext(AuthContext); 5 | 6 | export const AuthProvider = ({ children }) => { 7 | const [account, setAccount] = useState(null); 8 | const [isLoading, setIsLoading] = useState(true); 9 | const [isAuthenticated, setIsAuthenticated] = useState(false); 10 | 11 | const login = (postLoginRedirectUri, scopesToConsent) => { 12 | let url = "/auth/login"; 13 | 14 | const searchParams = new URLSearchParams({}); 15 | 16 | if (postLoginRedirectUri) { 17 | searchParams.append('postLoginRedirectUri', encodeURIComponent(postLoginRedirectUri)); 18 | } 19 | 20 | if (scopesToConsent) { 21 | searchParams.append('scopesToConsent', encodeURIComponent(scopesToConsent)); 22 | } 23 | 24 | url = `${url}?${searchParams.toString()}`; 25 | 26 | window.location.replace(url); 27 | } 28 | 29 | const logout = (postLogoutRedirectUri) => { 30 | setIsAuthenticated(false); 31 | setAccount(null); 32 | 33 | let url = "/auth/logout"; 34 | 35 | const searchParams = new URLSearchParams({}); 36 | 37 | if (postLogoutRedirectUri) { 38 | searchParams.append('postLoginRedirectUri', encodeURIComponent(postLogoutRedirectUri)); 39 | } 40 | 41 | url = `${url}?${searchParams.toString()}`; 42 | 43 | window.location.replace(url); 44 | } 45 | 46 | const getAccount = async () => { 47 | const response = await fetch('/auth/account'); 48 | const data = await response.json(); 49 | setIsAuthenticated(data ? true : false); 50 | setAccount(data); 51 | setIsLoading(false); 52 | } 53 | 54 | useEffect(() => { 55 | getAccount(); 56 | }, []) 57 | 58 | return ( 59 | 68 | {children} 69 | 70 | ); 71 | }; -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | 5 | import App from './App'; 6 | 7 | import 'bootstrap/dist/css/bootstrap.min.css'; 8 | import './styles/index.css'; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById('root')); 11 | root.render( 12 | 13 | 14 | 15 | 16 | 17 | ); -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import { Container } from 'react-bootstrap'; 2 | import { IdTokenData } from '../components/DataDisplay'; 3 | import { useAuth } from '../context/AuthContext'; 4 | 5 | export const Home = () => { 6 | const { account } = useAuth(); 7 | 8 | return ( 9 | <> 10 | 11 | {account ? : null} 12 | 13 | 14 | ); 15 | }; -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/pages/Profile.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { ProfileData } from '../components/DataDisplay'; 3 | import { useAuth } from "../context/AuthContext"; 4 | 5 | export const Profile = () => { 6 | const [graphData, setGraphData] = useState(null); 7 | const { account, login } = useAuth(); 8 | 9 | const fetchProfileData = async () => { 10 | try { 11 | const response = await fetch('/auth/profile'); 12 | 13 | if (response.ok) { 14 | const resData = await response.json(); 15 | setGraphData(resData); 16 | } else if (response.status === 401) { 17 | const errorData = await response.json(); 18 | 19 | if (errorData.scopes) { 20 | // if the error response contain scopes, pass them to the next login request 21 | login(window.location.href, errorData.scopes); 22 | } else { 23 | login(window.location.href); 24 | } 25 | } 26 | } 27 | catch (error) { 28 | console.log(error); 29 | } 30 | } 31 | 32 | useEffect(() => { 33 | if (!!graphData) { 34 | return; 35 | } 36 | 37 | fetchProfileData(); 38 | }, [account, graphData]); 39 | 40 | return <>{graphData ? : null}; 41 | }; -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/styles/App.css: -------------------------------------------------------------------------------- 1 | footer { 2 | position: fixed; 3 | bottom: 5%; 4 | width: 100%; 5 | } 6 | 7 | .footer-text { 8 | font-size: small; 9 | text-align: center; 10 | flex: 1 1 auto; 11 | } 12 | 13 | .App { 14 | text-align: center; 15 | } 16 | 17 | .data-area-div { 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: space-evenly 22 | } 23 | 24 | .todo-form { 25 | width: 60%; 26 | } 27 | 28 | .todo-list { 29 | width: 60%; 30 | } 31 | 32 | .todo-label { 33 | font-size: large; 34 | margin-right: 22%; 35 | margin-left: 3%; 36 | } 37 | 38 | .todo-view-btn { 39 | float: right; 40 | } 41 | 42 | .navbarStyle { 43 | padding: .5rem 1rem !important 44 | } 45 | 46 | .navbarButton { 47 | color: #fff !important; 48 | padding: .5rem 1rem !important; 49 | } 50 | 51 | .iconText { 52 | margin: 0 .5rem 53 | } 54 | 55 | .tableColumn { 56 | word-break: break-all 57 | } 58 | 59 | .table { 60 | max-height: 37rem; 61 | } -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/client/src/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | td:first-child { 16 | font-weight: bold 17 | } -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const getGraphClient = require('../utils/graphClient'); 2 | const { handleAnyClaimsChallenge, setClaims } = require('../utils/claimUtils'); 3 | 4 | const { 5 | msalConfig, 6 | REDIRECT_URI, 7 | POST_LOGOUT_REDIRECT_URI, 8 | GRAPH_ME_ENDPOINT 9 | } = require('../authConfig'); 10 | 11 | const AuthProvider = require("../auth/AuthProvider"); 12 | 13 | const authProvider = new AuthProvider({ 14 | msalConfig: msalConfig, 15 | redirectUri: REDIRECT_URI, 16 | postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI, 17 | }); 18 | 19 | exports.loginUser = async (req, res, next) => { 20 | let postLoginRedirectUri; 21 | let scopesToConsent; 22 | 23 | if (req.query && req.query.postLoginRedirectUri) { 24 | postLoginRedirectUri = decodeURIComponent(req.query.postLoginRedirectUri); 25 | } 26 | 27 | if (req.query && req.query.scopesToConsent) { 28 | scopesToConsent = decodeURIComponent(req.query.scopesToConsent); 29 | } 30 | 31 | return authProvider.login(req, res, next, { postLoginRedirectUri, scopesToConsent }); 32 | } 33 | 34 | exports.handleRedirect = async (req, res, next) => { 35 | return authProvider.handleRedirect(req, res, next); 36 | } 37 | 38 | exports.logoutUser = async (req, res, next) => { 39 | return authProvider.logout(req, res, next); 40 | } 41 | 42 | exports.getAccount = async (req, res, next) => { 43 | const account = authProvider.getAccount(req, res, next); 44 | res.status(200).json(account); 45 | } 46 | 47 | exports.getProfile = async (req, res, next) => { 48 | if (!authProvider.isAuthenticated(req, res, next)) { 49 | return res.status(401).json({ error: 'unauthorized' }); 50 | } 51 | 52 | try { 53 | const tokenResponse = await authProvider.acquireToken(req, res, next, { scopes: ['User.Read']}); 54 | const graphResponse = await getGraphClient(tokenResponse.accessToken).api('/me').responseType('raw').get(); 55 | const graphData = await handleAnyClaimsChallenge(graphResponse); 56 | 57 | res.status(200).json(graphData); 58 | } catch (error) { 59 | if (error.name === 'ClaimsChallengeAuthError') { 60 | setClaims(req.session, msalConfig.auth.clientId, GRAPH_ME_ENDPOINT, error.payload); 61 | return res.status(401).json({ error: error.name }); 62 | } 63 | 64 | if (error.name === 'InteractionRequiredAuthError') { 65 | return res.status(401).json({ error: error.name, scopes: error.payload }); 66 | } 67 | 68 | next(error); 69 | } 70 | } -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node app.js", 9 | "preinstall": "cd ./client && npm install", 10 | "prestart": "cd ./client && npm run build" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@azure/msal-node": "^2.7.0", 17 | "@microsoft/microsoft-graph-client": "^3.0.7", 18 | "axios": "^1.6.8", 19 | "cookie-parser": "^1.4.6", 20 | "express": "^4.19.2", 21 | "express-session": "^1.18.0", 22 | "helmet": "^7.1.0", 23 | "isomorphic-fetch": "^3.0.0", 24 | "lusca": "^1.7.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/routes/mainRoutes.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | 4 | const authController = require("../controllers/authController") 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/auth/login', authController.loginUser); 9 | router.get('/auth/logout', authController.logoutUser) 10 | router.post('/auth/redirect', authController.handleRedirect); 11 | router.get('/auth/account', authController.getAccount); 12 | router.get('/auth/profile', authController.getProfile); 13 | 14 | router.get('*', (req, res) => { 15 | res.sendFile(path.join(__dirname, '../client/build/index.html')); 16 | }); 17 | 18 | module.exports = router -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/App/utils/graphClient.js: -------------------------------------------------------------------------------- 1 | const graph = require('@microsoft/microsoft-graph-client'); 2 | require('isomorphic-fetch'); 3 | 4 | /** 5 | * Creating a Graph client instance via options method. For more information, visit: 6 | * https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options 7 | */ 8 | const getGraphClient = (accessToken) => { 9 | // Initialize Graph client 10 | const client = graph.Client.init({ 11 | // Use the provided access token to authenticate requests 12 | authProvider: (done) => { 13 | done(null, accessToken); 14 | }, 15 | }); 16 | 17 | return client; 18 | }; 19 | 20 | module.exports = getGraphClient; -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "A React SPA with a Node.Js (Express) back-end using the Backend For Frontend (BFF) Proxy pattern to authenticate users with Azure AD and calling Microsoft Graph", 4 | "Level": 300, 5 | "Client": "React SPA with Express backend", 6 | "Service": "MS Graph", 7 | "RepositoryUrl": "ms-identity-javascript-nodejs-tutorial", 8 | "Endpoint": "AAD v2.0", 9 | "Platform": "JavaScript", 10 | "SDK":"MsalNode", 11 | "Languages": ["javascript"], 12 | "Description": "A React SPA with a Node.Js (Express) back-end using the Backend For Frontend (BFF) Proxy pattern to authenticate users with Azure AD and calling Microsoft Graph on the user's behalf", 13 | "products": ["azure-active-directory", "msal-js", "msal-node", "ms-graph"] 14 | }, 15 | "AADApps": [ 16 | { 17 | "Id": "service", 18 | "Name": "msal-node-webapp", 19 | "Kind": "WebApp", 20 | "Audience": "AzureADMyOrg", 21 | "HomePage": "http://localhost:3000", 22 | "ReplyUrls": "http://localhost:3000/auth/redirect", 23 | "SampleSubPath": "5-AdvancedScenarios\\1-call-graph-bff\\App", 24 | "Sdk": "MsalNode", 25 | "Certificate": "Auto", 26 | "PasswordCredentials": "Auto", 27 | "ManualSteps": [], 28 | "OptionalClaims": { 29 | "IdTokenClaims": ["acct"] 30 | }, 31 | "RequiredResourcesAccess": [ 32 | { 33 | "Resource": "Microsoft Graph", 34 | "DelegatedPermissions": ["User.Read"] 35 | } 36 | ] 37 | } 38 | ], 39 | "CodeConfiguration": [ 40 | { 41 | "App": "service", 42 | "SettingKind": "Replace", 43 | "SettingFile": "\\..\\APP\\authConfig.js", 44 | "Mappings": [ 45 | { 46 | "key": "Enter_the_Application_Id_Here", 47 | "value": ".AppId" 48 | }, 49 | { 50 | "key": "Enter_the_Tenant_Id_Here", 51 | "value": "$tenantId" 52 | }, 53 | { 54 | "key": "Enter_the_Client_Secret_Here", 55 | "value": "service.AppKey" 56 | }, 57 | { 58 | "key": "YOUR_CERT_THUMBPRINT", 59 | "value": "$thumbprint" 60 | }, 61 | { 62 | "key": "PATH_TO_YOUR_PRIVATE_KEY_FILE", 63 | "value": "\"../AppCreationScripts/\"+$certificateName+\".key\"" 64 | } 65 | ] 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/5-AdvancedScenarios/1-call-graph-bff/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /5-AdvancedScenarios/1-call-graph-bff/ReadmeFiles/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/73e71223bd33720975bbed2b6518d4d8e2730913/5-AdvancedScenarios/1-call-graph-bff/ReadmeFiles/sequence.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 07/23/2022 4 | 5 | * Updated samples to the new version of wrapper library 6 | 7 | ## 10/11/2021 8 | 9 | * Updated samples to the new version of wrapper library 10 | 11 | ## 08/07/2021 12 | 13 | * Updated msal and wrapper 14 | * Added smoke tests 15 | * Added GitHub workflows 16 | * Removed improper cache implementation from all samples 17 | 18 | ## 05/15/2021 19 | 20 | * Initial sample 21 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | e2e 4 | docs 5 | jest.config.js -------------------------------------------------------------------------------- /Common/msal-node-wrapper/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "modules": true 7 | } 8 | }, 9 | "plugins": [ 10 | "@typescript-eslint", 11 | "security", 12 | "import", 13 | "header" 14 | ], 15 | "extends": [ 16 | "plugin:@typescript-eslint/eslint-recommended", 17 | "plugin:@typescript-eslint/recommended", 18 | "plugin:import/typescript" 19 | ], 20 | "rules": { 21 | "header/header": [2, "block", [ "", " * Copyright (c) Microsoft Corporation. All rights reserved.", " * Licensed under the MIT License.", " " ], 2], 22 | "@typescript-eslint/array-type": 0, 23 | "@typescript-eslint/ban-ts-comment": 0, 24 | "@typescript-eslint/ban-types": 0, 25 | "@typescript-eslint/camelcase": 0, 26 | "@typescript-eslint/explicit-member-accessibility": 0, 27 | "@typescript-eslint/explicit-module-boundary-types": 2, 28 | "@typescript-eslint/indent": [2, 4, { "SwitchCase": 1 }], 29 | "@typescript-eslint/interface-name-prefix": 0, 30 | "@typescript-eslint/member-delimiter-style": 0, 31 | "@typescript-eslint/no-angle-bracket-type-assertion": 0, 32 | "@typescript-eslint/no-empty-function": 1, 33 | "@typescript-eslint/no-explicit-any": 2, 34 | "@typescript-eslint/no-inferrable-types": 0, 35 | "@typescript-eslint/no-non-null-assertion": 2, 36 | "@typescript-eslint/no-object-literal-type-assertion": 0, 37 | "@typescript-eslint/no-unused-vars": 2, 38 | "@typescript-eslint/prefer-interface": 0, 39 | "@typescript-eslint/semi": 2, 40 | "@typescript-eslint/type-annotation-spacing": 0, 41 | "import/first": 2, 42 | "import/no-commonjs": 2, 43 | "import/no-duplicates": 2, 44 | "import/no-unresolved": 2, 45 | "import/no-unused-modules": 2, 46 | "import/no-useless-path-segments": 2, 47 | "max-len": [0, { "code": 130, "ignoreComments": false, "ignoreRegExpLiterals": true, "ignoreTrailingComments": false, "ignoreUrls": true}], 48 | "multiline-comment-style": [2, "starred-block" ], 49 | "no-console": 2, 50 | "no-multiple-empty-lines": [2, { "max": 1 }], 51 | "no-param-reassign": 2, 52 | "no-var": 2, 53 | "prefer-const": 2, 54 | "quotes": [2, "double"], 55 | "security/detect-non-literal-fs-filename": 0, 56 | "security/detect-object-injection": 0, 57 | "spaced-comment": 2, 58 | "eol-last": 2, 59 | "eqeqeq": 2 60 | }, 61 | "root": true 62 | } -------------------------------------------------------------------------------- /Common/msal-node-wrapper/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | # e2e test screenshots 106 | screenshots/ 107 | 108 | # distributables 109 | dist/ 110 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | module.exports = { 7 | verbose: true, 8 | moduleFileExtensions: [ 9 | "ts", 10 | "tsx", 11 | "js", 12 | "json", 13 | "jsx", 14 | "node" 15 | ], 16 | testMatch: [ 17 | "/test/**/*.spec.ts" 18 | ], 19 | transform: { 20 | "^.+\\.(ts|tsx)$": "ts-jest", 21 | }, 22 | coverageReporters: [["lcov", { "projectRoot": "./" }]] 23 | }; 24 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-beta", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "files": [ 7 | "dist" 8 | ], 9 | "engines": { 10 | "node": "16 || 18 || 20 || 22" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial.git" 15 | }, 16 | "scripts": { 17 | "build": "rollup -c --strictDeprecations --bundleConfigAsCjs", 18 | "build:watch": "rollup -c --watch --strictDeprecations --bundleConfigAsCjs", 19 | "test": "jest", 20 | "test:watch": "jest --watch", 21 | "test:coverage": "jest --coverage", 22 | "lint": "eslint . --ext .ts", 23 | "lint:fix": "npm run lint -- --fix", 24 | "docs": "typedoc" 25 | }, 26 | "name": "msal-node-wrapper", 27 | "author": "Microsoft", 28 | "module": "dist/msal-node-wrapper.esm.js", 29 | "devDependencies": { 30 | "@rollup/plugin-node-resolve": "^15.0.1", 31 | "@rollup/plugin-typescript": "^11.0.0", 32 | "@types/express": "^4.17.13", 33 | "@types/express-session": "^1.17.4", 34 | "@types/jest": "^25.2.3", 35 | "@types/node": "^13.13.4", 36 | "@types/sinon": "^10.0.14", 37 | "@typescript-eslint/eslint-plugin": "^5.54.1", 38 | "@typescript-eslint/parser": "^5.54.1", 39 | "eslint": "^8.35.0", 40 | "eslint-plugin-header": "^3.1.1", 41 | "eslint-plugin-import": "^2.27.5", 42 | "eslint-plugin-security": "^1.7.1", 43 | "jest": "^29.5.0", 44 | "rollup": "^3.20.1", 45 | "sinon": "^15.0.4", 46 | "supertest": "^6.1.6", 47 | "ts-jest": "^29.0.5", 48 | "tslib": "^1.10.0", 49 | "typedoc": "^0.23.28", 50 | "typescript": "^4.9.5" 51 | }, 52 | "dependencies": { 53 | "@azure/msal-node": "^2.7.0", 54 | "axios": "^1.6.8", 55 | "express": "^4.19.2", 56 | "express-session": "^1.17.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | /* eslint-disable import/no-commonjs */ 7 | 8 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 9 | import typescript from "@rollup/plugin-typescript"; 10 | import pkg from "./package.json"; 11 | 12 | const libraryHeader = `/*! ${pkg.name} v${pkg.version} ${new Date().toISOString().split("T")[0]} */`; 13 | const useStrictHeader = "'use strict';"; 14 | const fileHeader = `${libraryHeader}\n${useStrictHeader}`; 15 | 16 | export default [ 17 | { 18 | // for cjs build 19 | input: "src/index.ts", 20 | output: { 21 | dir: "dist", 22 | format: "cjs", 23 | preserveModules: true, 24 | preserveModulesRoot: "src", 25 | banner: fileHeader, 26 | sourcemap: true 27 | }, 28 | treeshake: { 29 | moduleSideEffects: false, 30 | propertyReadSideEffects: false 31 | }, 32 | external: [ 33 | ...Object.keys(pkg.dependencies || {}), 34 | ...Object.keys(pkg.peerDependencies || {}) 35 | ], 36 | plugins: [ 37 | typescript({ 38 | typescript: require("typescript"), 39 | tsconfig: "tsconfig.build.json" 40 | }), 41 | nodeResolve() 42 | ] 43 | }, 44 | { 45 | // for esm build 46 | input: "src/index.ts", 47 | output: { 48 | dir: "dist", 49 | format: "esm", 50 | entryFileNames: "[name].esm.js", 51 | preserveModules: true, 52 | preserveModulesRoot: "src", 53 | banner: fileHeader, 54 | sourcemap: true 55 | }, 56 | treeshake: { 57 | moduleSideEffects: false, 58 | propertyReadSideEffects: false 59 | }, 60 | external: [ 61 | ...Object.keys(pkg.dependencies || {}), 62 | ...Object.keys(pkg.peerDependencies || {}) 63 | ], 64 | plugins: [ 65 | typescript({ 66 | typescript: require("typescript"), 67 | tsconfig: "tsconfig.build.json" 68 | }), 69 | nodeResolve() 70 | ] 71 | } 72 | 73 | ]; 74 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/config/ConfigurationTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { NodeAuthOptions, NodeSystemOptions, CacheOptions } from "@azure/msal-node"; 7 | 8 | export type AuthConfig = { 9 | auth: Omit; 10 | system?: NodeSystemOptions, 11 | cache?: CacheOptions 12 | }; 13 | 14 | export type WebAppAuthConfig = AuthConfig & { 15 | auth: NodeAuthOptions & AuthRoutes; 16 | }; 17 | 18 | export type AuthRoutes = { 19 | redirectUri: string; 20 | frontChannelLogoutUri?: string; 21 | postLogoutRedirectUri?: string; 22 | }; 23 | 24 | export type ProtectedResourcesMap = Record; 25 | 26 | export type ProtectedResourceParams = { 27 | scopes: Array, 28 | routes: Array 29 | }; 30 | 31 | export enum AppType { 32 | WebApp 33 | } 34 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/error/AccessDeniedError.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { AuthError, AccountInfo } from "@azure/msal-node"; 7 | 8 | /** 9 | * Contains string constants used by error codes and messages. 10 | */ 11 | export const AccessDeniedErrorMessage = { 12 | unauthorizedAccessError: { 13 | code: "401", 14 | desc: "Unauthorized" 15 | }, 16 | forbiddenAccessError: { 17 | code: "403", 18 | desc: "Forbidden" 19 | } 20 | }; 21 | 22 | /** 23 | * Error thrown when the user is not authorized to access a route 24 | */ 25 | export class AccessDeniedError extends AuthError { 26 | route?: string; 27 | account?: AccountInfo; 28 | 29 | constructor(errorCode: string, errorMessage?: string, route?: string, account?: AccountInfo) { 30 | super(errorCode, errorMessage); 31 | this.name = "AccessDeniedError"; 32 | this.route = route; 33 | this.account = account; 34 | 35 | Object.setPrototypeOf(this, AccessDeniedError.prototype); 36 | } 37 | 38 | /** 39 | * Creates an error when access is unauthorized 40 | * 41 | * @returns {AccessDeniedError} Empty issuer error 42 | */ 43 | static createUnauthorizedAccessError(route?: string, account?: AccountInfo): AccessDeniedError { 44 | return new AccessDeniedError( 45 | AccessDeniedErrorMessage.unauthorizedAccessError.code, 46 | AccessDeniedErrorMessage.unauthorizedAccessError.desc, 47 | route, 48 | account 49 | ); 50 | } 51 | 52 | /** 53 | * Creates an error when the access is forbidden 54 | * 55 | * @returns {AccessDeniedError} Empty issuer error 56 | */ 57 | static createForbiddenAccessError(route?: string, account?: AccountInfo): AccessDeniedError { 58 | return new AccessDeniedError( 59 | AccessDeniedErrorMessage.forbiddenAccessError.code, 60 | AccessDeniedErrorMessage.forbiddenAccessError.desc, 61 | route, 62 | account 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/error/InteractionRequiredError.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { InteractionRequiredAuthError } from "@azure/msal-node"; 7 | import { LoginOptions, TokenRequestOptions } from "../middleware/MiddlewareOptions"; 8 | 9 | /** 10 | * Error thrown when user interaction is required. 11 | */ 12 | export class InteractionRequiredError extends InteractionRequiredAuthError { 13 | requestOptions: LoginOptions; 14 | 15 | constructor(errorCode: string, errorMessage?: string, subError?: string, originalRequest?: TokenRequestOptions) { 16 | super(errorCode, errorMessage, subError); 17 | this.name = "InteractionRequiredError"; 18 | this.requestOptions = { 19 | scopes: originalRequest?.scopes || [], 20 | claims: originalRequest?.claims, 21 | state: originalRequest?.state, 22 | sid: originalRequest?.sid, 23 | loginHint: originalRequest?.loginHint, 24 | domainHint: originalRequest?.domainHint, 25 | extraQueryParameters: originalRequest?.extraQueryParameters, 26 | extraScopesToConsent: originalRequest?.extraScopesToConsent, 27 | tokenBodyParameters: originalRequest?.tokenBodyParameters, 28 | tokenQueryParameters: originalRequest?.tokenQueryParameters, 29 | }; 30 | 31 | Object.setPrototypeOf(this, InteractionRequiredError.prototype); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { AccountInfo, AuthenticationResult, AuthorizationCodeRequest } from "@azure/msal-node"; 7 | import { AuthContext } from "./middleware/context/AuthContext"; 8 | 9 | declare module "express-session" { 10 | interface SessionData { 11 | account: AccountInfo; 12 | isAuthenticated: boolean; 13 | protectedResources: Record 14 | tokenRequestParams: AuthorizationCodeRequest; 15 | tokenCache?: string, 16 | } 17 | } 18 | 19 | declare global { 20 | // eslint-disable-next-line @typescript-eslint/no-namespace 21 | namespace Express { 22 | export interface Request { 23 | authContext: AuthContext; 24 | } 25 | } 26 | } 27 | 28 | export { 29 | InteractionRequiredAuthError, 30 | NodeSystemOptions, 31 | AuthError, 32 | Logger, 33 | AccountInfo 34 | } from "@azure/msal-node"; 35 | 36 | export { WebAppAuthProvider } from "./provider/WebAppAuthProvider"; 37 | export { AuthContext, RequestContext } from "./middleware/context/AuthContext"; 38 | 39 | export { 40 | WebAppAuthConfig, 41 | AuthConfig, 42 | AuthRoutes, 43 | ProtectedResourceParams, 44 | ProtectedResourcesMap, 45 | } from "./config/ConfigurationTypes"; 46 | 47 | export { 48 | RouteGuardOptions, 49 | AuthenticateMiddlewareOptions, 50 | LoginOptions, 51 | LogoutOptions, 52 | TokenRequestOptions, 53 | AppState, 54 | IdTokenClaims, 55 | } from "./middleware/MiddlewareOptions"; 56 | 57 | export { AccessDeniedError } from "./error/AccessDeniedError"; 58 | 59 | export { InteractionRequiredError } from "./error/InteractionRequiredError"; 60 | 61 | export { packageVersion } from "./packageMetadata"; 62 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/middleware/MiddlewareOptions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { CommonEndSessionRequest, TokenClaims } from "@azure/msal-common"; 7 | import { AuthorizationUrlRequest, AuthorizationCodeRequest, AccountInfo } from "@azure/msal-node"; 8 | import { ProtectedResourcesMap } from "../config/ConfigurationTypes"; 9 | 10 | export type AuthenticateMiddlewareOptions = { 11 | protectAllRoutes?: boolean; 12 | acquireTokenForResources?: ProtectedResourcesMap 13 | }; 14 | 15 | export type LoginOptions = Pick & Pick & { 16 | postLoginRedirectUri?: string; 17 | postFailureRedirectUri?: string; 18 | }; 19 | 20 | export type LogoutOptions = Pick & { 21 | postLogoutRedirectUri?: string; 22 | idpLogout?: boolean; 23 | clearCache?: boolean; 24 | }; 25 | 26 | export type TokenRequestOptions = LoginOptions & { 27 | account?: AccountInfo; 28 | }; 29 | 30 | export type TokenRequestMiddlewareOptions = { 31 | resourceName: string; 32 | }; 33 | 34 | export type RouteGuardOptions = { 35 | forceLogin?: boolean; 36 | postLoginRedirectUri?: string; 37 | postFailureRedirectUri?: string; 38 | idTokenClaims?: IdTokenClaims; 39 | }; 40 | 41 | export type AppState = { 42 | redirectTo: string; 43 | customState?: string; 44 | }; 45 | 46 | export type IdTokenClaims = TokenClaims & { 47 | aud?: string; 48 | roles?: string[]; 49 | groups?: string[]; 50 | _claim_names?: string[]; 51 | _claim_sources?: string[]; 52 | xms_cc?: string; 53 | acrs?: string[]; 54 | [key: string]: string | number | string[] | object | undefined | unknown; 55 | }; 56 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/middleware/errorMiddleware.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { Request, Response, NextFunction, ErrorRequestHandler } from "express"; 7 | import { WebAppAuthProvider } from "../provider/WebAppAuthProvider"; 8 | import { InteractionRequiredError } from "../error/InteractionRequiredError"; 9 | 10 | function errorMiddleware(this: WebAppAuthProvider): ErrorRequestHandler { 11 | return (err: unknown, req: Request, res: Response, next: NextFunction): Response | void => { 12 | if (err instanceof InteractionRequiredError) { 13 | return req.authContext.login({ 14 | postLoginRedirectUri: err.requestOptions.postLoginRedirectUri || req.originalUrl, 15 | ...err.requestOptions 16 | })(req, res, next); 17 | } 18 | 19 | next(err); 20 | }; 21 | } 22 | 23 | export default errorMiddleware; 24 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/middleware/guardMiddleware.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { Request, Response, NextFunction, RequestHandler } from "express"; 7 | import { WebAppAuthProvider } from "../provider/WebAppAuthProvider"; 8 | import { RouteGuardOptions } from "./MiddlewareOptions"; 9 | import { AccessDeniedError } from "../error/AccessDeniedError"; 10 | 11 | function guardMiddleware( 12 | this: WebAppAuthProvider, 13 | options: RouteGuardOptions 14 | ): RequestHandler { 15 | return (req: Request, res: Response, next: NextFunction): void | Response => { 16 | if (!req.authContext.isAuthenticated()) { 17 | if (options.forceLogin) { 18 | return req.authContext.login({ 19 | postLoginRedirectUri: req.originalUrl, 20 | scopes: [], 21 | })(req, res, next); 22 | } 23 | 24 | return next(AccessDeniedError.createUnauthorizedAccessError(req.originalUrl, req.authContext.getAccount())); 25 | } 26 | 27 | if (options.idTokenClaims) { 28 | const tokenClaims = req.authContext.getAccount()?.idTokenClaims || {}; 29 | const requiredClaims = options.idTokenClaims; 30 | 31 | const hasClaims = Object.keys(requiredClaims).every((claim: string) => { 32 | if (requiredClaims[claim] && tokenClaims[claim]) { 33 | switch (typeof requiredClaims[claim]) { 34 | case "string" || "number": 35 | return requiredClaims[claim] === tokenClaims[claim]; 36 | 37 | case "object": 38 | if (Array.isArray(requiredClaims[claim])) { 39 | const requiredClaimsArray = requiredClaims[claim] as []; 40 | const tokenClaimsArray = tokenClaims[claim] as []; 41 | 42 | return requiredClaimsArray.some( 43 | (requiredClaim) => tokenClaimsArray.indexOf(requiredClaim) >= 0 44 | ); 45 | } 46 | break; 47 | 48 | default: 49 | break; 50 | } 51 | } 52 | 53 | return false; 54 | }); 55 | 56 | if (!hasClaims) { 57 | return next(AccessDeniedError.createForbiddenAccessError(req.originalUrl, req.authContext.getAccount())); 58 | } 59 | } 60 | 61 | next(); 62 | }; 63 | } 64 | 65 | export default guardMiddleware; 66 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/middleware/handlers/loginHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { Request, Response, NextFunction, RequestHandler } from "express"; 7 | import { ResponseMode } from "@azure/msal-common"; 8 | import { AuthorizationCodeRequest, AuthorizationUrlRequest } from "@azure/msal-node"; 9 | import { WebAppAuthProvider } from "../../provider/WebAppAuthProvider"; 10 | import { LoginOptions, AppState } from "../MiddlewareOptions"; 11 | import { UrlUtils } from "../../utils/UrlUtils"; 12 | import { EMPTY_STRING } from "../../utils/Constants"; 13 | 14 | function loginHandler( 15 | this: WebAppAuthProvider, 16 | options: LoginOptions 17 | ): RequestHandler { 18 | return async (req: Request, res: Response, next: NextFunction): Promise => { 19 | this.getLogger().trace("loginHandler called"); 20 | 21 | const state: AppState = { 22 | redirectTo: options.postLoginRedirectUri || "/", 23 | customState: options.state 24 | }; 25 | 26 | const authUrlParams: AuthorizationUrlRequest = { 27 | state: this.getCryptoProvider().base64Encode(JSON.stringify(state)), 28 | redirectUri: UrlUtils.ensureAbsoluteUrl( 29 | this.webAppAuthConfig.auth.redirectUri, 30 | req.protocol, 31 | req.get("host") || req.hostname 32 | ), 33 | responseMode: ResponseMode.FORM_POST, 34 | scopes: options.scopes || [], 35 | prompt: options.prompt || undefined, 36 | claims: options.claims || undefined, 37 | account: options.account || undefined, 38 | sid: options.sid || undefined, 39 | loginHint: options.loginHint || undefined, 40 | domainHint: options.domainHint || undefined, 41 | extraQueryParameters: options.extraQueryParameters || undefined, 42 | extraScopesToConsent: options.extraScopesToConsent || undefined, 43 | }; 44 | 45 | req.session.tokenRequestParams = { 46 | scopes: authUrlParams.scopes, 47 | state: authUrlParams.state, 48 | redirectUri: authUrlParams.redirectUri, 49 | claims: authUrlParams.claims, 50 | tokenBodyParameters: options.tokenBodyParameters, 51 | tokenQueryParameters: options.tokenQueryParameters, 52 | code: EMPTY_STRING, 53 | } as AuthorizationCodeRequest; 54 | 55 | try { 56 | const response = await this.getMsalClient().getAuthCodeUrl(authUrlParams); 57 | res.redirect(response); 58 | } catch (error) { 59 | next(error); 60 | } 61 | }; 62 | } 63 | 64 | export default loginHandler; 65 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/middleware/handlers/logoutHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { Request, Response, RequestHandler } from "express"; 7 | import { WebAppAuthProvider } from "../../provider/WebAppAuthProvider"; 8 | import { LogoutOptions } from "../MiddlewareOptions"; 9 | import { UrlUtils } from "../../utils/UrlUtils"; 10 | 11 | function logoutHandler( 12 | this: WebAppAuthProvider, 13 | options: LogoutOptions 14 | ): RequestHandler { 15 | return async (req: Request, res: Response): Promise => { 16 | this.getLogger().trace("logoutHandler called"); 17 | 18 | const shouldLogoutFromIdp = options.idpLogout ? options.idpLogout : true; 19 | let logoutUri = options.postLogoutRedirectUri || "/"; 20 | 21 | const account = req.authContext.getAccount(); 22 | 23 | if (account) { 24 | try { 25 | const tokenCache = this.getMsalClient().getTokenCache(); 26 | const cachedAccount = await tokenCache.getAccountByHomeId(account.homeAccountId); 27 | 28 | if (cachedAccount) { 29 | await tokenCache.removeAccount(cachedAccount); 30 | } 31 | } catch (error) { 32 | this.logger.error(`Error occurred while clearing cache for user: ${JSON.stringify(error)}`); 33 | } 34 | } 35 | 36 | if (shouldLogoutFromIdp) { 37 | /** 38 | * Construct a logout URI and redirect the user to end the 39 | * session with Azure AD. For more information, visit: 40 | * (AAD) https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request 41 | * (B2C) https://docs.microsoft.com/azure/active-directory-b2c/openid-connect#send-a-sign-out-request 42 | */ 43 | 44 | const postLogoutRedirectUri = UrlUtils.ensureAbsoluteUrl( 45 | options.postLogoutRedirectUri || "/", 46 | req.protocol, 47 | req.get("host") || req.hostname 48 | ); 49 | 50 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 51 | logoutUri = `${UrlUtils.enforceTrailingSlash(this.getMsalConfig().auth.authority!)}/oauth2/v2.0/logout?post_logout_redirect_uri=${postLogoutRedirectUri}`; 52 | } 53 | 54 | req.session.destroy(() => { 55 | res.redirect(logoutUri); 56 | }); 57 | }; 58 | } 59 | 60 | export default logoutHandler; 61 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/middleware/handlers/redirectHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { Request, Response, NextFunction, RequestHandler } from "express"; 7 | import { StringUtils } from "@azure/msal-common"; 8 | import { AuthorizationCodePayload, AuthorizationCodeRequest } from "@azure/msal-node"; 9 | import { WebAppAuthProvider } from "../../provider/WebAppAuthProvider"; 10 | import { AppState } from "../MiddlewareOptions"; 11 | import { ErrorMessages } from "../../utils/Constants"; 12 | 13 | function redirectHandler(this: WebAppAuthProvider): RequestHandler { 14 | return async (req: Request, res: Response, next: NextFunction): Promise => { 15 | this.getLogger().trace("redirectHandler called"); 16 | 17 | if (!req.body || !req.body.code) { 18 | return next(new Error(ErrorMessages.AUTH_CODE_RESPONSE_NOT_FOUND)); 19 | } 20 | 21 | const tokenRequest = { 22 | ...req.session.tokenRequestParams, 23 | code: req.body.code as string 24 | } as AuthorizationCodeRequest; 25 | 26 | try { 27 | const msalInstance = this.getMsalClient(); 28 | 29 | if (req.session.tokenCache) { 30 | msalInstance.getTokenCache().deserialize(req.session.tokenCache); 31 | } 32 | 33 | const tokenResponse = await msalInstance.acquireTokenByCode( 34 | tokenRequest, 35 | req.body as AuthorizationCodePayload 36 | ); 37 | 38 | req.session.tokenCache = msalInstance.getTokenCache().serialize(); 39 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 40 | req.session.account = tokenResponse.account!; // account will never be null in this grant type 41 | req.session.isAuthenticated = true; 42 | 43 | const { redirectTo } = req.body.state ? 44 | StringUtils.jsonParseHelper( 45 | this.getCryptoProvider().base64Decode(req.body.state as string) 46 | ) as AppState 47 | : 48 | { redirectTo: "/" }; 49 | 50 | res.redirect(redirectTo); 51 | } catch (error) { 52 | next(error); 53 | } 54 | }; 55 | } 56 | 57 | export default redirectHandler; 58 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/packageMetadata.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | export const packageName = "msal-node-wrapper"; 7 | export const packageVersion = "beta"; 8 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/provider/BaseAuthProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { Logger } from "@azure/msal-common"; 7 | import { ConfidentialClientApplication, Configuration, CryptoProvider } from "@azure/msal-node"; 8 | import { AuthConfig } from "../config/ConfigurationTypes"; 9 | import { DEFAULT_LOGGER_OPTIONS } from "../utils/Constants"; 10 | import { packageName, packageVersion } from "../packageMetadata"; 11 | 12 | export abstract class BaseAuthProvider { 13 | protected authConfig: AuthConfig; 14 | protected msalConfig: Configuration; 15 | protected cryptoProvider: CryptoProvider; 16 | protected logger: Logger; 17 | 18 | protected constructor(authConfig: AuthConfig, msalConfig: Configuration) { 19 | this.authConfig = authConfig; 20 | this.msalConfig = msalConfig; 21 | this.cryptoProvider = new CryptoProvider(); 22 | this.logger = new Logger( 23 | this.msalConfig.system?.loggerOptions || DEFAULT_LOGGER_OPTIONS, 24 | packageName, 25 | packageVersion 26 | ); 27 | } 28 | 29 | getAuthConfig(): AuthConfig { 30 | return this.authConfig; 31 | } 32 | 33 | getMsalConfig(): Configuration { 34 | return this.msalConfig; 35 | } 36 | 37 | getCryptoProvider(): CryptoProvider { 38 | return this.cryptoProvider; 39 | } 40 | 41 | getLogger(): Logger { 42 | return this.logger; 43 | } 44 | 45 | getMsalClient(): ConfidentialClientApplication { 46 | return new ConfidentialClientApplication(this.msalConfig); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/src/utils/UrlUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { IUri, UrlString } from "@azure/msal-common"; 7 | import { Request } from "express"; 8 | 9 | export class UrlUtils { 10 | /** 11 | * Returns the absolute URL from a given request and path string 12 | * @param {string} url: a given URL 13 | * @param {string} protocol: protocol of the request 14 | * @param {string} host: host of the request 15 | * @returns {string} 16 | */ 17 | static ensureAbsoluteUrl = (url: string, protocol: string, host: string): string => { 18 | const urlComponents: IUri = new UrlString(url).getUrlComponents(); 19 | 20 | if (!urlComponents.Protocol) { 21 | if (!urlComponents.HostNameAndPort && !url.startsWith("www")) { 22 | if (!url.startsWith("/")) { 23 | return protocol + "://" + host + "/" + url; 24 | } 25 | return protocol + "://" + host + url; 26 | } 27 | return protocol + "://" + url; 28 | } else { 29 | return url; 30 | } 31 | }; 32 | 33 | /** 34 | * Given a URL string, ensures that it is an absolute URL 35 | * @param {Request} req: Express request object 36 | * @param {string} url: a given URL 37 | * @returns {string} 38 | */ 39 | static ensureAbsoluteUrlFromRequest = (req: Request, url?: string): string => { 40 | if (url) { 41 | return UrlUtils.ensureAbsoluteUrl(url, req.protocol, req.get("host") || req.hostname); 42 | } else { 43 | return UrlUtils.ensureAbsoluteUrl(req.originalUrl, req.protocol, req.get("host") || req.hostname); 44 | } 45 | }; 46 | 47 | /** 48 | * Checks if the URL from a given request matches a given URL 49 | * @param {Request} req: Express request object 50 | * @param {string} url: a given URL 51 | * @returns {boolean} 52 | */ 53 | static checkIfRequestsMatch = (req: Request, url: string): boolean => { 54 | return UrlUtils.ensureAbsoluteUrlFromRequest(req) === UrlUtils.ensureAbsoluteUrlFromRequest(req, url); 55 | }; 56 | 57 | /** 58 | * Returns the path segment from a given URL 59 | * @param {string} url: a given URL 60 | * @returns {string} 61 | */ 62 | static getPathFromUrl = (url: string): string => { 63 | const urlComponents: IUri = new UrlString(url).getUrlComponents(); 64 | return `/${urlComponents.PathSegments.join("/")}`; 65 | }; 66 | 67 | /** 68 | * Ensures that the path contains a leading slash at the start 69 | * @param {string} path: a given path 70 | * @returns {string} 71 | */ 72 | static enforceLeadingSlash = (path: string): string => { 73 | return path.split("")[0] === "/" ? path : "/" + path; 74 | }; 75 | 76 | /** 77 | * Ensures that the URL contains a trailing slash at the end 78 | * @param {string} url: a given path 79 | * @returns {string} 80 | */ 81 | static enforceTrailingSlash = (url: string): string => { 82 | return url.endsWith("/") ? url : url + "/"; 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/test/config/ConfigurationHelper.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { ConfigurationHelper } from "../../src/config/ConfigurationHelper"; 7 | import { TEST_AUTH_CONFING, TEST_MSAL_CONFIG } from "../TestConstants"; 8 | 9 | describe("MSAL configuration builder tests", () => { 10 | it("should instantiate a valid msal configuration object", () => { 11 | const msalConfig = ConfigurationHelper.getMsalConfiguration(TEST_AUTH_CONFING); 12 | 13 | expect(msalConfig).toBeDefined(); 14 | 15 | expect(msalConfig).toMatchObject(TEST_MSAL_CONFIG); 16 | }); 17 | }); 18 | 19 | describe("Configuration helper tests", () => { 20 | 21 | it("should detect a GUID", () => { 22 | const guid1 = "0D4C9F3E-A8C5-4D4C-B8B0-C8E8E8E8E8E8"; 23 | const guid2 = "81b8a568-2442-4d53-8d6c-ededab4b7c62"; 24 | const guid3 = "81b8a56824424d538d6cededab4b7c62"; 25 | const guid4 = "Very pleasant pineapple"; 26 | const guid5 = ""; 27 | 28 | expect(ConfigurationHelper.isGuid(guid1)).toBe(true); 29 | expect(ConfigurationHelper.isGuid(guid2)).toBe(true); 30 | expect(ConfigurationHelper.isGuid(guid3)).toBe(false); 31 | expect(ConfigurationHelper.isGuid(guid4)).toBe(false); 32 | expect(ConfigurationHelper.isGuid(guid5)).toBe(false); 33 | }); 34 | 35 | it("should get effective scopes from a given list of scopes", () => { 36 | const scopes = "email openid profile offline_access User.Read calendars.read".split(" "); 37 | const effectiveScopes = ConfigurationHelper.getEffectiveScopes(scopes); 38 | expect(effectiveScopes).toEqual(["User.Read", "calendars.read"]); 39 | expect(["User.Read", "calendars.read"].every(elem => effectiveScopes.includes(elem))).toBe(true); 40 | }); 41 | 42 | it("should return instance from a given authority", () => { 43 | expect(ConfigurationHelper.getInstanceFromAuthority("https://login.microsoftonline.com/81b8a56824424d538d6cededab4b7c62")) 44 | .toBe("login.microsoftonline.com"); 45 | 46 | expect(ConfigurationHelper.getInstanceFromAuthority("https://login.microsoftonline.com/81b8a56824424d538d6cededab4b7c62/")) 47 | .toBe("login.microsoftonline.com"); 48 | }); 49 | 50 | it("should return tenantId from a given authority", () => { 51 | expect(ConfigurationHelper.getTenantIdFromAuthority("https://login.microsoftonline.com/81b8a56824424d538d6cededab4b7c62")) 52 | .toBe("81b8a56824424d538d6cededab4b7c62"); 53 | 54 | expect(ConfigurationHelper.getTenantIdFromAuthority("https://login.microsoftonline.com/81b8a56824424d538d6cededab4b7c62/")) 55 | .toBe("81b8a56824424d538d6cededab4b7c62"); 56 | 57 | expect(ConfigurationHelper.getTenantIdFromAuthority("https://login.microsoftonline.com/mytenant.onmicrosoft.com")) 58 | .toBe("mytenant.onmicrosoft.com"); 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/test/utils/UrlUtils.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | import { UrlUtils } from "../../src/utils/UrlUtils"; 7 | 8 | describe("Url utilities tests", () => { 9 | it("should ensure a given url is absolute", () => { 10 | const absoluteUrl = "https://www.microsoft.com/myPath"; 11 | 12 | const url1 = "/myPath"; 13 | const url2 = "myPath"; 14 | const url3 = "www.microsoft.com/myPath"; 15 | const protocol = "https"; 16 | const host = "www.microsoft.com"; 17 | 18 | expect(UrlUtils.ensureAbsoluteUrl(url1, protocol, host)).toBe(absoluteUrl); 19 | expect(UrlUtils.ensureAbsoluteUrl(url2, protocol, host)).toBe(absoluteUrl); 20 | expect(UrlUtils.ensureAbsoluteUrl(url3, protocol, host)).toBe(absoluteUrl); 21 | }); 22 | 23 | it("should get path component from a given url", () => { 24 | const url1 = "https://localhost:8080/path/to/resource"; 25 | const url2 = "https://localhost:8080/path/to/resource?query=value"; 26 | const url3 = "https://localhost:8080/path/to/resource#fragment"; 27 | const url4 = "https://localhost:8080/path/to/resource?query=value#fragment"; 28 | const url5 = "/path/to/resource"; 29 | 30 | expect(UrlUtils.getPathFromUrl(url1)).toEqual("/path/to/resource"); 31 | expect(UrlUtils.getPathFromUrl(url2)).toEqual("/path/to/resource"); 32 | expect(UrlUtils.getPathFromUrl(url3)).toEqual("/path/to/resource"); 33 | expect(UrlUtils.getPathFromUrl(url4)).toEqual("/path/to/resource"); 34 | expect(UrlUtils.getPathFromUrl(url5)).toEqual("/path/to/resource"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "test" 5 | ], 6 | "exclude": [ 7 | "node_modules" 8 | ], 9 | "compilerOptions": { 10 | "target": "es2020", 11 | "module": "esnext", 12 | "declaration": true, 13 | "declarationMap": true, 14 | "lib": [ 15 | "dom", 16 | "esnext" 17 | ], 18 | "importHelpers": true, 19 | "sourceMap": true, 20 | "rootDir": "./", 21 | "outDir": "./dist", 22 | "strict": true, 23 | "noImplicitAny": true, 24 | "strictNullChecks": true, 25 | "strictFunctionTypes": true, 26 | "strictPropertyInitialization": false, 27 | "noImplicitThis": true, 28 | "alwaysStrict": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "resolveJsonModule": true, 34 | "moduleResolution": "node", 35 | "jsx": "react", 36 | "esModuleInterop": true, 37 | "experimentalDecorators": true 38 | }, 39 | "compileOnSave": false, 40 | "buildOnSave": false 41 | } 42 | -------------------------------------------------------------------------------- /Common/msal-node-wrapper/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": [ 3 | "src/index.ts", 4 | ], 5 | "out": "../../docs", 6 | "excludePrivate": true, 7 | "excludeProtected": true, 8 | "excludeExternals": true, 9 | "readme": "./README.md" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | --------------------------------------------------------------------------------