summaryrefslogtreecommitdiff
path: root/server.js
blob: 5b373dae61197e72789b9da5949fee901b93faf1 (plain)
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"));