├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ ├── node.js.yml │ └── stale.yml ├── .gitignore ├── 1-Authentication ├── 1-sign-in │ ├── App │ │ ├── authConfig.js │ │ ├── authPopup.js │ │ ├── authRedirect.js │ │ ├── favicon.svg │ │ ├── index.html │ │ ├── redirect.html │ │ ├── signout.html │ │ ├── styles.css │ │ └── ui.js │ ├── AppCreationScripts │ │ ├── AppCreationScripts.md │ │ ├── Cleanup.ps1 │ │ ├── Configure.ps1 │ │ └── sample.json │ ├── README.md │ ├── ReadmeFiles │ │ ├── screenshot.png │ │ └── topology_signin.png │ ├── package-lock.json │ ├── package.json │ ├── sample.test.js │ └── server.js └── 2-sign-in-b2c │ ├── App │ ├── authConfig.js │ ├── authPopup.js │ ├── authRedirect.js │ ├── favicon.svg │ ├── index.html │ ├── redirect.html │ ├── signout.html │ ├── styles.css │ ├── ui.js │ └── utils │ │ └── claimUtils.js │ ├── AppCreationScripts │ └── sample.json │ ├── README.md │ ├── ReadmeFiles │ ├── screenshot.png │ └── topology_b2c_signin.png │ ├── package-lock.json │ ├── package.json │ ├── sample.test.js │ └── server.js ├── 2-Authorization-I └── 1-call-graph │ ├── App │ ├── authConfig.js │ ├── authPopup.js │ ├── authRedirect.js │ ├── favicon.svg │ ├── fetch.js │ ├── graph.js │ ├── index.html │ ├── redirect.html │ ├── styles.css │ ├── ui.js │ └── utils │ │ └── storageUtils.js │ ├── AppCreationScripts │ ├── AppCreationScripts.md │ ├── Cleanup.ps1 │ ├── Configure.ps1 │ └── sample.json │ ├── README.md │ ├── ReadmeFiles │ ├── screenshot.png │ └── topology_callgraph.png │ ├── package-lock.json │ ├── package.json │ ├── sample.test.js │ └── server.js ├── 3-Authorization-II ├── 1-call-api │ ├── API │ │ ├── app.js │ │ ├── auth │ │ │ └── permissionUtils.js │ │ ├── authConfig.js │ │ ├── controllers │ │ │ └── todolist.js │ │ ├── data │ │ │ └── db.json │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── routes │ │ │ └── index.js │ │ └── sample.test.js │ ├── AppCreationScripts │ │ ├── AppCreationScripts.md │ │ ├── Cleanup.ps1 │ │ ├── Configure.ps1 │ │ └── sample.json │ ├── README.md │ ├── ReadmeFiles │ │ ├── screenshot.png │ │ └── topology_callapi.png │ └── SPA │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ ├── authConfig.js │ │ ├── authPopup.js │ │ ├── authRedirect.js │ │ ├── claimUtils.js │ │ ├── favicon.svg │ │ ├── fetch.js │ │ ├── index.html │ │ ├── redirect.html │ │ ├── styles.css │ │ └── ui.js │ │ ├── sample.test.js │ │ └── server.js └── 2-call-api-b2c │ ├── API │ ├── app.js │ ├── auth │ │ └── permissionUtils.js │ ├── authConfig.js │ ├── controllers │ │ └── todolist.js │ ├── data │ │ └── db.json │ ├── package-lock.json │ ├── package.json │ ├── routes │ │ └── index.js │ └── sample.test.js │ ├── AppCreationScripts │ └── sample.json │ ├── README.md │ ├── ReadmeFiles │ ├── screenshot.png │ └── topology_b2c_callapi.png │ └── SPA │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── authConfig.js │ ├── authPopup.js │ ├── authRedirect.js │ ├── claimUtils.js │ ├── favicon.svg │ ├── fetch.js │ ├── index.html │ ├── redirect.html │ ├── styles.css │ └── ui.js │ ├── sample.test.js │ └── server.js ├── 4-Deployment ├── README.md └── ReadmeFiles │ ├── api_step1.png │ ├── api_step2.png │ ├── api_step3.png │ ├── disable_easy_auth.png │ ├── enable_cors.png │ ├── screenshot.png │ ├── spa_step1.png │ ├── spa_step2.png │ ├── spa_step3.png │ ├── spa_step4.png │ └── topology_dep.png ├── CHANGELOG.md ├── CONTRIBUTING.md ├── 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-1) Protect and call a web API on Microsoft Entra ID 16 | - [ ] 3-2) Protect and call a web API on Azure Active Directory B2C 17 | - [ ] 4-1) Call a web API that calls Microsoft Graph on-behalf-of user 18 | - [ ] 4-2) Call a web API that calls another web API on-behalf-of user 19 | - [ ] 5) Deploy to Azure Storage and App Service 20 | ``` 21 | 22 | ## This issue is for a 23 | 24 | 25 | 26 | ```console 27 | - [ ] bug report -> please search issues before submitting 28 | - [ ] question 29 | - [ ] feature request 30 | - [ ] documentation issue or request 31 | ``` 32 | 33 | ### Minimal steps to reproduce 34 | 35 | > 36 | 37 | ### Any log messages given by the failure 38 | 39 | > 40 | 41 | ### Expected/desired behavior 42 | 43 | > 44 | 45 | ### Library version 46 | 47 | > 48 | 49 | ### Browser and version 50 | 51 | > Chrome, Edge, Firefox, Safari? 52 | 53 | ### Mention any other details that might be useful 54 | 55 | > 56 | 57 | Thanks! We'll be in touch soon. 58 | -------------------------------------------------------------------------------- /.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 | 15 | **Repro** 16 | 17 | ```csharp 18 | String your = code(here); 19 | ``` 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen (or code). 23 | 24 | **Actual behavior** 25 | A clear and concise description of what happens, e.g. an exception is thrown, UI freezes. 26 | 27 | **Possible solution** 28 | 29 | 30 | **Additional context / logs / screenshots / links to code** 31 | 32 | Add any other context about the problem here, such as logs and screenshots or links to code. 33 | -------------------------------------------------------------------------------- /.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 | * Test the code 44 | 45 | 46 | 47 | ```console 48 | 49 | ``` 50 | 51 | ## What to check 52 | 53 | ex: verify that the following are valid: 54 | 55 | * ... 56 | 57 | ## Other Information 58 | 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "Code Scan" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | CodeQL-Build: 10 | strategy: 11 | fail-fast: false 12 | 13 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | # Initializes the CodeQL tools for scanning. 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v2 23 | with: 24 | languages: javascript 25 | 26 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 27 | # If this step fails, then you should remove it and run the build manually (see below). 28 | #- name: Autobuild 29 | # uses: github/codeql-action/autobuild@v1 30 | 31 | # ℹ️ Command-line programs to run using the OS shell. 32 | # 📚 https://git.io/JvXDl 33 | 34 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 35 | # and modify them (or add more) to build your code if your project 36 | # uses a compiled language 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | -------------------------------------------------------------------------------- /.github/workflows/node.js.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" 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - run: | 29 | cd 1-Authentication/1-sign-in/ 30 | npm ci 31 | npm audit --production 32 | npm run test 33 | 34 | - run: | 35 | cd 1-Authentication/2-sign-in-b2c/ 36 | npm ci 37 | npm audit --production 38 | npm run test 39 | 40 | - run: | 41 | cd 2-Authorization-I/1-call-graph/ 42 | npm ci 43 | npm audit --production 44 | npm run test 45 | 46 | - run: | 47 | cd 3-Authorization-II/1-call-api/API 48 | npm ci 49 | npm audit --production 50 | npm run test 51 | 52 | - run: | 53 | cd 3-Authorization-II/1-call-api/SPA 54 | npm ci 55 | npm audit --production 56 | npm run test 57 | 58 | - run: | 59 | cd 3-Authorization-II/2-call-api-b2c/API 60 | npm ci 61 | npm audit --production 62 | npm run test 63 | 64 | - run: | 65 | cd 3-Authorization-II/2-call-api-b2c/SPA 66 | npm ci 67 | npm audit --production 68 | npm run test -------------------------------------------------------------------------------- /.github/workflows/stale.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 | 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 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Visual Studio Cache 107 | .vs/ 108 | 109 | # VS Code Cache 110 | .vscode/ 111 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration object to be passed to MSAL instance on creation. 3 | * For a full list of MSAL.js configuration parameters, visit: 4 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md 5 | */ 6 | 7 | const msalConfig = { 8 | auth: { 9 | clientId: 'Enter_the_Application_Id_Here', // This is the ONLY mandatory field that you need to supply. 10 | authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Info_Here', // Defaults to "https://login.microsoftonline.com/common" 11 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.href e.g. http://localhost:3000/ 12 | navigateToLoginRequestUrl: true, // If "true", will navigate back to the original request location before processing the auth code response. 13 | }, 14 | cache: { 15 | cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO. 16 | storeAuthStateInCookie: false, // set this to true if you have to support IE 17 | }, 18 | system: { 19 | loggerOptions: { 20 | loggerCallback: (level, message, containsPii) => { 21 | if (containsPii) { 22 | return; 23 | } 24 | switch (level) { 25 | case msal.LogLevel.Error: 26 | console.error(message); 27 | return; 28 | case msal.LogLevel.Info: 29 | console.info(message); 30 | return; 31 | case msal.LogLevel.Verbose: 32 | console.debug(message); 33 | return; 34 | case msal.LogLevel.Warning: 35 | console.warn(message); 36 | return; 37 | } 38 | }, 39 | }, 40 | }, 41 | }; 42 | 43 | /** 44 | * Scopes you add here will be prompted for user consent during sign-in. 45 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. 46 | * For more information about OIDC scopes, visit: 47 | * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes 48 | */ 49 | const loginRequest = { 50 | scopes: ["openid", "profile"], 51 | }; 52 | 53 | /** 54 | * An optional silentRequest object can be used to achieve silent SSO 55 | * between applications by providing a "login_hint" property. 56 | */ 57 | 58 | // const silentRequest = { 59 | // scopes: ["openid", "profile"], 60 | // loginHint: "example@domain.net" 61 | // }; 62 | 63 | // exporting config object for jest 64 | if (typeof exports !== 'undefined') { 65 | module.exports = { 66 | msalConfig: msalConfig, 67 | loginRequest: loginRequest, 68 | }; 69 | } -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/authPopup.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ""; 6 | 7 | function selectAccount () { 8 | 9 | /** 10 | * See here for more info on account retrieval: 11 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 12 | */ 13 | 14 | const currentAccounts = myMSALObj.getAllAccounts(); 15 | 16 | if (!currentAccounts || currentAccounts.length < 1) { 17 | return; 18 | } else if (currentAccounts.length > 1) { 19 | // Add your account choosing logic here 20 | console.warn("Multiple accounts detected."); 21 | } else if (currentAccounts.length === 1) { 22 | username = currentAccounts[0].username; 23 | welcomeUser(username); 24 | updateTable(); 25 | } 26 | } 27 | 28 | function handleResponse(response) { 29 | 30 | /** 31 | * To see the full list of response object properties, visit: 32 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response 33 | */ 34 | 35 | if (response !== null) { 36 | username = response.account.username; 37 | welcomeUser(username); 38 | updateTable(); 39 | } else { 40 | 41 | selectAccount(); 42 | 43 | /** 44 | * If you already have a session that exists with the authentication server, you can use the ssoSilent() API 45 | * to make request for tokens without interaction, by providing a "login_hint" property. To try this, comment the 46 | * line above and uncomment the section below. 47 | */ 48 | 49 | // myMSALObj.ssoSilent(silentRequest). 50 | // then(() => { 51 | // const currentAccounts = myMSALObj.getAllAccounts(); 52 | // username = currentAccounts[0].username; 53 | // welcomeUser(username); 54 | // updateTable(); 55 | // }).catch(error => { 56 | // console.error("Silent Error: " + error); 57 | // if (error instanceof msal.InteractionRequiredAuthError) { 58 | // signIn(); 59 | // } 60 | // }); 61 | } 62 | } 63 | 64 | function signIn() { 65 | 66 | /** 67 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 68 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 69 | */ 70 | 71 | myMSALObj.loginPopup(loginRequest) 72 | .then(handleResponse) 73 | .catch(error => { 74 | console.error(error); 75 | }); 76 | } 77 | 78 | function signOut() { 79 | 80 | /** 81 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 82 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 83 | */ 84 | 85 | // Choose which account to logout from by passing a username. 86 | const logoutRequest = { 87 | account: myMSALObj.getAccountByUsername(username), 88 | mainWindowRedirectUri: 'http://localhost:3000/signout', 89 | redirectUri: 'http://localhost:3000/redirect.html', 90 | }; 91 | 92 | myMSALObj.logoutPopup(logoutRequest); 93 | } 94 | 95 | selectAccount(); 96 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/authRedirect.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ""; 6 | 7 | /** 8 | * A promise handler needs to be registered for handling the 9 | * response returned from redirect flow. For more information, visit: 10 | * 11 | */ 12 | myMSALObj.handleRedirectPromise() 13 | .then(handleResponse) 14 | .catch((error) => { 15 | console.error(error); 16 | }); 17 | 18 | function selectAccount () { 19 | 20 | /** 21 | * See here for more info on account retrieval: 22 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 23 | */ 24 | 25 | const currentAccounts = myMSALObj.getAllAccounts(); 26 | 27 | if (!currentAccounts) { 28 | return; 29 | } else if (currentAccounts.length > 1) { 30 | // Add your account choosing logic here 31 | console.warn("Multiple accounts detected."); 32 | } else if (currentAccounts.length === 1) { 33 | username = currentAccounts[0].username; 34 | welcomeUser(username); 35 | updateTable(); 36 | } 37 | } 38 | 39 | function handleResponse(response) { 40 | 41 | /** 42 | * To see the full list of response object properties, visit: 43 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response 44 | */ 45 | 46 | if (response !== null) { 47 | username = response.account.username; 48 | welcomeUser(username); 49 | updateTable(); 50 | } else { 51 | 52 | selectAccount(); 53 | 54 | /** 55 | * If you already have a session that exists with the authentication server, you can use the ssoSilent() API 56 | * to make request for tokens without interaction, by providing a "login_hint" property. To try this, comment the 57 | * line above and uncomment the section below. 58 | */ 59 | 60 | // myMSALObj.ssoSilent(silentRequest). 61 | // then(() => { 62 | // const currentAccounts = myMSALObj.getAllAccounts(); 63 | // username = currentAccounts[0].username; 64 | // welcomeUser(username); 65 | // updateTable(); 66 | // }).catch(error => { 67 | // console.error("Silent Error: " + error); 68 | // if (error instanceof msal.InteractionRequiredAuthError) { 69 | // signIn(); 70 | // } 71 | // }); 72 | } 73 | } 74 | 75 | function signIn() { 76 | 77 | /** 78 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 79 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 80 | */ 81 | 82 | myMSALObj.loginRedirect(loginRequest); 83 | } 84 | 85 | function signOut() { 86 | 87 | /** 88 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 89 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 90 | */ 91 | 92 | // Choose which account to logout from by passing a username. 93 | const logoutRequest = { 94 | account: myMSALObj.getAccountByUsername(username), 95 | postLogoutRedirectUri: 'http://localhost:3000/signout', // Simply remove this line if you would like navigate to index page after logout. 96 | 97 | }; 98 | 99 | myMSALObj.logoutRedirect(logoutRequest); 100 | } 101 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/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 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Microsoft identity platform 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 |
30 |
Vanilla JavaScript single-page application secured with MSAL.js 31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
Claim TypeValue
44 |

45 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/redirect.html: -------------------------------------------------------------------------------- 1 | 7 |

MSAL Redirect

-------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/signout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Azure AD | Vanilla JavaScript SPA 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Goodbye!

15 |

You have signed out and your cache has been cleared.

16 | Take me back 17 |
18 | 19 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/styles.css: -------------------------------------------------------------------------------- 1 | .navbarStyle { 2 | padding: .5rem 1rem !important; 3 | } -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/App/ui.js: -------------------------------------------------------------------------------- 1 | // Select DOM elements to work with 2 | const signInButton = document.getElementById('signIn'); 3 | const signOutButton = document.getElementById('signOut') 4 | const titleDiv = document.getElementById('title-div'); 5 | const welcomeDiv = document.getElementById('welcome-div'); 6 | const tableDiv = document.getElementById('table-div'); 7 | const footerDiv = document.getElementById('footer'); 8 | const tableBody = document.getElementById('table-body-div'); 9 | 10 | function welcomeUser(username) { 11 | signInButton.classList.add('d-none'); 12 | signOutButton.classList.remove('d-none'); 13 | titleDiv.classList.add('d-none'); 14 | welcomeDiv.classList.remove('d-none'); 15 | welcomeDiv.innerHTML = `Welcome ${username}!` 16 | } 17 | 18 | function updateTable() { 19 | 20 | /** 21 | * In order to obtain the ID Token in the cached obtained previously, you can initiate a 22 | * silent token request by passing the current user's account and the scope "openid". 23 | */ 24 | myMSALObj.acquireTokenSilent({ 25 | account: myMSALObj.getAccountByUsername(username), 26 | scopes: ["openid"] 27 | }).then(response => { 28 | 29 | tableDiv.classList.remove('d-none'); 30 | footerDiv.classList.remove('d-none'); 31 | 32 | Object.entries(response.idTokenClaims).forEach(claim => { 33 | 34 | if (claim[0] === "name" || claim[0] === "preferred_username" || claim[0] === "oid") { 35 | let row = tableBody.insertRow(0); 36 | let cell1 = row.insertCell(0); 37 | let cell2 = row.insertCell(1); 38 | cell1.innerHTML = claim[0]; 39 | cell2.innerHTML = claim[1]; 40 | } 41 | }); 42 | }); 43 | } -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/AppCreationScripts/Cleanup.ps1: -------------------------------------------------------------------------------- 1 |  2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] 5 | [string] $tenantId, 6 | [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] 7 | [string] $azureEnvironmentName 8 | ) 9 | 10 | 11 | Function Cleanup 12 | { 13 | if (!$azureEnvironmentName) 14 | { 15 | $azureEnvironmentName = "Global" 16 | } 17 | 18 | <# 19 | .Description 20 | This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script 21 | #> 22 | 23 | # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant 24 | # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. 25 | 26 | # Connect to the Microsoft Graph API 27 | Write-Host "Connecting to Microsoft Graph" 28 | 29 | 30 | if ($tenantId -eq "") 31 | { 32 | Connect-MgGraph -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName 33 | } 34 | else 35 | { 36 | Connect-MgGraph -TenantId $tenantId -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName 37 | } 38 | 39 | $context = Get-MgContext 40 | $tenantId = $context.TenantId 41 | 42 | # Get the user running the script 43 | $currentUserPrincipalName = $context.Account 44 | $user = Get-MgUser -Filter "UserPrincipalName eq '$($context.Account)'" 45 | 46 | # get the tenant we signed in to 47 | $Tenant = Get-MgOrganization 48 | $tenantName = $Tenant.DisplayName 49 | 50 | $verifiedDomain = $Tenant.VerifiedDomains | where {$_.Isdefault -eq $true} 51 | $verifiedDomainName = $verifiedDomain.Name 52 | $tenantId = $Tenant.Id 53 | 54 | Write-Host ("Connected to Tenant {0} ({1}) as account '{2}'. Domain is '{3}'" -f $Tenant.DisplayName, $Tenant.Id, $currentUserPrincipalName, $verifiedDomainName) 55 | 56 | # Removes the applications 57 | Write-Host "Cleaning-up applications from tenant '$tenantId'" 58 | 59 | Write-Host "Removing 'client' (ms-identity-javascript-c1s1) if needed" 60 | try 61 | { 62 | Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c1s1'" | ForEach-Object {Remove-MgApplication -ApplicationId $_.Id } 63 | } 64 | catch 65 | { 66 | $message = $_ 67 | Write-Warning $Error[0] 68 | Write-Host "Unable to remove the application 'ms-identity-javascript-c1s1'. Error is $message. Try deleting manually." -ForegroundColor White -BackgroundColor Red 69 | } 70 | 71 | Write-Host "Making sure there are no more (ms-identity-javascript-c1s1) applications found, will remove if needed..." 72 | $apps = Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c1s1'" | Format-List Id, DisplayName, AppId, SignInAudience, PublisherDomain 73 | 74 | if ($apps) 75 | { 76 | Remove-MgApplication -ApplicationId $apps.Id 77 | } 78 | 79 | foreach ($app in $apps) 80 | { 81 | Remove-MgApplication -ApplicationId $app.Id 82 | Write-Host "Removed ms-identity-javascript-c1s1.." 83 | } 84 | 85 | # also remove service principals of this app 86 | try 87 | { 88 | Get-MgServicePrincipal -filter "DisplayName eq 'ms-identity-javascript-c1s1'" | ForEach-Object {Remove-MgServicePrincipal -ServicePrincipalId $_.Id -Confirm:$false} 89 | } 90 | catch 91 | { 92 | $message = $_ 93 | Write-Warning $Error[0] 94 | Write-Host "Unable to remove ServicePrincipal 'ms-identity-javascript-c1s1'. Error is $message. Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red 95 | } 96 | } 97 | 98 | # Pre-requisites 99 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph")) { 100 | Install-Module "Microsoft.Graph" -Scope CurrentUser 101 | } 102 | 103 | #Import-Module Microsoft.Graph 104 | 105 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) { 106 | Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser 107 | } 108 | 109 | Import-Module Microsoft.Graph.Authentication 110 | 111 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Identity.DirectoryManagement")) { 112 | Install-Module "Microsoft.Graph.Identity.DirectoryManagement" -Scope CurrentUser 113 | } 114 | 115 | Import-Module Microsoft.Graph.Identity.DirectoryManagement 116 | 117 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { 118 | Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser 119 | } 120 | 121 | Import-Module Microsoft.Graph.Applications 122 | 123 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Groups")) { 124 | Install-Module "Microsoft.Graph.Groups" -Scope CurrentUser 125 | } 126 | 127 | Import-Module Microsoft.Graph.Groups 128 | 129 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Users")) { 130 | Install-Module "Microsoft.Graph.Users" -Scope CurrentUser 131 | } 132 | 133 | Import-Module Microsoft.Graph.Users 134 | 135 | $ErrorActionPreference = "Stop" 136 | 137 | 138 | try 139 | { 140 | Cleanup -tenantId $tenantId -environment $azureEnvironmentName 141 | } 142 | catch 143 | { 144 | $_.Exception.ToString() | out-host 145 | $message = $_ 146 | Write-Warning $Error[0] 147 | Write-Host "Unable to register apps. Error is $message." -ForegroundColor White -BackgroundColor Red 148 | } 149 | 150 | Write-Host "Disconnecting from tenant" 151 | Disconnect-MgGraph 152 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "Vanilla JavaScript single-page application using MSAL.js to authenticate users with Azure Active Directory", 4 | "Level": 100, 5 | "Client": "Vanilla JavaScript SPA", 6 | "RepositoryUrl": "ms-identity-javascript-tutorial", 7 | "Endpoint": "AAD v2.0", 8 | "Languages": ["javascript"], 9 | "Description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users with Azure Active Directory", 10 | "Products": ["azure-active-directory", "msal-js", "msal-browser"], 11 | "Platform": "JavaScript" 12 | }, 13 | "AADApps": [ 14 | { 15 | "Id": "client", 16 | "Name": "ms-identity-javascript-c1s1", 17 | "Kind": "SinglePageApplication", 18 | "Audience": "AzureADMyOrg", 19 | "HomePage": "http://localhost:3000", 20 | "SampleSubPath": "1-Authorization\\1-sign-in", 21 | "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect", 22 | "OptionalClaims": { 23 | "IdTokenClaims": ["acct"] 24 | } 25 | } 26 | ], 27 | "CodeConfiguration": [ 28 | { 29 | "App": "client", 30 | "SettingKind": "Replace", 31 | "SettingFile": "\\..\\App\\authConfig.js", 32 | "Mappings": [ 33 | { 34 | "key": "Enter_the_Application_Id_Here", 35 | "value": ".AppId" 36 | }, 37 | { 38 | "key": "Enter_the_Tenant_Info_Here", 39 | "value": "$tenantId" 40 | } 41 | ] 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/1-Authentication/1-sign-in/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/ReadmeFiles/topology_signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/1-Authentication/1-sign-in/ReadmeFiles/topology_signin.png -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-identity-javascript-c1s1", 3 | "version": "1.0.0", 4 | "description": "Vanilla JavaScript single-page application (SPA) using MSAL.js to authenticate users against Azure Active Directory", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-tutorial.git" 14 | }, 15 | "keywords": [ 16 | "javascript", 17 | "msal", 18 | "authorization code", 19 | "authentication", 20 | "microsoft", 21 | "ms-identity", 22 | "azure-ad", 23 | "single-page app" 24 | ], 25 | "author": "derisen", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial/issues" 29 | }, 30 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial#readme", 31 | "dependencies": { 32 | "express": "^4.18.2", 33 | "express-rate-limit": "^6.7.0", 34 | "morgan": "^1.10.0" 35 | }, 36 | "devDependencies": { 37 | "jest": "^27.0.6", 38 | "nodemon": "^3.1.3", 39 | "supertest": "^6.1.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/sample.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | const request = require('supertest'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | 9 | const app = require('./server.js'); 10 | 11 | jest.dontMock('fs'); 12 | 13 | const html = fs.readFileSync(path.resolve(__dirname, './App/index.html'), 'utf8'); 14 | 15 | describe('Sanitize index page', () => { 16 | beforeAll(async() => { 17 | global.document.documentElement.innerHTML = html.toString(); 18 | }); 19 | 20 | it('should have valid cdn link', () => { 21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser"); 22 | }); 23 | }); 24 | 25 | describe('Sanitize configuration object', () => { 26 | beforeAll(() => { 27 | global.msalConfig = require('./App/authConfig.js').msalConfig; 28 | }); 29 | 30 | it('should define the config object', () => { 31 | expect(msalConfig).toBeDefined(); 32 | }); 33 | 34 | it('should not contain credentials', () => { 35 | 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; 36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(false); 37 | }); 38 | 39 | it('should contain authority URI', () => { 40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; 41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true); 42 | }); 43 | }); 44 | 45 | describe('Ensure pages served', () => { 46 | 47 | beforeAll(() => { 48 | process.env.NODE_ENV = 'test'; 49 | }); 50 | 51 | it('should get index page', async () => { 52 | const res = await request(app) 53 | .get('/'); 54 | 55 | const data = await fs.promises.readFile(path.join(__dirname, './App/index.html'), 'utf8'); 56 | expect(res.statusCode).toEqual(200); 57 | expect(res.text).toEqual(data); 58 | }); 59 | 60 | it('should get signout page', async () => { 61 | const res = await request(app) 62 | .get('/signout'); 63 | 64 | const data = await fs.promises.readFile(path.join(__dirname, './App/signout.html'), 'utf8'); 65 | expect(res.statusCode).toEqual(200); 66 | expect(res.text).toEqual(data); 67 | }); 68 | }); -------------------------------------------------------------------------------- /1-Authentication/1-sign-in/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const path = require('path'); 4 | 5 | const rateLimit = require('express-rate-limit'); 6 | 7 | const DEFAULT_PORT = process.env.PORT || 3000; 8 | 9 | // initialize express. 10 | const app = express(); 11 | 12 | 13 | /** 14 | * HTTP request handlers should not perform expensive operations such as accessing the file system, 15 | * executing an operating system command or interacting with a database without limiting the rate at 16 | * which requests are accepted. Otherwise, the application becomes vulnerable to denial-of-service attacks 17 | * where an attacker can cause the application to crash or become unresponsive by issuing a large number of 18 | * requests at the same time. For more information, visit: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html 19 | */ 20 | const limiter = rateLimit({ 21 | windowMs: 15 * 60 * 1000, // 15 minutes 22 | max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) 23 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 24 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 25 | }); 26 | 27 | 28 | // Apply the rate limiting middleware to all requests 29 | app.use(limiter); 30 | 31 | 32 | // Configure morgan module to log all requests. 33 | app.use(morgan('dev')); 34 | 35 | // Setup app folders. 36 | app.use(express.static('App')); 37 | 38 | // Set up a route for signout.html 39 | app.get('/signout', (req, res) => { 40 | res.sendFile(path.join(__dirname + '/App/signout.html')); 41 | }); 42 | 43 | app.get('/redirect', (req, res) => { 44 | res.sendFile(path.join(__dirname + '/App/redirect.html')); 45 | }); 46 | 47 | // Set up a route for index.html 48 | app.get('*', (req, res) => { 49 | res.sendFile(path.join(__dirname + '/index.html')); 50 | }); 51 | 52 | app.listen(DEFAULT_PORT, () => { 53 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`) 54 | }); 55 | 56 | module.exports = app; -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enter here the user flows and custom policies for your B2C application 3 | * To learn more about user flows, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview 4 | * To learn more about custom policies, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-overview 5 | */ 6 | const b2cPolicies = { 7 | names: { 8 | signUpSignIn: 'B2C_1_susi_v2', 9 | forgotPassword: 'B2C_1_reset_v3', 10 | editProfile: 'B2C_1_edit_profile_v2', 11 | }, 12 | authorities: { 13 | signUpSignIn: { 14 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_susi_v2', 15 | }, 16 | forgotPassword: { 17 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_reset_v3', 18 | }, 19 | editProfile: { 20 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_edit_profile_v2', 21 | }, 22 | }, 23 | authorityDomain: 'fabrikamb2c.b2clogin.com', 24 | }; 25 | 26 | /** 27 | * Configuration object to be passed to MSAL instance on creation. 28 | * For a full list of MSAL.js configuration parameters, visit: 29 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md 30 | * For more details on MSAL.js and Azure AD B2C, visit: 31 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/working-with-b2c.md 32 | */ 33 | 34 | const msalConfig = { 35 | auth: { 36 | clientId: '2fdd06f3-7b34-49a3-a78b-0cf1dd87878e', // Replace with your AppID/ClientID obtained from Azure Portal. 37 | authority: b2cPolicies.authorities.signUpSignIn.authority, // Choose sign-up/sign-in user-flow as your default. 38 | knownAuthorities: [b2cPolicies.authorityDomain], // You must identify your tenant's domain as a known authority. 39 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to "window.location.href". 40 | postLogoutRedirectUri: '/signout', // Simply remove this line if you would like navigate to index page after logout. 41 | navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response. 42 | }, 43 | cache: { 44 | cacheLocation: 'localStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO. 45 | storeAuthStateInCookie: false, // If you wish to store cache items in cookies as well as browser cache, set this to "true". 46 | }, 47 | system: { 48 | loggerOptions: { 49 | loggerCallback: (level, message, containsPii) => { 50 | if (containsPii) { 51 | return; 52 | } 53 | switch (level) { 54 | case msal.LogLevel.Error: 55 | console.error(message); 56 | return; 57 | case msal.LogLevel.Info: 58 | console.info(message); 59 | return; 60 | case msal.LogLevel.Verbose: 61 | console.debug(message); 62 | return; 63 | case msal.LogLevel.Warning: 64 | console.warn(message); 65 | return; 66 | } 67 | }, 68 | }, 69 | }, 70 | }; 71 | 72 | /** 73 | * Scopes you add here will be prompted for user consent during sign-in. 74 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. 75 | * For more information about OIDC scopes, visit: 76 | * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes 77 | */ 78 | const loginRequest = { 79 | scopes: ['openid', 'offline_access'], 80 | }; 81 | 82 | /** 83 | * An optional silentRequest object can be used to achieve silent SSO 84 | * between applications by providing a "login_hint" property. 85 | */ 86 | 87 | // const silentRequest = { 88 | // scopes: ["openid", "profile"], 89 | // loginHint: "example@domain.net" 90 | // }; 91 | 92 | // exporting config object for jest 93 | if (typeof exports !== 'undefined') { 94 | module.exports = { 95 | msalConfig: msalConfig, 96 | b2cPolicies: b2cPolicies, 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Icon-identity-228 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Microsoft identity platform 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 |
31 |
Vanilla JavaScript single-page application built with MSAL.js
32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
Claim TypeValueDescription
46 |
47 | 48 | 52 | 53 | 54 | 57 | 60 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/redirect.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/signout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Microsoft identity platform 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Goodbye!

14 |

You have signed out and your cache has been cleared.

15 | Take me back 16 |
17 | 18 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/styles.css: -------------------------------------------------------------------------------- 1 | .navbarStyle { 2 | padding: .5rem 1rem !important; 3 | } 4 | 5 | .profileButton { 6 | margin: .5rem .5rem; 7 | } 8 | 9 | 10 | .table-responsive-ms { 11 | max-height: 39rem !important; 12 | margin-left: 1.5rem; 13 | margin-right: 1.5rem; 14 | } -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/App/ui.js: -------------------------------------------------------------------------------- 1 | // Select DOM elements to work with 2 | const signInButton = document.getElementById('signIn'); 3 | const signOutButton = document.getElementById('signOut') 4 | const titleDiv = document.getElementById('title-div'); 5 | const welcomeDiv = document.getElementById('welcome-div'); 6 | const tableDiv = document.getElementById('table-div'); 7 | const tableBody = document.getElementById('table-body-div'); 8 | const footerDiv = document.getElementById('footer'); 9 | const editProfileButton = document.getElementById('editProfileButton'); 10 | const table = document.getElementById('table'); 11 | 12 | function welcomeUser(username) { 13 | 14 | signInButton.classList.add('d-none'); 15 | signOutButton.classList.remove('d-none'); 16 | titleDiv.classList.add('d-none'); 17 | editProfileButton.classList.remove('d-none'); 18 | welcomeDiv.classList.remove('d-none'); 19 | welcomeDiv.innerHTML = `Welcome ${username}!` 20 | table.style.overflow = 'scroll'; 21 | } 22 | 23 | function updateTable(idTokenClaims) { 24 | tableDiv.classList.remove('d-none'); 25 | footerDiv.classList.remove('d-none'); 26 | const tokenClaims = createClaimsTable(idTokenClaims); 27 | Object.keys(tokenClaims).forEach((key) => { 28 | let row = tableBody.insertRow(0); 29 | let cell1 = row.insertCell(0); 30 | let cell2 = row.insertCell(1); 31 | let cell3 = row.insertCell(2); 32 | cell1.innerHTML = tokenClaims[key][0]; 33 | cell2.innerHTML = tokenClaims[key][1]; 34 | cell3.innerHTML = tokenClaims[key][2]; 35 | }) 36 | 37 | } -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "Vanilla JavaScript single-page application (SPA) using MSAL.js to authenticate users against Azure AD B2C", 4 | "Level": 100, 5 | "Client": "Vanilla JavaScript SPA", 6 | "RepositoryUrl": "ms-identity-javascript-tutorial", 7 | "Endpoint": "AAD v2.0", 8 | "Languages": ["javascript"], 9 | "Description": "Vanilla JavaScript single-page application (SPA) using MSAL.js to authenticate users against Azure AD B2C", 10 | "Products": ["azure-active-directory-b2c", "msal-js", "msal-browser"], 11 | "Platform": "JavaScript", 12 | "Provider": "B2C" 13 | }, 14 | 15 | "AADApps": [ 16 | { 17 | "Id": "client", 18 | "Name": "ms-identity-javascript-c1s2", 19 | "Kind": "SinglePageApplication", 20 | "Audience": "AzureADandPersonalMicrosoftAccount", 21 | "HomePage": "http://localhost:6420", 22 | "SampleSubPath": "1-Authentication\\2-sign-in-b2c", 23 | "ReplyUrls": "http://localhost:6420, http://localhost:6420/redirect" 24 | } 25 | ], 26 | "CodeConfiguration": [ 27 | { 28 | "App": "client", 29 | "SettingKind": "Replace", 30 | "SettingFile": "\\..\\App\\src\\authConfig.js", 31 | "Mappings": [ 32 | { 33 | "key": "Enter_the_Application_Id_Here", 34 | "value": ".AppId" 35 | }, 36 | { 37 | "key": "policyName", 38 | "value": "Enter_The_Your_policy_Name" 39 | }, 40 | { 41 | "key": "b2cDomain", 42 | "value": "Enter_The_Tenant_Domain_name" 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/1-Authentication/2-sign-in-b2c/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/ReadmeFiles/topology_b2c_signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/1-Authentication/2-sign-in-b2c/ReadmeFiles/topology_b2c_signin.png -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-identity-b2c-javascript-c1s2", 3 | "version": "1.0.0", 4 | "description": "Vanilla JavaScript single-page application (SPA) using MSAL.js to authenticate users against Azure AD B2C", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-tutorial.git" 14 | }, 15 | "keywords": [ 16 | "javascript", 17 | "msal", 18 | "authorization code", 19 | "authentication", 20 | "microsoft", 21 | "ms-identity", 22 | "azure-ad-b2c", 23 | "single-page app" 24 | ], 25 | "author": "derisen", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial/issues" 29 | }, 30 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial#readme", 31 | "dependencies": { 32 | "express": "^4.17.1", 33 | "morgan": "^1.10.0" 34 | }, 35 | "devDependencies": { 36 | "jest": "^27.0.6", 37 | "nodemon": "^2.0.20", 38 | "supertest": "^6.1.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/sample.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | const request = require('supertest'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | 9 | const app = require('./server.js'); 10 | 11 | jest.dontMock('fs'); 12 | 13 | const html = fs.readFileSync(path.resolve(__dirname, './App/index.html'), 'utf8'); 14 | 15 | describe('Sanitize index page', () => { 16 | beforeAll(async() => { 17 | global.document.documentElement.innerHTML = html.toString(); 18 | }); 19 | 20 | it('should have valid cdn link', () => { 21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser"); 22 | }); 23 | }); 24 | 25 | describe('Sanitize configuration object', () => { 26 | beforeAll(() => { 27 | global.msalConfig = require('./App/authConfig.js').msalConfig; 28 | }); 29 | 30 | it('should define the config object', () => { 31 | expect(msalConfig).toBeDefined(); 32 | }); 33 | 34 | it('should contain credentials', () => { 35 | 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; 36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(true); 37 | }); 38 | 39 | it('should contain authority URI', () => { 40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; 41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true); 42 | }); 43 | }); 44 | 45 | describe('Ensure pages served', () => { 46 | 47 | beforeAll(() => { 48 | process.env.NODE_ENV = 'test'; 49 | }); 50 | 51 | it('should get index page', async () => { 52 | const res = await request(app) 53 | .get('/'); 54 | 55 | const data = await fs.promises.readFile(path.join(__dirname, './App/index.html'), 'utf8'); 56 | expect(res.statusCode).toEqual(200); 57 | expect(res.text).toEqual(data); 58 | }); 59 | 60 | it('should get signout page', async () => { 61 | const res = await request(app) 62 | .get('/signout'); 63 | 64 | const data = await fs.promises.readFile(path.join(__dirname, './App/signout.html'), 'utf8'); 65 | expect(res.statusCode).toEqual(200); 66 | expect(res.text).toEqual(data); 67 | }); 68 | }); -------------------------------------------------------------------------------- /1-Authentication/2-sign-in-b2c/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const path = require('path'); 4 | 5 | const DEFAULT_PORT = process.env.PORT || 6420; 6 | 7 | // initialize express. 8 | const app = express(); 9 | 10 | // Configure morgan module to log all requests. 11 | app.use(morgan('dev')); 12 | 13 | // Setup app folders. 14 | app.use(express.static('App')); 15 | 16 | // Set up a route for signout.html 17 | app.get('/signout', (req, res) => { 18 | res.sendFile(path.join(__dirname + '/App/signout.html')); 19 | }); 20 | 21 | app.get('/redirect', (req, res) => { 22 | res.sendFile(path.join(__dirname + '/App/redirect.html')); 23 | }); 24 | 25 | // Set up a route for index.html 26 | app.get('*', (req, res) => { 27 | res.sendFile(path.join(__dirname + '/index.html')); 28 | }); 29 | 30 | app.listen(DEFAULT_PORT, () => { 31 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`) 32 | }); 33 | 34 | module.exports = app; -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration object to be passed to MSAL instance on creation. 3 | * For a full list of MSAL.js configuration parameters, visit: 4 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md 5 | */ 6 | const msalConfig = { 7 | auth: { 8 | clientId: 'Enter_the_Application_Id_Here', // This is the ONLY mandatory field that you need to supply. 9 | authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Id_Here', // Defaults to "https://login.microsoftonline.com/common" 10 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.href 11 | postLogoutRedirectUri: '/', //Indicates the page to navigate after logout. 12 | clientCapabilities: ['CP1'], // this lets the resource owner know that this client is capable of handling claims challenge. 13 | }, 14 | cache: { 15 | cacheLocation: 'localStorage', // This configures where your cache will be stored 16 | storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge 17 | }, 18 | system: { 19 | /** 20 | * Below you can configure MSAL.js logs. For more information, visit: 21 | * https://docs.microsoft.com/azure/active-directory/develop/msal-logging-js 22 | */ 23 | loggerOptions: { 24 | loggerCallback: (level, message, containsPii) => { 25 | if (containsPii) { 26 | return; 27 | } 28 | switch (level) { 29 | case msal.LogLevel.Error: 30 | console.error(message); 31 | return; 32 | case msal.LogLevel.Info: 33 | console.info(message); 34 | return; 35 | case msal.LogLevel.Verbose: 36 | console.debug(message); 37 | return; 38 | case msal.LogLevel.Warning: 39 | console.warn(message); 40 | return; 41 | default: 42 | return; 43 | } 44 | }, 45 | }, 46 | }, 47 | }; 48 | 49 | // Add here the endpoints for MS Graph API services you would like to use. 50 | const graphConfig = { 51 | graphMeEndpoint: { 52 | uri: 'https://graph.microsoft.com/v1.0/me', 53 | scopes: ['User.Read'], 54 | }, 55 | graphContactsEndpoint: { 56 | uri: 'https://graph.microsoft.com/v1.0/me/contacts', 57 | scopes: ['Contacts.Read'], 58 | }, 59 | }; 60 | 61 | /** 62 | * Scopes you add here will be prompted for user consent during sign-in. 63 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. 64 | * For more information about OIDC scopes, visit: 65 | * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes 66 | */ 67 | const loginRequest = { 68 | scopes: ["User.Read"] 69 | }; 70 | 71 | // exporting config object for jest 72 | if (typeof exports !== 'undefined') { 73 | module.exports = { 74 | msalConfig: msalConfig, 75 | graphConfig: graphConfig 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/authPopup.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ''; 6 | 7 | /** 8 | * This method adds an event callback function to the MSAL object 9 | * to handle the response from redirect flow. For more information, visit: 10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md 11 | */ 12 | myMSALObj.addEventCallback((event) => { 13 | if ( 14 | (event.eventType === msal.EventType.LOGIN_SUCCESS || 15 | event.eventType === msal.EventType.ACQUIRE_TOKEN_SUCCESS) && 16 | event.payload.account 17 | ) { 18 | const account = event.payload.account; 19 | myMSALObj.setActiveAccount(account); 20 | } 21 | 22 | if (event.eventType === msal.EventType.LOGOUT_SUCCESS) { 23 | if (myMSALObj.getAllAccounts().length > 0) { 24 | myMSALObj.setActiveAccount(myMSALObj.getAllAccounts()[0]); 25 | } 26 | } 27 | }); 28 | 29 | function selectAccount() { 30 | /** 31 | * See here for more info on account retrieval: 32 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 33 | */ 34 | const currentAccounts = myMSALObj.getAllAccounts(); 35 | if (currentAccounts === null) { 36 | return; 37 | } else if (currentAccounts.length >= 1) { 38 | // Add choose account code here 39 | username = myMSALObj.getActiveAccount().username; 40 | showWelcomeMessage(username, currentAccounts); 41 | } 42 | } 43 | 44 | async function addAnotherAccount(event) { 45 | if (event.target.innerHTML.includes('@')) { 46 | const username = event.target.innerHTML; 47 | const account = myMSALObj.getAllAccounts().find((account) => account.username === username); 48 | const activeAccount = myMSALObj.getActiveAccount(); 49 | if (account && activeAccount.homeAccountId != account.homeAccountId) { 50 | try { 51 | myMSALObj.setActiveAccount(account); 52 | let res = await myMSALObj.ssoSilent({ 53 | ...loginRequest, 54 | account: account, 55 | }); 56 | closeModal(); 57 | handleResponse(res); 58 | window.location.reload(); 59 | } catch (error) { 60 | if (error instanceof msal.InteractionRequiredAuthError) { 61 | let res = await myMSALObj.loginPopup({ 62 | ...loginRequest, 63 | prompt: 'login', 64 | }); 65 | handleResponse(res); 66 | window.location.reload(); 67 | } 68 | } 69 | } else { 70 | closeModal(); 71 | } 72 | } else { 73 | try { 74 | myMSALObj.setActiveAccount(null); 75 | const res = await myMSALObj.loginPopup({ 76 | ...loginRequest, 77 | prompt: 'login', 78 | }); 79 | handleResponse(res); 80 | closeModal(); 81 | window.location.reload(); 82 | } catch (error) { 83 | console.log(error); 84 | } 85 | } 86 | } 87 | 88 | function handleResponse(response) { 89 | /** 90 | * To see the full list of response object properties, visit: 91 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response 92 | */ 93 | 94 | if (response !== null) { 95 | const accounts = myMSALObj.getAllAccounts(); 96 | username = response.account.username; 97 | showWelcomeMessage(username, accounts); 98 | } else { 99 | selectAccount(); 100 | } 101 | } 102 | 103 | function signIn() { 104 | /** 105 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 106 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 107 | */ 108 | 109 | myMSALObj 110 | .loginPopup(loginRequest) 111 | .then(handleResponse) 112 | .catch((error) => { 113 | console.error(error); 114 | }); 115 | } 116 | 117 | function signOut() { 118 | /** 119 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 120 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 121 | */ 122 | const account = myMSALObj.getAccountByUsername(username); 123 | const logoutRequest = { 124 | account: account, 125 | mainWindowRedirectUri: '/', 126 | }; 127 | clearStorage(account); 128 | myMSALObj.logoutPopup(logoutRequest).catch((error) => { 129 | console.log(error); 130 | }); 131 | } 132 | 133 | function seeProfile() { 134 | callGraph( 135 | username, 136 | graphConfig.graphMeEndpoint.scopes, 137 | graphConfig.graphMeEndpoint.uri, 138 | msal.InteractionType.Popup, 139 | myMSALObj 140 | ); 141 | } 142 | 143 | function readContacts() { 144 | callGraph( 145 | username, 146 | graphConfig.graphContactsEndpoint.scopes, 147 | graphConfig.graphContactsEndpoint.uri, 148 | msal.InteractionType.Popup, 149 | myMSALObj 150 | ); 151 | } 152 | 153 | selectAccount(); 154 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/authRedirect.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ''; 6 | 7 | /** 8 | * This method adds an event callback function to the MSAL object 9 | * to handle the response from redirect flow. For more information, visit: 10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md 11 | */ 12 | myMSALObj.addEventCallback((event) => { 13 | if ( 14 | (event.eventType === msal.EventType.LOGIN_SUCCESS || 15 | event.eventType === msal.EventType.ACQUIRE_TOKEN_SUCCESS) && 16 | event.payload.account 17 | ) { 18 | const account = event.payload.account; 19 | myMSALObj.setActiveAccount(account); 20 | } 21 | 22 | if (event.eventType === msal.EventType.LOGOUT_SUCCESS) { 23 | if (myMSALObj.getAllAccounts().length > 0) { 24 | myMSALObj.setActiveAccount(myMSALObj.getAllAccounts()[0]); 25 | } 26 | } 27 | }); 28 | 29 | /** 30 | * A promise handler needs to be registered for handling the 31 | * response returned from redirect flow. For more information, visit: 32 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/acquire-token.md 33 | */ 34 | myMSALObj 35 | .handleRedirectPromise() 36 | .then(handleResponse) 37 | .catch((error) => { 38 | console.error(error); 39 | }); 40 | 41 | function selectAccount() { 42 | /** 43 | * See here for more info on account retrieval: 44 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 45 | */ 46 | 47 | const currentAccounts = myMSALObj.getAllAccounts(); 48 | 49 | if (!currentAccounts) { 50 | return; 51 | } else if (currentAccounts.length >= 1) { 52 | // Add your account choosing logic here 53 | username = myMSALObj.getActiveAccount().username; 54 | showWelcomeMessage(username, currentAccounts); 55 | } 56 | } 57 | 58 | async function addAnotherAccount(event) { 59 | if (event.target.innerHTML.includes("@")) { 60 | const username = event.target.innerHTML; 61 | const account = myMSALObj.getAllAccounts().find((account) => account.username === username); 62 | const activeAccount = myMSALObj.getActiveAccount(); 63 | if (account && activeAccount.homeAccountId != account.homeAccountId) { 64 | try { 65 | myMSALObj.setActiveAccount(account); 66 | let res = await myMSALObj.ssoSilent({ 67 | ...loginRequest, 68 | account: account, 69 | }); 70 | handleResponse(res); 71 | closeModal(); 72 | window.location.reload(); 73 | } catch (error) { 74 | if (error instanceof msal.InteractionRequiredAuthError) { 75 | await myMSALObj.loginRedirect({ 76 | ...loginRequest, 77 | prompt: 'login', 78 | }); 79 | } 80 | } 81 | } else { 82 | closeModal(); 83 | } 84 | } else { 85 | try { 86 | myMSALObj.setActiveAccount(null); 87 | await myMSALObj.loginRedirect({ 88 | ...loginRequest, 89 | prompt: 'login', 90 | }); 91 | } catch (error) { 92 | console.log(error); 93 | } 94 | } 95 | } 96 | 97 | function handleResponse(response) { 98 | if (response !== null) { 99 | const accounts = myMSALObj.getAllAccounts(); 100 | username = response.account.username; 101 | showWelcomeMessage(username, accounts); 102 | } else { 103 | selectAccount(); 104 | } 105 | } 106 | 107 | function signIn() { 108 | 109 | /** 110 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 111 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 112 | */ 113 | 114 | myMSALObj.loginRedirect(loginRequest); 115 | } 116 | 117 | function signOut() { 118 | 119 | /** 120 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 121 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 122 | */ 123 | 124 | // Choose which account to logout from by passing a username. 125 | const account = myMSALObj.getAccountByUsername(username); 126 | const logoutRequest = { 127 | account: account, 128 | loginHint: account.idTokenClaims.login_hint, 129 | }; 130 | 131 | clearStorage(account); 132 | myMSALObj.logoutRedirect(logoutRequest); 133 | } 134 | 135 | function seeProfile() { 136 | callGraph( 137 | username, 138 | graphConfig.graphMeEndpoint.scopes, 139 | graphConfig.graphMeEndpoint.uri, 140 | msal.InteractionType.Redirect, 141 | myMSALObj 142 | ); 143 | } 144 | 145 | function readContacts() { 146 | callGraph( 147 | username, 148 | graphConfig.graphContactsEndpoint.scopes, 149 | graphConfig.graphContactsEndpoint.uri, 150 | msal.InteractionType.Redirect, 151 | myMSALObj 152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/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 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. 4 | */ 5 | 6 | /** 7 | * This method calls the Graph API by utilizing the graph client instance. 8 | * @param {String} username 9 | * @param {Array} scopes 10 | * @param {String} uri 11 | * @param {String} interactionType 12 | * @param {Object} myMSALObj 13 | * @returns 14 | */ 15 | const callGraph = async (username, scopes, uri, interactionType, myMSALObj) => { 16 | const account = myMSALObj.getAccountByUsername(username); 17 | try { 18 | let response = await getGraphClient({ 19 | account: account, 20 | scopes: scopes, 21 | interactionType: interactionType, 22 | }) 23 | .api(uri) 24 | .responseType('raw') 25 | .get(); 26 | 27 | response = await handleClaimsChallenge(account, response, uri); 28 | if (response && response.error === 'claims_challenge_occurred') throw response.error; 29 | updateUI(response, uri); 30 | } catch (error) { 31 | if (error === 'claims_challenge_occurred') { 32 | const resource = new URL(uri).hostname; 33 | const claims = 34 | account && 35 | getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`) 36 | ? window.atob( 37 | getClaimsFromStorage( 38 | `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` 39 | ) 40 | ) 41 | : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} 42 | let request = { 43 | account: account, 44 | scopes: scopes, 45 | claims: claims, 46 | }; 47 | switch (interactionType) { 48 | case msal.InteractionType.Popup: 49 | 50 | await myMSALObj.acquireTokenPopup({ 51 | ...request, 52 | redirectUri: '/redirect', 53 | }); 54 | break; 55 | case msal.InteractionType.Redirect: 56 | await myMSALObj.acquireTokenRedirect(request); 57 | break; 58 | default: 59 | await myMSALObj.acquireTokenRedirect(request); 60 | break; 61 | } 62 | } else if (error.toString().includes('404')) { 63 | return updateUI(null, uri); 64 | } else { 65 | console.log(error); 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * This method inspects the HTTPS response from a fetch call for the "www-authenticate header" 72 | * If present, it grabs the claims challenge from the header and store it in the localStorage 73 | * For more information, visit: https://docs.microsoft.com/en-us/azure/active-directory/develop/claims-challenge#claims-challenge-header-format 74 | * @param {object} response 75 | * @returns response 76 | */ 77 | const handleClaimsChallenge = async (account, response, apiEndpoint) => { 78 | if (response.status === 200) { 79 | return response.json(); 80 | } else if (response.status === 401) { 81 | if (response.headers.get('WWW-Authenticate')) { 82 | const authenticateHeader = response.headers.get('WWW-Authenticate'); 83 | const claimsChallenge = parseChallenges(authenticateHeader); 84 | /** 85 | * This method stores the claim challenge to the session storage in the browser to be used when acquiring a token. 86 | * To ensure that we are fetching the correct claim from the storage, we are using the clientId 87 | * of the application and oid (user’s object id) as the key identifier of the claim with schema 88 | * cc... 89 | */ 90 | addClaimsToStorage( 91 | claimsChallenge.claims, 92 | `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${new URL(apiEndpoint).hostname}` 93 | ); 94 | return { error: 'claims_challenge_occurred', payload: claimsChallenge.claims }; 95 | } 96 | 97 | throw new Error(`Unauthorized: ${response.status}`); 98 | } else { 99 | throw new Error(`Something went wrong with the request: ${response.status}`); 100 | } 101 | }; 102 | 103 | /** 104 | * This method parses WWW-Authenticate authentication headers 105 | * @param header 106 | * @return {Object} challengeMap 107 | */ 108 | const parseChallenges = (header) => { 109 | const schemeSeparator = header.indexOf(' '); 110 | const challenges = header.substring(schemeSeparator + 1).split(', '); 111 | const challengeMap = {}; 112 | 113 | challenges.forEach((challenge) => { 114 | const [key, value] = challenge.split('='); 115 | challengeMap[key.trim()] = window.decodeURI(value.replace(/(^"|"$)/g, '')); 116 | }); 117 | 118 | return challengeMap; 119 | } -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/graph.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The code below demonstrates how you can use MSAL as a custom authentication provider for the Microsoft Graph JavaScript SDK. 3 | * You do NOT need to implement a custom provider. Microsoft Graph JavaScript SDK v3.0 (preview) offers AuthCodeMSALBrowserAuthenticationProvider 4 | * which handles token acquisition and renewal for you automatically. For more information on how to use it, visit: 5 | * https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/AuthCodeMSALBrowserAuthenticationProvider.md 6 | */ 7 | 8 | /** 9 | * Returns a graph client object with the provided token acquisition options 10 | * @param {Object} providerOptions: object containing user account, required scopes and interaction type 11 | */ 12 | const getGraphClient = (providerOptions) => { 13 | 14 | /** 15 | * Pass the instance as authProvider in ClientOptions to instantiate the Client which will create and set the default middleware chain. 16 | * For more information, visit: https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md 17 | */ 18 | let clientOptions = { 19 | authProvider: new MsalAuthenticationProvider(providerOptions), 20 | }; 21 | 22 | const graphClient = MicrosoftGraph.Client.initWithMiddleware(clientOptions); 23 | 24 | return graphClient; 25 | }; 26 | 27 | /** 28 | * This class implements the IAuthenticationProvider interface, which allows a custom authentication provider to be 29 | * used with the Graph client. See: https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/src/IAuthenticationProvider.ts 30 | */ 31 | class MsalAuthenticationProvider { 32 | 33 | account; // user account object to be used when attempting silent token acquisition 34 | scopes; // array of scopes required for this resource endpoint 35 | interactionType; // type of interaction to fallback to when silent token acquisition fails 36 | claims; 37 | 38 | constructor(providerOptions) { 39 | this.account = providerOptions.account; 40 | this.scopes = providerOptions.scopes; 41 | this.interactionType = providerOptions.interactionType; 42 | const resource = new URL(graphConfig.graphMeEndpoint.uri).hostname; 43 | this.claims = 44 | this.account && getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${this.account.idTokenClaims.oid}.${resource}`) 45 | ? window.atob( 46 | getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${this.account.idTokenClaims.oid}.${resource}`) 47 | ) 48 | : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} 49 | } 50 | 51 | /** 52 | * This method will get called before every request to the ms graph server 53 | * This should return a Promise that resolves to an accessToken (in case of success) or rejects with error (in case of failure) 54 | * Basically this method will contain the implementation for getting and refreshing accessTokens 55 | */ 56 | getAccessToken() { 57 | return new Promise(async (resolve, reject) => { 58 | let response; 59 | 60 | try { 61 | response = await myMSALObj.acquireTokenSilent({ 62 | account: this.account, 63 | scopes: this.scopes, 64 | claims: this.claims 65 | }); 66 | 67 | if (response.accessToken) { 68 | resolve(response.accessToken); 69 | } else { 70 | reject(Error('Failed to acquire an access token')); 71 | } 72 | } catch (error) { 73 | // in case if silent token acquisition fails, fallback to an interactive method 74 | if (error instanceof msal.InteractionRequiredAuthError) { 75 | switch (this.interactionType) { 76 | case msal.InteractionType.Popup: 77 | response = await myMSALObj.acquireTokenPopup({ 78 | scopes: this.scopes, 79 | claims: this.claims, 80 | redirectUri: '/redirect', 81 | }); 82 | 83 | if (response.accessToken) { 84 | resolve(response.accessToken); 85 | } else { 86 | reject(Error('Failed to acquire an access token')); 87 | } 88 | break; 89 | 90 | case msal.InteractionType.Redirect: 91 | /** 92 | * This will cause the app to leave the current page and redirect to the consent screen. 93 | * Once consent is provided, the app will return back to the current page and then the 94 | * silent token acquisition will succeed. 95 | */ 96 | myMSALObj.acquireTokenRedirect({ 97 | scopes: this.scopes, 98 | claims: this.claims, 99 | }); 100 | break; 101 | 102 | default: 103 | break; 104 | } 105 | } 106 | } 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/redirect.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/styles.css: -------------------------------------------------------------------------------- 1 | .navbarStyle { 2 | padding: .5rem 1rem !important; 3 | } 4 | 5 | .dropdown-toggle { 6 | visibility: hidden; 7 | display: none !important; 8 | } 9 | 10 | .list-group-item { 11 | cursor: pointer; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/ui.js: -------------------------------------------------------------------------------- 1 | // Select DOM elements to work with 2 | const welcomeDiv = document.getElementById("WelcomeMessage"); 3 | const signInButton = document.getElementById("SignIn"); 4 | const dropdownButton = document.getElementById('dropdownMenuButton1'); 5 | const cardDiv = document.getElementById("card-div"); 6 | const mailButton = document.getElementById("readMail"); 7 | const profileButton = document.getElementById("seeProfile"); 8 | const profileDiv = document.getElementById("profile-div"); 9 | const listGroup = document.getElementById('list-group'); 10 | 11 | function showWelcomeMessage(username, accounts) { 12 | // Reconfiguring DOM elements 13 | cardDiv.style.display = 'initial'; 14 | signInButton.style.visibility = 'hidden'; 15 | welcomeDiv.innerHTML = `Welcome ${username}`; 16 | dropdownButton.setAttribute('style', 'display:inline !important; visibility:visible'); 17 | dropdownButton.innerHTML = username; 18 | accounts.forEach(account => { 19 | let item = document.getElementById(account.username); 20 | if (!item) { 21 | const listItem = document.createElement('li'); 22 | listItem.setAttribute('onclick', 'addAnotherAccount(event)'); 23 | listItem.setAttribute('id', account.username); 24 | listItem.innerHTML = account.username; 25 | if (account.username === username) { 26 | listItem.setAttribute('class', 'list-group-item active'); 27 | } else { 28 | listItem.setAttribute('class', 'list-group-item'); 29 | } 30 | listGroup.appendChild(listItem); 31 | } else { 32 | if (account.username === username) { 33 | item.setAttribute('class', 'list-group-item active'); 34 | } else { 35 | item.setAttribute('active', 'list-group-item'); 36 | } 37 | } 38 | }); 39 | } 40 | 41 | function closeModal() { 42 | const element = document.getElementById("closeModal"); 43 | element.click(); 44 | } 45 | 46 | function updateUI(data, endpoint) { 47 | console.log('Graph API responded at: ' + new Date().toString()); 48 | 49 | if (endpoint === graphConfig.graphMeEndpoint.uri) { 50 | profileDiv.innerHTML = ''; 51 | const title = document.createElement('p'); 52 | title.innerHTML = "Title: " + data.jobTitle; 53 | const email = document.createElement('p'); 54 | email.innerHTML = "Mail: " + data.mail; 55 | const phone = document.createElement('p'); 56 | phone.innerHTML = "Phone: " + data.businessPhones[0]; 57 | const address = document.createElement('p'); 58 | address.innerHTML = "Location: " + data.officeLocation; 59 | profileDiv.appendChild(title); 60 | profileDiv.appendChild(email); 61 | profileDiv.appendChild(phone); 62 | profileDiv.appendChild(address); 63 | 64 | } else if (endpoint === graphConfig.graphContactsEndpoint.uri) { 65 | if (!data || data.value.length < 1) { 66 | alert('Your contacts is empty!'); 67 | } else { 68 | const tabList = document.getElementById('list-tab'); 69 | tabList.innerHTML = ''; // clear tabList at each readMail call 70 | 71 | data.value.map((d, i) => { 72 | if (i < 10) { 73 | const listItem = document.createElement('a'); 74 | listItem.setAttribute('class', 'list-group-item list-group-item-action'); 75 | listItem.setAttribute('id', 'list' + i + 'list'); 76 | listItem.setAttribute('data-toggle', 'list'); 77 | listItem.setAttribute('href', '#list' + i); 78 | listItem.setAttribute('role', 'tab'); 79 | listItem.setAttribute('aria-controls', i); 80 | listItem.innerHTML = 81 | ' Name: ' + d.displayName + '

' + 'Note: ' + d.personalNotes + '...'; 82 | tabList.appendChild(listItem); 83 | } 84 | }); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/App/utils/storageUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This method stores the claim challenge to the localStorage in the browser to be used when acquiring a token 3 | * @param {String} claimsChallenge 4 | */ 5 | const addClaimsToStorage = (claimsChallenge, claimsChallengeId) => { 6 | sessionStorage.setItem(claimsChallengeId, claimsChallenge); 7 | }; 8 | 9 | /** 10 | * This method retrieves the claims challenge from the localStorage 11 | * @param {string} claimsChallengeId 12 | * @returns 13 | */ 14 | const getClaimsFromStorage = (claimsChallengeId) => { 15 | return sessionStorage.getItem(claimsChallengeId); 16 | }; 17 | 18 | /** 19 | * This method clears localStorage of any claims challenge entry 20 | * @param {Object} account 21 | */ 22 | const clearStorage = (account) => { 23 | for (var key in sessionStorage) { 24 | if (key.startsWith(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}`)) 25 | sessionStorage.removeItem(key); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/AppCreationScripts/Cleanup.ps1: -------------------------------------------------------------------------------- 1 |  2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] 5 | [string] $tenantId, 6 | [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] 7 | [string] $azureEnvironmentName 8 | ) 9 | 10 | 11 | Function Cleanup 12 | { 13 | if (!$azureEnvironmentName) 14 | { 15 | $azureEnvironmentName = "Global" 16 | } 17 | 18 | <# 19 | .Description 20 | This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script 21 | #> 22 | 23 | # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant 24 | # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. 25 | 26 | # Connect to the Microsoft Graph API 27 | Write-Host "Connecting to Microsoft Graph" 28 | 29 | 30 | if ($tenantId -eq "") 31 | { 32 | Connect-MgGraph -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName 33 | } 34 | else 35 | { 36 | Connect-MgGraph -TenantId $tenantId -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName 37 | } 38 | 39 | $context = Get-MgContext 40 | $tenantId = $context.TenantId 41 | 42 | # Get the user running the script 43 | $currentUserPrincipalName = $context.Account 44 | $user = Get-MgUser -Filter "UserPrincipalName eq '$($context.Account)'" 45 | 46 | # get the tenant we signed in to 47 | $Tenant = Get-MgOrganization 48 | $tenantName = $Tenant.DisplayName 49 | 50 | $verifiedDomain = $Tenant.VerifiedDomains | where {$_.Isdefault -eq $true} 51 | $verifiedDomainName = $verifiedDomain.Name 52 | $tenantId = $Tenant.Id 53 | 54 | Write-Host ("Connected to Tenant {0} ({1}) as account '{2}'. Domain is '{3}'" -f $Tenant.DisplayName, $Tenant.Id, $currentUserPrincipalName, $verifiedDomainName) 55 | 56 | # Removes the applications 57 | Write-Host "Cleaning-up applications from tenant '$tenantId'" 58 | 59 | Write-Host "Removing 'client' (ms-identity-javascript-c2s1) if needed" 60 | try 61 | { 62 | Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-MgApplication -ApplicationId $_.Id } 63 | } 64 | catch 65 | { 66 | $message = $_ 67 | Write-Warning $Error[0] 68 | Write-Host "Unable to remove the application 'ms-identity-javascript-c2s1'. Error is $message. Try deleting manually." -ForegroundColor White -BackgroundColor Red 69 | } 70 | 71 | Write-Host "Making sure there are no more (ms-identity-javascript-c2s1) applications found, will remove if needed..." 72 | $apps = Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" | Format-List Id, DisplayName, AppId, SignInAudience, PublisherDomain 73 | 74 | if ($apps) 75 | { 76 | Remove-MgApplication -ApplicationId $apps.Id 77 | } 78 | 79 | foreach ($app in $apps) 80 | { 81 | Remove-MgApplication -ApplicationId $app.Id 82 | Write-Host "Removed ms-identity-javascript-c2s1.." 83 | } 84 | 85 | # also remove service principals of this app 86 | try 87 | { 88 | Get-MgServicePrincipal -filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-MgServicePrincipal -ServicePrincipalId $_.Id -Confirm:$false} 89 | } 90 | catch 91 | { 92 | $message = $_ 93 | Write-Warning $Error[0] 94 | Write-Host "Unable to remove ServicePrincipal 'ms-identity-javascript-c2s1'. Error is $message. Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red 95 | } 96 | } 97 | 98 | # Pre-requisites 99 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph")) { 100 | Install-Module "Microsoft.Graph" -Scope CurrentUser 101 | } 102 | 103 | #Import-Module Microsoft.Graph 104 | 105 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) { 106 | Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser 107 | } 108 | 109 | Import-Module Microsoft.Graph.Authentication 110 | 111 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Identity.DirectoryManagement")) { 112 | Install-Module "Microsoft.Graph.Identity.DirectoryManagement" -Scope CurrentUser 113 | } 114 | 115 | Import-Module Microsoft.Graph.Identity.DirectoryManagement 116 | 117 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { 118 | Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser 119 | } 120 | 121 | Import-Module Microsoft.Graph.Applications 122 | 123 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Groups")) { 124 | Install-Module "Microsoft.Graph.Groups" -Scope CurrentUser 125 | } 126 | 127 | Import-Module Microsoft.Graph.Groups 128 | 129 | if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Users")) { 130 | Install-Module "Microsoft.Graph.Users" -Scope CurrentUser 131 | } 132 | 133 | Import-Module Microsoft.Graph.Users 134 | 135 | $ErrorActionPreference = "Stop" 136 | 137 | 138 | try 139 | { 140 | Cleanup -tenantId $tenantId -environment $azureEnvironmentName 141 | } 142 | catch 143 | { 144 | $_.Exception.ToString() | out-host 145 | $message = $_ 146 | Write-Warning $Error[0] 147 | Write-Host "Unable to register apps. Error is $message." -ForegroundColor White -BackgroundColor Red 148 | } 149 | 150 | Write-Host "Disconnecting from tenant" 151 | Disconnect-MgGraph 152 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Title": "Vanilla JavaScript single-page application using MSAL.js to authenticate users to call Microsoft Graph", 4 | "Level": 100, 5 | "Client": "Vanilla JavaScript SPA", 6 | "Service": "Microsoft Graph", 7 | "RepositoryUrl": "ms-identity-javascript-tutorial", 8 | "Endpoint": "AAD v2.0", 9 | "Languages": ["javascript"], 10 | "Description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users and calling the Microsoft Graph API on their behalf", 11 | "Products": ["azure-active-directory", "msal-js", "msal-browser"], 12 | "Platform": "JavaScript" 13 | }, 14 | "AADApps": [ 15 | { 16 | "Id": "client", 17 | "Name": "ms-identity-javascript-c2s1", 18 | "Kind": "SinglePageApplication", 19 | "HomePage": "http://localhost:3000/", 20 | "SampleSubPath": "2-Authorization-I\\1-call-graph", 21 | "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect", 22 | "Audience": "AzureADMyOrg", 23 | "OptionalClaims": { 24 | "IdTokenClaims": ["acct", "login_hint"] 25 | }, 26 | "RequiredResourcesAccess": [ 27 | { 28 | "Resource": "Microsoft Graph", 29 | "DelegatedPermissions": ["User.Read", "Contacts.Read"] 30 | } 31 | ] 32 | } 33 | ], 34 | "CodeConfiguration": [ 35 | { 36 | "App": "client", 37 | "SettingKind": "Replace", 38 | "SettingFile": "\\..\\App\\authConfig.js", 39 | "Mappings": [ 40 | { 41 | "key": "Enter_the_Application_Id_Here", 42 | "value": ".AppId" 43 | }, 44 | { 45 | "key": "Enter_the_Tenant_Id_Here", 46 | "value": "$tenantId" 47 | } 48 | ] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/ReadmeFiles/topology_callgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/2-Authorization-I/1-call-graph/ReadmeFiles/topology_callgraph.png -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-identity-javascript-c2s1", 3 | "version": "1.0.0", 4 | "description": "Vanilla JavaScript single-page application using MSAL.js to authorize users for calling Microsoft Graph", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "test": "jest --forceExit" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-tutorial.git" 14 | }, 15 | "keywords": [ 16 | "javascript", 17 | "msal", 18 | "authorization", 19 | "code", 20 | "authentication", 21 | "microsoft", 22 | "ms-identity", 23 | "azure-ad", 24 | "spa", 25 | "node.js", 26 | "msal.js" 27 | ], 28 | "author": "derisen", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial/issues" 32 | }, 33 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-tutorial#readme", 34 | "dependencies": { 35 | "express": "^4.17.1", 36 | "morgan": "^1.10.0" 37 | }, 38 | "devDependencies": { 39 | "jest": "^27.0.6", 40 | "nodemon": "^2.0.20", 41 | "supertest": "^6.1.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/sample.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | const request = require('supertest'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | 9 | const app = require('./server.js'); 10 | 11 | jest.dontMock('fs'); 12 | 13 | const html = fs.readFileSync(path.resolve(__dirname, './App/index.html'), 'utf8'); 14 | 15 | describe('Sanitize index page', () => { 16 | beforeAll(async() => { 17 | global.document.documentElement.innerHTML = html.toString(); 18 | }); 19 | 20 | it('should have valid cdn link', () => { 21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser"); 22 | }); 23 | }); 24 | 25 | describe('Sanitize configuration object', () => { 26 | beforeAll(() => { 27 | global.msalConfig = require('./App/authConfig.js').msalConfig; 28 | }); 29 | 30 | it('should define the config object', () => { 31 | expect(msalConfig).toBeDefined(); 32 | }); 33 | 34 | it('should not contain credentials', () => { 35 | 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; 36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(false); 37 | }); 38 | 39 | it('should contain authority URI', () => { 40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; 41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true); 42 | }); 43 | }); 44 | 45 | describe('Ensure pages served', () => { 46 | 47 | beforeAll(() => { 48 | process.env.NODE_ENV = 'test'; 49 | }); 50 | 51 | it('should get index page', async () => { 52 | const res = await request(app) 53 | .get('/'); 54 | 55 | const data = await fs.promises.readFile(path.join(__dirname, './App/index.html'), 'utf8'); 56 | expect(res.statusCode).toEqual(200); 57 | expect(res.text).toEqual(data); 58 | }); 59 | }); -------------------------------------------------------------------------------- /2-Authorization-I/1-call-graph/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const path = require('path'); 4 | 5 | const DEFAULT_PORT = process.env.PORT || 3000; 6 | 7 | // initialize express. 8 | const app = express(); 9 | 10 | // Configure morgan module to log all requests. 11 | app.use(morgan('dev')); 12 | 13 | // Setup app folders. 14 | app.use(express.static('App')); 15 | 16 | app.get('/redirect', (req, res) => { 17 | res.sendFile(path.join(__dirname + '/App/redirect.html')); 18 | }); 19 | 20 | // Set up a route for index.html 21 | app.get('*', (req, res) => { 22 | res.sendFile(path.join(__dirname + '/index.html')); 23 | }); 24 | 25 | app.listen(DEFAULT_PORT, () => { 26 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`) 27 | }); 28 | 29 | module.exports = app; 30 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const cors = require('cors'); 4 | const rateLimit = require('express-rate-limit'); 5 | 6 | const passport = require('passport'); 7 | const passportAzureAd = require('passport-azure-ad'); 8 | 9 | const authConfig = require('./authConfig'); 10 | const router = require('./routes/index'); 11 | 12 | const app = express(); 13 | 14 | /** 15 | * If your app is behind a proxy, reverse proxy or a load balancer, consider 16 | * letting express know that you are behind that proxy. To do so, uncomment 17 | * the line below. 18 | */ 19 | 20 | // app.set('trust proxy', /* numberOfProxies */); 21 | 22 | /** 23 | * HTTP request handlers should not perform expensive operations such as accessing the file system, 24 | * executing an operating system command or interacting with a database without limiting the rate at 25 | * which requests are accepted. Otherwise, the application becomes vulnerable to denial-of-service attacks 26 | * where an attacker can cause the application to crash or become unresponsive by issuing a large number of 27 | * requests at the same time. For more information, visit: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html 28 | */ 29 | const limiter = rateLimit({ 30 | windowMs: 15 * 60 * 1000, // 15 minutes 31 | max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) 32 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 33 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 34 | }); 35 | 36 | // Apply the rate limiting middleware to all requests 37 | app.use(limiter) 38 | 39 | /** 40 | * Enable CORS middleware. In production, modify as to allow only designated origins and methods. 41 | * If you are using Azure App Service, we recommend removing the line below and configure CORS on the App Service itself. 42 | */ 43 | app.use(cors()); 44 | 45 | app.use(express.json()); 46 | app.use(express.urlencoded({ extended: false })); 47 | app.use(morgan('dev')); 48 | 49 | const bearerStrategy = new passportAzureAd.BearerStrategy({ 50 | identityMetadata: `https://${authConfig.metadata.authority}/${authConfig.credentials.tenantID}/${authConfig.metadata.version}/${authConfig.metadata.discovery}`, 51 | issuer: `https://${authConfig.metadata.authority}/${authConfig.credentials.tenantID}/${authConfig.metadata.version}`, 52 | clientID: authConfig.credentials.clientID, 53 | audience: authConfig.credentials.clientID, // audience is this application 54 | validateIssuer: authConfig.settings.validateIssuer, 55 | passReqToCallback: authConfig.settings.passReqToCallback, 56 | loggingLevel: authConfig.settings.loggingLevel, 57 | loggingNoPII: authConfig.settings.loggingNoPII, 58 | }, (req, token, done) => { 59 | 60 | /** 61 | * Below you can do extended token validation and check for additional claims, such as: 62 | * - check if the caller's tenant is in the allowed tenants list via the 'tid' claim (for multi-tenant applications) 63 | * - check if the caller's account is homed or guest via the 'acct' optional claim 64 | * - check if the caller belongs to right roles or groups via the 'roles' or 'groups' claim, respectively 65 | * 66 | * Bear in mind that you can do any of the above checks within the individual routes and/or controllers as well. 67 | * For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/access-tokens#validate-the-user-has-permission-to-access-this-data 68 | */ 69 | 70 | 71 | /** 72 | * Lines below verifies if the caller's client ID is in the list of allowed clients. 73 | * This ensures only the applications with the right client ID can access this API. 74 | * To do so, we use "azp" claim in the access token. Uncomment the lines below to enable this check. 75 | */ 76 | 77 | // const myAllowedClientsList = [ 78 | // /* add here the client IDs of the applications that are allowed to call this API */ 79 | // ] 80 | 81 | // if (!myAllowedClientsList.includes(token.azp)) { 82 | // return done(new Error('Unauthorized'), {}, "Client not allowed"); 83 | // } 84 | 85 | 86 | /** 87 | * Access tokens that have neither the 'scp' (for delegated permissions) nor 88 | * 'roles' (for application permissions) claim are not to be honored. 89 | */ 90 | if (!token.hasOwnProperty('scp') && !token.hasOwnProperty('roles')) { 91 | return done(new Error('Unauthorized'), null, "No delegated or app permission claims found"); 92 | } 93 | 94 | /** 95 | * If needed, pass down additional user info to route using the second argument below. 96 | * This information will be available in the req.user object. 97 | */ 98 | return done(null, {}, token); 99 | }); 100 | 101 | app.use(passport.initialize()); 102 | 103 | passport.use(bearerStrategy); 104 | 105 | app.use('/api', (req, res, next) => { 106 | passport.authenticate('oauth-bearer', { 107 | session: false, 108 | 109 | /** 110 | * If you are building a multi-tenant application and you need supply the tenant ID or name dynamically, 111 | * uncomment the line below and pass in the tenant information. For more information, see: 112 | * https://github.com/AzureAD/passport-azure-ad#423-options-available-for-passportauthenticate 113 | */ 114 | 115 | // tenantIdOrName: 116 | 117 | }, (err, user, info) => { 118 | if (err) { 119 | /** 120 | * An error occurred during authorization. Either pass the error to the next function 121 | * for Express error handler to handle, or send a response with the appropriate status code. 122 | */ 123 | return res.status(401).json({ error: err.message }); 124 | } 125 | 126 | if (!user) { 127 | // If no user object found, send a 401 response. 128 | return res.status(401).json({ error: 'Unauthorized' }); 129 | } 130 | 131 | if (info) { 132 | // access token payload will be available in req.authInfo downstream 133 | req.authInfo = info; 134 | return next(); 135 | } 136 | })(req, res, next); 137 | }, 138 | router, // the router with all the routes 139 | (err, req, res, next) => { 140 | /** 141 | * Add your custom error handling logic here. For more information, see: 142 | * http://expressjs.com/en/guide/error-handling.html 143 | */ 144 | 145 | // set locals, only providing error in development 146 | res.locals.message = err.message; 147 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 148 | 149 | // send error response 150 | res.status(err.status || 500).send(err); 151 | } 152 | ); 153 | 154 | const port = process.env.PORT || 5000; 155 | 156 | app.listen(port, () => { 157 | console.log('Listening on port ' + port); 158 | }); 159 | 160 | module.exports = app; 161 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/auth/permissionUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Indicates whether the access token was issued to a user or an application. 3 | * @param {Object} accessTokenPayload 4 | * @returns {boolean} 5 | */ 6 | const isAppOnlyToken = (accessTokenPayload) => { 7 | /** 8 | * An access token issued by Azure AD will have at least one of the two claims. Access tokens 9 | * issued to a user will have the 'scp' claim. Access tokens issued to an application will have 10 | * the roles claim. Access tokens that contain both claims are issued only to users, where the scp 11 | * claim designates the delegated permissions, while the roles claim designates the user's role. 12 | * 13 | * To determine whether an access token was issued to a user (i.e delegated) or an application 14 | * more easily, we recommend enabling the optional claim 'idtyp'. For more information, see: 15 | * https://docs.microsoft.com/azure/active-directory/develop/access-tokens#user-and-application-tokens 16 | */ 17 | if (!accessTokenPayload.hasOwnProperty('idtyp')) { 18 | if (accessTokenPayload.hasOwnProperty('scp')) { 19 | return false; 20 | } else if (!accessTokenPayload.hasOwnProperty('scp') && accessTokenPayload.hasOwnProperty('roles')) { 21 | return true; 22 | } 23 | } 24 | 25 | return accessTokenPayload.idtyp === 'app'; 26 | }; 27 | 28 | /** 29 | * Ensures that the access token has the specified delegated permissions. 30 | * @param {Object} accessTokenPayload: Parsed access token payload 31 | * @param {Array} requiredPermission: list of required permissions 32 | * @returns {boolean} 33 | */ 34 | const hasRequiredDelegatedPermissions = (accessTokenPayload, requiredPermission) => { 35 | const normalizedRequiredPermissions = requiredPermission.map(permission => permission.toUpperCase()); 36 | 37 | if (accessTokenPayload.hasOwnProperty('scp') && accessTokenPayload.scp.split(' ') 38 | .some(claim => normalizedRequiredPermissions.includes(claim.toUpperCase()))) { 39 | return true; 40 | } 41 | 42 | return false; 43 | } 44 | 45 | /** 46 | * Ensures that the access token has the specified application permissions. 47 | * @param {Object} accessTokenPayload: Parsed access token payload 48 | * @param {Array} requiredPermission: list of required permissions 49 | * @returns {boolean} 50 | */ 51 | const hasRequiredApplicationPermissions = (accessTokenPayload, requiredPermission) => { 52 | const normalizedRequiredPermissions = requiredPermission.map(permission => permission.toUpperCase()); 53 | 54 | if (accessTokenPayload.hasOwnProperty('roles') && accessTokenPayload.roles 55 | .some(claim => normalizedRequiredPermissions.includes(claim.toUpperCase()))) { 56 | return true; 57 | } 58 | 59 | return false; 60 | } 61 | 62 | module.exports = { 63 | isAppOnlyToken, 64 | hasRequiredDelegatedPermissions, 65 | hasRequiredApplicationPermissions, 66 | } 67 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/authConfig.js: -------------------------------------------------------------------------------- 1 | const passportConfig = { 2 | credentials: { 3 | tenantID: "Enter_the_Tenant_Info_Here", 4 | clientID: "Enter_the_Application_Id_Here" 5 | }, 6 | metadata: { 7 | authority: "login.microsoftonline.com", 8 | discovery: ".well-known/openid-configuration", 9 | version: "v2.0" 10 | }, 11 | settings: { 12 | validateIssuer: true, 13 | passReqToCallback: true, 14 | loggingLevel: "info", 15 | loggingNoPII: true, 16 | }, 17 | protectedRoutes: { 18 | todolist: { 19 | endpoint: "/api/todolist", 20 | delegatedPermissions: { 21 | read: ["Todolist.Read", "Todolist.ReadWrite"], 22 | write: ["Todolist.ReadWrite"] 23 | }, 24 | applicationPermissions: { 25 | read: ["Todolist.Read.All", "Todolist.ReadWrite.All"], 26 | write: ["Todolist.ReadWrite.All"] 27 | } 28 | } 29 | } 30 | } 31 | 32 | module.exports = passportConfig; 33 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/controllers/todolist.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 | const { v4: uuidv4 } = require('uuid'); 6 | 7 | const { 8 | isAppOnlyToken, 9 | hasRequiredDelegatedPermissions, 10 | hasRequiredApplicationPermissions 11 | } = require('../auth/permissionUtils'); 12 | 13 | const authConfig = require('../authConfig'); 14 | 15 | exports.getTodo = (req, res, next) => { 16 | if (isAppOnlyToken(req.authInfo)) { 17 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.read)) { 18 | try { 19 | const id = req.params.id; 20 | 21 | const todo = db.get('todos') 22 | .find({ id: id }) 23 | .value(); 24 | 25 | res.status(200).send(todo); 26 | } catch (error) { 27 | next(error); 28 | } 29 | } else { 30 | next(new Error('Application does not have the required permissions')) 31 | } 32 | } else { 33 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) { 34 | try { 35 | /** 36 | * The 'oid' (object id) is the only claim that should be used to uniquely identify 37 | * a user in an Azure AD tenant. The token might have one or more of the following claim, 38 | * that might seem like a unique identifier, but is not and should not be used as such, 39 | * especially for systems which act as system of record (SOR): 40 | * 41 | * - upn (user principal name): might be unique amongst the active set of users in a tenant but 42 | * tend to get reassigned to new employees as employees leave the organization and 43 | * others take their place or might change to reflect a personal change like marriage. 44 | * 45 | * - email: might be unique amongst the active set of users in a tenant but tend to get 46 | * reassigned to new employees as employees leave the organization and others take their place. 47 | */ 48 | const owner = req.authInfo['oid']; 49 | const id = req.params.id; 50 | 51 | const todo = db.get('todos') 52 | .filter({ owner: owner }) 53 | .find({ id: id }) 54 | .value(); 55 | 56 | res.status(200).send(todo); 57 | } catch (error) { 58 | next(error); 59 | } 60 | } else { 61 | next(new Error('User does not have the required permissions')) 62 | } 63 | } 64 | } 65 | 66 | exports.getTodos = (req, res, next) => { 67 | if (isAppOnlyToken(req.authInfo)) { 68 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.read)) { 69 | try { 70 | const todos = db.get('todos') 71 | .value(); 72 | 73 | res.status(200).send(todos); 74 | } catch (error) { 75 | next(error); 76 | } 77 | } else { 78 | next(new Error('Application does not have the required permissions')) 79 | } 80 | } else { 81 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) { 82 | try { 83 | const owner = req.authInfo['oid']; 84 | 85 | const todos = db.get('todos') 86 | .filter({ owner: owner }) 87 | .value(); 88 | 89 | res.status(200).send(todos); 90 | } catch (error) { 91 | next(error); 92 | } 93 | } else { 94 | next(new Error('User does not have the required permissions')) 95 | } 96 | } 97 | } 98 | 99 | exports.postTodo = (req, res, next) => { 100 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.write) 101 | || 102 | hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.write) 103 | ) { 104 | try { 105 | const todo = { 106 | description: req.body.description, 107 | id: uuidv4(), 108 | owner: req.authInfo['oid'] // oid is the only claim that should be used to uniquely identify a user in an Azure AD tenant 109 | }; 110 | 111 | db.get('todos').push(todo).write(); 112 | 113 | res.status(200).json(todo); 114 | } catch (error) { 115 | next(error); 116 | } 117 | } else ( 118 | next(new Error('User or application does not have the required permissions')) 119 | ) 120 | } 121 | 122 | exports.deleteTodo = (req, res, next) => { 123 | if (isAppOnlyToken(req.authInfo)) { 124 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.write)) { 125 | try { 126 | const id = req.params.id; 127 | 128 | db.get('todos') 129 | .remove({ id: id }) 130 | .write(); 131 | 132 | res.status(200).json({ message: "success" }); 133 | } catch (error) { 134 | next(error); 135 | } 136 | } else { 137 | next(new Error('Application does not have the required permissions')) 138 | } 139 | } else { 140 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.write)) { 141 | try { 142 | const id = req.params.id; 143 | const owner = req.authInfo['oid']; 144 | 145 | db.get('todos') 146 | .remove({ owner: owner, id: id }) 147 | .write(); 148 | 149 | res.status(200).json({ message: "success" }); 150 | } catch (error) { 151 | next(error); 152 | } 153 | } else { 154 | next(new Error('User does not have the required permissions')) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/data/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [] 3 | } -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-identity-react-c3s1", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web API accepting authorized calls with Azure Active Directory", 5 | "author": "derisen", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon app.js", 9 | "test": "jest --forceExit" 10 | }, 11 | "dependencies": { 12 | "cors": "^2.8.5", 13 | "express": "^4.18.1", 14 | "express-rate-limit": "^6.5.2", 15 | "lowdb": "^1.0.0", 16 | "morgan": "^1.10.0", 17 | "passport": "^0.6.0", 18 | "passport-azure-ad": "^4.3.3", 19 | "uuid": "^9.0.0" 20 | }, 21 | "main": "app.js", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial.git" 25 | }, 26 | "keywords": [ 27 | "azure-ad", 28 | "ms-identity", 29 | "node", 30 | "api" 31 | ], 32 | "bugs": { 33 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/issues" 34 | }, 35 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial#readme", 36 | "devDependencies": { 37 | "jest": "^28.1.1", 38 | "nodemon": "^2.0.16", 39 | "supertest": "^6.2.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const todolist = require('../controllers/todolist'); 4 | 5 | // initialize router 6 | const router = express.Router(); 7 | 8 | router.get('/todolist', todolist.getTodos); 9 | 10 | router.get('/todolist/:id', todolist.getTodo); 11 | 12 | router.post('/todolist', todolist.postTodo); 13 | 14 | router.delete('/todolist/:id', todolist.deleteTodo); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/API/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const app = require('./app.js'); 4 | 5 | describe('Sanitize configuration object', () => { 6 | beforeAll(() => { 7 | global.config = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(config).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(config.credentials.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(config.credentials.tenantId)).toBe(false); 22 | }); 23 | }); 24 | 25 | describe('Ensure routes served', () => { 26 | 27 | beforeAll(() => { 28 | process.env.NODE_ENV = 'test'; 29 | }); 30 | 31 | it('should protect todolist endpoint', async () => { 32 | const res = await request(app) 33 | .get('/api'); 34 | 35 | expect(res.statusCode).toEqual(401); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Author": "derisen", 4 | "Title": "JavaScript single-page application using MSAL Brwoser to authorize users for calling a Express.js web API on Azure Active Directory", 5 | "Level": 200, 6 | "Client": "JavaScript SPA", 7 | "Service": "Node.js web API", 8 | "RepositoryUrl": "ms-identity-javascript-tutorial", 9 | "Endpoint": "AAD v2.0", 10 | "Languages": ["javascript", "nodejs"], 11 | "Description": "A JavaScript single-page application using MSAL Browser to authorize users for calling a protected Express.js web API on Azure Active Directory", 12 | "Products": ["azure-active-directory", "msal-js", "msal-js", "passport-azure-ad"] 13 | }, 14 | "AADApps": [ 15 | { 16 | "Id": "service", 17 | "Name": "msal-node-api", 18 | "Kind": "WebApi", 19 | "Audience": "AzureADMyOrg", 20 | "SDK": "MsalNode", 21 | "SampleSubPath": "3-Authorization-II\\1-call-api\\API", 22 | "Scopes": ["Todolist.Read", "Todolist.ReadWrite"], 23 | "AppRoles": [ 24 | { 25 | "AllowedMemberTypes": ["Application"], 26 | "Name": "Todolist.Read.All", 27 | "Description": "Allow this application to read every users Todolist items" 28 | }, 29 | { 30 | "AllowedMemberTypes": ["Application"], 31 | "Name": "Todolist.ReadWrite.All", 32 | "Description": "Allow this application to read and write every users Todolist items" 33 | } 34 | ], 35 | "OptionalClaims": { 36 | "AccessTokenClaims": ["idtyp", "acct"] 37 | } 38 | }, 39 | { 40 | "Id": "client", 41 | "Name": "msal-javascript-spa", 42 | "Kind": "SinglePageApplication", 43 | "Audience": "AzureADMyOrg", 44 | "HomePage": "http://localhost:3000", 45 | "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect", 46 | "SDK": "MsalJs", 47 | "SampleSubPath": "3-Authorization-II\\1-call-api\\SPA", 48 | "RequiredResourcesAccess": [ 49 | { 50 | "Resource": "service", 51 | "DelegatedPermissions": ["Todolist.Read", "Todolist.ReadWrite"] 52 | } 53 | ] 54 | } 55 | ], 56 | "CodeConfiguration": [ 57 | { 58 | "App": "service", 59 | "SettingKind": "Replace", 60 | "SettingFile": "\\..\\API\\authConfig.js", 61 | "Mappings": [ 62 | { 63 | "key": "Enter_the_Application_Id_Here", 64 | "value": ".AppId" 65 | }, 66 | { 67 | "key": "Enter_the_Tenant_Info_Here", 68 | "value": "$tenantId" 69 | } 70 | ] 71 | }, 72 | { 73 | "App": "client", 74 | "SettingKind": "Replace", 75 | "SettingFile": "\\..\\SPA\\public\\authConfig.js", 76 | "Mappings": [ 77 | { 78 | "key": "Enter_the_Application_Id_Here", 79 | "value": ".AppId" 80 | }, 81 | { 82 | "key": "Enter_the_Tenant_Info_Here", 83 | "value": "$tenantId" 84 | }, 85 | { 86 | "key": "Enter_the_Web_Api_Application_Id_Here", 87 | "value": "service.AppId" 88 | } 89 | ] 90 | } 91 | ] 92 | } -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/1-call-api/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/ReadmeFiles/topology_callapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/1-call-api/ReadmeFiles/topology_callapi.png -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ciam-sign-in-javascript", 3 | "version": "1.0.0", 4 | "description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users against Azure AD Customer Identity Access Management (Azure AD for Customers)", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "jest --forceExit" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@azure/msal-browser": "^2.37.0", 14 | "express": "^4.18.2", 15 | "morgan": "^1.10.0" 16 | }, 17 | "devDependencies": { 18 | "jest": "^29.5.0", 19 | "jest-environment-jsdom": "^29.5.0", 20 | "supertest": "^6.3.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration object to be passed to MSAL instance on creation. 3 | * For a full list of MSAL.js configuration parameters, visit: 4 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md 5 | */ 6 | const msalConfig = { 7 | auth: { 8 | clientId: 'Enter_the_Application_Id_Here', // This is the ONLY mandatory field that you need to supply. 9 | authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Info_Here', // Replace the placeholder with your tenant name 10 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.href e.g. http://localhost:3000/, 11 | postLogoutRedirectUri: '/', // Indicates the page to navigate after logout. 12 | }, 13 | cache: { 14 | cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO. 15 | storeAuthStateInCookie: false, // set this to true if you have to support IE 16 | }, 17 | system: { 18 | loggerOptions: { 19 | loggerCallback: (level, message, containsPii) => { 20 | if (containsPii) { 21 | return; 22 | } 23 | switch (level) { 24 | case msal.LogLevel.Error: 25 | console.error(message); 26 | return; 27 | case msal.LogLevel.Info: 28 | console.info(message); 29 | return; 30 | case msal.LogLevel.Verbose: 31 | console.debug(message); 32 | return; 33 | case msal.LogLevel.Warning: 34 | console.warn(message); 35 | return; 36 | default: 37 | return; 38 | } 39 | }, 40 | }, 41 | }, 42 | }; 43 | 44 | /** 45 | * Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see: 46 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md 47 | */ 48 | const protectedResources = { 49 | todolistApi: { 50 | endpoint: 'http://localhost:5000/api/todolist', 51 | scopes: { 52 | read: ['api://Enter_the_Web_Api_Application_Id_Here/Todolist.Read'], 53 | write: ['api://Enter_the_Web_Api_Application_Id_Here/Todolist.ReadWrite'], 54 | }, 55 | }, 56 | }; 57 | 58 | /** 59 | * Scopes you add here will be prompted for user consent during sign-in. 60 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. 61 | * For more information about OIDC scopes, visit: 62 | * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes 63 | */ 64 | const loginRequest = { 65 | scopes: [...protectedResources.todolistApi.scopes.read, ...protectedResources.todolistApi.scopes.write], 66 | }; 67 | 68 | /** 69 | * An optional silentRequest object can be used to achieve silent SSO 70 | * between applications by providing a "login_hint" property. 71 | */ 72 | 73 | // const silentRequest = { 74 | // scopes: ["openid", "profile"], 75 | // loginHint: "example@domain.net" 76 | // }; 77 | 78 | // exporting config object for jest 79 | if (typeof exports !== 'undefined') { 80 | module.exports = { 81 | msalConfig, 82 | loginRequest, 83 | protectedResources, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/authPopup.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ''; 6 | 7 | function selectAccount() { 8 | /** 9 | * See here for more info on account retrieval: 10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 11 | */ 12 | 13 | const currentAccounts = myMSALObj.getAllAccounts(); 14 | if (!currentAccounts || currentAccounts.length < 1) { 15 | return; 16 | } else if (currentAccounts.length > 1) { 17 | // Add your account choosing logic here 18 | console.warn('Multiple accounts detected.'); 19 | } else if (currentAccounts.length === 1) { 20 | username = currentAccounts[0].username; 21 | welcomeUser(username); 22 | updateTable(currentAccounts[0]); 23 | } 24 | } 25 | 26 | function handleResponse(response) { 27 | /** 28 | * To see the full list of response object properties, visit: 29 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response 30 | */ 31 | 32 | if (response !== null) { 33 | username = response.account.username; 34 | welcomeUser(username); 35 | updateTable(response.account); 36 | } else { 37 | selectAccount(); 38 | } 39 | } 40 | 41 | function signIn() { 42 | /** 43 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 44 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 45 | */ 46 | 47 | myMSALObj 48 | .loginPopup({ 49 | ...loginRequest, 50 | redirectUri: '/redirect', 51 | }) 52 | .then(handleResponse) 53 | .catch((error) => { 54 | console.log(error); 55 | }); 56 | } 57 | 58 | 59 | function getTokenPopup(request) { 60 | /** 61 | * See here for more information on account retrieval: 62 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 63 | */ 64 | request.account = myMSALObj.getAccountByUsername(username); 65 | return myMSALObj.acquireTokenSilent(request).catch((error) => { 66 | console.warn(error); 67 | console.warn('silent token acquisition fails. acquiring token using popup'); 68 | if (error instanceof msal.InteractionRequiredAuthError) { 69 | // fallback to interaction when silent call fails 70 | return myMSALObj 71 | .acquireTokenPopup(request) 72 | .then((response) => { 73 | return response; 74 | }) 75 | .catch((error) => { 76 | console.error(error); 77 | }); 78 | } else { 79 | console.warn(error); 80 | } 81 | }); 82 | } 83 | 84 | function signOut() { 85 | /** 86 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 87 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 88 | */ 89 | 90 | // Choose which account to logout from by passing a username. 91 | const logoutRequest = { 92 | account: myMSALObj.getAccountByUsername(username), 93 | }; 94 | myMSALObj.logoutPopup(logoutRequest).then(() => { 95 | window.location.reload(); 96 | }); 97 | } 98 | 99 | selectAccount(); 100 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/authRedirect.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ''; 6 | 7 | /** 8 | * A promise handler needs to be registered for handling the 9 | * response returned from redirect flow. For more information, visit: 10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirect-apis 11 | */ 12 | myMSALObj 13 | .handleRedirectPromise() 14 | .then(handleResponse) 15 | .catch((error) => { 16 | console.error(error); 17 | }); 18 | 19 | function selectAccount() { 20 | /** 21 | * See here for more info on account retrieval: 22 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 23 | */ 24 | 25 | const currentAccounts = myMSALObj.getAllAccounts(); 26 | 27 | if (!currentAccounts) { 28 | return; 29 | } else if (currentAccounts.length > 1) { 30 | // Add your account choosing logic here 31 | console.warn('Multiple accounts detected.'); 32 | } else if (currentAccounts.length === 1) { 33 | username = currentAccounts[0].username; 34 | welcomeUser(username); 35 | updateTable(currentAccounts[0]); 36 | } 37 | } 38 | 39 | function handleResponse(response) { 40 | /** 41 | * To see the full list of response object properties, visit: 42 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response 43 | */ 44 | 45 | if (response !== null) { 46 | username = response.account.username; 47 | welcomeUser(username); 48 | updateTable(response.account); 49 | } else { 50 | selectAccount(); 51 | } 52 | } 53 | 54 | function signIn() { 55 | /** 56 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 57 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 58 | */ 59 | 60 | myMSALObj.loginRedirect(loginRequest); 61 | } 62 | 63 | function getTokenRedirect(request) { 64 | /** 65 | * See here for more info on account retrieval: 66 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 67 | */ 68 | request.account = myMSALObj.getAccountByUsername(username); 69 | return myMSALObj.acquireTokenSilent(request).catch((error) => { 70 | console.error(error); 71 | console.warn('silent token acquisition fails. acquiring token using popup'); 72 | if (error instanceof msal.InteractionRequiredAuthError) { 73 | // fallback to interaction when silent call fails 74 | return myMSALObj.acquireTokenRedirect(request); 75 | } else { 76 | console.error(error); 77 | } 78 | }); 79 | } 80 | 81 | function signOut() { 82 | /** 83 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 84 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 85 | */ 86 | 87 | // Choose which account to logout from by passing a username. 88 | const logoutRequest = { 89 | account: myMSALObj.getAccountByUsername(username), 90 | }; 91 | 92 | myMSALObj.logoutRedirect(logoutRequest); 93 | } 94 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/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 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/fetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Execute a fetch request with the given options 3 | * @param {string} method: GET, POST, PUT, DELETE 4 | * @param {String} endpoint: The endpoint to call 5 | * @param {Object} data: The data to send to the endpoint, if any 6 | * @returns response 7 | */ 8 | function callApi(method, endpoint, token, data = null) { 9 | const headers = new Headers(); 10 | const bearer = `Bearer ${token}`; 11 | 12 | headers.append('Authorization', bearer); 13 | 14 | if (data) { 15 | headers.append('Content-Type', 'application/json'); 16 | } 17 | 18 | const options = { 19 | method: method, 20 | headers: headers, 21 | body: data ? JSON.stringify(data) : null, 22 | }; 23 | 24 | return fetch(endpoint, options) 25 | .then((response) => { 26 | const contentType = response.headers.get("content-type"); 27 | 28 | if (contentType && contentType.indexOf("application/json") !== -1) { 29 | return response.json(); 30 | } else { 31 | return response; 32 | } 33 | }); 34 | } 35 | 36 | 37 | /** 38 | * Handles todolist actions 39 | * @param {Object} task 40 | * @param {string} method 41 | * @param {string} endpoint 42 | */ 43 | async function handleToDoListActions(task, method, endpoint) { 44 | let listData; 45 | 46 | try { 47 | const accessToken = await getToken(); 48 | const data = await callApi(method, endpoint, accessToken, task); 49 | 50 | switch (method) { 51 | case 'POST': 52 | listData = JSON.parse(localStorage.getItem('todolist')); 53 | listData = [data, ...listData]; 54 | localStorage.setItem('todolist', JSON.stringify(listData)); 55 | AddTaskToToDoList(data); 56 | break; 57 | case 'DELETE': 58 | listData = JSON.parse(localStorage.getItem('todolist')); 59 | const index = listData.findIndex((todoItem) => todoItem.id === task.id); 60 | localStorage.setItem('todolist', JSON.stringify([...listData.splice(index, 1)])); 61 | showToDoListItems(listData); 62 | break; 63 | default: 64 | console.log('Unrecognized method.') 65 | break; 66 | } 67 | } catch (error) { 68 | console.error(error); 69 | } 70 | } 71 | 72 | /** 73 | * Handles todolist action GET action. 74 | */ 75 | async function getToDos() { 76 | try { 77 | const accessToken = await getToken(); 78 | 79 | const data = await callApi( 80 | 'GET', 81 | protectedResources.todolistApi.endpoint, 82 | accessToken 83 | ); 84 | 85 | if (data) { 86 | localStorage.setItem('todolist', JSON.stringify(data)); 87 | showToDoListItems(data); 88 | } 89 | } catch (error) { 90 | console.error(error); 91 | } 92 | } 93 | 94 | /** 95 | * Retrieves an access token. 96 | */ 97 | async function getToken() { 98 | let tokenResponse; 99 | 100 | if (typeof getTokenPopup === 'function') { 101 | tokenResponse = await getTokenPopup({ 102 | scopes: [...protectedResources.todolistApi.scopes.read], 103 | redirectUri: '/redirect' 104 | }); 105 | } else { 106 | tokenResponse = await getTokenRedirect({ 107 | scopes: [...protectedResources.todolistApi.scopes.read], 108 | }); 109 | } 110 | 111 | if (!tokenResponse) { 112 | return null; 113 | } 114 | 115 | return tokenResponse.accessToken; 116 | } 117 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Microsoft identity platform 8 | 9 | 10 | 11 | 14 | 15 | 17 | 18 | 19 | 20 | 32 |
33 |
Vanilla JavaScript single-page application secured with MSAL.js 34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
Claim TypeValueDescription
49 |
50 |
51 |
52 |
53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 |
    61 |
62 |
63 | 64 | 67 | 70 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/redirect.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/styles.css: -------------------------------------------------------------------------------- 1 | .navbarStyle { 2 | padding: .5rem 1rem !important; 3 | } 4 | 5 | .table-responsive-ms { 6 | max-height: 39rem !important; 7 | padding-left: 10%; 8 | padding-right: 10%; 9 | } 10 | 11 | form, .group-div { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .input-group, ul { 18 | width: 50% !important; 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/public/ui.js: -------------------------------------------------------------------------------- 1 | // Select DOM elements to work with 2 | const signInButton = document.getElementById('signIn'); 3 | const signOutButton = document.getElementById('signOut'); 4 | const titleDiv = document.getElementById('title-div'); 5 | const welcomeDiv = document.getElementById('welcome-div'); 6 | const tableDiv = document.getElementById('table-div'); 7 | const tableBody = document.getElementById('table-body-div'); 8 | const toDoListLink = document.getElementById('toDoListLink'); 9 | const toDoForm = document.getElementById('form'); 10 | const textInput = document.getElementById('textInput'); 11 | const toDoListDiv = document.getElementById('groupDiv'); 12 | const todoListItems = document.getElementById('toDoListItems'); 13 | 14 | toDoForm.addEventListener('submit', (e) => { 15 | e.preventDefault(); 16 | let task = { description: textInput.value }; 17 | handleToDoListActions(task, 'POST', protectedResources.todolistApi.endpoint); 18 | toDoForm.reset(); 19 | }); 20 | 21 | function welcomeUser(username) { 22 | signInButton.classList.add('d-none'); 23 | signOutButton.classList.remove('d-none'); 24 | toDoListLink.classList.remove('d-none'); 25 | titleDiv.classList.add('d-none'); 26 | welcomeDiv.classList.remove('d-none'); 27 | welcomeDiv.innerHTML = `Welcome ${username}!`; 28 | } 29 | 30 | function updateTable(account) { 31 | tableDiv.classList.remove('d-none'); 32 | const tokenClaims = createClaimsTable(account.idTokenClaims); 33 | 34 | Object.keys(tokenClaims).forEach((key) => { 35 | let row = tableBody.insertRow(0); 36 | let cell1 = row.insertCell(0); 37 | let cell2 = row.insertCell(1); 38 | let cell3 = row.insertCell(2); 39 | cell1.innerHTML = tokenClaims[key][0]; 40 | cell2.innerHTML = tokenClaims[key][1]; 41 | cell3.innerHTML = tokenClaims[key][2]; 42 | }); 43 | } 44 | 45 | function showToDoListItems(response) { 46 | todoListItems.replaceChildren(); 47 | tableDiv.classList.add('d-none'); 48 | toDoForm.classList.remove('d-none'); 49 | toDoListDiv.classList.remove('d-none'); 50 | if (!!response.length) { 51 | response.forEach((task) => { 52 | AddTaskToToDoList(task); 53 | }); 54 | } 55 | } 56 | 57 | function AddTaskToToDoList(task) { 58 | let li = document.createElement('li'); 59 | let button = document.createElement('button'); 60 | button.innerHTML = 'Delete'; 61 | button.classList.add('btn', 'btn-danger'); 62 | button.addEventListener('click', () => { 63 | handleToDoListActions(task, 'DELETE', protectedResources.todolistApi.endpoint + `/${task.id}`); 64 | }); 65 | li.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center'); 66 | li.innerHTML = task.description; 67 | li.appendChild(button); 68 | todoListItems.appendChild(li); 69 | } 70 | -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/sample.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | const request = require('supertest'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | 9 | const app = require('./server.js'); 10 | 11 | jest.dontMock('fs'); 12 | 13 | const html = fs.readFileSync(path.resolve(__dirname, './public/index.html'), 'utf8'); 14 | 15 | describe('Sanitize index page', () => { 16 | beforeAll(async() => { 17 | global.document.documentElement.innerHTML = html.toString(); 18 | }); 19 | 20 | it('should have valid cdn link', () => { 21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser"); 22 | }); 23 | }); 24 | 25 | describe('Sanitize configuration object', () => { 26 | beforeAll(() => { 27 | global.msalConfig = require('./public/authConfig.js').msalConfig; 28 | }); 29 | 30 | it('should define the config object', () => { 31 | expect(msalConfig).toBeDefined(); 32 | }); 33 | 34 | it('should not contain credentials', () => { 35 | 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; 36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(false); 37 | }); 38 | 39 | it('should contain authority URI', () => { 40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; 41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true); 42 | }); 43 | }); 44 | 45 | describe('Ensure pages served', () => { 46 | 47 | beforeAll(() => { 48 | process.env.NODE_ENV = 'test'; 49 | }); 50 | 51 | it('should get index page', async () => { 52 | const res = await request(app) 53 | .get('/'); 54 | 55 | const data = await fs.promises.readFile(path.join(__dirname, './public/index.html'), 'utf8'); 56 | expect(res.statusCode).toEqual(200); 57 | expect(res.text).toEqual(data); 58 | }); 59 | }); -------------------------------------------------------------------------------- /3-Authorization-II/1-call-api/SPA/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const path = require('path'); 4 | 5 | const DEFAULT_PORT = process.env.PORT || 3000; 6 | 7 | // initialize express. 8 | const app = express(); 9 | 10 | // Configure morgan module to log all requests. 11 | app.use(morgan('dev')); 12 | 13 | // Setup app folders. 14 | app.use(express.static('public')); 15 | 16 | // set up a route for redirect.html 17 | app.get('/redirect', (req, res) => { 18 | res.sendFile(path.join(__dirname + '/public/redirect.html')); 19 | }); 20 | 21 | // Set up a route for index.html 22 | app.get('/', (req, res) => { 23 | res.sendFile(path.join(__dirname + '/index.html')); 24 | }); 25 | 26 | app.listen(DEFAULT_PORT, () => { 27 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`); 28 | }); 29 | 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const cors = require('cors'); 4 | 5 | const rateLimit = require('express-rate-limit'); 6 | 7 | const passport = require('passport'); 8 | const passportAzureAd = require('passport-azure-ad'); 9 | 10 | 11 | const authConfig = require('./authConfig.js'); 12 | const router = require('./routes/index'); 13 | 14 | const app = express(); 15 | 16 | /** 17 | * If your app is behind a proxy, reverse proxy or a load balancer, consider 18 | * letting express know that you are behind that proxy. To do so, uncomment 19 | * the line below. 20 | */ 21 | 22 | // app.set('trust proxy', /* numberOfProxies */); 23 | 24 | /** 25 | * HTTP request handlers should not perform expensive operations such as accessing the file system, 26 | * executing an operating system command or interacting with a database without limiting the rate at 27 | * which requests are accepted. Otherwise, the application becomes vulnerable to denial-of-service attacks 28 | * where an attacker can cause the application to crash or become unresponsive by issuing a large number of 29 | * requests at the same time. For more information, visit: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html 30 | */ 31 | const limiter = rateLimit({ 32 | windowMs: 15 * 60 * 1000, // 15 minutes 33 | max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) 34 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 35 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 36 | }); 37 | 38 | // Apply the rate limiting middleware to all requests 39 | app.use(limiter); 40 | 41 | app.use(cors()); 42 | 43 | app.use(express.json()) 44 | app.use(express.urlencoded({ extended: false})); 45 | app.use(morgan('dev')); 46 | 47 | const options = { 48 | identityMetadata: `https://${authConfig.metadata.b2cDomain}/${authConfig.credentials.tenantName}/${authConfig.policies.policyName}/${authConfig.metadata.version}/${authConfig.metadata.discovery}`, 49 | clientID: authConfig.credentials.clientID, 50 | audience: authConfig.credentials.clientID, 51 | policyName: authConfig.policies.policyName, 52 | isB2C: authConfig.settings.isB2C, 53 | validateIssuer: authConfig.settings.validateIssuer, 54 | loggingLevel: authConfig.settings.loggingLevel, 55 | passReqToCallback: authConfig.settings.passReqToCallback, 56 | loggingNoPII: authConfig.settings.loggingNoPII, // set this to true in the authConfig.js if you want to enable logging and debugging 57 | }; 58 | 59 | const bearerStrategy = new passportAzureAd.BearerStrategy(options, (req,token, done) => { 60 | /** 61 | * Below you can do extended token validation and check for additional claims, such as: 62 | * - check if the delegated permissions in the 'scp' are the same as the ones declared in the application registration. 63 | * 64 | * Bear in mind that you can do any of the above checks within the individual routes and/or controllers as well. 65 | * For more information, visit: https://learn.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview 66 | */ 67 | 68 | /** 69 | * Lines below verifies if the caller's client ID is in the list of allowed clients. 70 | * This ensures only the applications with the right client ID can access this API. 71 | * To do so, we use "azp" claim in the access token. Uncomment the lines below to enable this check. 72 | */ 73 | // if (!myAllowedClientsList.includes(token.azp)) { 74 | // return done(new Error('Unauthorized'), {}, "Client not allowed"); 75 | // } 76 | 77 | // const myAllowedClientsList = [ 78 | // /* add here the client IDs of the applications that are allowed to call this API */ 79 | // ] 80 | 81 | /** 82 | * Access tokens that have no 'scp' (for delegated permissions). 83 | */ 84 | if (!token.hasOwnProperty('scp')) { 85 | return done(new Error('Unauthorized'), null, 'No delegated permissions found'); 86 | } 87 | 88 | done(null, {}, token); 89 | }); 90 | 91 | 92 | app.use(passport.initialize()); 93 | 94 | passport.use(bearerStrategy); 95 | 96 | app.use( 97 | '/api', 98 | (req, res, next) => { 99 | passport.authenticate( 100 | 'oauth-bearer', 101 | { 102 | session: false, 103 | }, 104 | (err, user, info) => { 105 | if (err) { 106 | /** 107 | * An error occurred during authorization. Either pass the error to the next function 108 | * for Express error handler to handle, or send a response with the appropriate status code. 109 | */ 110 | return res.status(401).json({ error: err.message }); 111 | } 112 | 113 | if (!user) { 114 | // If no user object found, send a 401 response. 115 | return res.status(401).json({ error: 'Unauthorized' }); 116 | } 117 | 118 | if (info) { 119 | // access token payload will be available in req.authInfo downstream 120 | req.authInfo = info; 121 | return next(); 122 | } 123 | } 124 | )(req, res, next); 125 | }, 126 | router, // the router with all the routes 127 | (err, req, res, next) => { 128 | /** 129 | * Add your custom error handling logic here. For more information, see: 130 | * http://expressjs.com/en/guide/error-handling.html 131 | */ 132 | 133 | // set locals, only providing error in development 134 | res.locals.message = err.message; 135 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 136 | 137 | // send error response 138 | res.status(err.status || 500).send(err); 139 | } 140 | ); 141 | 142 | const port = process.env.PORT || 5000; 143 | 144 | app.listen(port, () => { 145 | console.log('Listening on port ' + port); 146 | }); 147 | 148 | module.exports = app; 149 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/auth/permissionUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensures that the access token has the specified delegated permissions. 3 | * @param {Object} accessTokenPayload: Parsed access token payload 4 | * @param {Array} requiredPermission: list of required permissions 5 | * @returns {boolean} 6 | */ 7 | const hasRequiredDelegatedPermissions = (accessTokenPayload, requiredPermission) => { 8 | const normalizedRequiredPermissions = requiredPermission.map((permission) => permission.toUpperCase()); 9 | 10 | if (accessTokenPayload.hasOwnProperty('scp') && accessTokenPayload.scp.split(' ') 11 | .some(claim => normalizedRequiredPermissions.includes(claim.toUpperCase()))) { 12 | return true; 13 | } 14 | return false; 15 | 16 | }; 17 | 18 | module.exports = { 19 | hasRequiredDelegatedPermissions 20 | }; -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/authConfig.js: -------------------------------------------------------------------------------- 1 | const passportConfig = { 2 | credentials: { 3 | tenantName: 'fabrikamb2c.onmicrosoft.com', 4 | clientID: 'e29ac359-6a90-4f9e-b31c-8f64e1ac20cb', 5 | }, 6 | policies: { 7 | policyName: 'B2C_1_susi_v2', 8 | }, 9 | metadata: { 10 | b2cDomain: 'fabrikamb2c.b2clogin.com', 11 | authority: 'login.microsoftonline.com', 12 | discovery: '.well-known/openid-configuration', 13 | version: 'v2.0', 14 | }, 15 | settings: { 16 | isB2C: true, 17 | validateIssuer: false, 18 | passReqToCallback: true, 19 | loggingLevel: 'info', 20 | loggingNoPII: false, 21 | }, 22 | protectedRoutes: { 23 | todolist: { 24 | endpoint: '/api/todolist', 25 | delegatedPermissions: { 26 | read: ['ToDoList.Read', 'ToDoList.ReadWrite'], 27 | write: ['ToDoList.ReadWrite'], 28 | }, 29 | }, 30 | }, 31 | }; 32 | 33 | module.exports = passportConfig; 34 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/controllers/todolist.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 | const { v4: uuidv4 } = require('uuid'); 6 | 7 | const { 8 | hasRequiredDelegatedPermissions, 9 | } = require('../auth/permissionUtils'); 10 | 11 | const authConfig = require('../authConfig'); 12 | 13 | exports.getTodo = (req, res, next) => { 14 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) { 15 | try { 16 | /** 17 | * The 'oid' (object id) is the only claim that should be used to uniquely identify 18 | * a user in an Azure AD tenant. The token might have one or more of the following claim, 19 | * that might seem like a unique identifier, but is not and should not be used as such, 20 | * especially for systems which act as system of record (SOR): 21 | * 22 | * - upn (user principal name): might be unique amongst the active set of users in a tenant but 23 | * tend to get reassigned to new employees as employees leave the organization and 24 | * others take their place or might change to reflect a personal change like marriage. 25 | * 26 | * - email: might be unique amongst the active set of users in a tenant but tend to get 27 | * reassigned to new employees as employees leave the organization and others take their place. 28 | */ 29 | const owner = req.authInfo['oid']; 30 | const id = req.params.id; 31 | 32 | const todo = db.get('todos') 33 | .filter({ owner: owner }) 34 | .find({ id: id }) 35 | .value(); 36 | 37 | res.status(200).send(todo); 38 | } catch (error) { 39 | next(error); 40 | } 41 | } else { 42 | next(new Error('User does not have the required permissions')) 43 | } 44 | } 45 | 46 | exports.getTodos = (req, res, next) => { 47 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) { 48 | try { 49 | const owner = req.authInfo['oid']; 50 | 51 | const todos = db.get('todos') 52 | .filter({ owner: owner }) 53 | .value(); 54 | 55 | res.status(200).send(todos); 56 | } catch (error) { 57 | next(error); 58 | } 59 | } else { 60 | next(new Error('User does not have the required permissions')) 61 | } 62 | } 63 | 64 | exports.postTodo = (req, res, next) => { 65 | if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.write)) { 66 | try { 67 | const todo = { 68 | description: req.body.description, 69 | id: uuidv4(), 70 | owner: req.authInfo['oid'] // oid is the only claim that should be used to uniquely identify a user in an Azure AD tenant 71 | }; 72 | 73 | db.get('todos').push(todo).write(); 74 | 75 | res.status(200).json(todo); 76 | } catch (error) { 77 | next(error); 78 | } 79 | } else ( 80 | next(new Error('User or application does not have the required permissions')) 81 | ) 82 | } 83 | 84 | exports.deleteTodo = (req, res, next) => { 85 | if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.write)) { 86 | try { 87 | const id = req.params.id; 88 | const owner = req.authInfo['oid']; 89 | 90 | db.get('todos') 91 | .remove({ owner: owner, id: id }) 92 | .write(); 93 | 94 | res.status(200).json({ message: "success" }); 95 | } catch (error) { 96 | next(error); 97 | } 98 | } else { 99 | next(new Error('User does not have the required permissions')) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/data/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [] 3 | } -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-identity-react-c3s1", 3 | "version": "1.0.0", 4 | "description": "A Node.js & Express web API accepting authorized calls with Azure Active Directory", 5 | "author": "derisen", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon app.js", 9 | "test": "jest --forceExit" 10 | }, 11 | "dependencies": { 12 | "cors": "^2.8.5", 13 | "express": "^4.18.1", 14 | "express-rate-limit": "^6.5.2", 15 | "lowdb": "^1.0.0", 16 | "morgan": "^1.10.0", 17 | "passport": "^0.6.0", 18 | "passport-azure-ad": "^4.3.3", 19 | "uuid": "^9.0.0" 20 | }, 21 | "main": "app.js", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial.git" 25 | }, 26 | "keywords": [ 27 | "azure-ad", 28 | "ms-identity", 29 | "node", 30 | "api" 31 | ], 32 | "bugs": { 33 | "url": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/issues" 34 | }, 35 | "homepage": "https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial#readme", 36 | "devDependencies": { 37 | "jest": "^28.1.1", 38 | "nodemon": "^2.0.16", 39 | "supertest": "^6.2.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const todolist = require('../controllers/todolist'); 4 | 5 | // initialize router 6 | const router = express.Router(); 7 | 8 | router.get('/todolist', todolist.getTodos); 9 | 10 | router.get('/todolist/:id', todolist.getTodo); 11 | 12 | router.post('/todolist', todolist.postTodo); 13 | 14 | router.delete('/todolist/:id', todolist.deleteTodo); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/API/sample.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const app = require('./app.js'); 4 | 5 | describe('Sanitize configuration object', () => { 6 | beforeAll(() => { 7 | global.config = require('./authConfig.js'); 8 | }); 9 | 10 | it('should define the config object', () => { 11 | expect(config).toBeDefined(); 12 | }); 13 | 14 | it('should 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(config.credentials.clientID)).toBe(true); 17 | }); 18 | }); 19 | 20 | describe('Ensure routes served', () => { 21 | 22 | beforeAll(() => { 23 | process.env.NODE_ENV = 'test'; 24 | }); 25 | 26 | it('should protect todolist endpoint', async () => { 27 | const res = await request(app) 28 | .get('/api'); 29 | 30 | expect(res.statusCode).toEqual(401); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/AppCreationScripts/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sample": { 3 | "Author": "salman90", 4 | "Title": "A JavaScript single-page application using MSAL Browser to authorize users for calling a protected web API on Azure AD B2C", 5 | "Level": 200, 6 | "Client": "JavaScript SPA", 7 | "Service": "Node.js web API", 8 | "RepositoryUrl": "ms-identity-javascript-react-tutorial", 9 | "Endpoint": "AAD v2.0", 10 | "Description": "A JavaScript single-page application using MSAL Browser to authorize users for calling a protected web API on Azure AD B2C", 11 | "Languages": [ 12 | "javascript", 13 | "nodejs" 14 | ], 15 | "Products": [ 16 | "azure-active-directory-b2c", 17 | "msal-js", 18 | "passport-azure-ad" 19 | ], 20 | "Platform": "JavaScript", 21 | "Provider": "B2C" 22 | }, 23 | "AADApps": [ 24 | { 25 | "Id": "service", 26 | "Name": "msal-node-api", 27 | "Kind": "WebApi", 28 | "Audience": "AzureADandPersonalMicrosoftAccount", 29 | "SDK": "MsalNode", 30 | "Scopes": [ 31 | "ToDoList.Read", 32 | "ToDoList.ReadWrite" 33 | ], 34 | "SampleSubPath": "3-Authorization-II\\1-call-api-b2c\\API" 35 | }, 36 | { 37 | "Id": "client", 38 | "Name": "msal-javascript-spa", 39 | "Kind": "SinglePageApplication", 40 | "Audience": "AzureADandPersonalMicrosoftAccount", 41 | "HomePage": "http://localhost:3000", 42 | "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect", 43 | "SampleSubPath": "3-Authorization-II\\1-call-api-b2c\\SPA", 44 | "SDK": "MsalJs", 45 | "RequiredResourcesAccess": [ 46 | { 47 | "Resource": "service", 48 | "DelegatedPermissions": [ 49 | "ToDoList.Read", 50 | "ToDoList.ReadWrite" 51 | ] 52 | } 53 | ] 54 | } 55 | ], 56 | "CodeConfiguration": [ 57 | { 58 | "App": "service", 59 | "SettingKind": "JSON", 60 | "SettingFile": "\\..\\API\\authConfig.json", 61 | "Mappings": [ 62 | { 63 | "key": "clientID", 64 | "value": ".AppId" 65 | }, 66 | { 67 | "key": "tenantID", 68 | "value": "$tenantId" 69 | }, 70 | { 71 | "key": "policyName", 72 | "value": "Enter_The_Your_policy_Name" 73 | } 74 | ] 75 | }, 76 | { 77 | "App": "client", 78 | "SettingKind": "Replace", 79 | "SettingFile": "\\..\\SPA\\src\\authConfig.js", 80 | "Mappings": [ 81 | { 82 | "key": "Enter_the_Application_Id_Here", 83 | "value": ".AppId" 84 | }, 85 | { 86 | "key": "Enter_the_Tenant_Info_Here", 87 | "value": "$tenantId" 88 | }, 89 | { 90 | "key": "Enter_the_Web_Api_Scope_Here", 91 | "value": "service.Scope" 92 | }, 93 | { 94 | "key": "policyName", 95 | "value": "Enter_The_Your_policy_Name" 96 | }, 97 | { 98 | "key": "b2cDomain", 99 | "value": "Enter_The_Tenant_Domain_name" 100 | } 101 | ] 102 | } 103 | ] 104 | } -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/2-call-api-b2c/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/ReadmeFiles/topology_b2c_callapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/3-Authorization-II/2-call-api-b2c/ReadmeFiles/topology_b2c_callapi.png -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ciam-sign-in-javascript", 3 | "version": "1.0.0", 4 | "description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users against Azure AD Customer Identity Access Management (Azure AD for Customers)", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "jest --forceExit" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@azure/msal-browser": "^2.37.0", 14 | "express": "^4.18.2", 15 | "morgan": "^1.10.0" 16 | }, 17 | "devDependencies": { 18 | "jest": "^29.5.0", 19 | "jest-environment-jsdom": "^29.5.0", 20 | "supertest": "^6.3.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/authConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enter here the user flows and custom policies for your B2C application 3 | * To learn more about user flows, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview 4 | * To learn more about custom policies, visit: https://docs.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-overview 5 | */ 6 | const b2cPolicies = { 7 | names: { 8 | signUpSignIn: 'B2C_1_susi_v2', 9 | forgotPassword: 'B2C_1_reset_v3', 10 | editProfile: 'B2C_1_edit_profile_v2', 11 | }, 12 | authorities: { 13 | signUpSignIn: { 14 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_susi_v2', 15 | }, 16 | forgotPassword: { 17 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_reset_v3', 18 | }, 19 | editProfile: { 20 | authority: 'https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_edit_profile_v2', 21 | }, 22 | }, 23 | authorityDomain: 'fabrikamb2c.b2clogin.com', 24 | }; 25 | 26 | /** 27 | * Configuration object to be passed to MSAL instance on creation. 28 | * For a full list of MSAL.js configuration parameters, visit: 29 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md 30 | * For more details on MSAL.js and Azure AD B2C, visit: 31 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/working-with-b2c.md 32 | */ 33 | 34 | const msalConfig = { 35 | auth: { 36 | clientId: '2fdd06f3-7b34-49a3-a78b-0cf1dd87878e', // Replace with your AppID/ClientID obtained from Azure Portal. 37 | authority: b2cPolicies.authorities.signUpSignIn.authority, // Choose sign-up/sign-in user-flow as your default. 38 | knownAuthorities: [b2cPolicies.authorityDomain], // You must identify your tenant's domain as a known authority. 39 | redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to "window.location.href". 40 | postLogoutRedirectUri: '/signout', // Simply remove this line if you would like navigate to index page after logout. 41 | navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response. 42 | }, 43 | cache: { 44 | cacheLocation: 'localStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO. 45 | storeAuthStateInCookie: false, // If you wish to store cache items in cookies as well as browser cache, set this to "true". 46 | }, 47 | system: { 48 | loggerOptions: { 49 | loggerCallback: (level, message, containsPii) => { 50 | if (containsPii) { 51 | return; 52 | } 53 | switch (level) { 54 | case msal.LogLevel.Error: 55 | console.error(message); 56 | return; 57 | case msal.LogLevel.Info: 58 | console.info(message); 59 | return; 60 | case msal.LogLevel.Verbose: 61 | console.debug(message); 62 | return; 63 | case msal.LogLevel.Warning: 64 | console.warn(message); 65 | return; 66 | } 67 | }, 68 | }, 69 | }, 70 | }; 71 | 72 | /** 73 | * Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see: 74 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md 75 | */ 76 | const protectedResources = { 77 | todolistApi: { 78 | endpoint: 'http://localhost:5000/api/todolist', 79 | scopes: { 80 | read: ['https://fabrikamb2c.onmicrosoft.com/ToDoList.Read'], 81 | write: ['https://fabrikamb2c.onmicrosoft.com/ToDoList.ReadWrite'], 82 | }, 83 | }, 84 | }; 85 | 86 | /** 87 | * Scopes you add here will be prompted for user consent during sign-in. 88 | * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. 89 | * For more information about OIDC scopes, visit: 90 | * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes 91 | */ 92 | const loginRequest = { 93 | scopes: [...protectedResources.todolistApi.scopes.read, ...protectedResources.todolistApi.scopes.write], 94 | }; 95 | 96 | /** 97 | * An optional silentRequest object can be used to achieve silent SSO 98 | * between applications by providing a "login_hint" property. 99 | */ 100 | 101 | // const silentRequest = { 102 | // scopes: ["openid", "profile"], 103 | // loginHint: "example@domain.net" 104 | // }; 105 | 106 | // exporting config object for jest 107 | if (typeof exports !== 'undefined') { 108 | module.exports = { 109 | msalConfig, 110 | loginRequest, 111 | protectedResources, 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/authPopup.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ''; 6 | 7 | function selectAccount() { 8 | /** 9 | * See here for more info on account retrieval: 10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 11 | */ 12 | 13 | const currentAccounts = myMSALObj.getAllAccounts(); 14 | if (!currentAccounts || currentAccounts.length < 1) { 15 | return; 16 | } else if (currentAccounts.length > 1) { 17 | // Add your account choosing logic here 18 | console.warn('Multiple accounts detected.'); 19 | } else if (currentAccounts.length === 1) { 20 | username = currentAccounts[0].username; 21 | welcomeUser(username); 22 | updateTable(currentAccounts[0]); 23 | } 24 | } 25 | 26 | function handleResponse(response) { 27 | /** 28 | * To see the full list of response object properties, visit: 29 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response 30 | */ 31 | 32 | if (response !== null) { 33 | username = response.account.username; 34 | welcomeUser(username); 35 | updateTable(response.account); 36 | } else { 37 | selectAccount(); 38 | } 39 | } 40 | 41 | function signIn() { 42 | /** 43 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 44 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 45 | */ 46 | 47 | myMSALObj 48 | .loginPopup({ 49 | ...loginRequest, 50 | redirectUri: '/redirect', 51 | }) 52 | .then(handleResponse) 53 | .catch((error) => { 54 | console.log(error); 55 | // Error handling 56 | if (error.errorMessage) { 57 | // Check for forgot password error 58 | // Learn more about AAD error codes at https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes 59 | if (error.errorMessage.indexOf('AADB2C90118') > -1) { 60 | myMSALObj.loginPopup(b2cPolicies.authorities.forgotPassword); 61 | } 62 | } 63 | }); 64 | } 65 | 66 | 67 | function getTokenPopup(request) { 68 | /** 69 | * See here for more information on account retrieval: 70 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 71 | */ 72 | request.account = myMSALObj.getAccountByUsername(username); 73 | return myMSALObj.acquireTokenSilent(request).catch((error) => { 74 | console.warn(error); 75 | console.warn('silent token acquisition fails. acquiring token using popup'); 76 | if (error instanceof msal.InteractionRequiredAuthError) { 77 | // fallback to interaction when silent call fails 78 | return myMSALObj 79 | .acquireTokenPopup(request) 80 | .then((response) => { 81 | return response; 82 | }) 83 | .catch((error) => { 84 | console.error(error); 85 | }); 86 | } else { 87 | console.warn(error); 88 | } 89 | }); 90 | } 91 | 92 | function signOut() { 93 | /** 94 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 95 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 96 | */ 97 | 98 | // Choose which account to logout from by passing a username. 99 | const logoutRequest = { 100 | account: myMSALObj.getAccountByUsername(username), 101 | }; 102 | myMSALObj.logoutPopup(logoutRequest).then(() => { 103 | window.location.reload(); 104 | }); 105 | } 106 | 107 | function editProfile() { 108 | myMSALObj.loginPopup({ 109 | ...b2cPolicies.authorities.editProfile, 110 | }); 111 | } 112 | 113 | selectAccount(); 114 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/authRedirect.js: -------------------------------------------------------------------------------- 1 | // Create the main myMSALObj instance 2 | // configuration parameters are located at authConfig.js 3 | const myMSALObj = new msal.PublicClientApplication(msalConfig); 4 | 5 | let username = ''; 6 | 7 | /** 8 | * A promise handler needs to be registered for handling the 9 | * response returned from redirect flow. For more information, visit: 10 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirect-apis 11 | */ 12 | myMSALObj 13 | .handleRedirectPromise() 14 | .then(handleResponse) 15 | .catch((error) => { 16 | console.log(error); 17 | 18 | // Check for forgot password error 19 | // Learn more about AAD error codes at https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes 20 | if (error.errorMessage.indexOf('AADB2C90118') > -1) { 21 | try { 22 | myMSALObj.loginRedirect(b2cPolicies.authorities.forgotPassword); 23 | } catch (err) { 24 | console.log(err); 25 | } 26 | } 27 | }); 28 | 29 | function selectAccount() { 30 | /** 31 | * See here for more info on account retrieval: 32 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 33 | */ 34 | 35 | const currentAccounts = myMSALObj.getAllAccounts(); 36 | 37 | if (!currentAccounts) { 38 | return; 39 | } else if (currentAccounts.length > 1) { 40 | // Add your account choosing logic here 41 | console.warn('Multiple accounts detected.'); 42 | } else if (currentAccounts.length === 1) { 43 | username = currentAccounts[0].username; 44 | welcomeUser(username); 45 | updateTable(currentAccounts[0]); 46 | } 47 | } 48 | 49 | function handleResponse(response) { 50 | /** 51 | * To see the full list of response object properties, visit: 52 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response 53 | */ 54 | 55 | if (response !== null) { 56 | username = response.account.username; 57 | welcomeUser(username); 58 | updateTable(response.account); 59 | } else { 60 | selectAccount(); 61 | } 62 | } 63 | 64 | function signIn() { 65 | /** 66 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 67 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 68 | */ 69 | 70 | myMSALObj.loginRedirect(loginRequest); 71 | } 72 | 73 | function getTokenRedirect(request) { 74 | /** 75 | * See here for more info on account retrieval: 76 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md 77 | */ 78 | request.account = myMSALObj.getAccountByUsername(username); 79 | return myMSALObj.acquireTokenSilent(request).catch((error) => { 80 | console.error(error); 81 | console.warn('silent token acquisition fails. acquiring token using popup'); 82 | if (error instanceof msal.InteractionRequiredAuthError) { 83 | // fallback to interaction when silent call fails 84 | return myMSALObj.acquireTokenRedirect(request); 85 | } else { 86 | console.error(error); 87 | } 88 | }); 89 | } 90 | 91 | function signOut() { 92 | /** 93 | * You can pass a custom request object below. This will override the initial configuration. For more information, visit: 94 | * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request 95 | */ 96 | 97 | // Choose which account to logout from by passing a username. 98 | const logoutRequest = { 99 | account: myMSALObj.getAccountByUsername(username), 100 | }; 101 | 102 | myMSALObj.logoutRedirect(logoutRequest); 103 | } 104 | 105 | function editProfile() { 106 | myMSALObj.loginRedirect(b2cPolicies.authorities.editProfile); 107 | } -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/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 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/fetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Execute a fetch request with the given options 3 | * @param {string} method: GET, POST, PUT, DELETE 4 | * @param {String} endpoint: The endpoint to call 5 | * @param {Object} data: The data to send to the endpoint, if any 6 | * @returns response 7 | */ 8 | function callApi(method, endpoint, token, data = null) { 9 | const headers = new Headers(); 10 | const bearer = `Bearer ${token}`; 11 | 12 | headers.append('Authorization', bearer); 13 | 14 | if (data) { 15 | headers.append('Content-Type', 'application/json'); 16 | } 17 | 18 | const options = { 19 | method: method, 20 | headers: headers, 21 | body: data ? JSON.stringify(data) : null, 22 | }; 23 | 24 | return fetch(endpoint, options) 25 | .then((response) => { 26 | const contentType = response.headers.get("content-type"); 27 | 28 | if (contentType && contentType.indexOf("application/json") !== -1) { 29 | return response.json(); 30 | } else { 31 | return response; 32 | } 33 | }); 34 | } 35 | 36 | 37 | /** 38 | * Handles todolist actions 39 | * @param {Object} task 40 | * @param {string} method 41 | * @param {string} endpoint 42 | */ 43 | async function handleToDoListActions(task, method, endpoint) { 44 | let listData; 45 | 46 | try { 47 | const accessToken = await getToken(); 48 | const data = await callApi(method, endpoint, accessToken, task); 49 | 50 | switch (method) { 51 | case 'POST': 52 | listData = JSON.parse(localStorage.getItem('todolist')); 53 | listData = [data, ...listData]; 54 | localStorage.setItem('todolist', JSON.stringify(listData)); 55 | AddTaskToToDoList(data); 56 | break; 57 | case 'DELETE': 58 | listData = JSON.parse(localStorage.getItem('todolist')); 59 | const index = listData.findIndex((todoItem) => todoItem.id === task.id); 60 | localStorage.setItem('todolist', JSON.stringify([...listData.splice(index, 1)])); 61 | showToDoListItems(listData); 62 | break; 63 | default: 64 | console.log('Unrecognized method.') 65 | break; 66 | } 67 | } catch (error) { 68 | console.error(error); 69 | } 70 | } 71 | 72 | /** 73 | * Handles todolist action GET action. 74 | */ 75 | async function getToDos() { 76 | try { 77 | const accessToken = await getToken(); 78 | 79 | const data = await callApi( 80 | 'GET', 81 | protectedResources.todolistApi.endpoint, 82 | accessToken 83 | ); 84 | 85 | if (data) { 86 | localStorage.setItem('todolist', JSON.stringify(data)); 87 | showToDoListItems(data); 88 | } 89 | } catch (error) { 90 | console.error(error); 91 | } 92 | } 93 | 94 | /** 95 | * Retrieves an access token. 96 | */ 97 | async function getToken() { 98 | let tokenResponse; 99 | 100 | if (typeof getTokenPopup === 'function') { 101 | tokenResponse = await getTokenPopup({ 102 | scopes: [...protectedResources.todolistApi.scopes.read], 103 | redirectUri: '/redirect' 104 | }); 105 | } else { 106 | tokenResponse = await getTokenRedirect({ 107 | scopes: [...protectedResources.todolistApi.scopes.read], 108 | }); 109 | } 110 | 111 | if (!tokenResponse) { 112 | return null; 113 | } 114 | 115 | return tokenResponse.accessToken; 116 | } 117 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Azure AD B2C 8 | 9 | 10 | 11 | 14 | 15 | 17 | 18 | 19 | 20 | 34 |
35 |
Vanilla JavaScript single-page application secured with MSAL.js 36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
Claim TypeValueDescription
51 |
52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 |
    63 |
64 |
65 | 66 | 69 | 72 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/redirect.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/styles.css: -------------------------------------------------------------------------------- 1 | .navbarStyle { 2 | padding: .5rem 1rem !important; 3 | } 4 | 5 | .table-responsive-ms { 6 | max-height: 39rem !important; 7 | padding-left: 10%; 8 | padding-right: 10%; 9 | } 10 | 11 | form, .group-div { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .input-group, ul { 18 | width: 50% !important; 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/public/ui.js: -------------------------------------------------------------------------------- 1 | // Select DOM elements to work with 2 | const signInButton = document.getElementById('signIn'); 3 | const signOutButton = document.getElementById('signOut'); 4 | const titleDiv = document.getElementById('title-div'); 5 | const welcomeDiv = document.getElementById('welcome-div'); 6 | const tableDiv = document.getElementById('table-div'); 7 | const tableBody = document.getElementById('table-body-div'); 8 | const toDoListLink = document.getElementById('toDoListLink'); 9 | const toDoForm = document.getElementById('form'); 10 | const textInput = document.getElementById('textInput'); 11 | const toDoListDiv = document.getElementById('groupDiv'); 12 | const todoListItems = document.getElementById('toDoListItems'); 13 | const editProfileButton = document.getElementById('editProfileButton'); 14 | 15 | toDoForm.addEventListener('submit', (e) => { 16 | e.preventDefault(); 17 | let task = { description: textInput.value }; 18 | handleToDoListActions(task, 'POST', protectedResources.todolistApi.endpoint); 19 | toDoForm.reset(); 20 | }); 21 | 22 | function welcomeUser(username) { 23 | signInButton.classList.add('d-none'); 24 | signOutButton.classList.remove('d-none'); 25 | toDoListLink.classList.remove('d-none'); 26 | editProfileButton.classList.remove('d-none'); 27 | titleDiv.classList.add('d-none'); 28 | welcomeDiv.classList.remove('d-none'); 29 | welcomeDiv.innerHTML = `Welcome ${username}!`; 30 | } 31 | 32 | function updateTable(account) { 33 | tableDiv.classList.remove('d-none'); 34 | const tokenClaims = createClaimsTable(account.idTokenClaims); 35 | 36 | Object.keys(tokenClaims).forEach((key) => { 37 | let row = tableBody.insertRow(0); 38 | let cell1 = row.insertCell(0); 39 | let cell2 = row.insertCell(1); 40 | let cell3 = row.insertCell(2); 41 | cell1.innerHTML = tokenClaims[key][0]; 42 | cell2.innerHTML = tokenClaims[key][1]; 43 | cell3.innerHTML = tokenClaims[key][2]; 44 | }); 45 | } 46 | 47 | function showToDoListItems(response) { 48 | todoListItems.replaceChildren(); 49 | tableDiv.classList.add('d-none'); 50 | toDoForm.classList.remove('d-none'); 51 | toDoListDiv.classList.remove('d-none'); 52 | if (!!response.length) { 53 | response.forEach((task) => { 54 | AddTaskToToDoList(task); 55 | }); 56 | } 57 | } 58 | 59 | function AddTaskToToDoList(task) { 60 | let li = document.createElement('li'); 61 | let button = document.createElement('button'); 62 | button.innerHTML = 'Delete'; 63 | button.classList.add('btn', 'btn-danger'); 64 | button.addEventListener('click', () => { 65 | handleToDoListActions(task, 'DELETE', protectedResources.todolistApi.endpoint + `/${task.id}`); 66 | }); 67 | li.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center'); 68 | li.innerHTML = task.description; 69 | li.appendChild(button); 70 | todoListItems.appendChild(li); 71 | } 72 | -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/sample.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | const request = require('supertest'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | 9 | const app = require('./server.js'); 10 | 11 | jest.dontMock('fs'); 12 | 13 | const html = fs.readFileSync(path.resolve(__dirname, './public/index.html'), 'utf8'); 14 | 15 | describe('Sanitize index page', () => { 16 | beforeAll(async() => { 17 | global.document.documentElement.innerHTML = html.toString(); 18 | }); 19 | 20 | it('should have valid cdn link', () => { 21 | expect(document.getElementById("load-msal").getAttribute("src")).toContain("https://alcdn.msauth.net/browser"); 22 | }); 23 | }); 24 | 25 | describe('Sanitize configuration object', () => { 26 | beforeAll(() => { 27 | global.msalConfig = require('./public/authConfig.js').msalConfig; 28 | }); 29 | 30 | it('should define the config object', () => { 31 | expect(msalConfig).toBeDefined(); 32 | }); 33 | 34 | it('should contain credentials', () => { 35 | 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; 36 | expect(regexGuid.test(msalConfig.auth.clientId)).toBe(true); 37 | }); 38 | 39 | it('should contain authority URI', () => { 40 | const regexUri = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; 41 | expect(regexUri.test(msalConfig.auth.authority)).toBe(true); 42 | }); 43 | }); 44 | 45 | describe('Ensure pages served', () => { 46 | 47 | beforeAll(() => { 48 | process.env.NODE_ENV = 'test'; 49 | }); 50 | 51 | it('should get index page', async () => { 52 | const res = await request(app) 53 | .get('/'); 54 | 55 | const data = await fs.promises.readFile(path.join(__dirname, './public/index.html'), 'utf8'); 56 | expect(res.statusCode).toEqual(200); 57 | expect(res.text).toEqual(data); 58 | }); 59 | }); -------------------------------------------------------------------------------- /3-Authorization-II/2-call-api-b2c/SPA/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const path = require('path'); 4 | 5 | const DEFAULT_PORT = process.env.PORT || 6420; 6 | 7 | // initialize express. 8 | const app = express(); 9 | 10 | // Configure morgan module to log all requests. 11 | app.use(morgan('dev')); 12 | 13 | // Setup app folders. 14 | app.use(express.static('public')); 15 | 16 | // set up a route for redirect.html 17 | app.get('/redirect', (req, res) => { 18 | res.sendFile(path.join(__dirname + '/public/redirect.html')); 19 | }); 20 | 21 | // Set up a route for index.html 22 | app.get('/', (req, res) => { 23 | res.sendFile(path.join(__dirname + '/index.html')); 24 | }); 25 | 26 | app.listen(DEFAULT_PORT, () => { 27 | console.log(`Sample app listening on port ${DEFAULT_PORT}!`); 28 | }); 29 | 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/api_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/api_step1.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/api_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/api_step2.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/api_step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/api_step3.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/disable_easy_auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/disable_easy_auth.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/enable_cors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/enable_cors.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/screenshot.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/spa_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step1.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/spa_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step2.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/spa_step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step3.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/spa_step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/spa_step4.png -------------------------------------------------------------------------------- /4-Deployment/ReadmeFiles/topology_dep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-javascript-tutorial/89beb80e69a211a78a689c7740bddbbbe93e0079/4-Deployment/ReadmeFiles/topology_dep.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 01/04/2021 4 | 5 | * New chapter (4-2) added. 6 | 7 | ## 12/07/2020 8 | 9 | * Updated MSAL.js to 2.7.0 10 | * B2C null access token issue resolved. 11 | * UI update code removed from auth methods. 12 | * Folder structure and naming revised. 13 | 14 | ## 10/20/2020 15 | 16 | * Initial sample. 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------