This project illustrates a simple wrapper around the ConfidentialClientApplication class of the Microsoft Authentication Library for Node.js (MSAL Node), in order to streamline routine authentication tasks such as login, logout and token acquisition, as well as securing routes and controlling access.
This is an open source project. Suggestions and contributions are welcome!
đź“Ł This project backs the tutorial: Enable your Node.js web app to sign-in users and call APIs with the Microsoft identity platform
- Simple API for authN/authZ with the Microsoft identity platform
- Fetch credentials from Azure Key Vault
- Handle role-based access with Azure AD App Roles and Security Groups
- (coming soon) Enable Conditional Access and Zero-Trust
- (coming soon) Run custom policies with Azure AD B2C
⚠️ Protected web API scenarios are currently not supported.
- Node 12 LTS or higher
- Express.js 4x or higher
- @azure/msal-node
- express-session (or a similar session solution)
npm install azure-samples/msal-express-wrapper
or download and extract the repository .zip file.
git clone https://github.com/Azure-Samples/msal-express-wrapper.git
cd msal-express-wrapper
npm install
npm run build
ℹ️ This project is generated using tsdx.
Check out the demo app. Read below for how to configure it.
- Initialize the wrapper in your app by providing a settings object. The object looks like the follows:
const appSettings = {
appCredentials: {
clientId: "CLIENT_ID", // Application (client) ID on Azure AD
tenantId: "TENANT_ID", // alt. "common" "organizations" "consumers"
clientSecret: "CLIENT_SECRET" // alt. client certificate or key vault credential
},
authRoutes: {
redirect: "/redirect", // redirect URI configured on Azure AD
error: "/error", // errors will be redirected to this route
unauthorized: "/unauthorized" // unauthorized access attempts will be redirected to this route
},
remoteResources: {
graphAPI: {
endpoint: "https://graph.microsoft.com/v1.0/me", // Microsoft Graph
scopes: ["user.read"]
},
armAPI: {
endpoint: "https://management.azure.com/tenants?api-version=2020-01-01", // Azure REST API
scopes: ["https://management.azure.com/user_impersonation"]
}
}
}
- If you are authenticating with Azure AD B2C, user-flows should be provided as well. The first item is used as default authority.
const appSettings = {
// ...
b2cPolicies: {
signUpSignIn: {
authority: "https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_susi"
}
}
}
Import the package and instantiate AuthProvider class, which exposes the middleware you can use in your routes. The constructor takes the settings object and an (optional) persistent cache:
const express = require('express');
const session = require('express-session');
const msalWrapper = require('msal-express-wrapper');
const settings = require('./appSettings');
const cache = require('./utils/cachePlugin');
const router = require('./routes/router');
const SERVER_PORT = process.env.PORT || 4000;
const app = express();
app.use(session({
secret: 'ENTER_YOUR_SECRET_HERE',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // set to true on deployment
}
}));
const authProvider = new msalWrapper.AuthProvider(settings, cache);
app.use(authProvider.initialize()); // initialize default routes
app.use(router(authProvider)); // use authProvider in routers downstream
app.listen(SERVER_PORT, () => console.log(`Server is listening on port ${SERVER_PORT}!`));
The wrapper stores user data on req.session
variable. Below are some of the useful variables:
req.session.isAuthenticated
: indicates if user is currently authenticated (boolean)req.session.account
: MSAL.js account object containing useful information like ID token claims (see AccountInfo)req.session.remoteResources.{resourceName}
: Contains parameters related to an Azure AD / Azure AD B2C protected resource, including raw access tokens (see Resource)
Add signIn() and signOut() middleware to routes you want users to sign-in and sign-out, respectively. You will need to pass the routes for redirect after as parameters to each:
const express = require('express');
const appSettings = require('../appSettings');
const mainController = require('../controllers/mainController');
module.exports = (authProvider) => {
// initialize router
const router = express.Router();
router.get('/', (req, res, next) => res.redirect('/home'));
router.get('/home', (req, res, next) => {
res.render('home', { isAuthenticated: req.session.isAuthenticated });
});
// unauthorized
router.get('/error', (req, res) => res.redirect('/401.html'));
// error
router.get('/unauthorized', (req, res) => res.redirect('/500.html'));
// auth routes
router.get('/signin',
authProvider.signIn({
successRedirect: "/",
}),
);
router.get('/signout',
authProvider.signOut({
successRedirect: "/",
}),
);
router.get('*', (req, res) => res.redirect('/404.html'));
return router;
}
Simply add the isAuthenticated() middleware before the controller that serves the page you would like to secure:
// secure routes
app.get('/id',
authProvider.isAuthenticated({
unauthorizedRedirect: "/sign-in"
}
), // checks if authenticated via session
(req, res, next) => {
res.render('id', { isAuthenticated: req.session.isAuthenticated, claims: req.session.account.idTokenClaims });
}
);
getToken() can be used before middleware that calls a web API. The access token will be available via req.session
:
router.get('/profile',
authProvider.isAuthenticated(),
authProvider.getToken({
resource: {
endpoint: "https://graph.microsoft.com/v1.0/me",
scopes: [ "User.Read" ]
}
}),
async(req, res, next) => {
try {
// use axios or a similar alternative
const response = await axios.default.get("https://graph.microsoft.com/v1.0/me", {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
res.render('profile', { isAuthenticated: req.session.isAuthenticated, profile: response.data });
} catch (error) {
console.log(error);
next(error);
}
}
); // get token for this route to call web API
Use hasAccess() middleware to control access for Azure AD App Roles and/or Security Groups:
router.use('/admin',
authProvider.isAuthenticated(),
authProvider.hasAccess({
accessRule: {
methods: [ "GET", "POST", "DELETE" ],
roles: [ "admin_role" ]
}
}),
(req, res) => {
const users = User.getAllUsers();
res.render('dashboard', { isAuthenticated: req.session.isAuthenticated, users: users });
}
);
Session support in this sample is provided by the express-session package using in-memory session store. in-memory session store is unfit for production, and you should either use a compatible session store or implement your own storage solution.
MSAL Node has an in-memory cache by default. The demo app also features a persistent cache plugin in order to save the cache to disk. This plugin is not meant to be production-ready. As such, you might want to implement persistent caching using a 3rd party library like redis.