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