Code Monkey home page Code Monkey logo

auth's People

Contributors

artecoop avatar bugs5382 avatar dependabot[bot] avatar dragonfriend0013 avatar eomm avatar fdawgs avatar jonnydgreen avatar lity avatar mcollina avatar ninnjak avatar sameer-coder avatar sharmapukar217 avatar tomastm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

auth's Issues

Improving the documentation

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:

  • Does the docs structure make sense? What could be improved?
  • Is the README sufficient? Is it missing any key information?
  • Do we want a best practices/examples section?
  • Anything else that is missing or think should be included?

Keen to hear everyone's thoughts on this!

How I can set custom directives to the current field?

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!

Directive filterSchema: true -- directive to prevent "message"

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?

Tests fail on the latest main for Node.js 20.x

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 :)

Auth directive on Union type ignores the applyPolicy function

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

feature: filtered schema

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.

Subscription authentication

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.

Review process and initial plan

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:

  • Get plumbing code in (it's pretty much ready to go now)
  • Core functionality for the auth plugin with 100% test coverage
  • Examples
  • Documentation
  • Typings
  • Release package

Typescript support

Will there be any support to use with typescript i.e: proper typings?

support repeatable directive

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:

const authDirective = astNode.directives.find(directive => directive.name.value === this[kAuthDirective])

find should be filter instead.
Changing the output to an array impacts to the getPolicy() public API. I'm not sure about the impacts

auth directive on type not being processed, federated service

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();
});

Tests fail upon a fresh install

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:

Screenshot 2022-04-27 at 10 37 56

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 :)

Failed authorization throws Http 500

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?

Is there a way to make schema filtering work with external policy?

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);

feature: multiple directive

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:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.