Skip to content

This sample app performs 2 things #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["ms-azuretools.ms-entra"]
}

68 changes: 68 additions & 0 deletions 1-Authentication/7-sign-in-express-mfa/App/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

require('dotenv').config();

var path = require('path');
var express = require('express');
var session = require('express-session');
var createError = require('http-errors');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var authRouter = require('./routes/auth');

// initialize express
var app = express();

/**
* Using express-session middleware for persistent user session. Be sure to
* familiarize yourself with available options. Visit: https://www.npmjs.com/package/express-session
*/
app.use(
session({
secret: process.env.EXPRESS_SESSION_SECRET || 'Enter_the_Express_Session_Secret_Here',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false, // set this to true on production
},
})
);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');

app.use(logger('dev'));
app.use(express.json());
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/auth', authRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;
246 changes: 246 additions & 0 deletions 1-Authentication/7-sign-in-express-mfa/App/auth/AuthProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
const msal = require('@azure/msal-node');
const axios = require('axios');
const { msalConfig, TENANT_SUBDOMAIN, REDIRECT_URI, POST_LOGOUT_REDIRECT_URI } = require('../authConfig');

class AuthProvider {
config;
cryptoProvider;

constructor(config) {
this.config = config;
this.cryptoProvider = new msal.CryptoProvider();
}

getMsalInstance(msalConfig) {
return new msal.ConfidentialClientApplication(msalConfig);
}

async login(req, res, next, options = {}) {
// create a GUID for crsf
req.session.csrfToken = this.cryptoProvider.createNewGuid();

/**
* The MSAL Node library allows you to pass your custom state as state parameter in the Request object.
* The state parameter can also be used to encode information of the app's state before redirect.
* You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
*/
const state = this.cryptoProvider.base64Encode(
JSON.stringify({
csrfToken: req.session.csrfToken,
redirectTo: '/',
})
);

const authCodeUrlRequestParams = {
state: state,

/**
* By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
scopes: options.scopes ?? [],
};

const authCodeRequestParams = {
state: state,

/**
* By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
scopes: options.scopes ?? [],
};

/**
* If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will
* make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making
* metadata discovery calls, thereby improving performance of token acquisition process.
*/
if (!this.config.msalConfig.auth.authorityMetadata) {
const authorityMetadata = await this.getAuthorityMetadata();
this.config.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
}

const msalInstance = this.getMsalInstance(this.config.msalConfig);

// trigger the first leg of auth code flow
return this.redirectToAuthCodeUrl(
req,
res,
next,
authCodeUrlRequestParams,
authCodeRequestParams,
msalInstance
);
}

async handleRedirect(req, res, next) {
const authCodeRequest = {
...req.session.authCodeRequest,
code: req.body.code, // authZ code
codeVerifier: req.session.pkceCodes.verifier, // PKCE Code Verifier
};

try {
const msalInstance = this.getMsalInstance(this.config.msalConfig);
msalInstance.getTokenCache().deserialize(req.session.tokenCache);

const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);

req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.accessToken = tokenResponse.accessToken;
req.session.idToken = tokenResponse.idToken;
req.session.account = tokenResponse.account;
req.session.isAuthenticated = true;

const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state));
res.redirect(state.redirectTo);
} catch (error) {
next(error);
}
}

/**
*
* @param req: Express request object
* @param res: Express response object
* @param next: Express next function
* @param scopes: Array of strings
* @param redirectUri: redirect Url
*/
getToken(scopes, redirectUri = "http://localhost:3000/") {
return async function (req, res, next) {
console.log(scopes);
const msalInstance = authProvider.getMsalInstance(authProvider.config.msalConfig);
try {
msalInstance.getTokenCache().deserialize(req.session.tokenCache);

const silentRequest = {
account: req.session.account,
scopes: scopes,
};

const tokenResponse = await msalInstance.acquireTokenSilent(silentRequest);

req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.accessToken = tokenResponse.accessToken;
next();
} catch (error) {
if (error instanceof msal.InteractionRequiredAuthError) {
req.session.csrfToken = authProvider.cryptoProvider.createNewGuid();

const state = authProvider.cryptoProvider.base64Encode(
JSON.stringify({
redirectTo: 'http://localhost:3000/users/updateProfile',
csrfToken: req.session.csrfToken,
})
);

const authCodeUrlRequestParams = {
state: state,
scopes: scopes,
};

const authCodeRequestParams = {
state: state,
scopes: scopes,
};

authProvider.redirectToAuthCodeUrl(
req,
res,
next,
authCodeUrlRequestParams,
authCodeRequestParams,
msalInstance
);
}

next(error);
}
};
}

async logout(req, res, next) {
/**
* Construct a logout URI and redirect the user to end the
* session with Azure AD. For more information, visit:
* https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
*/
const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;

req.session.destroy(() => {
res.redirect(logoutUri);
});
}

/**
* Prepares the auth code request parameters and initiates the first leg of auth code flow
* @param req: Express request object
* @param res: Express response object
* @param next: Express next function
* @param authCodeUrlRequestParams: parameters for requesting an auth code url
* @param authCodeRequestParams: parameters for requesting tokens using auth code
*/
async redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
// Generate PKCE Codes before starting the authorization flow
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();

// Set generated PKCE codes and method as session vars
req.session.pkceCodes = {
challengeMethod: 'S256',
verifier: verifier,
challenge: challenge,
};

/**
* By manipulating the request objects below before each request, we can obtain
* auth artifacts with desired claims. For more information, visit:
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest
* https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationcoderequest
**/

req.session.authCodeUrlRequest = {
...authCodeUrlRequestParams,
redirectUri: this.config.redirectUri,
responseMode: 'form_post', // recommended for confidential clients
codeChallenge: req.session.pkceCodes.challenge,
codeChallengeMethod: req.session.pkceCodes.challengeMethod,
};

req.session.authCodeRequest = {
...authCodeRequestParams,
redirectUri: this.config.redirectUri,
code: '',
};

try {
const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
res.redirect(authCodeUrlResponse);
} catch (error) {
next(error);
}
}

/**
* Retrieves oidc metadata from the openid endpoint
* @returns
*/
async getAuthorityMetadata() {
const endpoint = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/v2.0/.well-known/openid-configuration`;
try {
const response = await axios.get(endpoint);
return await response.data;
} catch (error) {
console.log(error);
}
}
}

const authProvider = new AuthProvider({
msalConfig: msalConfig,
redirectUri: REDIRECT_URI,
postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI,
});

module.exports = authProvider;
49 changes: 49 additions & 0 deletions 1-Authentication/7-sign-in-express-mfa/App/authConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

require('dotenv').config({ path: '.env.dev' });

const TENANT_SUBDOMAIN = process.env.TENANT_SUBDOMAIN || 'Enter_the_Tenant_Subdomain_Here';
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/auth/redirect';
const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI || 'http://localhost:3000';

/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL Node configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
*/
const msalConfig = {
auth: {
clientId: process.env.CLIENT_ID || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // Replace the placeholder with your tenant name
clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app registration in Azure portal
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: 'Info',
},
},
};

const GRAPH_API_ENDPOINT = process.env.GRAPH_API_ENDPOINT || "graph_end_point";
// Refers to the user that is single user singed in.
// https://learn.microsoft.com/en-us/graph/api/user-update?view=graph-rest-1.0&tabs=http
const GRAPH_ME_ENDPOINT = GRAPH_API_ENDPOINT + "v1.0/me";

const mfaProtectedResourceScope = process.env.MFA_PROTECTED_SCOPE || 'Add_your_protected_scope_here';

module.exports = {
msalConfig,
mfaProtectedResourceScope,
REDIRECT_URI,
POST_LOGOUT_REDIRECT_URI,
TENANT_SUBDOMAIN,
GRAPH_API_ENDPOINT,
GRAPH_ME_ENDPOINT,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const authProvider = require('../auth/AuthProvider');

exports.signIn = async (req, res, next) => {
return authProvider.login(req, res, next, {scopes:["User.Read"]});
};

exports.handleRedirect = async (req, res, next) => {
return authProvider.handleRedirect(req, res, next);
}

exports.signOut = async (req, res, next) => {
return authProvider.logout(req, res, next);
};
Loading