Commit 4134bb0a authored by Ben Galloway's avatar Ben Galloway

Working version, simplified to check tokens with calls to Microsoft Graph

parent b8c57503
...@@ -68,7 +68,7 @@ const generateAuthDirective = authRoleMap => class AuthDirective extends _graphq ...@@ -68,7 +68,7 @@ const generateAuthDirective = authRoleMap => class AuthDirective extends _graphq
} }
const context = args[2]; const context = args[2];
const userToken = context.request.headers.authorization || ""; const accessToken = context.request.headers.authorization ? context.request.headers.authorization.split(" ")[1] : null;
const userIPType = _ipaddr.default.parse(context.request.ip).range(); const userIPType = _ipaddr.default.parse(context.request.ip).range();
...@@ -77,13 +77,14 @@ const generateAuthDirective = authRoleMap => class AuthDirective extends _graphq ...@@ -77,13 +77,14 @@ const generateAuthDirective = authRoleMap => class AuthDirective extends _graphq
console.info("Request from IP address", context.request.ip, authRequired ? `requires auth ${withRole}` : "does not require auth"); console.info("Request from IP address", context.request.ip, authRequired ? `requires auth ${withRole}` : "does not require auth");
if (authRequired) { if (authRequired) {
const user = await (0, _userUtils.getUser)(userToken); const user = await (0, _userUtils.getUser)(accessToken);
const userHasRequiredRole = await user.hasRole(requiredRoleGroupId);
if (!user.hasRole(requiredRoleGroupId)) { if (!userHasRequiredRole) {
throw new Error(`This request requires ${requiredRole} privileges`); throw new Error(`This request requires ${requiredRole} privileges`);
} }
console.info(`Request from user ${user.preferred_username} at IP address ${context.request.ip} was authorized`); console.info(`Request from user ${user.displayName} at IP address ${context.request.ip} was authorized`);
} }
return resolve.apply(this, args); return resolve.apply(this, args);
......
...@@ -15,17 +15,9 @@ Object.defineProperty(exports, "getUser", { ...@@ -15,17 +15,9 @@ Object.defineProperty(exports, "getUser", {
return _userUtils.getUser; return _userUtils.getUser;
} }
}); });
Object.defineProperty(exports, "generateCustomScalarMap", {
enumerable: true,
get: function () {
return _generateCustomScalarMap.default;
}
});
var _generateAuthDirective = _interopRequireDefault(require("./generateAuthDirective")); var _generateAuthDirective = _interopRequireDefault(require("./generateAuthDirective"));
var _userUtils = require("./userUtils"); var _userUtils = require("./userUtils");
var _generateCustomScalarMap = _interopRequireDefault(require("./generateCustomScalarMap"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
\ No newline at end of file
...@@ -5,36 +5,67 @@ Object.defineProperty(exports, "__esModule", { ...@@ -5,36 +5,67 @@ Object.defineProperty(exports, "__esModule", {
}); });
exports.getUser = void 0; exports.getUser = void 0;
var _azureAdJwt = _interopRequireDefault(require("azure-ad-jwt")); var _nodeFetch = _interopRequireDefault(require("node-fetch"));
var _config = require("./config");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const verifyAadToken = (token, options = {}) => // The options object is passed down to the jsonwebtokens module const callMsGraph = requestUrl => async token => {
// so any supported verify options there can be used: try {
// https://github.com/auth0/node-jsonwebtoken const response = await (0, _nodeFetch.default)(requestUrl, {
new Promise((resolve, reject) => _azureAdJwt.default.verify(token, options, (err, result) => { headers: {
if (err) return reject(err); Authorization: `Bearer ${token}`
return resolve(result); }
})); });
const json = await response.json();
const userHasRole = user => roleGroupId => { if (json.error) throw new Error(`${json.error.code}: ${json.error.message}`);
if (roleGroupId === true) return true; return json;
if (!user.groups) return undefined; } catch (error) {
return user.groups.includes(roleGroupId); console.error(error);
return null;
}
};
const getUserFromMsGraph = callMsGraph("https://graph.microsoft.com/v1.0/me/");
const getGroupsFromMsGraph = callMsGraph("https://graph.microsoft.com/v1.0/me/memberOf");
const getUserFromAccessToken = async token => {
/**
* Verify that the access token for MS Graph is valid
* - simply by calling MS Graph!
* This is a workaround for the current restrictions on standalone APIs
* with the v2.0 endpoint.
* --BAG 20 Feb 2019
*/
try {
const userDetails = await getUserFromMsGraph(token);
if (!userDetails) throw new Error("Unable to fetch user details from Microsoft Graph");
const upnDomain = userDetails.userPrincipalName.split("@")[1];
if (upnDomain !== "gsc.org.uk") throw new Error("User was authenticated but not with a GSC account");
return userDetails;
} catch (err) {
console.error(err);
return null;
}
};
const userHasRole = async (roleGroupId, token) => {
if (roleGroupId === true) return true; // Fetch the user's groups from Microsoft Graph
const fullUserGroupDetails = await getGroupsFromMsGraph(token);
if (fullUserGroupDetails === null) return undefined;
return fullUserGroupDetails.value.map(group => group.id).includes(roleGroupId);
}; };
const getUser = async token => { const getUser = async token => {
try { try {
const user = await verifyAadToken(token); const user = await getUserFromAccessToken(token);
if (user.tid !== _config.gscAzureTenantId) throw new Error("User was authenticated but not with a GSC account"); if (!user) throw new Error("Couldn't use provided access token to get user details");
return { ...user, return { ...user,
validated: true, validated: true,
hasRole: roleGroupId => userHasRole(user)(roleGroupId) hasRole: async roleGroupId => await userHasRole(roleGroupId, token)
}; };
} catch (err) { } catch (err) {
const errMsg = `User token verification failed ${err.message && "with error: " + err.message}`; const errMsg = `User verification failed ${err.message && "with error: " + err.message}`;
throw new Error(errMsg); throw new Error(errMsg);
} }
}; };
......
...@@ -10,8 +10,7 @@ ...@@ -10,8 +10,7 @@
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"dependencies": { "dependencies": {
"azure-ad-jwt": "^1.1.0", "node-fetch": "^2.3.0"
"nconf": "^0.10.0"
}, },
"peerDependencies": { "peerDependencies": {
"graphql-yoga": "^1.16.9" "graphql-yoga": "^1.16.9"
......
import nconf from "nconf";
nconf.env("__");
nconf.file("./config.json");
nconf.defaults({
gscAzureTenantId: "f62a415a-76c0-4075-9eb3-f31250de2db2",
});
const config = nconf.get();
export default config;
...@@ -58,7 +58,9 @@ const generateAuthDirective = authRoleMap => ...@@ -58,7 +58,9 @@ const generateAuthDirective = authRoleMap =>
} }
const context = args[2]; const context = args[2];
const userToken = context.request.headers.authorization || ""; const accessToken = context.request.headers.authorization
? context.request.headers.authorization.split(" ")[1]
: null;
const userIPType = ipaddr.parse(context.request.ip).range(); const userIPType = ipaddr.parse(context.request.ip).range();
const authRequired = const authRequired =
...@@ -71,14 +73,13 @@ const generateAuthDirective = authRoleMap => ...@@ -71,14 +73,13 @@ const generateAuthDirective = authRoleMap =>
); );
if (authRequired) { if (authRequired) {
const user = await getUser(userToken); const user = await getUser(accessToken);
if (!user.hasRole(requiredRoleGroupId)) { const userHasRequiredRole = await user.hasRole(requiredRoleGroupId);
if (!userHasRequiredRole) {
throw new Error(`This request requires ${requiredRole} privileges`); throw new Error(`This request requires ${requiredRole} privileges`);
} }
console.info( console.info(
`Request from user ${user.preferred_username} at IP address ${ `Request from user ${user.displayName} at IP address ${context.request.ip} was authorized`
context.request.ip
} was authorized`
); );
} }
......
import generateAuthDirective from "./generateAuthDirective"; import generateAuthDirective from "./generateAuthDirective";
import { getUser } from "./userUtils"; import { getUser } from "./userUtils";
import generateCustomScalarMap from "./generateCustomScalarMap";
export { generateAuthDirective, getUser, generateCustomScalarMap }; export { generateAuthDirective, getUser };
import aad from "azure-ad-jwt"; import fetch from "node-fetch";
import { gscAzureTenantId } from "./config";
const verifyAadToken = (token, options = {}) => const callMsGraph = requestUrl => async token => {
// The options object is passed down to the jsonwebtokens module try {
// so any supported verify options there can be used: const response = await fetch(requestUrl, {
// https://github.com/auth0/node-jsonwebtoken headers: {
new Promise((resolve, reject) => Authorization: `Bearer ${token}`,
aad.verify(token, options, (err, result) => { },
if (err) return reject(err); });
return resolve(result); const json = await response.json();
}) if (json.error) throw new Error(`${json.error.code}: ${json.error.message}`);
); return json;
} catch (error) {
console.error(error);
return null;
}
};
const getUserFromMsGraph = callMsGraph("https://graph.microsoft.com/v1.0/me/");
const getGroupsFromMsGraph = callMsGraph("https://graph.microsoft.com/v1.0/me/memberOf");
const userHasRole = user => roleGroupId => { const getUserFromAccessToken = async token => {
/**
* Verify that the access token for MS Graph is valid
* - simply by calling MS Graph!
* This is a workaround for the current restrictions on standalone APIs
* with the v2.0 endpoint.
* --BAG 20 Feb 2019
*/
try {
const userDetails = await getUserFromMsGraph(token);
if (!userDetails) throw new Error("Unable to fetch user details from Microsoft Graph");
const upnDomain = userDetails.userPrincipalName.split("@")[1];
if (upnDomain !== "gsc.org.uk") throw new Error("User was authenticated but not with a GSC account");
return userDetails;
} catch (err) {
console.error(err);
return null;
}
};
const userHasRole = async (roleGroupId, token) => {
if (roleGroupId === true) return true; if (roleGroupId === true) return true;
if (!user.groups) return undefined;
return user.groups.includes(roleGroupId); // Fetch the user's groups from Microsoft Graph
const fullUserGroupDetails = await getGroupsFromMsGraph(token);
if (fullUserGroupDetails === null) return undefined;
return fullUserGroupDetails.value.map(group => group.id).includes(roleGroupId);
}; };
export const getUser = async token => { export const getUser = async token => {
try { try {
const user = await verifyAadToken(token); const user = await getUserFromAccessToken(token);
if (user.tid !== gscAzureTenantId) throw new Error("User was authenticated but not with a GSC account"); if (!user) throw new Error("Couldn't use provided access token to get user details");
return { return {
...user, ...user,
validated: true, validated: true,
hasRole: roleGroupId => userHasRole(user)(roleGroupId), hasRole: async roleGroupId => await userHasRole(roleGroupId, token),
}; };
} catch (err) { } catch (err) {
const errMsg = `User token verification failed ${err.message && "with error: " + err.message}`; const errMsg = `User verification failed ${err.message && "with error: " + err.message}`;
throw new Error(errMsg); throw new Error(errMsg);
} }
}; };
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment