├── .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 | Claim
24 | Value
25 |
26 |
27 |
28 | <% for (const [key, value] of Object.entries(claims)) { %>
29 |
30 | <%= key %>
31 | <%= value %>
32 |
33 | <% } %>
34 |
35 |
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 |
2 | Microsoft identity platform
3 |
4 | <% if (isAuthenticated) { %>
5 |
ID
6 |
Sign-out
7 | <% } else { %>
8 |
Sign-in
9 | <% } %>
10 |
11 |
--------------------------------------------------------------------------------
/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 | Claim
24 | Value
25 |
26 |
27 |
28 | <% for (const [key, value] of Object.entries(claims)) { %>
29 |
30 | <%= key %>
31 | <%= value %>
32 |
33 | <% } %>
34 |
35 |
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 |
2 | Microsoft identity platform
3 |
4 | <% if (isAuthenticated) { %>
5 |
ID
6 |
Sign-out
7 | <% } else { %>
8 |
Sign-in
9 | <% } %>
10 |
11 |
--------------------------------------------------------------------------------
/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 | Claim
24 | Value
25 |
26 |
27 |
28 | <% for (const [key, value] of Object.entries(claims)) { %>
29 |
30 | <%= key %>
31 | <%= value %>
32 |
33 | <% } %>
34 |
35 |
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 | Microsoft identity platform
3 |
4 | <% if (isAuthenticated) { %>
5 |
ID
6 |
Sign-out
7 | <% } else { %>
8 |
Sign-in
9 | <% } %>
10 |
11 |
--------------------------------------------------------------------------------
/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 |
18 | resource: User object
19 | endpoint: https://graph.microsoft.com/v1.0/me
20 | scope: user.read
21 |
22 |
Contents of the response is below:
23 |
24 |
25 |
26 |
27 |
28 |
29 | Claim
30 | Value
31 |
32 |
33 |
34 | <% for (const [key, value] of Object.entries(profile)) { %>
35 |
36 | <%= key %>
37 | <%= value %>
38 |
39 | <% } %>
40 |
41 |
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 |
18 | resource: Tenant object
19 | endpoint: https://management.azure.com/tenants?api-version=2020-01-01
20 | scope: https://management.azure.com/user_impersonation
21 |
22 |
Contents of the response is below:
23 |
24 |
25 |
26 |
27 |
28 |
29 | Claim
30 | Value
31 |
32 |
33 |
34 | <% for (const [key, value] of Object.entries(tenant)) { %>
35 |
36 | <%= key %>
37 | <%= value %>
38 |
39 | <% } %>
40 |
41 |
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 | Claim
24 | Value
25 |
26 |
27 |
28 | <% for (const [key, value] of Object.entries(claims)) { %>
29 |
30 | <%= key %>
31 | <%= value %>
32 |
33 | <% } %>
34 |
35 |
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 |
2 | Microsoft identity platform
3 |
4 | <% if (isAuthenticated) { %>
5 |
ID
6 |
Sign-out
7 | <% } else { %>
8 |
Sign-in
9 | <% } %>
10 |
11 |
--------------------------------------------------------------------------------
/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 |
18 | resource: User object
19 | endpoint: https://graph.microsoft.com/v1.0/me
20 | scope: user.read
21 |
22 |
Contents of the response is below:
23 |
24 |
25 |
26 |
27 |
28 |
29 | Claim
30 | Value
31 |
32 |
33 |
34 | <% for (const [key, value] of Object.entries(profile)) { %>
35 |
36 | <%= key %>
37 | <%= value %>
38 |
39 | <% } %>
40 |
41 |
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 |
18 | resource: Tenant object
19 | endpoint: https://management.azure.com/tenants?api-version=2020-01-01
20 | scope: https://management.azure.com/user_impersonation
21 |
22 |
Contents of the response is below:
23 |
24 |
25 |
26 |
27 |
28 |
29 | Claim
30 | Value
31 |
32 |
33 |
34 | <% for (const [key, value] of Object.entries(tenant)) { %>
35 |
36 | <%= key %>
37 | <%= value %>
38 |
39 | <% } %>
40 |
41 |
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 | Task
24 | Owner
25 |
26 |
27 |
28 | <% for (let i=0; i < todos.length; i++) { %>
29 |
30 | <%= todos[i]['name'] %>
31 | <%= todos[i]['owner'] %>
32 |
33 | <% } %>
34 |
35 |
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 | Claim
24 | Value
25 |
26 |
27 |
28 | <% for (const [key, value] of Object.entries(claims)) { %>
29 |
30 | <%= key %>
31 | <%= value %>
32 |
33 | <% } %>
34 |
35 |
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 |
2 | Microsoft identity platform
3 |
4 | <% if (isAuthenticated) { %>
5 |
ID
6 |
Sign-out
7 | <% } else { %>
8 |
Sign-in
9 | <% } %>
10 |
11 |
--------------------------------------------------------------------------------
/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 |
24 |
25 |
26 |
27 |
28 |
29 | <% for (let i=0; i < todos.length; i++) { %>
30 |
31 | <%= todos[i]['name'] %>
32 |
33 |
38 |
39 |
40 | <% } %>
41 |
42 |
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 | Task
24 | Owner
25 |
26 |
27 |
28 | <% for (let i=0; i < todos.length; i++) { %>
29 |
30 | <%= todos[i]['name'] %>
31 | <%= todos[i]['owner'] %>
32 |
33 | <% } %>
34 |
35 |
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 | Claim
24 | Value
25 |
26 |
27 |
28 | <% for (const [key, value] of Object.entries(claims)) { %>
29 |
30 | <%= key %>
31 | <%= value %>
32 |
33 | <% } %>
34 |
35 |
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 |
2 | Microsoft identity platform
3 |
4 | <% if (isAuthenticated) { %>
5 |
ID
6 |
Sign-out
7 | <% } else { %>
8 |
Sign-in
9 | <% } %>
10 |
11 |
--------------------------------------------------------------------------------
/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 |
24 |
25 |
26 |
27 |
28 |
29 | <% for (let i=0; i < todos.length; i++) { %>
30 |
31 | <%= todos[i]['name'] %>
32 |
33 |
38 |
39 |
40 | <% } %>
41 |
42 |
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 | You need to enable JavaScript to run this app.
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 | { logout(); }}
22 | >
23 | Sign out
24 |
25 |
26 | >
27 | ) : (
28 | <>
29 |
30 | { login(); }}
36 | >
37 | Sign in
38 |
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 |
--------------------------------------------------------------------------------