@envelop/generic-auth
This plugin allows you to implement custom authentication flow by providing a custom user resolver based on the original HTTP request. The resolved user is injected into the GraphQL execution context
, and you can use it in your resolvers to fetch the current user.
The plugin also comes with an optional @auth
directive that can be added to your GraphQL schema and helps you to protect your GraphQL schema in a declarative way.
There are several possible flows for using this plugin (see below for setup examples):
- Option #1 - Complete Protection: protected the entire GraphQL schema from unauthenticated access.
- Option #2 - Manual Validation: the plugin will just resolve the user and injects it into the
context
without validating the user. - Option #3 - Automatic validation using GraphQL directives: Look for
@auth
directive and automatically protect specific GraphQL fields.
Getting Started
Start by installing the plugin:
yarn add @envelop/generic-auth
Then, define your authentication methods:
- Resolve your user from the request by implementing
resolveUserFn
:
Use this method to only extract the user from the context, with any custom code, for example:
import { ResolveUserFn } from '@envelop/generic-auth';
type UserType = {
id: string;
};
const resolveUserFn: ResolveUserFn<UserType> = async context => {
try {
const user = await context.authApi.authenticateUser(context.req.headers.authorization);
return user;
} catch (e) {
console.error('Failed to validate token');
return null;
}
};
- Define an optional validation method by implementing
validateUser
:
This method is optional; the default method will just verify the value returned by resolveUser
and throw an error in case of a false value (false | null | undefined
).
import { ValidateUserFn } from '@envelop/generic-auth';
const validateUser: ValidateUserFn<UserType> = async (user, context) => {
if (!user) {
throw new Error(`Unauthenticated!`);
}
};
Now, configure your plugin based on the mode you wish to use:
Option #1 - protect-all
This mode offers complete protection for the entire API. It protects your entire GraphQL schema by validating the user before executing the request. You can optionally skip auth validation for specific GraphQL fields by using the @skipAuth
directive.
To setup this mode, use the following config:
import { envelop } from '@envelop/core';
import { useGenericAuth, ResolveUserFn, ValidateUserFn } from '@envelop/generic-auth';
type UserType = {
id: string;
};
const resolveUserFn: ResolveUserFn<UserType> = async context => {
};
const validateUser: ValidateUserFn<UserType> = async (user, context) => {
};
const getEnveloped = envelop({
plugins: [
useGenericAuth({
resolveUserFn,
validateUser,
mode: 'protect-all',
}),
],
});
By default, we assume that you have the GraphQL directive definition as part of your GraphQL schema (directive @skipAuth on FIELD_DEFINITION
).
Then, in your GraphQL schema SDL, you can add @skipAuth
directive to your fields, and the validateUser
will not get called while resolving that specific field:
type Query {
me: User!
protectedField: String
publicField: String @skipAuth
}
You can apply that directive to any GraphQL field
definition, not only to root fields.
If you are using a different directive for authentication, you can pass authDirectiveName
configuration to customize it.
Option #2 - resolve-only
This mode uses the plugin to inject the authenticated user into the context
, and later you can verify it in your resolvers.
import { envelop } from '@envelop/core';
import { useGenericAuth, ResolveUserFn, ValidateUserFn } from '@envelop/generic-auth';
type UserType = {
id: string;
};
const resolveUserFn: ResolveUserFn<UserType> = async context => {
};
const validateUser: ValidateUserFn<UserType> = async (user, context) => {
};
const getEnveloped = envelop({
plugins: [
useGenericAuth({
resolveUserFn,
validateUser,
mode: 'resolve-only',
}),
],
});
Then, in your resolvers, you can execute the check method based on your needs:
const resolvers = {
Query: {
me: async (root, args, context) => {
await context.validateUser();
const currentUser = context.currentUser;
return currentUser;
},
},
};
Option #3 - protect-auth-directive
This mode is similar to option #2, but it uses @auth
SDL directive to automatically protect specific GraphQL fields.
import { envelop } from '@envelop/core';
import { useGenericAuth, ResolveUserFn, ValidateUserFn } from '@envelop/generic-auth';
type UserType = {
id: string;
};
const resolveUserFn: ResolveUserFn<UserType> = async context => {
};
const validateUser: ValidateUserFn<UserType> = async (user, context) => {
};
const getEnveloped = envelop({
plugins: [
useGenericAuth({
resolveUserFn,
validateUser,
mode: 'protect-auth-directive',
}),
],
});
By default, we assume that you have the GraphQL directive definition as part of your GraphQL schema (directive @auth on FIELD_DEFINITION
).
Then, in your GraphQL schema SDL, you can add @auth
directive to your fields, and the validateUser
will get called only while resolving that specific field:
type Query {
me: User! @auth
protectedField: String @auth
}
You can apply that directive to any GraphQL field
definition, not only to root fields.
If you are using a different directive for authentication, you can pass authDirectiveName
configuration to customize it.
Extend authentication with custom directive logic
You can also specify a custom validateUser
function and get access to the GraphQLResolveInfo
object while using the protect-auth-directive
mode:
import { ValidateUserFn } from '@envelop/generic-auth';
const validateUser: ValidateUserFn<UserType> = async (user, context, { root, args, context, info }) => {
if (!user) {
throw new Error(`Unauthenticated!`);
}
};
And it's also possible to add custom parameters to your @auth
directive. Here's an example for adding role-aware authentication:
enum Role {
ADMIN
MEMBER
}
directive @auth(role: Role!) on FIELD_DEFINITION
Then, you use the directiveNode
parameter to check the arguments:
import { ValidateUserFn } from '@envelop/generic-auth';
const validateUser: ValidateUserFn<UserType> = async (user, context, { root, args, context, info }, directiveNode) => {
if (!user) {
throw new Error(`Unauthenticated!`);
}
const valueNode = directiveNode.arguments.find(arg => arg.name.value === 'role').value as EnumValueNode;
const role = valueNode.value;
if (role !== user.role) {
throw new Error(`No permissions!`);
}
};