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
}
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();
......@@ -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");
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`);
}
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);
......
......@@ -15,17 +15,9 @@ Object.defineProperty(exports, "getUser", {
return _userUtils.getUser;
}
});
Object.defineProperty(exports, "generateCustomScalarMap", {
enumerable: true,
get: function () {
return _generateCustomScalarMap.default;
}
});
var _generateAuthDirective = _interopRequireDefault(require("./generateAuthDirective"));
var _userUtils = require("./userUtils");
var _generateCustomScalarMap = _interopRequireDefault(require("./generateCustomScalarMap"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
\ No newline at end of file
......@@ -5,36 +5,67 @@ Object.defineProperty(exports, "__esModule", {
});
exports.getUser = void 0;
var _azureAdJwt = _interopRequireDefault(require("azure-ad-jwt"));
var _config = require("./config");
var _nodeFetch = _interopRequireDefault(require("node-fetch"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const verifyAadToken = (token, options = {}) => // The options object is passed down to the jsonwebtokens module
// so any supported verify options there can be used:
// https://github.com/auth0/node-jsonwebtoken
new Promise((resolve, reject) => _azureAdJwt.default.verify(token, options, (err, result) => {
if (err) return reject(err);
return resolve(result);
}));
const userHasRole = user => roleGroupId => {
if (roleGroupId === true) return true;
if (!user.groups) return undefined;
return user.groups.includes(roleGroupId);
const callMsGraph = requestUrl => async token => {
try {
const response = await (0, _nodeFetch.default)(requestUrl, {
headers: {
Authorization: `Bearer ${token}`
}
});
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 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 => {
try {
const user = await verifyAadToken(token);
if (user.tid !== _config.gscAzureTenantId) throw new Error("User was authenticated but not with a GSC account");
const user = await getUserFromAccessToken(token);
if (!user) throw new Error("Couldn't use provided access token to get user details");
return { ...user,
validated: true,
hasRole: roleGroupId => userHasRole(user)(roleGroupId)
hasRole: async roleGroupId => await userHasRole(roleGroupId, token)
};
} 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);
}
};
......
......@@ -10,8 +10,7 @@
"license": "MIT",
"private": true,
"dependencies": {
"azure-ad-jwt": "^1.1.0",
"nconf": "^0.10.0"
"node-fetch": "^2.3.0"
},
"peerDependencies": {
"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 =>
}
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 authRequired =
......@@ -71,14 +73,13 @@ const generateAuthDirective = authRoleMap =>
);
if (authRequired) {
const user = await getUser(userToken);
if (!user.hasRole(requiredRoleGroupId)) {
const user = await getUser(accessToken);
const userHasRequiredRole = await user.hasRole(requiredRoleGroupId);
if (!userHasRequiredRole) {
throw new Error(`This request requires ${requiredRole} privileges`);
}
console.info(
`Request from user ${user.preferred_username} at IP address ${
context.request.ip
} was authorized`
`Request from user ${user.displayName} at IP address ${context.request.ip} was authorized`
);
}
......
import generateAuthDirective from "./generateAuthDirective";
import { getUser } from "./userUtils";
import generateCustomScalarMap from "./generateCustomScalarMap";
export { generateAuthDirective, getUser, generateCustomScalarMap };
export { generateAuthDirective, getUser };
import aad from "azure-ad-jwt";
import { gscAzureTenantId } from "./config";
import fetch from "node-fetch";
const verifyAadToken = (token, options = {}) =>
// The options object is passed down to the jsonwebtokens module
// so any supported verify options there can be used:
// https://github.com/auth0/node-jsonwebtoken
new Promise((resolve, reject) =>
aad.verify(token, options, (err, result) => {
if (err) return reject(err);
return resolve(result);
})
);
const callMsGraph = requestUrl => async token => {
try {
const response = await fetch(requestUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
});
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 (!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 => {
try {
const user = await verifyAadToken(token);
if (user.tid !== gscAzureTenantId) throw new Error("User was authenticated but not with a GSC account");
const user = await getUserFromAccessToken(token);
if (!user) throw new Error("Couldn't use provided access token to get user details");
return {
...user,
validated: true,
hasRole: roleGroupId => userHasRole(user)(roleGroupId),
hasRole: async roleGroupId => await userHasRole(roleGroupId, token),
};
} 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);
}
};
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