Skip to content

Commit 24cba4d

Browse files
committed
chore(auth): move jwt util functions into own file for testing
1 parent 5e1440e commit 24cba4d

File tree

2 files changed

+92
-87
lines changed

2 files changed

+92
-87
lines changed

src/service/passport/jwtAuthHandler.js

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,3 @@
1-
const axios = require("axios");
2-
const jwt = require("jsonwebtoken");
3-
const jwkToPem = require("jwk-to-pem");
4-
5-
/**
6-
* Obtain the JSON Web Key Set (JWKS) from the OIDC authority.
7-
* @param {string} authorityUrl the OIDC authority URL. e.g. https://login.microsoftonline.com/{tenantId}
8-
* @return {Promise<object[]>} the JWKS keys
9-
*/
10-
async function getJwks(authorityUrl) {
11-
try {
12-
const { data } = await axios.get(`${authorityUrl}/.well-known/openid-configuration`);
13-
const jwksUri = data.jwks_uri;
14-
15-
const { data: jwks } = await axios.get(jwksUri);
16-
return jwks.keys;
17-
} catch (error) {
18-
console.error("Error fetching JWKS:", error);
19-
throw new Error("Failed to fetch JWKS");
20-
}
21-
}
22-
23-
/**
24-
* Validate a JWT token using the OIDC configuration.
25-
* @param {*} token the JWT token
26-
* @param {*} authorityUrl the OIDC authority URL
27-
* @param {*} clientID the OIDC client ID
28-
* @param {*} expectedAudience the expected audience for the token
29-
* @return {Promise<object>} the verified payload or an error
30-
*/
31-
async function validateJwt(token, authorityUrl, clientID, expectedAudience) {
32-
try {
33-
const jwks = await getJwks(authorityUrl);
34-
35-
const decodedHeader = await jwt.decode(token, { complete: true });
36-
if (!decodedHeader || !decodedHeader.header || !decodedHeader.header.kid) {
37-
throw new Error("Invalid JWT: Missing key ID (kid)");
38-
}
39-
40-
const { kid } = decodedHeader.header;
41-
const jwk = jwks.find((key) => key.kid === kid);
42-
if (!jwk) {
43-
throw new Error("No matching key found in JWKS");
44-
}
45-
46-
const pubKey = jwkToPem(jwk);
47-
48-
const verifiedPayload = jwt.verify(token, pubKey, {
49-
algorithms: ["RS256"],
50-
issuer: authorityUrl,
51-
audience: expectedAudience,
52-
});
53-
54-
if (verifiedPayload.azp !== clientID) {
55-
throw new Error("JWT client ID does not match");
56-
}
57-
58-
return { verifiedPayload };
59-
} catch (error) {
60-
const errorMessage = `JWT validation failed: ${error.message}\n`;
61-
console.error(errorMessage);
62-
return { error: errorMessage };
63-
}
64-
}
65-
66-
/**
67-
* Assign roles to the user based on the role mappings provided in the jwtConfig.
68-
*
69-
* If no role mapping is provided, the user will not have any roles assigned (i.e. user.admin = false).
70-
* @param {*} roleMapping the role mapping configuration
71-
* @param {*} payload the JWT payload
72-
* @param {*} user the req.user object to assign roles to
73-
*/
74-
function assignRoles(roleMapping, payload, user) {
75-
if (roleMapping) {
76-
for (const role of Object.keys(roleMapping)) {
77-
const claimValuePair = roleMapping[role];
78-
const claim = Object.keys(claimValuePair)[0];
79-
const value = claimValuePair[claim];
80-
81-
if (payload[claim] && payload[claim] === value) {
82-
user[role] = true;
83-
}
84-
}
85-
}
86-
}
87-
881
const jwtAuthHandler = () => {
892
return async (req, res, next) => {
903
const apiAuthMethods = require('../../config').getAPIAuthMethods();

src/service/passport/jwtUtils.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const axios = require("axios");
2+
const jwt = require("jsonwebtoken");
3+
const jwkToPem = require("jwk-to-pem");
4+
5+
/**
6+
* Obtain the JSON Web Key Set (JWKS) from the OIDC authority.
7+
* @param {string} authorityUrl the OIDC authority URL. e.g. https://login.microsoftonline.com/{tenantId}
8+
* @return {Promise<object[]>} the JWKS keys
9+
*/
10+
async function getJwks(authorityUrl) {
11+
try {
12+
const { data } = await axios.get(`${authorityUrl}/.well-known/openid-configuration`);
13+
const jwksUri = data.jwks_uri;
14+
15+
const { data: jwks } = await axios.get(jwksUri);
16+
return jwks.keys;
17+
} catch (error) {
18+
console.error("Error fetching JWKS:", error);
19+
throw new Error("Failed to fetch JWKS");
20+
}
21+
}
22+
23+
/**
24+
* Validate a JWT token using the OIDC configuration.
25+
* @param {*} token the JWT token
26+
* @param {*} authorityUrl the OIDC authority URL
27+
* @param {*} clientID the OIDC client ID
28+
* @param {*} expectedAudience the expected audience for the token
29+
* @return {Promise<object>} the verified payload or an error
30+
*/
31+
async function validateJwt(token, authorityUrl, clientID, expectedAudience) {
32+
try {
33+
const jwks = await getJwks(authorityUrl);
34+
35+
const decodedHeader = await jwt.decode(token, { complete: true });
36+
if (!decodedHeader || !decodedHeader.header || !decodedHeader.header.kid) {
37+
throw new Error("Invalid JWT: Missing key ID (kid)");
38+
}
39+
40+
const { kid } = decodedHeader.header;
41+
const jwk = jwks.find((key) => key.kid === kid);
42+
if (!jwk) {
43+
throw new Error("No matching key found in JWKS");
44+
}
45+
46+
const pubKey = jwkToPem(jwk);
47+
48+
const verifiedPayload = jwt.verify(token, pubKey, {
49+
algorithms: ["RS256"],
50+
issuer: authorityUrl,
51+
audience: expectedAudience,
52+
});
53+
54+
if (verifiedPayload.azp !== clientID) {
55+
throw new Error("JWT client ID does not match");
56+
}
57+
58+
return { verifiedPayload };
59+
} catch (error) {
60+
const errorMessage = `JWT validation failed: ${error.message}\n`;
61+
console.error(errorMessage);
62+
return { error: errorMessage };
63+
}
64+
}
65+
66+
/**
67+
* Assign roles to the user based on the role mappings provided in the jwtConfig.
68+
*
69+
* If no role mapping is provided, the user will not have any roles assigned (i.e. user.admin = false).
70+
* @param {*} roleMapping the role mapping configuration
71+
* @param {*} payload the JWT payload
72+
* @param {*} user the req.user object to assign roles to
73+
*/
74+
function assignRoles(roleMapping, payload, user) {
75+
if (roleMapping) {
76+
for (const role of Object.keys(roleMapping)) {
77+
const claimValuePair = roleMapping[role];
78+
const claim = Object.keys(claimValuePair)[0];
79+
const value = claimValuePair[claim];
80+
81+
if (payload[claim] && payload[claim] === value) {
82+
user[role] = true;
83+
}
84+
}
85+
}
86+
}
87+
88+
module.exports = {
89+
getJwks,
90+
validateJwt,
91+
assignRoles,
92+
};

0 commit comments

Comments
 (0)