@envelop/generic-auth
Advanced tools
Comparing version 8.0.0-alpha-20240812113941-0c30c454 to 8.0.0-alpha-20240812120620-e6a7b402
108
cjs/index.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.useGenericAuth = exports.defaultProtectSingleValidateFn = exports.defaultProtectAllValidateFn = exports.createUnauthenticatedError = exports.SKIP_AUTH_DIRECTIVE_SDL = exports.DIRECTIVE_SDL = void 0; | ||
const types_1 = require("util/types"); | ||
exports.useGenericAuth = exports.defaultExtractScopes = exports.defaultProtectSingleValidateFn = exports.defaultProtectAllValidateFn = exports.createUnauthenticatedError = exports.POLICY_DIRECTIVE_SDL = exports.REQUIRES_SCOPES_DIRECTIVE_SDL = exports.SKIP_AUTH_DIRECTIVE_SDL = exports.DIRECTIVE_SDL = void 0; | ||
const graphql_1 = require("graphql"); | ||
@@ -14,4 +13,10 @@ const extended_validation_1 = require("@envelop/extended-validation"); | ||
`; | ||
exports.REQUIRES_SCOPES_DIRECTIVE_SDL = ` | ||
directive @requiresScopes(scopes: [[String!]!]!) on FIELD_DEFINITION | ||
`; | ||
exports.POLICY_DIRECTIVE_SDL = ` | ||
directive @policy(policies: [String!]!) on FIELD_DEFINITION | ||
`; | ||
function createUnauthenticatedError(params) { | ||
return (0, utils_1.createGraphQLError)('Unauthorized field or type', { | ||
return (0, utils_1.createGraphQLError)(params?.message ?? 'Unauthorized field or type', { | ||
nodes: params?.fieldNode ? [params.fieldNode] : undefined, | ||
@@ -35,4 +40,41 @@ path: params?.path, | ||
} | ||
return validateScopes(params); | ||
} | ||
exports.defaultProtectAllValidateFn = defaultProtectAllValidateFn; | ||
function areRolesValid(requiredRoles, userRoles) { | ||
for (const roles of requiredRoles) { | ||
if (roles.every(role => userRoles.includes(role))) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
function validateScopes(params) { | ||
if (params.typeScopes && !areRolesValid(params.typeScopes, params.userScopes)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
if (params.fieldScopes && !areRolesValid(params.fieldScopes, params.userScopes)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
} | ||
function validatePolicies(params) { | ||
if (params.typePolicies && !areRolesValid(params.typePolicies, params.userPolicies)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
if (params.fieldPolicies && !areRolesValid(params.fieldPolicies, params.userPolicies)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
} | ||
function defaultProtectSingleValidateFn(params) { | ||
@@ -45,8 +87,27 @@ if (params.user == null && (params.fieldAuthArgs || params.typeAuthArgs)) { | ||
} | ||
const error = validateScopes(params); | ||
if (error) { | ||
return error; | ||
} | ||
return validatePolicies(params); | ||
} | ||
exports.defaultProtectSingleValidateFn = defaultProtectSingleValidateFn; | ||
function defaultExtractScopes(user) { | ||
if (user != null && typeof user === 'object' && 'scope' in user) { | ||
if (typeof user.scope === 'string') { | ||
return user.scope.split(' '); | ||
} | ||
if (Array.isArray(user.scope)) { | ||
return user.scope; | ||
} | ||
} | ||
return []; | ||
} | ||
exports.defaultExtractScopes = defaultExtractScopes; | ||
const useGenericAuth = (options) => { | ||
const contextFieldName = options.contextFieldName || 'currentUser'; | ||
if (options.mode === 'protect-all' || options.mode === 'protect-granular') { | ||
const directiveOrExtensionFieldName = options.authDirectiveName ?? (options.mode === 'protect-all' ? 'skipAuth' : 'authenticated'); | ||
const authDirectiveName = options.authDirectiveName ?? (options.mode === 'protect-all' ? 'skipAuth' : 'authenticated'); | ||
const requiresScopesDirectiveName = options.scopesDirectiveName ?? 'requiresScopes'; | ||
const policyDirectiveName = options.policyDirectiveName ?? 'policy'; | ||
const validateUser = options.validateUser ?? | ||
@@ -56,3 +117,5 @@ (options.mode === 'protect-all' | ||
: defaultProtectSingleValidateFn); | ||
const extractScopes = options.extractScopes ?? defaultExtractScopes; | ||
const rejectUnauthenticated = 'rejectUnauthenticated' in options ? options.rejectUnauthenticated !== false : true; | ||
const policiesByContext = new WeakMap(); | ||
return { | ||
@@ -85,5 +148,11 @@ onPluginInit({ addPlugin }) { | ||
const typeDirectives = parentType && (0, utils_1.getDirectiveExtensions)(parentType, schema); | ||
const typeAuthArgs = typeDirectives[directiveOrExtensionFieldName]?.[0]; | ||
const typeAuthArgs = typeDirectives[authDirectiveName]?.[0]; | ||
const typeScopes = typeDirectives[requiresScopesDirectiveName]?.[0]?.scopes; | ||
const typePolicies = typeDirectives[policyDirectiveName]?.[0]?.policies; | ||
const fieldDirectives = (0, utils_1.getDirectiveExtensions)(field, schema); | ||
const fieldAuthArgs = fieldDirectives[directiveOrExtensionFieldName]?.[0]; | ||
const fieldAuthArgs = fieldDirectives[authDirectiveName]?.[0]; | ||
const fieldScopes = fieldDirectives[requiresScopesDirectiveName]?.[0]?.scopes; | ||
const fieldPolicies = fieldDirectives[policyDirectiveName]?.[0]?.policies; | ||
const userScopes = extractScopes(user); | ||
const policies = policiesByContext.get(context) ?? []; | ||
const resolvePath = []; | ||
@@ -101,2 +170,4 @@ let curr = args.document; | ||
parentType, | ||
typeScopes, | ||
typePolicies, | ||
typeAuthArgs, | ||
@@ -108,3 +179,7 @@ typeDirectives, | ||
fieldAuthArgs, | ||
fieldScopes, | ||
fieldPolicies, | ||
userScopes, | ||
path: resolvePath, | ||
userPolicies: policies, | ||
}); | ||
@@ -156,20 +231,13 @@ }; | ||
}, | ||
onContextBuilding({ context, extendContext }) { | ||
const user$ = options.resolveUserFn(context); | ||
if ((0, types_1.isPromise)(user$)) { | ||
return user$.then(user => { | ||
// @ts-expect-error - Fix this | ||
if (context[contextFieldName] !== user) { | ||
// @ts-expect-error - Fix this | ||
extendContext({ | ||
[contextFieldName]: user, | ||
}); | ||
} | ||
}); | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
if (options.extractPolicies) { | ||
const policies = await options.extractPolicies(user, context); | ||
policiesByContext.set(context, policies); | ||
} | ||
// @ts-expect-error - Fix this | ||
if (context[contextFieldName] !== user$) { | ||
if (context[contextFieldName] !== user) { | ||
// @ts-expect-error - Fix this | ||
extendContext({ | ||
[contextFieldName]: user$, | ||
[contextFieldName]: user, | ||
}); | ||
@@ -176,0 +244,0 @@ } |
105
esm/index.js
@@ -1,2 +0,1 @@ | ||
import { isPromise } from 'util/types'; | ||
import { getNamedType, isInterfaceType, isIntrospectionType, isObjectType, isUnionType, } from 'graphql'; | ||
@@ -11,4 +10,10 @@ import { useExtendedValidation } from '@envelop/extended-validation'; | ||
`; | ||
export const REQUIRES_SCOPES_DIRECTIVE_SDL = /* GraphQL */ ` | ||
directive @requiresScopes(scopes: [[String!]!]!) on FIELD_DEFINITION | ||
`; | ||
export const POLICY_DIRECTIVE_SDL = /* GraphQL */ ` | ||
directive @policy(policies: [String!]!) on FIELD_DEFINITION | ||
`; | ||
export function createUnauthenticatedError(params) { | ||
return createGraphQLError('Unauthorized field or type', { | ||
return createGraphQLError(params?.message ?? 'Unauthorized field or type', { | ||
nodes: params?.fieldNode ? [params.fieldNode] : undefined, | ||
@@ -31,3 +36,40 @@ path: params?.path, | ||
} | ||
return validateScopes(params); | ||
} | ||
function areRolesValid(requiredRoles, userRoles) { | ||
for (const roles of requiredRoles) { | ||
if (roles.every(role => userRoles.includes(role))) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
function validateScopes(params) { | ||
if (params.typeScopes && !areRolesValid(params.typeScopes, params.userScopes)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
if (params.fieldScopes && !areRolesValid(params.fieldScopes, params.userScopes)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
} | ||
function validatePolicies(params) { | ||
if (params.typePolicies && !areRolesValid(params.typePolicies, params.userPolicies)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
if (params.fieldPolicies && !areRolesValid(params.fieldPolicies, params.userPolicies)) { | ||
return createUnauthenticatedError({ | ||
fieldNode: params.fieldNode, | ||
path: params.path, | ||
}); | ||
} | ||
} | ||
export function defaultProtectSingleValidateFn(params) { | ||
@@ -40,7 +82,25 @@ if (params.user == null && (params.fieldAuthArgs || params.typeAuthArgs)) { | ||
} | ||
const error = validateScopes(params); | ||
if (error) { | ||
return error; | ||
} | ||
return validatePolicies(params); | ||
} | ||
export function defaultExtractScopes(user) { | ||
if (user != null && typeof user === 'object' && 'scope' in user) { | ||
if (typeof user.scope === 'string') { | ||
return user.scope.split(' '); | ||
} | ||
if (Array.isArray(user.scope)) { | ||
return user.scope; | ||
} | ||
} | ||
return []; | ||
} | ||
export const useGenericAuth = (options) => { | ||
const contextFieldName = options.contextFieldName || 'currentUser'; | ||
if (options.mode === 'protect-all' || options.mode === 'protect-granular') { | ||
const directiveOrExtensionFieldName = options.authDirectiveName ?? (options.mode === 'protect-all' ? 'skipAuth' : 'authenticated'); | ||
const authDirectiveName = options.authDirectiveName ?? (options.mode === 'protect-all' ? 'skipAuth' : 'authenticated'); | ||
const requiresScopesDirectiveName = options.scopesDirectiveName ?? 'requiresScopes'; | ||
const policyDirectiveName = options.policyDirectiveName ?? 'policy'; | ||
const validateUser = options.validateUser ?? | ||
@@ -50,3 +110,5 @@ (options.mode === 'protect-all' | ||
: defaultProtectSingleValidateFn); | ||
const extractScopes = options.extractScopes ?? defaultExtractScopes; | ||
const rejectUnauthenticated = 'rejectUnauthenticated' in options ? options.rejectUnauthenticated !== false : true; | ||
const policiesByContext = new WeakMap(); | ||
return { | ||
@@ -79,5 +141,11 @@ onPluginInit({ addPlugin }) { | ||
const typeDirectives = parentType && getDirectiveExtensions(parentType, schema); | ||
const typeAuthArgs = typeDirectives[directiveOrExtensionFieldName]?.[0]; | ||
const typeAuthArgs = typeDirectives[authDirectiveName]?.[0]; | ||
const typeScopes = typeDirectives[requiresScopesDirectiveName]?.[0]?.scopes; | ||
const typePolicies = typeDirectives[policyDirectiveName]?.[0]?.policies; | ||
const fieldDirectives = getDirectiveExtensions(field, schema); | ||
const fieldAuthArgs = fieldDirectives[directiveOrExtensionFieldName]?.[0]; | ||
const fieldAuthArgs = fieldDirectives[authDirectiveName]?.[0]; | ||
const fieldScopes = fieldDirectives[requiresScopesDirectiveName]?.[0]?.scopes; | ||
const fieldPolicies = fieldDirectives[policyDirectiveName]?.[0]?.policies; | ||
const userScopes = extractScopes(user); | ||
const policies = policiesByContext.get(context) ?? []; | ||
const resolvePath = []; | ||
@@ -95,2 +163,4 @@ let curr = args.document; | ||
parentType, | ||
typeScopes, | ||
typePolicies, | ||
typeAuthArgs, | ||
@@ -102,3 +172,7 @@ typeDirectives, | ||
fieldAuthArgs, | ||
fieldScopes, | ||
fieldPolicies, | ||
userScopes, | ||
path: resolvePath, | ||
userPolicies: policies, | ||
}); | ||
@@ -150,20 +224,13 @@ }; | ||
}, | ||
onContextBuilding({ context, extendContext }) { | ||
const user$ = options.resolveUserFn(context); | ||
if (isPromise(user$)) { | ||
return user$.then(user => { | ||
// @ts-expect-error - Fix this | ||
if (context[contextFieldName] !== user) { | ||
// @ts-expect-error - Fix this | ||
extendContext({ | ||
[contextFieldName]: user, | ||
}); | ||
} | ||
}); | ||
async onContextBuilding({ context, extendContext }) { | ||
const user = await options.resolveUserFn(context); | ||
if (options.extractPolicies) { | ||
const policies = await options.extractPolicies(user, context); | ||
policiesByContext.set(context, policies); | ||
} | ||
// @ts-expect-error - Fix this | ||
if (context[contextFieldName] !== user$) { | ||
if (context[contextFieldName] !== user) { | ||
// @ts-expect-error - Fix this | ||
extendContext({ | ||
[contextFieldName]: user$, | ||
[contextFieldName]: user, | ||
}); | ||
@@ -170,0 +237,0 @@ } |
{ | ||
"name": "@envelop/generic-auth", | ||
"version": "8.0.0-alpha-20240812113941-0c30c454", | ||
"version": "8.0.0-alpha-20240812120620-e6a7b402", | ||
"sideEffects": false, | ||
@@ -10,3 +10,3 @@ "peerDependencies": { | ||
"dependencies": { | ||
"@envelop/extended-validation": "4.1.0-alpha-20240812113941-0c30c454", | ||
"@envelop/extended-validation": "4.1.0-alpha-20240812120620-e6a7b402", | ||
"@graphql-tools/utils": "^10.5.1", | ||
@@ -13,0 +13,0 @@ "tslib": "^2.5.0" |
@@ -16,2 +16,6 @@ import { ExecutionArgs, FieldNode, GraphQLError, GraphQLField, GraphQLInterfaceType, GraphQLObjectType } from 'graphql'; | ||
typeDirectives?: ReturnType<typeof getDirectiveExtensions>; | ||
/** Scopes that type requires */ | ||
typeScopes?: string[][]; | ||
/** Policies that type requires */ | ||
typePolicies?: string[][]; | ||
/** The object field */ | ||
@@ -23,2 +27,10 @@ field: GraphQLField<any, any>; | ||
fieldDirectives?: ReturnType<typeof getDirectiveExtensions>; | ||
/** Scopes that field requires */ | ||
fieldScopes?: string[][]; | ||
/** Policies that field requires */ | ||
fieldPolicies?: string[][]; | ||
/** Extracted scopes from the user object */ | ||
userScopes: string[]; | ||
/** Policies for the user */ | ||
userPolicies: string[]; | ||
/** The args passed to the execution function (including operation context and variables) **/ | ||
@@ -32,2 +44,4 @@ executionArgs: ExecutionArgs; | ||
export declare const SKIP_AUTH_DIRECTIVE_SDL = "\n directive @skipAuth on FIELD_DEFINITION\n"; | ||
export declare const REQUIRES_SCOPES_DIRECTIVE_SDL = "\n directive @requiresScopes(scopes: [[String!]!]!) on FIELD_DEFINITION\n"; | ||
export declare const POLICY_DIRECTIVE_SDL = "\n directive @policy(policies: [String!]!) on FIELD_DEFINITION\n"; | ||
export type GenericAuthPluginOptions<UserType extends {} = {}, ContextType = DefaultContext, CurrentUserKey extends string = 'currentUser'> = { | ||
@@ -46,2 +60,24 @@ /** | ||
contextFieldName?: CurrentUserKey; | ||
/** | ||
* Overrides the default directive name for marking a field that requires specific scopes. | ||
* | ||
* @default requiresScopes | ||
*/ | ||
scopesDirectiveName?: 'requiresScopes'; | ||
/** | ||
* Extracts the scopes from the user object. | ||
* | ||
* @default defaultExtractScopes | ||
*/ | ||
extractScopes?(user: UserType): string[]; | ||
/** | ||
* Overrides the default directive name for @policy directive | ||
* | ||
* @default policy | ||
*/ | ||
policyDirectiveName?: string; | ||
/** | ||
* Extracts the policies for the user object. | ||
*/ | ||
extractPolicies?(user: UserType, context: ContextType): PromiseOrValue<string[]>; | ||
} & ({ | ||
@@ -102,4 +138,5 @@ /** | ||
export declare function defaultProtectSingleValidateFn<UserType>(params: ValidateUserFnParams<UserType>): void | GraphQLError; | ||
export declare function defaultExtractScopes<UserType>(user: UserType): string[]; | ||
export declare const useGenericAuth: <UserType extends {} = {}, ContextType extends Record<any, any> = DefaultContext, CurrentUserKey extends string = "currentUser">(options: GenericAuthPluginOptions<UserType, ContextType, CurrentUserKey>) => Plugin<{ | ||
validateUser: ValidateUserFn<UserType>; | ||
} & Record<CurrentUserKey, UserType>>; |
Sorry, the diff of this file is not supported yet
51686
634