mercurius-js / auth Goto Github PK
View Code? Open in Web Editor NEWMercurius Auth Plugin
License: MIT License
Mercurius Auth Plugin
License: MIT License
We should take a look at the Mercurius Auth documentation to make sure it is super clear for new and existing users. We should also make sure it provides everything a developer needs to use and understand Mercurius Auth (without having to look closer at the underlying code/tests for example). Before starting an implementation, it would be great to understand the following:
Keen to hear everyone's thoughts on this!
Hi guys,
How I can set custom directives to the current field through Javascript code?
I created a custom Directive:
const authDirective = new GraphQLDirective({
name: 'auth',
locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
args: ...
})
Added to the schema:
const schema = new GraphQLSchema({
directives: [authDirective],
query: new GraphQLObjectType({
fields: {
secretData: ...
}
})
})
How I can use my directive only for the secretData field?
I tried to add a directives parameter for field definition but it doesn't work:
{
type: validateWidgetQLType,
args: validateWidgetQLArgs,
directives: [
{ name: 'auth', arguments: { requires: 0 }}
],
}
Please help!
So... I did a "fork" of the repo, and no issue with the default package. However, maybe I am thinking what this "option" does.
Lets start with the GraphQL Query itself:
query user($first: Int, $after: String, $last: Int, $before: String) {
user(first: $first, after: $after, last: $last, before: $before) {
totalCount
edges {
cursor
node {
firstName
roles
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
I have this directive:
directive @auth(requires: Role, explicit: Role, permissions: [String!]) on OBJECT | FIELD_DEFINITION
and this is i the type:
type UserQuery {
_id: String!
dateCreated: String!
email: String!
firstName: String!
lastName: String!
localLogin: Boolean!
roles: [String] @auth(explicit: ["OPS"])
username: String!
}
This is the directive code:
fastify.register(mercuriusAuth, {
authContext(context) {
return {
identity: context.reply.request.headers.authorization?.split(' ')[1],
refresh: context.reply.request.headers['x-refresh-token'],
};
},
applyPolicy: async function hasPermissionPolicy (policy, parent, args, context, info) {
/* DB Query Removed *
switch (typeEnforcement) {
case 'explicit': {
const hasGrant = /* Removed */
if (!hasGrant) {
return false /// When this is sent, I suspect that if the user does not have the role "admin" this would be just removed out or return "Role" as null.
}
return true
}
default:
return new Error(`Internal Error. Invalid Auth Enforcement Type: ${typeEnforcement}` );
}
return true;
},
authDirective: 'auth'
})
So I would epect if "hasGrant" is false, and returning fale, it would just not give a response to the end user. Right now it's sending a GraphQL error sying that the "user" does not have the right permission, etc.
Ideas?
When running the tests against the latest commit on main
with Node.js 20.x, I get the following error for the following test:
Test:
test('the single filter preExecution lets the app crash', async (t) => {
const app = Fastify()
t.teardown(app.close.bind(app))
app.register(mercurius, {
schema,
resolvers
})
app.register(mercuriusAuth, {
authContext,
applyPolicy: authPolicy,
filterSchema: true,
authDirective: 'auth'
})
app.register(async function plugin () {
throw new Error('boom')
})
try {
await app.ready()
} catch (error) {
t.equal(error.message, 'boom')
}
})
Error:
test/introspection-filter.js 2>
test/introspection-filter.js 2> # /Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node[96991]: void node::InternalCallbackScope::Close() at ../src/api/callback.cc:145
test/introspection-filter.js 2> # Assertion failed: (env_->execution_async_id()) == (0)
test/introspection-filter.js 2>
test/introspection-filter.js 2> ----- Native stack trace -----
test/introspection-filter.js 2>
test/introspection-filter.js 2> 1: 0x10cd5a6c5 node::Abort() [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 2: 0x10cd5a413 node::Assert(node::AssertionInfo const&) [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 3: 0x10cc6fd39 node::InternalCallbackScope::Close() [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 4: 0x10cc6f79e node::InternalCallbackScope::~InternalCallbackScope() [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 5: 0x10cdcaa46 node::PerIsolatePlatformData::RunForegroundTask(std::__1::unique_ptr<v8::Task, std::__1::default_delete<v8::Task> >) [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 6: 0x10cdc9477 node::PerIsolatePlatformData::FlushForegroundTasksInternal() [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 7: 0x10d872931 uv__async_io [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 8: 0x10d8862cc uv__io_poll [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 9: 0x10d872f36 uv_run [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 10: 0x10cc70700 node::SpinEventLoopInternal(node::Environment*) [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 11: 0x10cda293b node::NodeMainInstance::Run(node::ExitCode*, node::Environment*) [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 12: 0x10cda26ac node::NodeMainInstance::Run() [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 13: 0x10cd1eb93 node::Start(int, char**) [/Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node]
test/introspection-filter.js 2> 14: 0x11dee052e
FAIL test/introspection-filter.js 19 OK 5s
command: /Users/jonny/.asdf/installs/nodejs/20.11.1/bin/node
args:
- test/introspection-filter.js
exitCode: null
signal: SIGABRT
After some debugging and a quick look at the code, it seems like the issue is with the following line: https://github.com/mercurius-js/auth/blob/main/lib/filter-schema.js#L16
Gracefully handling the error by logging out instead seems to solve the issue whilst still preserving the integrity of the test:
// the filter hook must be the last one to be executed (after all the authContextHook ones)
app.ready(err => {
if (err) {
app.log.warn('Error occurred during the app.ready hook, failing gracefully: ', err)
} else {
app.graphql.addHook('preExecution', filterGraphQLSchemaHook.bind(app))
}
})
wdyt?
I'll get a PR up for this tomorrow, let me know if you think of any other/better suggestions on the solution :)
Given this schema:
directive @auth(
role: String
) on OBJECT
type Query {
searchData: Grid
}
union Grid = AdminGrid | ModeratorGrid | UserGrid
type AdminGrid @auth(role: "admin") {
totalRevenue: Float
}
type ModeratorGrid @auth(role: "moderator") {
banHammer: Boolean
}
type UserGrid @auth(role: "user") {
basicColumn: String
}
and this plugin setup:
app.register(require('mercurius-auth'), {
authContext (context) {
// you can validate the headers here
return {
identity: context.reply.request.headers['x-user-type']
}
},
async applyPolicy (policy, parent, args, context, info) {
const role = policy.arguments[0].value.value
app.log.info('Applying policy %s on user %s', role, context.auth.identity)
// we compare the schema role directive with the user role
return context.auth.identity === role
},
authDirective: 'auth'
})
The applyPolicy
function is never executed.
If I change the schema to:
type Query {
- searchData: Grid
+ searchData: AdminGrid
}
The function is executed instead.
Here a complete code example + test (skipped) Eomm/blog-posts@7ec5f23
Right now, setting the directive schema as following, it returns the information to all the clients:
directive @auth(
requires: Role = ADMIN,
) on OBJECT | FIELD_DEFINITION
enum Role {
ADMIN
REVIEWER
USER
UNKNOWN
}
type Query {
add(x: Int, y: Int): Int @auth(requires: ADMIN)
}
Then running the query:
{
__schema {
queryType {
fields {
name
}
}
}
}
Returns the meta-fields
{
"data": {
"__schema": {
"queryType": {
"fields": [
{
"name": "add"
}
]
}
}
}
}
Hasura applies a different technique: it returns only the schema that applies its rules.
So, using this logic to the @auth
directive, we could filter the returned GraphQL schema.
The user's client will see only those query
and field
it should see.
This requires that the user adds to the client additional information (such as an auth token) to get access to all the GraphQL Schema and documentation.
I'm using the directive mode to perform some authentication rules and it works as expected for Queries and Mutations, but for Subscriptions, the authContext
is never triggered.
After the pubSub publishes a message, the applyPolicy is triggered and the authentication logic can block the content to be published, but that means we ma have a number of unauthorized connections consuming resources. Is that the expected behaviour? Is there any configuration I can use to avoid these connections?
During my tests, I'm using the following configuration for the mercuriusAuth. It is the same as described in the README, just added the log messages.
app.register(mercuriusAuth, {
authContext (context) {
context.log.debug('authContext triggered');
return {
identity: context.reply.request.headers['x-user']
}
},
async applyPolicy (authDirectiveAST, parent, args, context, info) {
context.log.debug('applyPolicy triggered');
return context.auth.identity === 'admin'
},
authDirective: 'auth'
})
When I check my logs, I can see only the applyPolicy triggered
message.
Hi, thanks again for setting this repo up - really appreciate it! :) I've got a fair bit of boilerplate code to push up so we should have everything required straight away and I have also made a start on the core functionality. Before I commit anything to the repo, I just wanted to double check what your preference is regarding the commit + review process for a pre-release repo like this?
Here's my initial plan in chronological order to get the first release ready:
Will there be any support to use with typescript i.e: proper typings?
At the moment a repeatable directive is executed once: the first directive only.
The 2nd one instead is not executed or processed.
const Fastify = require('fastify')
const mercurius = require('mercurius')
const mercuriusAuth = require('mercurius-auth')
const schema = `
directive @hasPermission (grant: String!) repeatable on OBJECT | FIELD_DEFINITION
type Message {
title: String!
notes: String @hasPermission(grant: "lv1") @hasPermission(grant: "lv2")
}
type Query {
publicMessages(org: String): [Message!]
}
`
const resolvers = {
Query: {
publicMessages: async () => {
return [
{ title: 'one', message: 'acme one', notes: 'acme-one' },
{ title: 'two', message: 'acme two', notes: 'acme-two' }
]
}
}
}
const app = Fastify()
app.register(mercurius, {
graphiql: true,
schema,
resolvers
})
app.register(mercuriusAuth, {
authContext: hasPermissionContext,
applyPolicy: hasPermissionPolicy,
namespace: 'authorization-filtering',
authDirective: 'hasPermission'
})
app.listen(3000)
function hasPermissionContext (context) {
return { permission: context.reply.request.headers['x-permission'].split(',') }
}
async function hasPermissionPolicy (authDirectiveAST, parent, args, context, info) {
const needed = authDirectiveAST.arguments.find(arg => arg.name.value === 'grant').value.value
const hasGrant = context.auth.permission.includes(needed)
if (!hasGrant) {
throw new Error(`Needed ${needed} grant`)
}
return true
}
I think the issue is on this line:
Line 29 in 16db4b5
find
should be filter
instead.
Changing the output to an array impacts to the getPolicy()
public API. I'm not sure about the impacts
ERROR: onGatewayReplaceSchema hook not supported! thrown
I have multiple federated services, and each schema looks similar to the following:
directive @auth(requires: String!) on OBJECT | FIELD_DEFINITION
extend type Query {
"""
Provide information about an account's balances
"""
inqAcctAvail(
clientId: Int!
acctId: String!
suffix: Int
): AcctAvail @auth(requires: "inqAcctAvail")
}
type AcctAvail @auth(requires: "inqAcctAvail") @key(fields: "clientId acctId suffix") {
clientId: Int
acctId: String
suffix: Int
"""
Available money from AAS realtime auth system
"""
availMoney: Float
"""
Available cash from AAS realtime auth system
"""
availCash: Float
}
My auth function looks like so:
app.register(mercuriusAuth, {
authContext(context) {
const {
reply: {
request: {
headers,
},
},
} = context;
if (headers.introspect === 'true') {
return {};
}
if (!headers['gql-auth']) {
throw new Error('Not authorized');
}
return {
identity: JSON.parse(context.reply.request.headers['gql-auth']),
};
},
async applyPolicy(
authDirectiveAST,
source,
{ clientId: argClientId },
{
auth: {
identity,
},
},
) {
const findArg = (arg, ast) => {
let result;
ast.arguments.forEach((a) => {
if (a.kind === 'Argument'
&& a.name.value === arg) {
result = a.value.value;
}
});
return result;
};
const requires = findArg('requires', authDirectiveAST);
let srcClientId;
if (source) {
srcClientId = source.clientId;
}
const clientId = argClientId || srcClientId;
if (!requires
|| (
process.env.NODE_ENV !== 'production'
&& identity.roles.includes(':admin')
)) {
return true;
}
if (!identity.roles.includes(`${pad(clientId, 4)}:${requires}`)) {
throw new Error('User does not have access');
}
return true;
},
authDirective: 'auth',
});
When testing my inqAcctAvail query, the auth works perfectly. However when testing the AcctAvail type through an entity call, the applyPolicy is not being triggered through my test, thus it is failing.
Test:
test('should throw error, entity not authorized', async (t) => {
const variables = {
representations: [
{
__typename: 'AcctAvail',
clientId: 5555,
acctId: '10000000001',
suffix: 0,
},
],
};
const query = '
query EntitiesQuery($representations: [_Any!]!) {
_entities(representations: $representations) {
__typename
... on AcctAvail {
clientId
suffix
acctId
}
}
}';
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query,
variables,
},
headers: {
'gql-auth': JSON.stringify({
roles: ['9999:fail'],
}),
},
});
const response = JSON.parse(res.body);
t.equal(response.errors ? response.errors[0] : null, 'User does not have access');
t.end();
});
Noticed the failing tests when fixing this PR: #73 and found that upon a fresh npm install
, some tests fail in the test file: test/introspection-filter-basics.js
. One example failure is as follows:
No idea indication as to why yet but I suspect a downstream module has updated it's behaviour. Will document findings in this issue before moving to a fix :)
Hello everyone!
When this plugin returns an error, of any type, it uses HTTP 500 as status code. In this case Apollo (for Angular) treat it as networkError instead of graphQlErrors, making it difficult to handle.
I'm doing something wrong?
Hi i'm fairly new to Fastify and Mercurius, I'm trying to setup a graphql api project for 1 of my side project. I saw there's a documentation to filter schema for auth policy directive, n it works great, but for my use case i prefer to use the external auth policy, is there a way to make external policy work for schema filtering, as i might need to expose the graphql playground for potential authenticated users. Here's a minimal snippets of my setup:
const schema = await loadSchema('src/schemas/*.graphql', {
loaders: [new GraphQLFileLoader()],
});
const options: MercuriusAuthOptions<any, any, MercuriusContext, TPolicy> = {
mode: 'external',
authContext(ctx): TAuthContext {
const { device, location, role, staff, token, venue } = ctx;
return { device, location, role, staff, token, venue };
},
async applyPolicy(policy, _parent, _args, ctx, _info) {
const isRolesPolicySatisfied = policy.roles?.length
? !!(ctx.auth!.role && policy.roles.includes(ctx.auth!.role.type as USER_ROLE))
: true;
const isUserRequiredPolicySatisfied = policy.isUserRequired ? !!ctx.auth!.user : true;
const isVenueRequiredPolicySatisfied = policy.isVenueRequired ? !!ctx.auth!.venue : true;
const isRoleRequiredPolicySatisfied = policy.isRoleRequired ? !!ctx.auth!.role : true;
const isLocationRequiredPolicySatisfied = policy.isLocationRequired ? !!ctx.auth!.location : true;
const isDeviceRequiredPolicySatisfied = policy.isDeviceRequired ? !!ctx.auth!.device : true;
const areAllConditionsSatisfied =
isRolesPolicySatisfied &&
isUserRequiredPolicySatisfied &&
isVenueRequiredPolicySatisfied &&
isRoleRequiredPolicySatisfied &&
isLocationRequiredPolicySatisfied &&
isDeviceRequiredPolicySatisfied;
return areAllConditionsSatisfied;
},
policy: {
// TODO: Add more policies
Query: {
getCurrentMenu: {
isVenueRequired: true,
},
},
Mutation: {},
Subscription: {},
Location: {
current_orders: {
roles: AUTHENTICATED_ROLES,
},
venue: {
roles: AUTHENTICATED_ROLES,
},
},
Menu: {
posCategories: {
roles: AUTHENTICATED_ROLES,
},
},
Category: {
posItems: {
roles: AUTHENTICATED_ROLES,
},
},
Item: {
posOptions: {
roles: AUTHENTICATED_ROLES,
},
},
},
};
type TPolicy = {
isRoleRequired?: boolean;
isUserRequired?: boolean;
isVenueRequired?: boolean;
isLocationRequired?: boolean;
isDeviceRequired?: boolean;
roles?: USER_ROLE[];
};
await fastify.register(mercurius, {
schema,
resolvers,
loaders,
ide: true,
graphiql: true,
path: '/graphql',
allowBatchedQueries: true,
queryDepth: 10,
jit: 1,
context: buildContext,
subscription: {
emitter,
context: buildSubscriptionContext,
},
validationRules: isRelease ? [NoSchemaIntrospectionCustomRule] : [],
});
await fastify.register(mercuriusAuth, options);
It would be great to support multiple directives:
@hasRole
@hasPermission
Example:
type Article {
id: ID!
title: String!
content: String! @hasPermission(resources: ["subscribed"])
}
type Query {
listArticles: [Article]!
}
type Mutation {
publishArticle(articleId: ID!): Article! @hasRole(role: "editor")
unpublishArticle(articleId: ID!):Boolean @hasPermission(resources: ["publish","delete"])
}
Doing so, it should be open to integrating external Identity Management such as:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.