Commit 482288a4 authored by Ben Galloway's avatar Ben Galloway

Initial commit

parents
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": []
}
# Project specifics
config.json
dist
# https://github.com/github/gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
\ No newline at end of file
{
"name": "@gsc/graphql-auth",
"version": "1.0.0",
"description": "A GraphQL auth directive to verify MS AAD tokens and validate AD group membership",
"main": "dist/index.js",
"scripts": {
"build": "NODE_ENV=production babel src -d dist --copy-files"
},
"author": "Ben Galloway <ben.galloway@gsc.org.uk>",
"license": "MIT",
"private": true,
"dependencies": {
"azure-ad-jwt": "^1.1.0",
"nconf": "^0.10.0"
},
"peerDependencies": {
"graphql-yoga": "^1.16.9"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3"
}
}
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;
import { SchemaDirectiveVisitor } from "graphql-tools";
import { defaultFieldResolver } from "graphql";
import ipaddr from "ipaddr.js";
import { getUser } from "./userUtils";
const generateAuthDirective = authRoleMap =>
class AuthDirective extends SchemaDirectiveVisitor {
// Based on https://www.apollographql.com/docs/graphql-tools/schema-directives.html#Enforcing-access-permissions
constructor(...args) {
super(...args);
this.authRoleMap = authRoleMap;
}
checkAuthRolePresent(role) {
if (role && !Object.keys(this.authRoleMap).includes(role))
throw new TypeError(`Could not find an AAD group UUID for ${role} in the authRoleMap`);
else return;
}
visitObject(type) {
this.ensureFieldsWrapped(type);
this.checkAuthRolePresent(this.args.requires);
type._requiredAuthRole = this.args.requires;
type._requiredAuthRoleId = this.authRoleMap[this.args.requires];
type._disallowIpBasedAuthz = this.args.evenInternally;
}
// Visitor methods for nested types like fields and arguments
// also receive a details object that provides information about
// the parent and grandparent types.
visitFieldDefinition(field, details) {
this.ensureFieldsWrapped(details.objectType);
this.checkAuthRolePresent(this.args.requires);
field._requiredAuthRole = this.args.requires;
field._requiredAuthRoleId = this.authRoleMap[this.args.requires];
field._disallowIpBasedAuthz = this.args.evenInternally;
}
ensureFieldsWrapped(objectType) {
// Mark the GraphQLObjectType object to avoid re-wrapping:
if (objectType._authFieldsWrapped) return;
objectType._authFieldsWrapped = true;
const fields = objectType.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
const { resolve = defaultFieldResolver } = field;
field.resolve = async function(...args) {
// Get the required Role from the field first, falling back
// to the objectType if no Role is required by the field:
const requiredRole = field._requiredAuthRole || objectType._requiredAuthRole;
const requiredRoleGroupId = field._requiredAuthRoleId || objectType._requiredAuthRoleId;
const alwaysAuthenticate = field._disallowIpBasedAuthz || objectType._disallowIpBasedAuthz;
if (!requiredRole) {
return resolve.apply(this, args);
}
const context = args[2];
const userToken = context.request.headers.authorization || "";
const userIPType = ipaddr.parse(context.request.ip).range();
const authRequired =
alwaysAuthenticate || !["private", "loopback", "uniqueLocal"].includes(userIPType);
const withRole = requiredRoleGroupId === true ? "" : `with role ${requiredRole}`;
console.info(
"Request from IP address",
context.request.ip,
authRequired ? `requires auth ${withRole}` : "does not require auth"
);
if (authRequired) {
const user = await getUser(userToken);
if (!user.hasRole(requiredRoleGroupId)) {
throw new Error(`This request requires ${requiredRole} privileges`);
}
console.info(
`Request from user ${user.preferred_username} at IP address ${
context.request.ip
} was authorized`
);
}
return resolve.apply(this, args);
};
});
}
};
export default generateAuthDirective;
import generateAuthDirective from "./generateAuthDirective";
import { getUser } from "./userUtils";
import generateCustomScalarMap from "./generateCustomScalarMap";
export { generateAuthDirective, getUser, generateCustomScalarMap };
import aad from "azure-ad-jwt";
import { gscAzureTenantId } from "./config";
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 userHasRole = user => roleGroupId => {
if (roleGroupId === true) return true;
if (!user.groups) return undefined;
return user.groups.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");
return {
...user,
validated: true,
hasRole: roleGroupId => userHasRole(user)(roleGroupId),
};
} catch (err) {
const errMsg = `User token verification failed ${err.message && "with error: " + err.message}`;
throw new Error(errMsg);
}
};
This source diff could not be displayed because it is too large. You can view the blob instead.
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