Skip to content

Commit cb007fc

Browse files
committed
Added a sample app showing update profile which requires mfa
1 parent 19d6400 commit cb007fc

32 files changed

Lines changed: 3348 additions & 0 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
// See http://go.microsoft.com/fwlink/?LinkId=827846
3+
// for the documentation about the extensions.json format
4+
"recommendations": ["ms-azuretools.ms-entra"]
5+
}
6+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
TENANT_SUBDOMAIN="4ugust" # cloud instance string should end with a trailing slash
2+
CLIENT_ID="675e572a-0493-4518-8464-c3d87466233b"
3+
CLIENT_SECRET="-G98Q~RMOcCK_6VnTiyHGumtwpjB_VPH99Kv-awL"
4+
5+
REDIRECT_URI="http://localhost:3000/auth/redirect"
6+
POST_LOGOUT_REDIRECT_URI="http://localhost:3000"
7+
GRAPH_API_ENDPOINT="https://graph.microsoft.com/" # graph api endpoint string should end with a trailing slash
8+
MFA_PROTECTED_SCOPE="api://84b54cf8-bb6f-4b97-ad6d-5633401b8789/user.mfa"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
require('dotenv').config();
7+
8+
var path = require('path');
9+
var express = require('express');
10+
var session = require('express-session');
11+
var createError = require('http-errors');
12+
var cookieParser = require('cookie-parser');
13+
var logger = require('morgan');
14+
15+
var indexRouter = require('./routes/index');
16+
var usersRouter = require('./routes/users');
17+
var authRouter = require('./routes/auth');
18+
19+
// initialize express
20+
var app = express();
21+
22+
/**
23+
* Using express-session middleware for persistent user session. Be sure to
24+
* familiarize yourself with available options. Visit: https://www.npmjs.com/package/express-session
25+
*/
26+
app.use(
27+
session({
28+
secret: process.env.EXPRESS_SESSION_SECRET || 'Enter_the_Express_Session_Secret_Here',
29+
resave: false,
30+
saveUninitialized: false,
31+
cookie: {
32+
httpOnly: true,
33+
secure: false, // set this to true on production
34+
},
35+
})
36+
);
37+
38+
// view engine setup
39+
app.set('views', path.join(__dirname, 'views'));
40+
app.set('view engine', 'hbs');
41+
42+
app.use(logger('dev'));
43+
app.use(express.json());
44+
app.use(cookieParser());
45+
app.use(express.urlencoded({ extended: false }));
46+
app.use(express.static(path.join(__dirname, 'public')));
47+
48+
app.use('/', indexRouter);
49+
app.use('/users', usersRouter);
50+
app.use('/auth', authRouter);
51+
52+
// catch 404 and forward to error handler
53+
app.use(function (req, res, next) {
54+
next(createError(404));
55+
});
56+
57+
// error handler
58+
app.use(function (err, req, res, next) {
59+
// set locals, only providing error in development
60+
res.locals.message = err.message;
61+
res.locals.error = req.app.get('env') === 'development' ? err : {};
62+
63+
// render the error page
64+
res.status(err.status || 500);
65+
res.render('error');
66+
});
67+
68+
module.exports = app;
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
const msal = require('@azure/msal-node');
2+
const axios = require('axios');
3+
const { msalConfig, TENANT_SUBDOMAIN, REDIRECT_URI, POST_LOGOUT_REDIRECT_URI } = require('../authConfig');
4+
5+
class AuthProvider {
6+
config;
7+
cryptoProvider;
8+
9+
constructor(config) {
10+
this.config = config;
11+
this.cryptoProvider = new msal.CryptoProvider();
12+
}
13+
14+
getMsalInstance(msalConfig) {
15+
return new msal.ConfidentialClientApplication(msalConfig);
16+
}
17+
18+
async login(req, res, next, options = {}) {
19+
// create a GUID for crsf
20+
req.session.csrfToken = this.cryptoProvider.createNewGuid();
21+
22+
/**
23+
* The MSAL Node library allows you to pass your custom state as state parameter in the Request object.
24+
* The state parameter can also be used to encode information of the app's state before redirect.
25+
* You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
26+
*/
27+
const state = this.cryptoProvider.base64Encode(
28+
JSON.stringify({
29+
csrfToken: req.session.csrfToken,
30+
redirectTo: '/',
31+
})
32+
);
33+
34+
const authCodeUrlRequestParams = {
35+
state: state,
36+
37+
/**
38+
* By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit:
39+
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
40+
*/
41+
scopes: options.scopes ?? [],
42+
};
43+
44+
const authCodeRequestParams = {
45+
state: state,
46+
47+
/**
48+
* By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit:
49+
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
50+
*/
51+
scopes: options.scopes ?? [],
52+
};
53+
54+
/**
55+
* If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will
56+
* make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making
57+
* metadata discovery calls, thereby improving performance of token acquisition process.
58+
*/
59+
if (!this.config.msalConfig.auth.authorityMetadata) {
60+
const authorityMetadata = await this.getAuthorityMetadata();
61+
this.config.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
62+
}
63+
64+
const msalInstance = this.getMsalInstance(this.config.msalConfig);
65+
66+
// trigger the first leg of auth code flow
67+
return this.redirectToAuthCodeUrl(
68+
req,
69+
res,
70+
next,
71+
authCodeUrlRequestParams,
72+
authCodeRequestParams,
73+
msalInstance
74+
);
75+
}
76+
77+
async handleRedirect(req, res, next) {
78+
const authCodeRequest = {
79+
...req.session.authCodeRequest,
80+
code: req.body.code, // authZ code
81+
codeVerifier: req.session.pkceCodes.verifier, // PKCE Code Verifier
82+
};
83+
84+
try {
85+
const msalInstance = this.getMsalInstance(this.config.msalConfig);
86+
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
87+
88+
const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
89+
90+
req.session.tokenCache = msalInstance.getTokenCache().serialize();
91+
req.session.accessToken = tokenResponse.accessToken;
92+
req.session.idToken = tokenResponse.idToken;
93+
req.session.account = tokenResponse.account;
94+
req.session.isAuthenticated = true;
95+
96+
const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state));
97+
res.redirect(state.redirectTo);
98+
} catch (error) {
99+
next(error);
100+
}
101+
}
102+
103+
/**
104+
*
105+
* @param req: Express request object
106+
* @param res: Express response object
107+
* @param next: Express next function
108+
* @param scopes: Array of strings
109+
* @param redirectUri: redirect Url
110+
*/
111+
getToken(scopes, redirectUri = "http://localhost:3000/") {
112+
return async function (req, res, next) {
113+
console.log(scopes);
114+
const msalInstance = authProvider.getMsalInstance(authProvider.config.msalConfig);
115+
try {
116+
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
117+
118+
const silentRequest = {
119+
account: req.session.account,
120+
scopes: scopes,
121+
};
122+
123+
const tokenResponse = await msalInstance.acquireTokenSilent(silentRequest);
124+
125+
req.session.tokenCache = msalInstance.getTokenCache().serialize();
126+
req.session.accessToken = tokenResponse.accessToken;
127+
next();
128+
} catch (error) {
129+
if (error instanceof msal.InteractionRequiredAuthError) {
130+
req.session.csrfToken = authProvider.cryptoProvider.createNewGuid();
131+
132+
const state = authProvider.cryptoProvider.base64Encode(
133+
JSON.stringify({
134+
redirectTo: 'http://localhost:3000/users/updateProfile',
135+
csrfToken: req.session.csrfToken,
136+
})
137+
);
138+
139+
const authCodeUrlRequestParams = {
140+
state: state,
141+
scopes: scopes,
142+
};
143+
144+
const authCodeRequestParams = {
145+
state: state,
146+
scopes: scopes,
147+
};
148+
149+
authProvider.redirectToAuthCodeUrl(
150+
req,
151+
res,
152+
next,
153+
authCodeUrlRequestParams,
154+
authCodeRequestParams,
155+
msalInstance
156+
);
157+
}
158+
159+
next(error);
160+
}
161+
};
162+
}
163+
164+
async logout(req, res, next) {
165+
/**
166+
* Construct a logout URI and redirect the user to end the
167+
* session with Azure AD. For more information, visit:
168+
* https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
169+
*/
170+
const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
171+
172+
req.session.destroy(() => {
173+
res.redirect(logoutUri);
174+
});
175+
}
176+
177+
/**
178+
* Prepares the auth code request parameters and initiates the first leg of auth code flow
179+
* @param req: Express request object
180+
* @param res: Express response object
181+
* @param next: Express next function
182+
* @param authCodeUrlRequestParams: parameters for requesting an auth code url
183+
* @param authCodeRequestParams: parameters for requesting tokens using auth code
184+
*/
185+
async redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
186+
// Generate PKCE Codes before starting the authorization flow
187+
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
188+
189+
// Set generated PKCE codes and method as session vars
190+
req.session.pkceCodes = {
191+
challengeMethod: 'S256',
192+
verifier: verifier,
193+
challenge: challenge,
194+
};
195+
196+
/**
197+
* By manipulating the request objects below before each request, we can obtain
198+
* auth artifacts with desired claims. For more information, visit:
199+
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest
200+
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationcoderequest
201+
**/
202+
203+
req.session.authCodeUrlRequest = {
204+
...authCodeUrlRequestParams,
205+
redirectUri: this.config.redirectUri,
206+
responseMode: 'form_post', // recommended for confidential clients
207+
codeChallenge: req.session.pkceCodes.challenge,
208+
codeChallengeMethod: req.session.pkceCodes.challengeMethod,
209+
};
210+
211+
req.session.authCodeRequest = {
212+
...authCodeRequestParams,
213+
redirectUri: this.config.redirectUri,
214+
code: '',
215+
};
216+
217+
try {
218+
const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
219+
res.redirect(authCodeUrlResponse);
220+
} catch (error) {
221+
next(error);
222+
}
223+
}
224+
225+
/**
226+
* Retrieves oidc metadata from the openid endpoint
227+
* @returns
228+
*/
229+
async getAuthorityMetadata() {
230+
const endpoint = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/v2.0/.well-known/openid-configuration`;
231+
try {
232+
const response = await axios.get(endpoint);
233+
return await response.data;
234+
} catch (error) {
235+
console.log(error);
236+
}
237+
}
238+
}
239+
240+
const authProvider = new AuthProvider({
241+
msalConfig: msalConfig,
242+
redirectUri: REDIRECT_URI,
243+
postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI,
244+
});
245+
246+
module.exports = authProvider;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
require('dotenv').config({ path: '.env.dev' });
7+
8+
const TENANT_SUBDOMAIN = process.env.TENANT_SUBDOMAIN || 'Enter_the_Tenant_Subdomain_Here';
9+
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/auth/redirect';
10+
const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI || 'http://localhost:3000';
11+
12+
/**
13+
* Configuration object to be passed to MSAL instance on creation.
14+
* For a full list of MSAL Node configuration parameters, visit:
15+
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
16+
*/
17+
const msalConfig = {
18+
auth: {
19+
clientId: process.env.CLIENT_ID || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
20+
authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // Replace the placeholder with your tenant name
21+
clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app registration in Azure portal
22+
},
23+
system: {
24+
loggerOptions: {
25+
loggerCallback(loglevel, message, containsPii) {
26+
console.log(message);
27+
},
28+
piiLoggingEnabled: false,
29+
logLevel: 'Info',
30+
},
31+
},
32+
};
33+
34+
const GRAPH_API_ENDPOINT = process.env.GRAPH_API_ENDPOINT || "graph_end_point";
35+
// Refers to the user that is single user singed in.
36+
// https://learn.microsoft.com/en-us/graph/api/user-update?view=graph-rest-1.0&tabs=http
37+
const GRAPH_ME_ENDPOINT = GRAPH_API_ENDPOINT + "v1.0/me";
38+
39+
const mfaProtectedResourceScope = process.env.MFA_PROTECTED_SCOPE || 'Add_your_protected_scope_here';
40+
41+
module.exports = {
42+
msalConfig,
43+
mfaProtectedResourceScope,
44+
REDIRECT_URI,
45+
POST_LOGOUT_REDIRECT_URI,
46+
TENANT_SUBDOMAIN,
47+
GRAPH_API_ENDPOINT,
48+
GRAPH_ME_ENDPOINT,
49+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const authProvider = require('../auth/AuthProvider');
2+
3+
exports.signIn = async (req, res, next) => {
4+
return authProvider.login(req, res, next, {scopes:["User.Read"]});
5+
};
6+
7+
exports.handleRedirect = async (req, res, next) => {
8+
return authProvider.handleRedirect(req, res, next);
9+
}
10+
11+
exports.signOut = async (req, res, next) => {
12+
return authProvider.logout(req, res, next);
13+
};

0 commit comments

Comments
 (0)