Full-stack MERN π₯ (MongoDB, Express, React, Node.js) app π secured via Microsoft Identity π (Azure AD). Features π§ authentication with MSAL π (frontend) & access token ποΈ validation with jwks-rsa + jsonwebtoken (backend).
| π§± Layer | π§° Stack |
|---|---|
| π¨ Frontend | βοΈ React + π¦ React Router + π MSAL React |
| π§ Backend | π© Node.js + π§ Express + π jwks-rsa + πͺͺ JWT |
| π₯ Identity | βοΈ Azure AD (Microsoft Identity) |
| π Flow | π Authorization Code Flow w/ PKCE |
1οΈβ£ Go to Azure Portal β π Microsoft Entra ID β π App registrations β β New registration
2οΈβ£ Name: new-app
3οΈβ£ Account types: π’ My org only
- Platform: π§βπ» SPA
- Redirect URI:
http://localhost:3000 - βοΈ Check
ID tokens
-
App ID URI:
api://<CLIENT_ID> -
β Scope:
- Name:
access_as_user - Admin consent name:
Access new-app API - β Enabled
- Name:
- β:
openid,profile, & custom scope - β Grant admin consent
- π Generate Client Secret
- ποΈ Save the Value
Create server/.env:
PORT=5000
CLIENT_ID=your-client-id
TENANT_ID=your-tenant-id
CLIENT_SECRET=your-client-secretrequire('dotenv').config();
const express = require('express');
const cors = require('cors');
const auth = require('./middleware/auth');
const app = express();
app.use(cors({ origin: 'http://localhost:3000' }));
app.use(express.json());
app.get('/api/protected', auth, (req, res) => {
res.json({ message: 'π Hello, protected!', user: req.user });
});
app.listen(process.env.PORT || 5000, () =>
console.log(`π Server on ${process.env.PORT}`)
);const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: `https://login.microsoftonline.com/${process.env.TENANT_ID}/discovery/v2.0/keys`
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key.getPublicKey());
});
}
module.exports = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).send('π« No token');
jwt.verify(token, getKey, {
audience: process.env.CLIENT_ID,
issuer: `https://login.microsoftonline.com/${process.env.TENANT_ID}/v2.0`,
algorithms: ['RS256'],
}, (err, decoded) => {
if (err) return res.status(401).send('π« Unauthorized');
req.user = decoded;
next();
});
};π οΈ Install:
npm i @azure/msal-browser @azure/msal-react react-router-dom axiosexport const msalConfig = {
auth: {
clientId: 'your-client-id',
authority: 'https://login.microsoftonline.com/your-tenant-id',
redirectUri: 'http://localhost:3000',
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false,
}
};
export const loginRequest = {
scopes: ['api://your-client-id/access_as_user']
};import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import AutoLogout from './AutoLogout';
import { PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
import { msalConfig } from './authConfig';
const msalInstance = new PublicClientApplication(msalConfig);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<MsalProvider instance={msalInstance}>
<AutoLogout>
<App />
</AutoLogout>
</MsalProvider>
);import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './Home';
import Dashboard from './Dashboard';
import Navbar from './Navbar';
import ProtectedRoute from './ProtectedRoute';
function App() {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
</Routes>
</Router>
);
}
export default App;import { Navigate } from 'react-router-dom';
import { useIsAuthenticated } from '@azure/msal-react';
export default function ProtectedRoute({ children }) {
const isAuthenticated = useIsAuthenticated();
return isAuthenticated ? children : <Navigate to="/" replace />;
}import { useMsal } from '@azure/msal-react';
import { useEffect, useRef } from 'react';
export default function AutoLogout({ children }) {
const { instance } = useMsal();
const timer = useRef();
const reset = () => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
instance.logoutRedirect();
}, 2 * 60 * 1000);
};
useEffect(() => {
window.addEventListener('mousemove', reset);
window.addEventListener('keydown', reset);
reset();
return () => {
clearTimeout(timer.current);
window.removeEventListener('mousemove', reset);
window.removeEventListener('keydown', reset);
};
}, []);
return <>{children}</>;
}import { useMsal } from '@azure/msal-react';
import { loginRequest } from './authConfig';
export default function Home() {
const { instance } = useMsal();
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<h1>π Welcome</h1>
<button onClick={() => instance.loginRedirect(loginRequest)}>
π Login with Microsoft
</button>
</div>
);
}import { useEffect, useState } from 'react';
import { useMsal, useIsAuthenticated } from '@azure/msal-react';
import { loginRequest } from './authConfig';
import axiosInstance from './axiosInstance';
export default function Dashboard() {
const { instance, accounts } = useMsal();
const isAuthenticated = useIsAuthenticated();
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
if (!isAuthenticated || accounts.length === 0) return;
try {
const authResult = await instance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
const res = await axiosInstance.get('/protected', {
headers: { Authorization: `Bearer ${authResult.accessToken}` }
});
setData(res.data);
} catch (err) {
console.error(err);
}
};
fetchData();
}, [isAuthenticated, accounts]);
return (
<div style={{ padding: 20 }}>
<h2>π Dashboard</h2>
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'β³ Loading...'}
</div>
);
}import { Link } from 'react-router-dom';
import { useMsal } from '@azure/msal-react';
export default function Navbar() {
const { instance, accounts } = useMsal();
const loggedIn = accounts.length > 0;
return (
<nav style={{ background: '#222', padding: 10, color: '#fff' }}>
<Link to="/" style={{ color: '#fff', marginRight: 20 }}>π Home</Link>
{loggedIn && (
<>
<Link to="/dashboard" style={{ color: '#fff', marginRight: 20 }}>π Dashboard</Link>
<span>{accounts[0].username}</span>
<button onClick={() => instance.logoutRedirect()} style={{ marginLeft: 20 }}>
πͺ Logout
</button>
</>
)}
</nav>
);
}import axios from 'axios';
const axiosInstance = axios.create({
baseURL: 'http://localhost:5000/api',
withCredentials: true
});
export default axiosInstance;| βοΈ Scenario | π― Result |
|---|---|
Access /dashboard w/o login |
βͺοΈ Redirect to / |
Login β /dashboard |
β Shows protected π |
| Missing token | β 401 Unauthorized |
| Invalid token | β 401 Unauthorized |
| Idle > 2 mins | π Auto logout |
# π§ Backend
cd server
npm i
node server.js
# π₯οΈ Frontend
npm i
npm start- π MSAL.js Docs
- π§ Azure Portal
- π jwks-rsa