I am creating a Node.js application with Azure AD B2C authentication.
The logic I want to implement is within this sample:
For those familiar with OAuth2 and Azure AD B2C:
The sample contains all Express route handlers and middleware functions in the application entry file, i.e: index.js
.
I am able to get the sample application working, as is, with no modifications.
Rather than have all the logic in one file, however, I would like to split it out into separate route and middleware modules.
I have done this before in other applications, so I understand how to create that structure and functionality.
But I am having difficulty answering these questions:
index.js
and into their own modules?The specific values that need to be available across modules are anything that is referenced by a middleware function, i.e:
Is it just a matter of:
Creating a module for each of the 8 values above?
Importing them into whatever module references them?
At a high level, my doubts about the above approach relate to:
If I import a module with a 'global' value into multiple other modules, will that create some sort of duplication?
Will it work? I.e: should some values only be defined once, and not imported into multiple modules? E.g:
const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
In case it is of any help in answering the question, below is how my app will be structured eventually:
Application
config
dist
middleware
node_modules
routes
src
view
.gitignore
app.js
babel.config.json
package.json
webpack.config.json
app.js
// various imports here
const routes = require('./routes').routes; // route handlers are defined in routes/index.js
app.use(express.static('dist')); // this is just to specify the path for js, css, images etc
app.use('/', routes);
// template engine logic here
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
routes/index.js
import express from 'express';
const router = express.Router();
// routes
import { route01 } from './route01';
import { route02 } from './route02;
import { route03 } from './route03';
import { route04 } from './route04';
/*
data flow:
hit > app.js > app.use('/', routes) > routes/index.js > relevant route > relevant middleware > response
*/
router.use('/', route01);
router.use('/route02', route02);
router.use('/route03', route03);
router.use('/route04', route04);
export { router as routes };
routes/route01
import express from 'express';
const router = express.Router();
import { route01_middleware } from '../middleware/route01_middleware';
router.route('/route01').get(route01_middleware);
export { router as route01 };
middleware/route01_middleware.js
const route01_middleware = async (req, res) => {
/*
this could include references to:
- req.session.accessToken
- APP_STATES
- confidentialClientApplication.acquireTokenByCode(tokenRequest).then((response)=>{ ... })
- getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, apiConfig.webApiScopes, APP_STATES.LOGIN, res);
- etc
*/
};
export { route01_middleware };
After testing I can confirm that:
import
statements to import dependencies in these filesexport
statements to make these values available in other files(In regards to my concern about code being 'duplicated' when importing the same module into multiple other modules, there seems to be some sort of module caching that occurs by default in JavaScript, so I think I don't need to worry about that).
After verifying the above, however, I decided to:
azureAuthConfig.js
I also decided to use the ES Module system rather than CommonJS Module system so that I could use import/export
statements and not require()
. So I added type: module
to my application's package.json
file. And I stopped using babel to convert my server side code (as ES Modules 'just work' now).
In the end, my 'non-monolithic' version of the sample code ended up looking something like this:
configuration/azureAuthConfig.js
These are all the 'global values' that were originally in the sample application's entry file.
I have put them in this separate file and export/import them where required.
import * as dotenv from 'dotenv';
dotenv.config();
import * as msal from "@azure/msal-node";
const confidentialClientConfig = {
auth: {
clientId: process.env.APP_CLIENT_ID,
authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,
clientSecret: process.env.APP_CLIENT_SECRET,
knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array
redirectUri: process.env.APP_REDIRECT_URI,
validateAuthority: false
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
const apiConfig = {
webApiScopes: [`https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.read`, `https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.write`],
anonymousUri: 'http://localhost:3000/public',
protectedUri: 'http://localhost:3000/hello'
};
const APP_STATES = {
LOGIN: 'login',
LOGOUT: 'logout',
CALL_API:'call_api',
PASSWORD_RESET: 'password_reset',
EDIT_PROFILE : 'update_profile'
}
const authCodeRequest = {
redirectUri: confidentialClientConfig.auth.redirectUri,
};
const tokenRequest = {
redirectUri: confidentialClientConfig.auth.redirectUri,
};
const sessionConfig = {
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // set this to true on production
}
}
const getAuthCode = (authority, scopes, state, res) => {
// prepare the request
console.log(`Fetching Authorization code`);
authCodeRequest.authority = authority;
authCodeRequest.scopes = scopes;
authCodeRequest.state = state;
//Each time you fetch Authorization code, update the relevant authority in the tokenRequest configuration
tokenRequest.authority = authority;
// request an authorization code to exchange for a token
return confidentialClientApplication.getAuthCodeUrl(authCodeRequest)
.then((response) => {
console.log(`\n\ngetAuthCodeUrl response (aka AuthCodeURL):\n${response}\n\n`);
//redirect to the auth code URL/send code to
res.redirect(response);
})
.catch((error) => {
res.status(500).send(error);
});
}
export { confidentialClientApplication, apiConfig, APP_STATES, authCodeRequest, tokenRequest, sessionConfig, getAuthCode };
app.js
import * as dotenv from 'dotenv';
dotenv.config();
const port = process.env.PORT || 3000;
import express from 'express';
const app = express();
import * as fs from 'fs';
import session from 'express-session';
import { sessionConfig } from './configuration/azureAuthConfig.js';
import { routes } from './routes/index.js';
app.use(express.static('dist'));
app.use(session(sessionConfig));
app.use('/', routes);
// my template logic
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
routes/index.js
import express from 'express';
const router = express.Router();
// routes
import { pagesRoute } from './pagesRoute.js';
import { signinRoute } from './signinRoute.js';
import { signoutRoute } from './signoutRoute.js';
import { redirectRoute } from './redirectRoute.js';
import { passwordRoute } from './passwordRoute.js';
import { profileRoute } from './profileRoute.js';
import { apiRoute } from './apiRoute.js';
import { helloRoute } from './helloRoute.js';
import { publicRoute } from './publicRoute.js';
/*
data flow:
hit > app.js > app.use() middleware > routes/index.js > relevant route > relevant middleware > response
*/
router.use('/signin', signinRoute);
router.use('/', pagesRoute);
router.use('/signout', signoutRoute);
router.use('/redirect', redirectRoute);
router.use('/password', passwordRoute);
router.use('/profile', profileRoute);
router.use('/api', apiRoute);
router.use('/hello', helloRoute);
router.use('/public', publicRoute);
export { router as routes };
routes/pagesRoute.js
import express from 'express';
const router = express.Router();
import { api_pages_get } from '../middleware/pages/api_pages_get.js';
router.route('/').get(api_pages_get);
export { router as pagesRoute };
middleware/pages/api_pages_get.js
// this is just some html stored in variables for testing functionality
import { launch_page_html, signed_in_page_html } from '../auth/html.js';
const api_pages_get = async (req, res) => {
if(!req.session.accessToken){
//User is not logged in
try {
console.log("\n\n############ THE USER IS NOT LOGGED IN ############\n\n");
res.redirect("/signin");
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}else{
console.log(`\n\n############ THE USER IS LOGGED IN ############\n\n`);
//Users have the accessToken because they signed in and the accessToken is still in the session
console.log(`\n\nreq.session.accessToken:\n${req.session.accessToken}\n\n`);
try {
res.render('index', { page_html: signed_in_page_html });
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}
};
export { api_pages_get };