├── .dockerignore ├── .env.local.sample ├── .eslintrc.js ├── .gitignore ├── .prettierrc.yaml ├── .vscode └── settings.json ├── README.md ├── deploy ├── Dockerfile └── default.conf ├── docs ├── app1.png └── app2.png ├── package-lock.json ├── package.json ├── public ├── index.html └── logo.png └── src ├── App.vue ├── assets ├── logo.png ├── logo.svg └── mssymbol.svg ├── components ├── DetailsModal.vue ├── Login.vue └── Search.vue ├── main.js └── services ├── auth.js └── graph.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Standard stuff 2 | Dockerfile* 3 | **/*Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | readme.md 9 | **/readme.md 10 | **/LICENSE 11 | .vscode/ 12 | 13 | # Node stuff 14 | node_modules 15 | **/node_modules 16 | **/*.log 17 | 18 | # Dotenv secrets! 19 | **/.env 20 | 21 | # Outputs, binaries etc 22 | **/dist/ 23 | -------------------------------------------------------------------------------- /.env.local.sample: -------------------------------------------------------------------------------- 1 | # Rename .env or .env.local 2 | # See https://cli.vuejs.org/guide/mode-and-env.html 3 | 4 | # Client ID of application registered in Azure AD 5 | # !!! MUST BE SET !!! 6 | VUE_APP_CLIENT_ID="CHANGE_ME" 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es6: true, 5 | jest: true 6 | }, 7 | extends: ['eslint:recommended', 'plugin:vue/recommended'], 8 | rules: { 9 | // Errors & best practices 10 | 'no-var': 'error', 11 | 'no-console': 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-unused-vars': ['error', { argsIgnorePattern: 'next|res|req' }], 14 | curly: 'error', 15 | 16 | // Vue 17 | 'vue/html-closing-bracket-newline': 'off', 18 | 'vue/max-attributes-per-line': 'off', 19 | 'vue/html-self-closing': 'off', 20 | 'vue/singleline-html-element-content-newline': 'off' 21 | }, 22 | parserOptions: { 23 | parser: 'babel-eslint' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/reports/ 6 | selenium-debug.log 7 | chromedriver.log 8 | geckodriver.log 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | pnpm-debug.log* 19 | 20 | # Editor directories and files 21 | .idea 22 | .vscode 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | printWidth: 120 4 | tabWidth: 2 5 | arrowParens: always 6 | bracketSpacing: true 7 | useTabs: false 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // { 2 | // // ESLint config for Vue 3 | // "editor.codeActionsOnSave": { 4 | // "source.fixAll.eslint": true 5 | // }, 6 | // "eslint.format.enable": true, 7 | // "[typescript]": { 8 | // "editor.defaultFormatter": "dbaeumer.vscode-eslint" 9 | // }, 10 | // "eslint.workingDirectories": [ 11 | // "./src" 12 | // ], 13 | // } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSAL & Microsoft Graph - Vue.js 2 | 3 | This project consists of two things: 4 | 5 | - A sample Vue.js application. 6 | - Drop in helper modules for MSAL.js and Microsoft Graph. [See docs below](#using-as-a-drop-in-library) 7 | 8 | The sample app is an example of using [MSAL for JS v2](https://github.com/AzureAD/microsoft-authentication-library-for-js) to authenticate against [Azure AD from a single page JS application](https://docs.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa). 9 | After signing in, an access token is requested and used to query the [Microsoft Graph API](https://developer.microsoft.com/en-us/graph) 10 | 11 | This uses the newer MSAL.js 2.0+ library which supports the authorization code flow with PKCE for single page apps, and not the older implicit flow. 12 | 13 | The app provides demonstration of some fundamental use cases: 14 | 15 | - Signing in users from a single page application (SPA) 16 | - Login, logout, user account caching 17 | - Requesting and using scoped access tokens 18 | - Calling the Microsoft Graph API 19 | - Searching the Microsoft Graph with OData 20 | 21 | ![screen shot](https://user-images.githubusercontent.com/14982936/87789050-4931a180-c836-11ea-8c97-16b1c7e19895.png) 22 | 23 | This app only uses `User.Read` and `User.ReadBasic.All` permissions in the Graph, so does not require admin consent 24 | 25 | The use of a registered _multi-tenant application_ and the v2 Azure AD 'common' endpoint is assumed, but single tenanted apps would also work 26 | 27 | Note. The MSAL library is used directly rather than any Vue specific wrapper, as there's enough layers of abstraction to deal with as it is, without one more 28 | 29 | # Set Up & Deployment 30 | 31 | ### Pre-reqs - Register app in Azure AD 32 | 33 | Using the Azure CLI create the new app registration 34 | 35 | ``` 36 | az ad app create --display-name="Graph Demo App" \ 37 | --available-to-other-tenants=true \ 38 | --query "appId" -o tsv 39 | ``` 40 | 41 | Make a note of the GUID returned, this is the app ID, or client ID 42 | 43 | Follow the guide here to further configure the app, this currently can't be done from the CLI 44 | https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-app-registration#redirect-uri-msaljs-20-with-auth-code-flow 45 | 46 | Quick summary of the steps: 47 | 48 | - Click 'Authentication' 49 | - UNSELECT the checkbox 'ID tokens (used for implicit and hybrid flows)' 50 | - Click 'Add a platform' 51 | - Click 'Single page application' 52 | - Enter `http://localhost:8080` as the redirect URI 53 | 54 | Copy `.env.local.sample` to `.env.local` and place the app ID in the setting `VUE_APP_CLIENT_ID` 55 | 56 | ### Docker 57 | 58 | There is a Dockerfile to build the app and serve it via NGINX. The Azure AD client ID needs to be set at build time (as this is a Vue.js app) 59 | 60 | Run from root of project, and set CLIENT_ID and image tag as required 61 | 62 | ```bash 63 | docker build . -f deploy/Dockerfile --build-arg CLIENT_ID="CHANGE_ME" -t msal-graph-vue 64 | ``` 65 | 66 | # Running Locally 67 | 68 | This project was created with [the Vue CLI](https://cli.vuejs.org/) 69 | 70 | ## Project setup 71 | 72 | ```bash 73 | npm install 74 | ``` 75 | 76 | ### Compiles and hot-reloads for development 77 | 78 | ```bash 79 | npm run serve 80 | ``` 81 | 82 | Local server runs on `http://localhost:8080` by default 83 | 84 | ### Compiles and minifies for production 85 | 86 | ```bash 87 | npm run build 88 | ``` 89 | 90 | ### Lint code with ESLint 91 | 92 | ```bash 93 | npm run lint 94 | ``` 95 | 96 | # Using as a drop in library 97 | 98 | The `src/services/auth.js` and `src/services/graph.js` files are ES6 modules and have been written to be as reusable as possible, so can be copied and dropped into any SPA project. 99 | 100 | ## auth.js 101 | 102 | This is a fairly opinionated wrapper around MSAL.js 2.0 providing methods for configuring MSAL, login, logout, acquiring tokens etc. It also supports a special 'dummy user mode' where MSAL has been stubbed for when you want your app to optionally support user auth e.g. demos and test environments. 103 | 104 | ### Setup 105 | 106 | First call `configure()` this accepts the Azure AD clientId you wish to use, the second parameter is to enable/disable dummy user mode, dummy user mode is only switch on if clientId is null or empty 107 | 108 | Call this once as your app is initialized. 109 | 110 | ```js 111 | import auth from './services/auth' 112 | 113 | // Example of getting client id, there might be other mechanisms you want to use to fetch this value 114 | const clientId = process.env.VUE_APP_CLIENT_ID 115 | 116 | // Set up auth helper with dummy user disabled 117 | auth.configure(clientId, false) 118 | 119 | // Set up auth helper with dummy user enabled, it's only enabled when clientId is undefined/blank 120 | //auth.configure(clientId, true) 121 | ``` 122 | 123 | ### Method Reference 124 | 125 | - `configure(clientId, enableDummyUser)` - Configure and setup the helper 126 | - `login(scopes)` - Prompt user to login, scopes parameter is optional, the defaults are: `user.read, openid, profile, email` 127 | - `logout()` - Perform a full logout. 128 | - `user()` - Get the current user, returns the [MSAL AccountInfo object](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#accountinfo). 129 | - `acquireToken(scopes)` - Attempt to get an access token, silent is called first, then pop-up if that fails. The access token as a string is returned. 130 | - `clearLocal()` - Clear local cache, use for a 'shallow' local only logout. 131 | - `isConfigured()` - Returns if the helper is configured . 132 | - `clientId()` - Returns the clientId used to configure the helper. 133 | 134 | ## graph.js 135 | 136 | This is a helper for calling the Microsoft Graph, it requires `auth.js` which has been setup and configured previous to any calls to this library. This is a thin wrapper around the beta Graph endpoint `https://graph.microsoft.com/beta` endpoint, and it silently acquires tokens for you. 137 | 138 | > Note. It requests an access token with the `user.readbasic.all` scope, in order to search the directory, this scope does NOT require the application to have admin consent. 139 | 140 | ### Method Reference 141 | 142 | - `getSelf()` - Calls the `/me` endpoint to get details of the currently signed in user. [See Graph docs](https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-beta&tabs=http) 143 | - `getPhoto()` - Returns the current user's photo as a blob object. 144 | - `searchUsers(searchString, maxResults)` - Search the directory for users, looks in the _displayName_ and _userPrincipalName_ fields, by default returns 50 results. 145 | - `getAccessToken()` - Returns the current accesstoken in use. 146 | -------------------------------------------------------------------------------- /deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | # ================================================================================================ 2 | # === Stage 1: Build and bundle the Vue app with the Vue CLI ===================================== 3 | # ================================================================================================ 4 | FROM node:12-alpine as bundle 5 | 6 | ARG CLIENT_ID="" 7 | ENV VUE_APP_CLIENT_ID=${CLIENT_ID} 8 | 9 | WORKDIR /build 10 | 11 | # Install all packages and app dependencies 12 | COPY ./package*.json ./ 13 | RUN npm install --silent 14 | 15 | # Copy in the app source 16 | COPY ./public ./public 17 | COPY ./src ./src 18 | 19 | # Now build & bundle, this will output to ./dist (i.e. /build/dist) 20 | RUN npm run build 21 | 22 | # ================================================================================================ 23 | # === Stage 2: Copy static bundle into NGINX server ============================================== 24 | # ================================================================================================ 25 | FROM nginx:mainline-alpine 26 | 27 | COPY deploy/default.conf /etc/nginx/conf.d/ 28 | 29 | # Copy in output from Vue bundle (the 'dist' directory) 30 | COPY --from=bundle /build/dist /var/www/html 31 | 32 | EXPOSE 80 33 | -------------------------------------------------------------------------------- /deploy/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; ## listen for ipv4; this line is default and implied 3 | listen [::]:80 default ipv6only=on; ## listen for ipv6 4 | 5 | root /var/www/html; 6 | index index.html; 7 | 8 | server_tokens off; # disable the Server nginx header 9 | 10 | server_name _; # all hostnames 11 | 12 | # enable gzip 13 | gzip on; 14 | gzip_disable "msie6"; 15 | 16 | gzip_comp_level 6; 17 | gzip_min_length 1100; 18 | gzip_buffers 16 8k; 19 | gzip_proxied any; 20 | gzip_types 21 | text/plain 22 | text/css 23 | text/js 24 | text/xml 25 | text/javascript 26 | application/javascript 27 | application/x-javascript 28 | application/json 29 | application/xml 30 | application/rss+xml 31 | image/svg+xml; 32 | 33 | location / { 34 | try_files $uri /index.html; # redirect all request to index.html 35 | } 36 | } -------------------------------------------------------------------------------- /docs/app1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benc-uk/msal-graph-vue/9425056751e25f0c1f38b9630ec320a073853fd1/docs/app1.png -------------------------------------------------------------------------------- /docs/app2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benc-uk/msal-graph-vue/9425056751e25f0c1f38b9630ec320a073853fd1/docs/app2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-graph-demo", 3 | "version": "1.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "eslint src", 9 | "start": "npm run serve" 10 | }, 11 | "dependencies": { 12 | "@azure/msal-browser": "^2.13.0", 13 | "vue": "^2.6.12" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-eslint": "^4.5.12", 17 | "@vue/cli-service": "~4.5.12", 18 | "babel-eslint": "^10.1.0", 19 | "eslint": "^6.7.2", 20 | "eslint-plugin-prettier-vue": "^2.1.1", 21 | "eslint-plugin-vue": "^7.8.0", 22 | "vue-template-compiler": "^2.6.12" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | MSAL & Graph - Demo 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benc-uk/msal-graph-vue/9425056751e25f0c1f38b9630ec320a073853fd1/public/logo.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 117 | 118 | 208 | 209 | 240 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benc-uk/msal-graph-vue/9425056751e25f0c1f38b9630ec320a073853fd1/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/assets/mssymbol.svg: -------------------------------------------------------------------------------- 1 | MS-SymbolLockup -------------------------------------------------------------------------------- /src/components/DetailsModal.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 100 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: function (h) { return h(App) }, 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /src/services/auth.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Ben Coleman, 2021 3 | // Licensed under the MIT License. 4 | // 5 | // Drop in MSAL.js 2.x service wrapper & helper for SPAs 6 | // v2.1.0 - Ben Coleman 2019 7 | // Updated 2021 - Switched to @azure/msal-browser 8 | // ---------------------------------------------------------------------------- 9 | 10 | import * as msal from '@azure/msal-browser' 11 | 12 | // MSAL object used for signing in users with MS identity platform 13 | let msalApp 14 | 15 | export default { 16 | // 17 | // Configure with clientId or empty string/null to set in "demo" mode 18 | // 19 | async configure(clientId, enableDummyUser = true) { 20 | // Can only call configure once 21 | if (msalApp) { 22 | return 23 | } 24 | 25 | // If no clientId provided & enableDummyUser then create a mock MSAL UserAgentApplication 26 | // Allows us to run without Azure AD for demos & local dev 27 | if (!clientId && enableDummyUser) { 28 | console.log('### Azure AD sign-in: disabled. Will run in demo mode with dummy demo@example.net account') 29 | 30 | const dummyUser = { 31 | accountIdentifier: 'e11d4d0c-1c70-430d-a644-aed03a60e059', 32 | homeAccountIdentifier: '', 33 | username: 'demo@example.net', 34 | name: 'Demo User', 35 | idToken: null, 36 | sid: '', 37 | environment: '', 38 | idTokenClaims: { 39 | tid: 'fake-tenant' 40 | } 41 | } 42 | 43 | // Stub out all the functions we call and return static dummy user where required 44 | // Use localStorage to simulate MSAL caching and logging out 45 | msalApp = { 46 | config: { 47 | auth: { 48 | clientId: null 49 | } 50 | }, 51 | 52 | loginPopup() { 53 | localStorage.setItem('dummyAccount', JSON.stringify(dummyUser)) 54 | return new Promise((resolve) => resolve()) 55 | }, 56 | logout() { 57 | localStorage.removeItem('dummyAccount') 58 | window.location.href = '/' 59 | return new Promise((resolve) => resolve()) 60 | }, 61 | acquireTokenSilent() { 62 | return new Promise((resolve) => resolve({ accessToken: '1234567890' })) 63 | }, 64 | cacheStorage: { 65 | clear() { 66 | localStorage.removeItem('dummyAccount') 67 | } 68 | }, 69 | getAllAccounts() { 70 | return [JSON.parse(localStorage.getItem('dummyAccount'))] 71 | } 72 | } 73 | return 74 | } 75 | 76 | // Can't configure if clientId blank/null/undefined 77 | if (!clientId) { 78 | return 79 | } 80 | 81 | const config = { 82 | auth: { 83 | clientId: clientId, 84 | redirectUri: window.location.origin, 85 | authority: 'https://login.microsoftonline.com/common' 86 | }, 87 | cache: { 88 | cacheLocation: 'localStorage' 89 | } 90 | // Only uncomment when you *really* need to debug what is going on in MSAL 91 | /* system: { 92 | logger: new msal.Logger( 93 | (logLevel, msg) => { console.log(msg) }, 94 | { 95 | level: msal.LogLevel.Verbose 96 | } 97 | ) 98 | } */ 99 | } 100 | console.log('### Azure AD sign-in: enabled\n', config) 101 | 102 | // Create our shared/static MSAL app object 103 | msalApp = new msal.PublicClientApplication(config) 104 | }, 105 | 106 | // 107 | // Return the configured client id 108 | // 109 | clientId() { 110 | if (!msalApp) { 111 | return null 112 | } 113 | 114 | return msalApp.config.auth.clientId 115 | }, 116 | 117 | // 118 | // Login a user with a popup 119 | // 120 | async login(scopes = ['user.read', 'openid', 'profile', 'email']) { 121 | if (!msalApp) { 122 | return 123 | } 124 | 125 | await msalApp.loginPopup({ 126 | scopes, 127 | prompt: 'select_account' 128 | }) 129 | }, 130 | 131 | // 132 | // Logout any stored user 133 | // 134 | logout() { 135 | if (!msalApp) { 136 | return 137 | } 138 | 139 | msalApp.logoutPopup() 140 | }, 141 | 142 | // 143 | // Call to get user, probably cached and stored locally by MSAL 144 | // 145 | user() { 146 | if (!msalApp) { 147 | return null 148 | } 149 | 150 | const currentAccounts = msalApp.getAllAccounts() 151 | if (!currentAccounts || currentAccounts.length === 0) { 152 | // No user signed in 153 | return null 154 | } else if (currentAccounts.length > 1) { 155 | return currentAccounts[0] 156 | } else { 157 | return currentAccounts[0] 158 | } 159 | }, 160 | 161 | // 162 | // Call through to acquireTokenSilent or acquireTokenPopup 163 | // 164 | async acquireToken(scopes = ['user.read']) { 165 | if (!msalApp) { 166 | return null 167 | } 168 | 169 | // Set scopes for token request 170 | const accessTokenRequest = { 171 | scopes, 172 | account: this.user() 173 | } 174 | 175 | let tokenResp 176 | try { 177 | // 1. Try to acquire token silently 178 | tokenResp = await msalApp.acquireTokenSilent(accessTokenRequest) 179 | console.log('### MSAL acquireTokenSilent was successful') 180 | } catch (err) { 181 | // 2. Silent process might have failed so try via popup 182 | tokenResp = await msalApp.acquireTokenPopup(accessTokenRequest) 183 | console.log('### MSAL acquireTokenPopup was successful') 184 | } 185 | 186 | // Just in case check, probably never triggers 187 | if (!tokenResp.accessToken) { 188 | throw new Error("### accessToken not found in response, that's bad") 189 | } 190 | 191 | return tokenResp.accessToken 192 | }, 193 | 194 | // 195 | // Clear any stored/cached user 196 | // 197 | clearLocal() { 198 | if (msalApp) { 199 | for (let entry of Object.entries(localStorage)) { 200 | let key = entry[0] 201 | if (key.includes('login.windows')) { 202 | localStorage.removeItem(key) 203 | } 204 | } 205 | } 206 | }, 207 | 208 | // 209 | // Check if we have been setup & configured 210 | // 211 | isConfigured() { 212 | return msalApp != null 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/services/graph.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Copyright (c) Ben Coleman, 2020 3 | // Licensed under the MIT License. 4 | // 5 | // Set of methods to call the beta Microsoft Graph API, using REST and fetch 6 | // Requires auth.js 7 | // ---------------------------------------------------------------------------- 8 | 9 | import auth from './auth' 10 | 11 | const GRAPH_BASE = 'https://graph.microsoft.com/beta' 12 | const GRAPH_SCOPES = ['user.read', 'user.readbasic.all'] 13 | 14 | let accessToken 15 | 16 | export default { 17 | // 18 | // Get details of user, and return as JSON 19 | // https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#response-1 20 | // 21 | async getSelf() { 22 | let resp = await callGraph('/me') 23 | if (resp) { 24 | let data = await resp.json() 25 | return data 26 | } 27 | }, 28 | 29 | // 30 | // Get user's photo and return as a blob object URL 31 | // https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL 32 | // 33 | async getPhoto() { 34 | let resp = await callGraph('/me/photos/240x240/$value') 35 | if (resp) { 36 | let blob = await resp.blob() 37 | return URL.createObjectURL(blob) 38 | } 39 | }, 40 | 41 | // 42 | // Search for users 43 | // https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL 44 | // 45 | async searchUsers(searchString, max = 50) { 46 | let resp = await callGraph( 47 | `/users?$filter=startswith(displayName, '${searchString}') or startswith(userPrincipalName, '${searchString}')&$top=${max}` 48 | ) 49 | if (resp) { 50 | let data = await resp.json() 51 | return data 52 | } 53 | }, 54 | 55 | // 56 | // Accessor for access token, only included for demo purposes 57 | // 58 | getAccessToken() { 59 | return accessToken 60 | } 61 | } 62 | 63 | // 64 | // Common fetch wrapper (private) 65 | // 66 | async function callGraph(apiPath) { 67 | // Acquire an access token to call APIs (like Graph) 68 | // Safe to call repeatedly as MSAL caches tokens locally 69 | accessToken = await auth.acquireToken(GRAPH_SCOPES) 70 | 71 | let resp = await fetch(`${GRAPH_BASE}${apiPath}`, { 72 | headers: { authorization: `bearer ${accessToken}` } 73 | }) 74 | 75 | if (!resp.ok) { 76 | throw new Error(`Call to ${GRAPH_BASE}${apiPath} failed: ${resp.statusText}`) 77 | } 78 | 79 | return resp 80 | } 81 | --------------------------------------------------------------------------------