Skip to content

Commit 0dd3890

Browse files
author
derisen
committed
adding ch1 about the code
1 parent 630f51d commit 0dd3890

12 files changed

Lines changed: 701 additions & 15 deletions

File tree

1-Authentication/1-sign-in/App/routes/router.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ router.get('/signin', msal.signIn);
2020
router.get('/signout', msal.signOut);
2121
router.get('/redirect', msal.handleRedirect);
2222

23-
// protected routes
23+
// authenticated routes
2424
router.get('/id', msal.isAuthenticated, mainController.getIdPage);
2525

2626
// 404

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

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,15 +170,164 @@ Were we successful in addressing your learning objective? Consider taking a mome
170170

171171
## About the code
172172

173-
### Configuration
173+
## Initialization
174+
175+
`MsalNodeWrapper` class is initialized in the [routes/router.js](./App/routes/router.js). It expects two parameters, a JSON configuration object (see [auth.json](./auth.json)), and an optional cache plug-in (see [cachePlugin.js](./App/utils/cachePlugin.js)) if you wish to save your cache to disk. Otherwise, in-memory only cache is used.
176+
177+
Once initialized, `MsalNodeWrapper` middleware can be used in routes:
178+
179+
```javascript
180+
const express = require('express');
181+
182+
const msal = new MsalNodeWrapper(config, cache);
183+
184+
// initialize router
185+
const router = express.Router();
186+
187+
router.get('/signin', msal.signIn);
188+
router.get('/signout', msal.signOut);
189+
router.get('/redirect', msal.handleRedirect);
190+
```
191+
192+
Under the hood, the wrapper creates an **MSAL Node** [configuration object](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md) and initializes [msal.ConfidentialClientApplication](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/client/ConfidentialClientApplication.ts) by passing it.
193+
194+
```javascript
195+
constructor(config, cache = null) {
196+
MsalNodeWrapper.validateConfiguration(config);
197+
198+
this.rawConfig = config;
199+
this.msalConfig = MsalNodeWrapper.shapeConfiguration(config, cache);
200+
this.msalClient = new msal.ConfidentialClientApplication(this.msalConfig);
201+
};
202+
```
174203

175204
### Sign-in
176205

177-
### ID Token validation
206+
The user clicks on the **sign-in** button and routes to `/signin`. `msal.signIn` middleware takes over. First it creates session variables:
207+
208+
```javascript
209+
signIn = (req, res, next) => {
210+
211+
if (!req.session['authCodeRequest']) {
212+
req.session.authCodeRequest = {
213+
authority: "",
214+
scopes: [],
215+
state: {},
216+
redirectUri: ""
217+
};
218+
}
219+
220+
if (!req.session['tokenRequest']) {
221+
req.session.tokenRequest = {
222+
authority: "",
223+
scopes: [],
224+
state: {},
225+
redirectUri: ""
226+
};
227+
}
228+
229+
// current account id
230+
req.session.homeAccountId = "";
231+
```
232+
233+
Then, creates and encodes a state object to pass with an authorization code request. The object is passed to the `state` parameter as a means of controlling the application flow. For more information, see [Pass custom state in authentication requests using MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request).
234+
235+
```javascript
236+
// sign-in as usual
237+
let state = CryptoUtilities.base64EncodeUrl(
238+
JSON.stringify({
239+
stage: constants.AppStages.SIGN_IN,
240+
path: req.route.path,
241+
nonce: req.session.nonce
242+
}));
243+
244+
// get url to sign user in (and consent to scopes needed for application)
245+
this.getAuthCode(
246+
this.msalConfig.auth.authority,
247+
Object.values(constants.OIDCScopes), // pass standard openid scopes as permissions
248+
state,
249+
this.msalConfig.auth.redirectUri,
250+
req, // express request object
251+
res // express response object
252+
);
253+
```
254+
255+
Under the hood, `getAuthCode()` assigns request parameters to session, and calls the **MSAL Node** `getAuthCodeUrl()` API
256+
257+
```javascript
258+
const response = await this.msalClient.getAuthCodeUrl(req.session.authCodeRequest);
259+
```
260+
261+
### Get a token
262+
263+
After making an authorization code URL request, the user is redirected to the redirect route defined in the **Azure AD** app registration. Once redirected, the `handleRedirect` middleware takes over. It first checks for `nonce` parameter in state against *cross-site resource forgery* (csrf) attacks, and then for the current app stage. Then, using the `code` in query parameters, tokens are requested using the **MSAL Node** `acquireTokenByCode()` API, and the response is appended to the *express-session** variable.
264+
265+
```javascript
266+
handleRedirect = async(req, res, next) => {
267+
//...
268+
269+
if (state.nonce === req.session.nonce) {
270+
if (state.stage === constants.AppStages.SIGN_IN) {
271+
272+
// token request should have auth code
273+
const tokenRequest = {
274+
redirectUri: this.msalConfig.auth.redirectUri,
275+
scopes: Object.keys(constants.OIDCScopes),
276+
code: req.query.code,
277+
};
278+
279+
try {
280+
// exchange auth code for tokens
281+
const tokenResponse = await this.msalClient.acquireTokenByCode(tokenRequest)
282+
console.log("\nResponse: \n:", tokenResponse);
283+
284+
if (this.validateIdToken(tokenResponse.idTokenClaims)) {
285+
286+
req.session.homeAccountId = tokenResponse.account.homeAccountId;
287+
288+
// assign session variables
289+
req.session.idTokenClaims = tokenResponse.idTokenClaims;
290+
req.session.isAuthenticated = true;
291+
292+
return res.status(200).redirect(this.rawConfig.configuration.homePageRoute);
293+
} else {
294+
console.log('invalid token');
295+
return res.status(401).send("Not Permitted");
296+
}
297+
298+
//...
299+
}
300+
```
301+
302+
### ID token validation
303+
304+
Web apps (and confidential client apps in general) should validate ID Tokens. **MSAL Node** decodes the ID token. In `MsalNodeWrapper`, we add the ID token to session, and then validate it:
305+
306+
```javascript
307+
const checkAudience = idTokenClaims["aud"] === this.msalConfig.auth.clientId ? true : false;
308+
const checkTimestamp = idTokenClaims["iat"] <= now && idTokenClaims["exp"] >= now ? true : false;
309+
const checkTenant = (this.rawConfig.hasOwnProperty('policies') && !idTokenClaims["tid"]) || idTokenClaims["tid"] === this.rawConfig.credentials.tenantId ? true : false;
310+
311+
return checkAudience && checkTimestamp && checkTenant;
312+
```
313+
314+
ID token validation should be validated according to the guide [ID Token validation](https://docs.microsoft.com/azure/active-directory/develop/id-tokens#validating-an-id_token). Implementation can vary, and it is the app developers responsibility.
178315
179316
### Sign-out
180317
181-
### National Clouds
318+
We construct a logout URL following the [guide here](https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request). Then, we destroy the current **express-session** and redirect the user to **sign-out endpoint**.
319+
320+
```javascript
321+
signOut = (req, res, next) => {
322+
const logoutURI = '<your-tenanted-logout-url>';
323+
324+
req.session.isAuthenticated = false;
325+
326+
req.session.destroy(() => {
327+
res.redirect(logoutURI);
328+
});
329+
};
330+
```
182331
183332
## Next Tutorial
184333

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

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,164 @@ Were we successful in addressing your learning objective? Consider taking a mome
174174

175175
## About the code
176176

177-
## Configuration
177+
## Initialization
178+
179+
`MsalNodeWrapper` class is initialized in the [routes/router.js](./App/routes/router.js). It expects two parameters, a JSON configuration object (see [auth.json](./auth.json)), and an optional cache plug-in (see [cachePlugin.js](./App/utils/cachePlugin.js)) if you wish to save your cache to disk. Otherwise, in-memory only cache is used.
180+
181+
Once initialized, `MsalNodeWrapper` middleware can be used in routes:
182+
183+
```javascript
184+
const express = require('express');
185+
186+
const msal = new MsalNodeWrapper(config, cache);
187+
188+
// initialize router
189+
const router = express.Router();
190+
191+
router.get('/signin', msal.signIn);
192+
router.get('/signout', msal.signOut);
193+
router.get('/redirect', msal.handleRedirect);
194+
```
195+
196+
Under the hood, the wrapper creates an **MSAL Node** [configuration object](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md) and initializes [msal.ConfidentialClientApplication](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/client/ConfidentialClientApplication.ts) by passing it.
197+
198+
```javascript
199+
constructor(config, cache = null) {
200+
MsalNodeWrapper.validateConfiguration(config);
201+
202+
this.rawConfig = config;
203+
this.msalConfig = MsalNodeWrapper.shapeConfiguration(config, cache);
204+
this.msalClient = new msal.ConfidentialClientApplication(this.msalConfig);
205+
};
206+
```
178207

179208
### Sign-in
180209

181-
### ID Token validation
210+
The user clicks on the **sign-in** button and routes to `/signin`. `msal.signIn` middleware takes over. First it creates session variables:
211+
212+
```javascript
213+
signIn = (req, res, next) => {
214+
215+
if (!req.session['authCodeRequest']) {
216+
req.session.authCodeRequest = {
217+
authority: "",
218+
scopes: [],
219+
state: {},
220+
redirectUri: ""
221+
};
222+
}
223+
224+
if (!req.session['tokenRequest']) {
225+
req.session.tokenRequest = {
226+
authority: "",
227+
scopes: [],
228+
state: {},
229+
redirectUri: ""
230+
};
231+
}
232+
233+
// current account id
234+
req.session.homeAccountId = "";
235+
```
236+
237+
Then, creates and encodes a state object to pass with an authorization code request. The object is passed to the `state` parameter as a means of controlling the application flow. For more information, see [Pass custom state in authentication requests using MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request).
238+
239+
```javascript
240+
// sign-in as usual
241+
let state = CryptoUtilities.base64EncodeUrl(
242+
JSON.stringify({
243+
stage: constants.AppStages.SIGN_IN,
244+
path: req.route.path,
245+
nonce: req.session.nonce
246+
}));
247+
248+
// get url to sign user in (and consent to scopes needed for application)
249+
this.getAuthCode(
250+
this.msalConfig.auth.authority,
251+
Object.values(constants.OIDCScopes), // pass standard openid scopes as permissions
252+
state,
253+
this.msalConfig.auth.redirectUri,
254+
req, // express request object
255+
res // express response object
256+
);
257+
```
258+
259+
Under the hood, `getAuthCode()` assigns request parameters to session, and calls the **MSAL Node** `getAuthCodeUrl()` API
260+
261+
```javascript
262+
const response = await this.msalClient.getAuthCodeUrl(req.session.authCodeRequest);
263+
```
264+
265+
### Get a token
266+
267+
After making an authorization code URL request, the user is redirected to the redirect route defined in the **Azure AD** app registration. Once redirected, the `handleRedirect` middleware takes over. It first checks for `nonce` parameter in state against *cross-site resource forgery* (csrf) attacks, and then for the current app stage. Then, using the `code` in query parameters, tokens are requested using the **MSAL Node** `acquireTokenByCode()` API, and the response is appended to the *express-session** variable.
268+
269+
```javascript
270+
handleRedirect = async(req, res, next) => {
271+
//...
272+
273+
if (state.nonce === req.session.nonce) {
274+
if (state.stage === constants.AppStages.SIGN_IN) {
275+
276+
// token request should have auth code
277+
const tokenRequest = {
278+
redirectUri: this.msalConfig.auth.redirectUri,
279+
scopes: Object.keys(constants.OIDCScopes),
280+
code: req.query.code,
281+
};
282+
283+
try {
284+
// exchange auth code for tokens
285+
const tokenResponse = await this.msalClient.acquireTokenByCode(tokenRequest)
286+
console.log("\nResponse: \n:", tokenResponse);
287+
288+
if (this.validateIdToken(tokenResponse.idTokenClaims)) {
289+
290+
req.session.homeAccountId = tokenResponse.account.homeAccountId;
291+
292+
// assign session variables
293+
req.session.idTokenClaims = tokenResponse.idTokenClaims;
294+
req.session.isAuthenticated = true;
295+
296+
return res.status(200).redirect(this.rawConfig.configuration.homePageRoute);
297+
} else {
298+
console.log('invalid token');
299+
return res.status(401).send("Not Permitted");
300+
}
301+
302+
//...
303+
}
304+
```
305+
306+
### ID token validation
307+
308+
Web apps (and confidential client apps in general) should validate ID Tokens. **MSAL Node** decodes the ID token. In `MsalNodeWrapper`, we add the ID token to session, and then validate it:
309+
310+
```javascript
311+
const checkAudience = idTokenClaims["aud"] === this.msalConfig.auth.clientId ? true : false;
312+
const checkTimestamp = idTokenClaims["iat"] <= now && idTokenClaims["exp"] >= now ? true : false;
313+
const checkTenant = (this.rawConfig.hasOwnProperty('policies') && !idTokenClaims["tid"]) || idTokenClaims["tid"] === this.rawConfig.credentials.tenantId ? true : false;
314+
315+
return checkAudience && checkTimestamp && checkTenant;
316+
```
317+
318+
ID token validation should be validated according to the guide [ID Token validation](https://docs.microsoft.com/azure/active-directory/develop/id-tokens#validating-an-id_token). Implementation can vary, and it is the app developers responsibility.
182319
183320
### Sign-out
184321
185-
### National Clouds
322+
We construct a logout URL following the [guide here](https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request). Then, we destroy the current **express-session** and redirect the user to **sign-out endpoint**.
323+
324+
```javascript
325+
signOut = (req, res, next) => {
326+
const logoutURI = '<your-tenanted-logout-url>';
327+
328+
req.session.isAuthenticated = false;
329+
330+
req.session.destroy(() => {
331+
res.redirect(logoutURI);
332+
});
333+
};
334+
```
186335
187336
## More information
188337

1-Authentication/2-sign-in-b2c/App/routes/router.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ router.get('/signin', msal.signIn);
2020
router.get('/signout', msal.signOut);
2121
router.get('/redirect', msal.handleRedirect);
2222

23-
// protected routes
23+
// authorized routes
2424
router.get('/id', msal.isAuthenticated, mainController.getIdPage); // get token for this route to call web API
2525

2626
// 404

0 commit comments

Comments
 (0)