1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
const express = require("express");
const crypto = require("crypto");
const querystring = require("querystring");
const jwt = require("jsonwebtoken");
const jwksClient = require("jwks-rsa");
const app = express();
const CLIENT_ID = "ec014b23-edde-4a09-9a41-36bff5630829"; // appID
const CLIENT_SECRET = "Y.Q8Q~HuoEbCsICK18eG4oAtqjMe5eGyWSilLaZI"; // appid secret
const TENANT = "publicanub.onmicrosoft.com";
const TENANT_ID = "23c95e59-28bd-472a-bbd4-4e310dd8f031"; // organisation id
const REDIRECT_URI = "http://localhost:3000/callback";
const AUTH_URL = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0`;
const SCOPES = "openid profile email";
// Fetch Microsoft's public key by kid to verify JWT signatures
const keys = jwksClient({ jwksUri: "https://login.microsoftonline.com/common/discovery/v2.0/keys" });
function getKey(header, cb) {
keys.getSigningKey(header.kid, (err, key) => cb(err, key?.getPublicKey()));
}
function verifyJwt(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
audience: CLIENT_ID,
// Multi-tenant: accept tokens from any Azure AD tenant
issuer: (iss) => iss.startsWith("https://login.microsoftonline.com/") && iss.endsWith("/v2.0"),
algorithms: ["RS256"],
}, (err, decoded) => err ? reject(err) : resolve(decoded));
});
}
// POST to Microsoft's token endpoint
function fetchTokens(code) {
return new Promise((resolve, reject) => {
const body = querystring.stringify({
client_id: CLIENT_ID, client_secret: CLIENT_SECRET,
grant_type: "authorization_code", code, redirect_uri: REDIRECT_URI, scope: SCOPES,
});
const req = require("https").request({
hostname: "login.microsoftonline.com",
path: `/${TENANT}/oauth2/v2.0/token`,
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": Buffer.byteLength(body) },
}, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => resolve(JSON.parse(data)));
});
req.on("error", reject);
req.write(body);
req.end();
});
}
app.get("/", (_, res) => res.send('<h1>OIDC Demo</h1><a href="/login">Sign In with Azure AD</a>'));
app.get("/login", (_, res) => {
const params = querystring.stringify({
client_id: CLIENT_ID, response_type: "code", redirect_uri: REDIRECT_URI,
scope: SCOPES, state: crypto.randomBytes(16).toString("hex"),
nonce: crypto.randomBytes(16).toString("hex"), response_mode: "query",
});
res.redirect(`${AUTH_URL}/authorize?${params}`);
});
// Microsoft redirects here with ?code=<authorization_code>&state=<state>
// The code is a one-time-use temporary key that we exchange + client_secret for JWT tokens
app.get("/callback", async (req, res) => {
const { code, error, error_description } = req.query;
if (error) return res.send(`<pre>Error: ${error}: ${error_description}</pre>`);
try {
const tokens = await fetchTokens(code);
if (tokens.error) return res.send(`<pre>Token error: ${JSON.stringify(tokens, null, 2)}</pre><a href="/">Home</a>`);
// Verify JWT signature against Microsoft's public key + check aud, iss, exp
const v = await verifyJwt(tokens.id_token);
// // Only allow specific tenants
// const ALLOWED_TENANTS = ["23c95e59-28bd-472a-bbd4-4e310dd8f031"];
// if (!ALLOWED_TENANTS.includes(v.tid)) {
// return res.status(403).send(`<pre>Tenant not allowed: ${v.tid}</pre><a href="/">Home</a>`);
// }
res.send(`
<style>body{font-family:monospace;font-size:12px;padding:10px}h1{font-size:16px}h2{font-size:13px;margin-top:14px}pre{font-size:11px;background:#f4f4f4;padding:8px;overflow:auto}</style>
<h1>JWT Verified</h1>
<h2>Callback received (req.query)</h2>
<pre>${JSON.stringify(req.query, null, 2)}</pre>
<p>This code was exchanged + client_secret for the tokens below</p>
<h2>Checks</h2>
<p>Signature: VALID (RS256, Microsoft public key)</p>
<p>Audience: ${v.aud} | Issuer: ${v.iss} (tenant: ${v.tid})</p>
<p>Expires: ${new Date(v.exp * 1000).toISOString()}</p>
<h2>User</h2>
<p>oid: ${v.oid} | ${v.name} | ${v.preferred_username}</p>
<h2>Claims</h2>
<pre>${JSON.stringify(v, null, 2)}</pre>
<a href="/">Start over</a>
`);
} catch (err) {
res.status(401).send(`<pre>JWT verification failed: ${err.message}</pre><a href="/">Try again</a>`);
}
});
app.listen(3000, () => console.log("http://localhost:3000"));
|