Skip to content

Commit 99ca747

Browse files
committed
add about the code sections
1 parent f2a97e6 commit 99ca747

10 files changed

Lines changed: 995 additions & 148 deletions

File tree

1-Authentication/1-sign-in/README.md

Lines changed: 348 additions & 20 deletions
Large diffs are not rendered by default.

1-Authentication/2-sign-in-b2c/README.md

Lines changed: 348 additions & 20 deletions
Large diffs are not rendered by default.

2-Authorization/1-call-graph/README.md

Lines changed: 162 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ This sample also demonstrates how to use the [Microsoft Graph JavaScript SDK](ht
3535
|-----------------------------|---------------------------------------------------------------|
3636
| `AppCreationScripts/` | Contains Powershell scripts to automate app registration. |
3737
| `ReadmeFiles/` | Contains illustrations and screenshots. |
38-
| `App/appSettings.json` | Authentication parameters and settings |
38+
| `App/appSettings.js` | Authentication parameters and settings |
3939
| `App/cache.json` | Stores MSAL Node token cache data. |
4040
| `App/app.js` | Application entry point. |
4141
| `App/utils/graphManager.js` | Handles calls to Microsoft Graph using Graph JS SDK. |
@@ -148,7 +148,7 @@ Open the project in your IDE (like Visual Studio or Visual Studio Code) to confi
148148

149149
> In the steps below, "ClientID" is the same as "Application ID" or "AppId".
150150
151-
1. Open the `App/appSettings.json` file.
151+
1. Open the `App/appSettings.js` file.
152152
1. Find the key `clientId` and replace the existing value with the application ID (clientId) of `msal-node-webapp` app copied from the Azure portal.
153153
1. Find the key `tenantId` and replace the existing value with your Azure AD tenant ID.
154154
1. Find the key `clientSecret` and replace the existing value with the key you saved during the creation of `msal-node-webapp` copied from the Azure portal.
@@ -210,19 +210,176 @@ Scopes can come in various forms so it pays off to be familiar with them. The fo
210210
### Acquiring an access token
211211

212212
```javascript
213+
const express = require('express');
214+
const session = require('express-session');
215+
const msalWrapper = require('msal-express-wrapper');
216+
217+
// initialize express
218+
const app = express();
219+
220+
app.use(session({
221+
secret: 'ENTER_YOUR_SECRET_HERE',
222+
resave: false,
223+
saveUninitialized: false,
224+
cookie: {
225+
secure: false, // set this to true on production
226+
}
227+
}));
228+
229+
// instantiate the wrapper
230+
const authProvider = new msalWrapper.AuthProvider(config, cache);
213231

232+
// initialize the wrapper
233+
app.use(authProvider.initialize());
234+
235+
// authentication routes
236+
app.get('/signin',
237+
authProvider.signIn({
238+
successRedirect: '/'
239+
}
240+
));
241+
242+
app.get('/signout',
243+
authProvider.signOut({
244+
successRedirect: '/'
245+
}
246+
));
247+
248+
app.get('/profile',
249+
authProvider.getToken({
250+
resource: config.remoteResources.graphAPI
251+
}),
252+
mainController.getProfilePage
253+
);
254+
255+
app.get('/tenant',
256+
authProvider.getToken({
257+
resource: config.remoteResources.armAPI
258+
}),
259+
mainController.getTenantPage
260+
);
261+
262+
app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`));
214263
```
215264

216-
Under the hood, the `getToken` middleware grabs resource endpoint and associated scope from [appSettings.json](./App/appSettings.json), and attempts to obtain an access token from cache silently and attaches it to session. If silent token acquisition fails for some reason (e.g. consent required), it makes an auth code request, which triggers the first leg of auth code flow.
265+
Under the hood, the [getToken()](https://azure-samples.github.io/msal-express-wrapper/classes/authprovider.html#gettoken) middleware grabs resource endpoint and associated scope from [appSettings.js](./App/appSettings.js), and attempts to obtain an access token from cache silently and attaches it to session. If silent token acquisition fails for some reason (e.g. consent required), it makes an auth code request, which triggers the first leg of auth code flow.
217266

218267
```typescript
268+
getToken = (options: TokenRequestOptions): RequestHandler => {
269+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
270+
// get scopes for token request
271+
const scopes = options.resource.scopes;
272+
273+
const resourceName = this.getResourceNameFromScopes(scopes)
219274

275+
if (!req.session.remoteResources) {
276+
req.session.remoteResources = {};
277+
}
278+
279+
req.session.remoteResources = {
280+
[resourceName]: {
281+
...this.appSettings.remoteResources[resourceName],
282+
accessToken: null,
283+
} as Resource
284+
};
285+
286+
try {
287+
const silentRequest: SilentFlowRequest = {
288+
account: req.session.account,
289+
scopes: scopes,
290+
};
291+
292+
// acquire token silently to be used in resource call
293+
const tokenResponse = await this.msalClient.acquireTokenSilent(silentRequest);
294+
console.log("\nSuccessful silent token acquisition:\n Response: \n:", tokenResponse);
295+
296+
// In B2C scenarios, sometimes an access token is returned empty.
297+
// In that case, we will acquire token interactively instead.
298+
if (StringUtils.isEmpty(tokenResponse.accessToken)) {
299+
console.log(ErrorMessages.TOKEN_NOT_FOUND);
300+
throw new InteractionRequiredAuthError(ErrorMessages.INTERACTION_REQUIRED);
301+
}
302+
303+
req.session.remoteResources[resourceName].accessToken = tokenResponse.accessToken;
304+
next();
305+
} catch (error) {
306+
// in case there are no cached tokens, initiate an interactive call
307+
if (error instanceof InteractionRequiredAuthError) {
308+
const state = this.cryptoProvider.base64Encode(
309+
JSON.stringify({
310+
stage: AppStages.ACQUIRE_TOKEN,
311+
path: req.route.path,
312+
nonce: req.session.nonce,
313+
})
314+
);
315+
316+
const params: AuthCodeParams = {
317+
authority: this.msalConfig.auth.authority,
318+
scopes: scopes,
319+
state: state,
320+
redirect: UrlUtils.ensureAbsoluteUrl(req, this.appSettings.authRoutes.redirect),
321+
account: req.session.account,
322+
};
323+
324+
// get an auth code url and initiate the first leg of auth code grant to get token
325+
return this.getAuthCode(req, res, next, params);
326+
} else {
327+
console.log(error);
328+
next(error);
329+
}
330+
}
331+
}
332+
};
220333
```
221334

222-
In the second leg of auth code flow, the auth code from redirect response is used to request a new access token (and refresh token) via the `handleRedirect` middleware.
335+
In the second leg of auth code flow, the auth code from redirect response is used to request a new access token (and refresh token) via the [handleRedirect](https://azure-samples.github.io/msal-express-wrapper/classes/authprovider.html#handleredirect) middleware.
223336

224337
```typescript
225-
338+
private handleRedirect = (options?: HandleRedirectOptions): RequestHandler => {
339+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
340+
if (req.query.state) {
341+
const state = JSON.parse(this.cryptoProvider.base64Decode(req.query.state as string));
342+
343+
// check if nonce matches
344+
if (state.nonce === req.session.nonce) {
345+
switch (state.stage) {
346+
347+
// ...
348+
349+
case AppStages.ACQUIRE_TOKEN: {
350+
// get the name of the resource associated with scope
351+
const resourceName = this.getResourceNameFromScopes(req.session.tokenRequest.scopes);
352+
353+
req.session.tokenRequest.code = req.query.code as string
354+
355+
try {
356+
const tokenResponse = await this.msalClient.acquireTokenByCode(req.session.tokenRequest);
357+
console.log("\nResponse: \n:", tokenResponse);
358+
req.session.remoteResources[resourceName].accessToken = tokenResponse.accessToken;
359+
res.redirect(state.path);
360+
} catch (error) {
361+
console.log(ErrorMessages.TOKEN_ACQUISITION_FAILED);
362+
console.log(error);
363+
next(error);
364+
}
365+
break;
366+
}
367+
368+
default:
369+
console.log(ErrorMessages.CANNOT_DETERMINE_APP_STAGE);
370+
res.redirect(this.appSettings.authRoutes.error);
371+
break;
372+
}
373+
} else {
374+
console.log(ErrorMessages.NONCE_MISMATCH);
375+
res.redirect(this.appSettings.authRoutes.unauthorized);
376+
}
377+
} else {
378+
console.log(ErrorMessages.STATE_NOT_FOUND)
379+
res.redirect(this.appSettings.authRoutes.unauthorized);
380+
}
381+
}
382+
};
226383
```
227384

228385
### Access Token validation

3-Deployment/App/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async function main() {
3131

3232
/**
3333
* Using express-session middleware. Be sure to familiarize yourself with available options
34-
* and set the desired options. Visit: https://www.npmjs.com/package/express-session
34+
* and set them as desired. Visit: https://www.npmjs.com/package/express-session
3535
*/
3636
const sessionConfig = {
3737
secret: 'ENTER_YOUR_SECRET_HERE',

3-Deployment/README.md

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Finally, you need to add a few environment variables to the App Service where yo
151151
1. Add the following variables (name-value):
152152
1. **KEY_VAULT_URI**: the name of the key vault you've created, e.g. `example-key-vault`
153153
1. **SECRET_NAME**: the name of the certificate you specified when importing it to key vault, e.g. `ExampleSecret`
154-
1. **NODE_ENV**: enter `production`
154+
1. **NODE_ENV**: enter `production` (:information_source: this enables your application to set cookies to secure and trust App service proxy)
155155

156156
Wait for a few minutes for your changes on **App Service** to take effect. You should then be able to visit your published website and sign-in accordingly.
157157

@@ -172,10 +172,128 @@ Were we successful in addressing your learning objective? Consider taking a mome
172172

173173
### Accessing Key Vault using Managed Identity
174174

175+
In [appSettings.js](./App/appSettings.js) file, we enter the parameters needed for accessing [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/basic-concepts) to fetch the application's credentials:
176+
177+
```javascript
178+
const appSettings = {
179+
appCredentials: {
180+
clientId: "Enter_the_Application_Id_Here",
181+
tenantId: "Enter_the_Tenant_Info_Here",
182+
keyVaultCredential: {
183+
credentialType: "secret", // credential type: secret | certificate
184+
credentialName: process.env["SECRET_NAME"], // you may enter your secret's name manually for local development
185+
keyVaultUrl: process.env["KEY_VAULT_URI"] // you may enter your key vault uri manually for local development
186+
}
187+
},
188+
authRoutes: {
189+
redirect: "/redirect",
190+
error: "/error", // the wrapper will redirect to this route in case of any error
191+
unauthorized: "/unauthorized" // the wrapper will redirect to this route in case of unauthorized access attempt
192+
},
193+
remoteResources: {
194+
graphAPI: {
195+
endpoint: "https://graph.microsoft.com/v1.0/me",
196+
scopes: ["user.read"]
197+
},
198+
armAPI: {
199+
endpoint: "https://management.azure.com/tenants?api-version=2020-01-01",
200+
scopes: ["https://management.azure.com/user_impersonation"]
201+
}
202+
}
203+
}
204+
```
205+
206+
Then in [app.js](./App/app.js), we instantiate an **authProvider** object asynchronously using the [buildAsync](https://azure-samples.github.io/msal-express-wrapper/classes/authprovider.html#buildasync) method of [AuthProvider](https://azure-samples.github.io/msal-express-wrapper/classes/authprovider.html). To do so, we need to start the express server asynchronously:
207+
175208
```javascript
209+
const express = require('express');
210+
const session = require('express-session');
211+
const msalWrapper = require('msal-express-wrapper');
212+
213+
// async function to wait for key vault credentials before start
214+
async function main() {
215+
const app = express();
216+
217+
/**
218+
* Using express-session middleware. Be sure to familiarize yourself with available options
219+
* and set them as desired. Visit: https://www.npmjs.com/package/express-session
220+
*/
221+
app.use(session({
222+
secret: 'ENTER_YOUR_SECRET_HERE',
223+
resave: false,
224+
saveUninitialized: false,
225+
cookie: {
226+
secure: false, // set true on production
227+
}
228+
}));
229+
230+
// fetching credentials from key vault
231+
const authProvider = await msalWrapper.AuthProvider.buildAsync(settings, cache);
232+
233+
app.use(authProvider.initialize());
234+
235+
// pass authProvider instance to your routers
236+
app.use(mainRouter(authProvider));
237+
238+
app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`));
239+
}
240+
241+
main();
176242
```
177243

244+
Under the hood, the wrapper calls the **Azure Key Vault** to access credentials needed for the application to authenticate with Azure AD using the [KeyVaultManager](https://azure-samples.github.io/msal-express-wrapper/classes/keyvaultmanager.html) class. This class is leveraging the [@azure/identity](https://www.npmjs.com/package/@azure/identity) and [@azure/key-vault](https://www.npmjs.com/package/@azure/keyvault-secrets) packages:
245+
178246
```typescript
247+
import { CertificateClient, KeyVaultCertificate } from "@azure/keyvault-certificates";
248+
import { DefaultAzureCredential } from "@azure/identity";
249+
import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets";
250+
251+
export class KeyVaultManager {
252+
253+
// updates appSettings object with credentials from key vault
254+
async getCredentialFromKeyVault(config: AppSettings): Promise<AppSettings> {
255+
256+
const credential = new DefaultAzureCredential();
257+
258+
if (!config.appCredentials.keyVaultCredential) {
259+
return config;
260+
}
261+
262+
switch (config.appCredentials.keyVaultCredential.credentialType) {
263+
case KeyVaultCredentialTypes.SECRET: {
264+
try {
265+
const secretResponse = await this.getSecretCredential(config, credential);
266+
config.appCredentials.clientSecret = secretResponse.value;
267+
return config;
268+
} catch (error) {
269+
console.log(error);
270+
}
271+
break;
272+
}
273+
274+
// ...
275+
276+
default:
277+
break;
278+
}
279+
};
280+
281+
// ...
282+
283+
async getSecretCredential(config: AppSettings, credential: DefaultAzureCredential): Promise<KeyVaultSecret> {
284+
285+
// instantiate @azure/key-vault-secrets secret client
286+
const secretClient = new SecretClient(config.appCredentials.keyVaultCredential.keyVaultUrl, credential);
287+
288+
try {
289+
const keyVaultSecret = await secretClient.getSecret(config.appCredentials.keyVaultCredential.credentialName);
290+
return keyVaultSecret;
291+
} catch (error) {
292+
console.log(error);
293+
return error;
294+
}
295+
}
296+
}
179297
```
180298

181299
## More information

4-AccessControl/1-app-roles/App/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const methodOverride = require('method-override');
99
const path = require('path');
1010

1111
const msalWrapper = require('msal-express-wrapper');
12+
1213
const config = require('./appSettings.js');
1314
const cache = require('./utils/cachePlugin');
1415
const mainRouter = require('./routes/mainRoutes');

0 commit comments

Comments
 (0)