@envelop/generic-auth
Advanced tools
Comparing version 0.0.1-alpha-ca52e73.0 to 0.0.1
@@ -5,10 +5,81 @@ 'use strict'; | ||
function getDirective(info, name) { | ||
const { parentType, fieldName, schema } = info; | ||
const schemaType = schema.getType(parentType.name); | ||
const field = schemaType.getFields()[fieldName]; | ||
const astNode = field.astNode; | ||
const directives = astNode.directives; | ||
const authDirective = directives.find(d => d.name.value === name); | ||
return authDirective || null; | ||
} | ||
class UnauthenticatedError extends Error { | ||
} | ||
const DIRECTIVE_SDL = /* GraphQL */ ` | ||
directive @auth on FIELD_DEFINITION | ||
`; | ||
function defaultValidateFn(user, contextType) { | ||
if (!user) { | ||
throw new UnauthenticatedError('Unauthenticated!'); | ||
} | ||
} | ||
const useGenericAuth = (options) => { | ||
const fieldName = options.contextFieldName || 'currentUser'; | ||
const validateUser = options.validateUser || defaultValidateFn; | ||
if (options.mode === 'protect-all') { | ||
return { | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
await validateUser(user, context); | ||
extendContext({ | ||
[fieldName]: user, | ||
}); | ||
}, | ||
}; | ||
} | ||
else if (options.mode === 'resolve-only') { | ||
return { | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
extendContext({ | ||
[fieldName]: user, | ||
validateUser: () => validateUser(user, context), | ||
}); | ||
}, | ||
}; | ||
} | ||
else if (options.mode === 'protect-auth-directive') { | ||
return { | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
extendContext({ | ||
[fieldName]: user, | ||
validateUser: () => validateUser(user, context), | ||
}); | ||
}, | ||
onExecute() { | ||
return { | ||
async onResolverCalled({ args, root, context, info }) { | ||
const authDirectiveNode = getDirective(info, options.authDirectiveName || 'auth'); | ||
if (authDirectiveNode) { | ||
await context.validateUser(context[fieldName], context, { | ||
info, | ||
context: context, | ||
args, | ||
root, | ||
}, authDirectiveNode); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; | ||
} | ||
return {}; | ||
}; | ||
exports.DIRECTIVE_SDL = DIRECTIVE_SDL; | ||
exports.UnauthenticatedError = UnauthenticatedError; | ||
exports.defaultValidateFn = defaultValidateFn; | ||
exports.getDirective = getDirective; | ||
exports.useGenericAuth = useGenericAuth; | ||
//# sourceMappingURL=index.cjs.js.map |
@@ -1,5 +0,57 @@ | ||
import { Plugin } from '@envelop/types'; | ||
import { DefaultContext, Plugin } from '@envelop/types'; | ||
import { DirectiveNode, GraphQLResolveInfo } from 'graphql'; | ||
export * from './utils'; | ||
export declare class UnauthenticatedError extends Error { | ||
} | ||
export declare type GenericAuthPluginOptions = {}; | ||
export declare const useGenericAuth: (options: GenericAuthPluginOptions) => Plugin; | ||
export declare type ResolveUserFn<UserType, ContextType = unknown> = (context: ContextType) => null | UserType | Promise<UserType | null>; | ||
export declare type ValidateUserFn<UserType, ContextType = unknown> = (user: UserType, context: ContextType, resolverInfo?: { | ||
root: unknown; | ||
args: Record<string, unknown>; | ||
context: ContextType; | ||
info: GraphQLResolveInfo; | ||
}, directiveNode?: DirectiveNode) => void | Promise<void>; | ||
export declare const DIRECTIVE_SDL = "\n directive @auth on FIELD_DEFINITION\n"; | ||
export declare type GenericAuthPluginOptions<UserType, ContextType> = { | ||
/** | ||
* Here you can implement any custom sync/async code, and use the context built so far in Envelop and the HTTP request | ||
* to find the current user. | ||
* Common practice is to use a JWT token here, validate it, and use the payload as-is, or fetch the user from an external services. | ||
* Make sure to either return `null` or the user object. | ||
*/ | ||
resolveUserFn: ResolveUserFn<UserType, ContextType>; | ||
/** | ||
* Here you can implement any custom to check if the user is valid and have access to the server. | ||
* This method is being triggered in different flows, besed on the mode you chose to implement. | ||
*/ | ||
validateUser?: ValidateUserFn<UserType, ContextType>; | ||
/** | ||
* Overrides the default field name for injecting the user into the execution `context`. | ||
* @default currentUser | ||
*/ | ||
contextFieldName?: 'currentUser' | string; | ||
} & ({ | ||
/** | ||
* This mode offers complete protection for the entire API. | ||
* It protects your entire GraphQL schema, by validating the user before executing the request. | ||
*/ | ||
mode: 'protect-all'; | ||
} | { | ||
/** | ||
* Just resolves the user and inject to authenticated user into the `context`. | ||
* User validation needs to be implemented by you, in your resolvers. | ||
*/ | ||
mode: 'resolve-only'; | ||
} | { | ||
/** | ||
* resolves the user and inject to authenticated user into the `context`. | ||
* And checks for `@auth` directives usages to run validation automatically. | ||
*/ | ||
mode: 'protect-auth-directive'; | ||
/** | ||
* Overrides the default directive name | ||
* @default auth | ||
*/ | ||
authDirectiveName?: 'auth' | string; | ||
}); | ||
export declare function defaultValidateFn<UserType, ContextType>(user: UserType, contextType: ContextType): void; | ||
export declare const useGenericAuth: <UserType extends {}, ContextType extends DefaultContext = DefaultContext>(options: GenericAuthPluginOptions<UserType, ContextType>) => Plugin<ContextType>; |
@@ -0,8 +1,76 @@ | ||
function getDirective(info, name) { | ||
const { parentType, fieldName, schema } = info; | ||
const schemaType = schema.getType(parentType.name); | ||
const field = schemaType.getFields()[fieldName]; | ||
const astNode = field.astNode; | ||
const directives = astNode.directives; | ||
const authDirective = directives.find(d => d.name.value === name); | ||
return authDirective || null; | ||
} | ||
class UnauthenticatedError extends Error { | ||
} | ||
const DIRECTIVE_SDL = /* GraphQL */ ` | ||
directive @auth on FIELD_DEFINITION | ||
`; | ||
function defaultValidateFn(user, contextType) { | ||
if (!user) { | ||
throw new UnauthenticatedError('Unauthenticated!'); | ||
} | ||
} | ||
const useGenericAuth = (options) => { | ||
const fieldName = options.contextFieldName || 'currentUser'; | ||
const validateUser = options.validateUser || defaultValidateFn; | ||
if (options.mode === 'protect-all') { | ||
return { | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
await validateUser(user, context); | ||
extendContext({ | ||
[fieldName]: user, | ||
}); | ||
}, | ||
}; | ||
} | ||
else if (options.mode === 'resolve-only') { | ||
return { | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
extendContext({ | ||
[fieldName]: user, | ||
validateUser: () => validateUser(user, context), | ||
}); | ||
}, | ||
}; | ||
} | ||
else if (options.mode === 'protect-auth-directive') { | ||
return { | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
extendContext({ | ||
[fieldName]: user, | ||
validateUser: () => validateUser(user, context), | ||
}); | ||
}, | ||
onExecute() { | ||
return { | ||
async onResolverCalled({ args, root, context, info }) { | ||
const authDirectiveNode = getDirective(info, options.authDirectiveName || 'auth'); | ||
if (authDirectiveNode) { | ||
await context.validateUser(context[fieldName], context, { | ||
info, | ||
context: context, | ||
args, | ||
root, | ||
}, authDirectiveNode); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; | ||
} | ||
return {}; | ||
}; | ||
export { UnauthenticatedError, useGenericAuth }; | ||
export { DIRECTIVE_SDL, UnauthenticatedError, defaultValidateFn, getDirective, useGenericAuth }; | ||
//# sourceMappingURL=index.esm.js.map |
{ | ||
"name": "@envelop/generic-auth", | ||
"version": "0.0.1-alpha-ca52e73.0", | ||
"version": "0.0.1", | ||
"sideEffects": false, | ||
@@ -5,0 +5,0 @@ "peerDependencies": { |
114
README.md
## `@envelop/generic-auth` | ||
This plugin allow you to implement custom authentication flow, by providing a custom user extraction based on the original HTTP request. The extract user is being injected into the GraphQL execution `context` and you can use it in your resolvers to fetch the current user. | ||
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 declerative way. | ||
> 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**: to protect your entire GraphQL schema, by validating the user before executing the request. | ||
- **Option #2 - Fine-grain Protection**: Use the plugin to inject to authenticated user into the `context`, and later you can verify it in your resolvers. | ||
- **Option #3 - Fine-grain Protection with Directives**: Uses `@auth` SDL directive to automatically protect specific GraphQL fields. | ||
- **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. | ||
@@ -23,3 +23,3 @@ ## Getting Started | ||
1. Extract your user from the request by implementing `extractUser`: | ||
1. Resolve your user from the request by implementing `resolveUserFn`: | ||
@@ -29,3 +29,3 @@ Use this method to only extract the user from the context, with any custom code, for example: | ||
```ts | ||
import { ExtractUserFn } from '@envelop/generic-auth'; | ||
import { ResolveUserFn } from '@envelop/generic-auth'; | ||
@@ -36,3 +36,3 @@ type UserType = { | ||
const extractUserFn: ExtractUserFn<UserType> = async context => { | ||
const resolveUserFn: ResolveUserFn<UserType> = async context => { | ||
// Here you can implement any custom sync/async code, and use the context built so far in Envelop and the HTTP request | ||
@@ -57,3 +57,3 @@ // to find the current user. | ||
This method is optional, by default, it will just check the value returned by `extractUserFn` and throw an error in case of a falsey value. | ||
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`). | ||
@@ -67,2 +67,4 @@ ```ts | ||
// If you are using the `protect-auth-directive` mode, you'll also get 2 additional parameters: the resolver parameters as object and the DirectiveNode of the auth directive. | ||
if (!user) { | ||
@@ -74,7 +76,7 @@ throw new Error(`Unauthenticated!`); | ||
Now, configure your plugin based on the mode you with to use: | ||
Now, configure your plugin based on the mode you wish to use: | ||
#### Option #1 - `authenticate-all` | ||
#### 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. | ||
This mode offers complete protection for the entire API. It protects your entire GraphQL schema by validating the user before executing the request. | ||
@@ -85,3 +87,3 @@ To setup this mode, use the following config: | ||
import { envelop } from '@envelop/core'; | ||
import { useGenericAuth, ExtractUserFn, ValidateUserFn } from '@envelop/generic-auth'; | ||
import { useGenericAuth, resolveUser, ValidateUserFn } from '@envelop/generic-auth'; | ||
@@ -91,3 +93,3 @@ type UserType = { | ||
}; | ||
const extractUserFn: ExtractUserFn<UserType> = async context => { | ||
const resolveUserFn: ResolveUserFn<UserType> = async context => { | ||
/* ... */ | ||
@@ -103,5 +105,5 @@ }; | ||
useGenericAuth({ | ||
extractUserFn, | ||
resolveUser, | ||
validateUser, | ||
mode: 'authenticate-all', | ||
mode: 'protect-all', | ||
}), | ||
@@ -112,9 +114,9 @@ ], | ||
#### Option #2 - `just-extract` | ||
#### Option #2 - `resolve-only` | ||
This mode uses the plugin to inject to authenticated user into the `context`, and later you can verify it in your resolvers. | ||
This mode uses the plugin to inject the authenticated user into the `context`, and later you can verify it in your resolvers. | ||
```ts | ||
import { envelop } from '@envelop/core'; | ||
import { useGenericAuth, ExtractUserFn, ValidateUserFn } from '@envelop/generic-auth'; | ||
import { useGenericAuth, resolveUser, ValidateUserFn } from '@envelop/generic-auth'; | ||
@@ -124,3 +126,3 @@ type UserType = { | ||
}; | ||
const extractUserFn: ExtractUserFn<UserType> = async context => { | ||
const resolveUserFn: ResolveUserFn<UserType> = async context => { | ||
/* ... */ | ||
@@ -136,5 +138,5 @@ }; | ||
useGenericAuth({ | ||
extractUserFn, | ||
resolveUser, | ||
validateUser, | ||
mode: 'just-extract', | ||
mode: 'resolve-only', | ||
}), | ||
@@ -160,3 +162,3 @@ ], | ||
#### Option #3 - `auth-directive` | ||
#### Option #3 - `protect-auth-directive` | ||
@@ -167,3 +169,3 @@ 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, ExtractUserFn, ValidateUserFn } from '@envelop/generic-auth'; | ||
import { useGenericAuth, resolveUser, ValidateUserFn } from '@envelop/generic-auth'; | ||
@@ -173,3 +175,3 @@ type UserType = { | ||
}; | ||
const extractUserFn: ExtractUserFn<UserType> = async context => { | ||
const resolveUserFn: ResolveUserFn<UserType> = async context => { | ||
/* ... */ | ||
@@ -185,5 +187,5 @@ }; | ||
useGenericAuth({ | ||
extractUserFn, | ||
resolveUser, | ||
validateUser, | ||
mode: 'auth-directive', | ||
mode: 'protect-auth-directive', | ||
}), | ||
@@ -194,2 +196,4 @@ ], | ||
> 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: | ||
@@ -201,6 +205,58 @@ | ||
protectedField: String @auth | ||
publicField: String | ||
# publicField: String | ||
} | ||
``` | ||
> You can apply that directive to any GraphQL `field` definition. | ||
> 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: | ||
```ts | ||
import { ValidateUserFn } from '@envelop/generic-auth'; | ||
const validateUser: ValidateUserFn<UserType> = async (user, context, { root, args, context, info }) => { | ||
// Now you can use the 3rd parameter to implement custom logic for user validation, with access | ||
// to the resolver data and information. | ||
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: | ||
```graphql | ||
enum Role { | ||
ADMIN | ||
MEMBER | ||
} | ||
directive @auth(role: Role!) on FIELD_DEFINITION | ||
``` | ||
Then, you use the `directiveNode` parameter to check the arguments: | ||
```ts | ||
import { ValidateUserFn } from '@envelop/generic-auth'; | ||
const validateUser: ValidateUserFn<UserType> = async (user, context, { root, args, context, info }, directiveNode) => { | ||
// Now you can use the 3rd parameter to implement custom logic for user validation, with access | ||
// to the resolver data and information. | ||
if (!user) { | ||
throw new Error(`Unauthenticated!`); | ||
} | ||
const valueNode = authDirectiveNode.arguments.find(arg => arg.name.value === 'role').value as EnumValueNode; | ||
const role = valueNode.value; | ||
if (role !== user.role) { | ||
throw new Error(`No permissions!`); | ||
} | ||
}; | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
27649
8
213
245
1