@nxgt/shared-graphql 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -52
- package/dist/directives/authenticated.d.ts +42 -0
- package/dist/directives/authenticated.d.ts.map +1 -0
- package/dist/directives/federation.d.ts +6 -0
- package/dist/directives/federation.d.ts.map +1 -1
- package/dist/directives/index.d.ts +1 -2
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/directives/permission.d.ts +20 -6
- package/dist/directives/permission.d.ts.map +1 -1
- package/dist/directives/removed-check.d.ts +15 -0
- package/dist/directives/removed-check.d.ts.map +1 -0
- package/dist/directives/scope.d.ts +3 -3
- package/dist/directives/scope.d.ts.map +1 -1
- package/dist/directives/sdl.d.ts +14 -4
- package/dist/directives/sdl.d.ts.map +1 -1
- package/dist/directives/validate.d.ts +8 -14
- package/dist/directives/validate.d.ts.map +1 -1
- package/dist/index.js +1055 -927
- package/dist/index.js.map +35 -31
- package/dist/integrations/hono.d.ts +2 -2
- package/dist/integrations/hono.d.ts.map +1 -1
- package/dist/plugins/apply-authenticated.d.ts +21 -0
- package/dist/plugins/apply-authenticated.d.ts.map +1 -0
- package/dist/plugins/apply-keto-checks.d.ts +10 -7
- package/dist/plugins/apply-keto-checks.d.ts.map +1 -1
- package/dist/plugins/auth.d.ts +17 -1
- package/dist/plugins/auth.d.ts.map +1 -1
- package/dist/plugins/authenticated.d.ts +14 -0
- package/dist/plugins/authenticated.d.ts.map +1 -0
- package/dist/plugins/extract-jwt.d.ts +19 -6
- package/dist/plugins/extract-jwt.d.ts.map +1 -1
- package/dist/plugins/field-guard.d.ts +30 -0
- package/dist/plugins/field-guard.d.ts.map +1 -0
- package/dist/plugins/gateway-trust.d.ts +47 -0
- package/dist/plugins/gateway-trust.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/keto-checks.d.ts +3 -2
- package/dist/plugins/keto-checks.d.ts.map +1 -1
- package/dist/plugins/keto-helpers.d.ts +1 -1
- package/dist/plugins/keto-helpers.d.ts.map +1 -1
- package/dist/plugins/ory-auth.d.ts +3 -3
- package/dist/shared.d.ts +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/utils/errors/denial.d.ts +20 -0
- package/dist/utils/errors/denial.d.ts.map +1 -0
- package/dist/utils/errors/format-error.d.ts +2 -1
- package/dist/utils/errors/format-error.d.ts.map +1 -1
- package/dist/utils/errors/index.d.ts +1 -0
- package/dist/utils/errors/index.d.ts.map +1 -1
- package/dist/utils/errors/mask-error.d.ts +2 -1
- package/dist/utils/errors/mask-error.d.ts.map +1 -1
- package/dist/utils/ws-context.d.ts +1 -1
- package/docs/README.md +8 -6
- package/docs/guide/authentication.md +187 -0
- package/docs/guide/errors.md +37 -5
- package/docs/guide/migrating-to-3.md +276 -0
- package/docs/guide/permissions.md +39 -50
- package/docs/roadmap.md +30 -29
- package/docs/troubleshooting.md +151 -48
- package/package.json +7 -11
- package/dist/directives/check.d.ts +0 -32
- package/dist/directives/check.d.ts.map +0 -1
- package/dist/directives/deprecation.d.ts +0 -7
- package/dist/directives/deprecation.d.ts.map +0 -1
- package/dist/directives/requirements.d.ts +0 -14
- package/dist/directives/requirements.d.ts.map +0 -1
- package/graphql/directives/check.graphqls +0 -90
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { YogaInitialContext, YogaServerInstance } from 'graphql-yoga';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
-
import { type RenderSandboxOptions } from '../utils';
|
|
4
|
-
export declare function
|
|
3
|
+
import { type RenderSandboxOptions } from '../utils/sandbox';
|
|
4
|
+
export declare function sandboxExplorer(options: RenderSandboxOptions): import("hono").MiddlewareHandler<any, string, {}, Response>;
|
|
5
5
|
type HonoYogaOptions = {
|
|
6
6
|
sandbox?: RenderSandboxOptions & {
|
|
7
7
|
endpoint?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/integrations/hono.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,
|
|
1
|
+
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/integrations/hono.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,kBAAkB,CAAC;AAE5E,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,+DAI5D;AAED,KAAK,eAAe,GAAG;IACtB,OAAO,CAAC,EAAE,oBAAoB,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD,CAAC;AAEF,wBAAgB,QAAQ,CACvB,aAAa,SAAS,OAAO,CAAC,kBAAkB,CAAC,EACjD,WAAW,SAAS,EAAE,EACrB,IAAI,EAAE,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,+DAMrD;AAED,wBAAgB,cAAc,CAC7B,aAAa,SAAS,OAAO,CAAC,kBAAkB,CAAC,EACjD,WAAW,SAAS,EAAE,EAEtB,IAAI,EAAE,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,EACpD,OAAO,CAAC,EAAE,eAAe,8EA0BzB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type GraphQLSchema } from 'graphql';
|
|
2
|
+
import { type AuthenticatedOptions } from '../directives/authenticated';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps every object field that an `@authenticated` applies to — on the field,
|
|
5
|
+
* on its type, on one of its type's interfaces or on that interface's field,
|
|
6
|
+
* or on the scalar or enum it returns — so the caller is checked before its
|
|
7
|
+
* resolver (and a subscription's `subscribe`) runs: none is
|
|
8
|
+
* `UNAUTHENTICATED` (401), one of a type no `type:` names is `FORBIDDEN`
|
|
9
|
+
* (403). Every one that applies must hold.
|
|
10
|
+
*
|
|
11
|
+
* Read and validated when the schema is built: `type: []`, a type outside
|
|
12
|
+
* `options.types` (`session`, `token` by default) and restrictions with no
|
|
13
|
+
* type in common each throw a `TypeError` naming `Type.field`.
|
|
14
|
+
*
|
|
15
|
+
* The caller is `context.user`, which `useOryAuth` and `useAuth` set; its
|
|
16
|
+
* type is `context.ory.kind`, or `context.user.tokenType` without Ory. A field
|
|
17
|
+
* already guarded is left alone, and a schema with none left to guard is
|
|
18
|
+
* returned as it is, so the plugin's `onSchemaChange` settles.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyAuthenticated(schema: GraphQLSchema, options?: AuthenticatedOptions): GraphQLSchema;
|
|
21
|
+
//# sourceMappingURL=apply-authenticated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-authenticated.d.ts","sourceRoot":"","sources":["../../src/plugins/apply-authenticated.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,KAAK,aAAa,EAKlB,MAAM,SAAS,CAAC;AACjB,OAAO,EACN,KAAK,oBAAoB,EAKzB,MAAM,6BAA6B,CAAC;AAUrC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,oBAAyB,GAChC,aAAa,CAaf"}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { GraphQLSchema } from 'graphql';
|
|
2
2
|
import { type ReadOptions } from '../directives';
|
|
3
3
|
/**
|
|
4
|
-
* Wraps every field carrying `@
|
|
5
|
-
*
|
|
4
|
+
* Wraps every field carrying `@permission` so the permission is answered
|
|
5
|
+
* before its resolver runs.
|
|
6
6
|
*
|
|
7
7
|
* Every requirement is read and validated here, when the schema is built: a
|
|
8
8
|
* malformed path, an argument the field does not declare, an empty group, a
|
|
9
|
-
* namespace outside `options.namespaces
|
|
10
|
-
* which no resolver ever runs through —
|
|
11
|
-
* `Type.field`, so the server does not boot.
|
|
9
|
+
* namespace outside `options.namespaces`, a guard on an interface field,
|
|
10
|
+
* which no resolver ever runs through, and a `@check` — removed in 3.0 — each
|
|
11
|
+
* throw a `TypeError` naming `Type.field`, so the server does not boot.
|
|
12
12
|
*
|
|
13
13
|
* Modelled on `applyGraphqlPolicy` in `@nxgt/security`. Exported on its own
|
|
14
|
-
* because a schema transform is far easier to test than a plugin.
|
|
14
|
+
* because a schema transform is far easier to test than a plugin. A field it
|
|
15
|
+
* already guarded is left as it is, and a schema with no field left to guard
|
|
16
|
+
* is returned as it is — so it never wraps a field twice, and the plugin's
|
|
17
|
+
* `onSchemaChange` settles.
|
|
15
18
|
*/
|
|
16
19
|
export declare function applyKetoChecks(schema: GraphQLSchema, options?: ReadOptions): GraphQLSchema;
|
|
17
20
|
//# sourceMappingURL=apply-keto-checks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply-keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/apply-keto-checks.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"apply-keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/apply-keto-checks.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAsB,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAGN,KAAK,WAAW,EAGhB,MAAM,eAAe,CAAC;AAWvB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,WAAgB,GACvB,aAAa,CAmBf"}
|
package/dist/plugins/auth.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { Plugin } from 'graphql-yoga';
|
|
2
2
|
import type { GraphQLBaseContext } from '../types';
|
|
3
|
-
|
|
3
|
+
import { type GatewayTrustOptions } from './gateway-trust';
|
|
4
|
+
export type UseAuthOptions = GatewayTrustOptions;
|
|
5
|
+
/**
|
|
6
|
+
* The caller a gateway resolved, read from the GraphQL request's `extensions`
|
|
7
|
+
* — `user` and `token` — and ONLY for a request `trustedGateway` vouches for.
|
|
8
|
+
*
|
|
9
|
+
* `extensions` sits in the request body, which any client writes. A request
|
|
10
|
+
* without the gateway's proof leaves the context as it was: no `user`, no
|
|
11
|
+
* `token`, whatever its `extensions` claim. Without a `trustedGateway` this
|
|
12
|
+
* throws when it is called, so a server cannot boot trusting the body.
|
|
13
|
+
*
|
|
14
|
+
* useAuth({ trustedGateway: gatewaySecret({ secret: env.GATEWAY_SECRET }) })
|
|
15
|
+
*
|
|
16
|
+
* An API that resolves its own callers — a Kratos session, a Hydra token —
|
|
17
|
+
* uses `useOryAuth(ory)` instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useAuth(options: UseAuthOptions): Plugin<GraphQLBaseContext>;
|
|
4
20
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/plugins/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/plugins/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EACN,KAAK,mBAAmB,EAGxB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAmB3E"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Plugin } from 'graphql-yoga';
|
|
2
|
+
import type { AuthenticatedOptions } from '../directives/authenticated';
|
|
3
|
+
import type { GraphQLBaseContext } from '../types';
|
|
4
|
+
import type { OryContext } from './ory-auth';
|
|
5
|
+
/**
|
|
6
|
+
* Enforces `@authenticated` and `@authenticated(type:)` on every schema the
|
|
7
|
+
* server uses. Goes after the plugin that sets the caller — `useOryAuth(ory)`
|
|
8
|
+
* or `useAuth({ trustedGateway })`.
|
|
9
|
+
*
|
|
10
|
+
* `applyAuthenticated` returns a schema with no field left to guard as it is,
|
|
11
|
+
* so the replacement settles alongside `useKetoChecks` whatever their order.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useAuthenticated(options?: AuthenticatedOptions): Plugin<GraphQLBaseContext & OryContext>;
|
|
14
|
+
//# sourceMappingURL=authenticated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticated.d.ts","sourceRoot":"","sources":["../../src/plugins/authenticated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,GAAE,oBAAyB,GAChC,MAAM,CAAC,kBAAkB,GAAG,UAAU,CAAC,CAOzC"}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
import type { ApolloServerPlugin } from '@apollo/server';
|
|
1
2
|
import type { TokenPrincipal } from '@nxgt/shared';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { type GatewayTrustOptions } from './gateway-trust';
|
|
4
|
+
export type ExtractJwtOptions = GatewayTrustOptions;
|
|
5
|
+
/** What `extractJwtPlugin()` sets on an Apollo context. */
|
|
6
|
+
export type JwtContext = {
|
|
7
|
+
jwt?: {
|
|
8
|
+
payload: TokenPrincipal;
|
|
9
|
+
};
|
|
8
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Apollo Server's counterpart of `useAuth()`: the JWT payload a gateway
|
|
13
|
+
* verified and forwarded in `request.extensions.payload`, copied onto
|
|
14
|
+
* `context.jwt` — ONLY for a request `trustedGateway` vouches for.
|
|
15
|
+
*
|
|
16
|
+
* A request without the gateway's proof leaves `context.jwt` as the context
|
|
17
|
+
* function built it, whatever its `extensions` claim.
|
|
18
|
+
*
|
|
19
|
+
* plugins: [extractJwtPlugin({ trustedGateway: gatewaySecret({ secret }) })]
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractJwtPlugin(options: ExtractJwtOptions): ApolloServerPlugin<JwtContext>;
|
|
9
22
|
//# sourceMappingURL=extract-jwt.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract-jwt.d.ts","sourceRoot":"","sources":["../../src/plugins/extract-jwt.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"extract-jwt.d.ts","sourceRoot":"","sources":["../../src/plugins/extract-jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EACN,KAAK,mBAAmB,EAGxB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAEpD,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG;IACxB,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC;CAClC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,iBAAiB,GACxB,kBAAkB,CAAC,UAAU,CAAC,CAUhC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type GraphQLFieldConfig } from 'graphql';
|
|
2
|
+
type FieldConfig = GraphQLFieldConfig<unknown, unknown>;
|
|
3
|
+
/** What a guard asks before a field runs; it throws to refuse. */
|
|
4
|
+
export type FieldCheck = (source: unknown, args: Record<string, unknown>, context: unknown) => void | Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* One transform's guard on one field: its mark, its rank (lower runs first),
|
|
7
|
+
* its check, and whether the check can run before a subscription's stream is
|
|
8
|
+
* opened — only when it reads nothing off an event (`parent.<path>`).
|
|
9
|
+
*/
|
|
10
|
+
export type FieldGuard = {
|
|
11
|
+
mark: string;
|
|
12
|
+
rank: number;
|
|
13
|
+
check: FieldCheck;
|
|
14
|
+
onSubscribe: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Whether the transform owning `mark` guards this field as it stands. A
|
|
18
|
+
* merged schema's unguarded half, or a field whose resolver was replaced,
|
|
19
|
+
* is not — so a transform run again wraps it, never one twice.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isGuarded(config: FieldConfig, mark: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The field with `guard` added. Every guard runs by rank — `@authenticated`
|
|
24
|
+
* before `@permission`, whichever transform ran first — before the resolver;
|
|
25
|
+
* those with `onSubscribe` also run before `subscribe`, so a refused
|
|
26
|
+
* subscription opens no stream.
|
|
27
|
+
*/
|
|
28
|
+
export declare function guardField(config: FieldConfig, guard: FieldGuard): FieldConfig;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=field-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-guard.d.ts","sourceRoot":"","sources":["../../src/plugins/field-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,kBAAkB,EAEvB,MAAM,SAAS,CAAC;AAEjB,KAAK,WAAW,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAGxD,kEAAkE;AAClE,MAAM,MAAM,UAAU,GAAG,CACxB,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,OAAO,KACZ,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACrB,CAAC;AA6BF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,GACf,WAAW,CA6Bb"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { TokenPrincipal } from '@nxgt/shared';
|
|
2
|
+
/** One request header — a fetch `Headers`, or Apollo Server's `HeaderMap`. */
|
|
3
|
+
export type HeaderReader = {
|
|
4
|
+
get(name: string): string | null | undefined;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Whether a request came from the gateway allowed to name the caller in the
|
|
8
|
+
* GraphQL request's `extensions`.
|
|
9
|
+
*
|
|
10
|
+
* `extensions` is part of the request body, which any client writes. It names
|
|
11
|
+
* the caller only when this answers `true` — for a request carrying proof the
|
|
12
|
+
* client cannot forge. `gatewaySecret()` is the usual proof; a function of
|
|
13
|
+
* your own (an mTLS header set by your proxy, say) works as well.
|
|
14
|
+
*/
|
|
15
|
+
export type GatewayTrust = (headers: HeaderReader) => boolean | Promise<boolean>;
|
|
16
|
+
/** What `useAuth()` and `extractJwtPlugin()` take: who may set the caller. */
|
|
17
|
+
export type GatewayTrustOptions = {
|
|
18
|
+
trustedGateway: GatewayTrust;
|
|
19
|
+
};
|
|
20
|
+
export type GatewaySecretOptions = {
|
|
21
|
+
/** Shared with the gateway only. At least 16 characters. */
|
|
22
|
+
secret: string;
|
|
23
|
+
/** The header the gateway sends it in. `x-gateway-secret` by default. */
|
|
24
|
+
header?: string;
|
|
25
|
+
};
|
|
26
|
+
export declare const GATEWAY_SECRET_HEADER = "x-gateway-secret";
|
|
27
|
+
/**
|
|
28
|
+
* A `GatewayTrust` that holds for a request whose `header` carries `secret`,
|
|
29
|
+
* compared in constant time. The gateway adds the header to every subgraph
|
|
30
|
+
* request; a client reaching the service directly does not know it.
|
|
31
|
+
*
|
|
32
|
+
* Refuses, when it is built, a secret shorter than 16 characters: an unset
|
|
33
|
+
* environment variable must stop the server, not trust an empty header.
|
|
34
|
+
*/
|
|
35
|
+
export declare function gatewaySecret({ secret, header, }: GatewaySecretOptions): GatewayTrust;
|
|
36
|
+
/**
|
|
37
|
+
* The `trustedGateway` a plugin was given, or a `TypeError` naming the plugin:
|
|
38
|
+
* without one there is no caller it could read, and a plugin that silently
|
|
39
|
+
* reads none is a server whose every caller is anonymous.
|
|
40
|
+
*/
|
|
41
|
+
export declare function requireGatewayTrust(options: Partial<GatewayTrustOptions> | undefined, plugin: string): GatewayTrust;
|
|
42
|
+
/**
|
|
43
|
+
* The caller a trusted gateway put in `extensions`, when it is shaped like
|
|
44
|
+
* one: an object with a non-empty string `sub`. Anything else names nobody.
|
|
45
|
+
*/
|
|
46
|
+
export declare function principalOf(value: unknown): TokenPrincipal | undefined;
|
|
47
|
+
//# sourceMappingURL=gateway-trust.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-trust.d.ts","sourceRoot":"","sources":["../../src/plugins/gateway-trust.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG;IAC1B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC7C,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,CAC1B,OAAO,EAAE,YAAY,KACjB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IACjC,cAAc,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAClC,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAKxD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC7B,MAAM,EACN,MAA8B,GAC9B,EAAE,oBAAoB,GAAG,YAAY,CAYrC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,SAAS,EACjD,MAAM,EAAE,MAAM,GACZ,YAAY,CAQd;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAMtE"}
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
export * from './apply-authenticated';
|
|
1
2
|
export * from './apply-keto-checks';
|
|
2
3
|
export * from './auth';
|
|
4
|
+
export * from './authenticated';
|
|
3
5
|
export * from './extract-jwt';
|
|
6
|
+
export * from './gateway-trust';
|
|
4
7
|
export * from './keto-checker';
|
|
5
8
|
export * from './keto-checks';
|
|
6
9
|
export * from './keto-helpers';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
|
@@ -14,8 +14,9 @@ export type KetoChecksOptions = ReadOptions;
|
|
|
14
14
|
* Goes after `useOryAuth(ory)`, which is what puts `ory.subject` there.
|
|
15
15
|
*
|
|
16
16
|
* `replaceSchema` inside `onSchemaChange` re-enters this hook with the new
|
|
17
|
-
* schema,
|
|
18
|
-
*
|
|
17
|
+
* schema, and with every schema another plugin makes of it. `applyKetoChecks`
|
|
18
|
+
* returns a schema with no field left to guard as it is, so the replacement settles
|
|
19
|
+
* instead of looping, alongside `useAuthenticated` whatever their order.
|
|
19
20
|
*
|
|
20
21
|
* `OryUnavailable` is deliberately not caught anywhere here: a Keto outage is
|
|
21
22
|
* a 503 through `createMaskError`, never a denial.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAE5C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC5B,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,iBAAsB,GAC7B,MAAM,CAAC,kBAAkB,GAAG,UAAU,GAAG,iBAAiB,CAAC,CAU7D"}
|
|
@@ -19,7 +19,7 @@ export type CanQuestion = {
|
|
|
19
19
|
id: string;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* The caller, or an `UNAUTHENTICATED` refusal
|
|
22
|
+
* The caller, or an `UNAUTHENTICATED` refusal: a `GraphQLError` answered 401.
|
|
23
23
|
* An absent caller is not an error to swallow in a resolver that needs one.
|
|
24
24
|
*/
|
|
25
25
|
export declare function requireUser(context: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keto-helpers.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"keto-helpers.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GACjD,UAAU,GACV,iBAAiB,CAAC;AAEnB,yDAAyD;AACzD,MAAM,MAAM,WAAW,GAAG;IACzB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAC;CACX,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACpC,IAAI,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CAC7B,GAAG,cAAc,CAKjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,GAAG,CACxB,OAAO,EAAE,UAAU,GAAG,iBAAiB,EACvC,QAAQ,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAgBlB"}
|
|
@@ -60,9 +60,9 @@ export declare function resolveOryPrincipal(ory: Ory, headers: Headers): Promise
|
|
|
60
60
|
/**
|
|
61
61
|
* `useAuth()` for an Ory-native API: resolves the caller through Kratos
|
|
62
62
|
* (session cookie, session token) or Hydra (Bearer, introspected) and puts
|
|
63
|
-
* `user`, `claims`, `token` and `ory` on the context. Wire it
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* `user`, `claims`, `token` and `ory` on the context. Wire it ahead of
|
|
64
|
+
* `useAuthenticated()`, which enforces `@authenticated` from `context.user`
|
|
65
|
+
* and `context.ory.kind`.
|
|
66
66
|
*
|
|
67
67
|
* The `Ory` instance comes from the app's `createOry()` — one per process —
|
|
68
68
|
* and is the same one the services use for `isAllowed`.
|
package/dist/shared.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SHARED_TYPE_DEFS = "\ntype Query {\n\t_empty: String\n}\ntype Mutation {\n\t_empty: String\n}\ntype Subscription {\n\t_empty: String\n}\n\ndirective @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @shareable on OBJECT | FIELD_DEFINITION\n\ndirective @link(url: String!, import: [String!]) on SCHEMA\n\n";
|
|
1
|
+
export declare const SHARED_TYPE_DEFS = "\ntype Query {\n\t_empty: String\n}\ntype Mutation {\n\t_empty: String\n}\ntype Subscription {\n\t_empty: String\n}\n\n\"\"\"\nA signed-in caller \u2014 of one of the types `type` names, when it names some.\n\"\"\"\ndirective @authenticated(\n\t\"\"\"\n\tThe kinds of caller admitted: \"session\" or \"token\" by default. Any caller\n\twhen omitted.\n\t\"\"\"\n\ttype: [String!]\n) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @shareable on OBJECT | FIELD_DEFINITION\n\ndirective @link(url: String!, import: [String!]) on SCHEMA\n\n";
|
|
2
2
|
//# sourceMappingURL=shared.d.ts.map
|
package/dist/shared.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB,ypBAkB5B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ErrorCode } from '@nxgt/shared-exceptions';
|
|
2
|
+
import { GraphQLError } from 'graphql';
|
|
3
|
+
/** The refusals a guard answers, each with its HTTP status. */
|
|
4
|
+
export type DenialCode = ErrorCode.Unauthenticated | ErrorCode.Forbidden | ErrorCode.NotFound;
|
|
5
|
+
/**
|
|
6
|
+
* A refusal, as the `GraphQLError` a client reads: `extensions.code` and
|
|
7
|
+
* `extensions.http.status` — `UNAUTHENTICATED` 401, `FORBIDDEN` 403,
|
|
8
|
+
* `NOT_FOUND` 404 — so any server answers it with its status, with or without
|
|
9
|
+
* `createMaskError`. What `@permission`, `@authenticated`, `requireUser` and
|
|
10
|
+
* `can` throw.
|
|
11
|
+
*
|
|
12
|
+
* `message` is an i18n key (the shared `errors.*` one by default). It is
|
|
13
|
+
* translated here with `@nxgt/i18n`'s resources, in the request's language
|
|
14
|
+
* when one is known; `createMaskError(translate)` and
|
|
15
|
+
* `createFormatError(translate)` translate it again with yours.
|
|
16
|
+
*/
|
|
17
|
+
export declare function denial(code: DenialCode, message?: string): GraphQLError;
|
|
18
|
+
/** The i18n key of a `denial()`, or `undefined` for any other error. */
|
|
19
|
+
export declare function denialMessageKey(error: unknown): string | undefined;
|
|
20
|
+
//# sourceMappingURL=denial.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"denial.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/denial.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GACnB,SAAS,CAAC,eAAe,GACzB,SAAS,CAAC,SAAS,GACnB,SAAS,CAAC,QAAQ,CAAC;AAsBtB;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,YAAY,CAOvE;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAInE"}
|
|
@@ -5,7 +5,8 @@ import type { GraphQLBaseContext } from '../../types';
|
|
|
5
5
|
* Apollo Server's `formatError`, taught this repo's exceptions — the Apollo
|
|
6
6
|
* counterpart of `createMaskError`. `OryUnavailable` answers
|
|
7
7
|
* `SERVICE_UNAVAILABLE`, never a denial and never an opaque
|
|
8
|
-
* `INTERNAL_SERVER_ERROR
|
|
8
|
+
* `INTERNAL_SERVER_ERROR`; a `denial()` keeps its code, and its message is
|
|
9
|
+
* translated with `translate`.
|
|
9
10
|
*/
|
|
10
11
|
export declare function createFormatError<K extends LocaleKey, C extends GraphQLBaseContext = GraphQLBaseContext>(translate?: (message: K, context?: TranslationContext) => string, production?: boolean): ApolloServerOptions<C>['formatError'];
|
|
11
12
|
//# sourceMappingURL=format-error.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/format-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAK1D,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"format-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/format-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAK1D,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAItD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAChC,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,EAEjD,SAAS,GAAE,CACV,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAsB,EAC3B,UAAU,CAAC,EAAE,OAAO,GAClB,mBAAmB,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAgEvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC"}
|
|
@@ -12,7 +12,8 @@ type Translate<K extends LocaleKey> = (message: K, context?: TranslationContext)
|
|
|
12
12
|
* (`UNAUTHENTICATED` 401, `FORBIDDEN` 403, `NOT_FOUND` 404); a Mongoose error
|
|
13
13
|
* goes through `castError` first, as `createFormatError` does for Apollo.
|
|
14
14
|
* `OryUnavailable` (@nxgt/ory-sdk) becomes a 503 `SERVICE_UNAVAILABLE`, never
|
|
15
|
-
* a denial.
|
|
15
|
+
* a denial. A `denial()` — what the directives, `requireUser` and `can` throw
|
|
16
|
+
* — keeps its code and status, and its message is translated with `translate`. Everything else is masked as Yoga's default masks it: a
|
|
16
17
|
* `GraphQLError` thrown on purpose — or raised by validation — passes, and one
|
|
17
18
|
* that only wraps a plain `Error` a resolver threw is replaced by `message`,
|
|
18
19
|
* so an internal message never reaches the client.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mask-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/mask-error.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"mask-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/mask-error.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,IAAI,CACrC,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAM,CAAC;AAEZ;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,EAClD,SAAS,GAAE,SAAS,CAAC,CAAC,CAAiB,GACrC,SAAS,CAmCX"}
|
|
@@ -8,7 +8,7 @@ export interface ResolvedWsUser {
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Resolves the authenticated TokenPrincipal for a `graphql-ws` connection - the
|
|
11
|
-
* WS-transport equivalent of the HTTP-path `
|
|
11
|
+
* WS-transport equivalent of the HTTP-path `useOryAuth()` / `useAuth()`
|
|
12
12
|
* plugins. There is no gateway hop for WS connections, so each app performs
|
|
13
13
|
* the OAuth introspection call itself using its own `auth` client.
|
|
14
14
|
*
|
package/docs/README.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
The package's [README](../README.md) is the short version, with one example
|
|
4
4
|
per section. These pages are the long one.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
| Page | Read it when |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| [Migrating to 3.0](./guide/migrating-to-3.md) | you upgrade from 2.x: every break, with the code before and after |
|
|
9
|
+
| [Authentication](./guide/authentication.md) | you wire `useOryAuth`, `useAuth` behind a trusted gateway, or `@authenticated(type:)` |
|
|
10
|
+
| [Permissions](./guide/permissions.md) | you guard a field with `@permission`, or call `requireUser` / `can` |
|
|
11
|
+
| [Errors](./guide/errors.md) | you decide what a client receives: denials, `createMaskError`, `createFormatError`, outages |
|
|
12
|
+
| [Troubleshooting](./troubleshooting.md) | you have an error message in hand |
|
|
13
|
+
| [Roadmap](./roadmap.md) | you want to know what is next, and what shipped in 3.0 |
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Authentication: who is calling, and `@authenticated`
|
|
2
|
+
|
|
3
|
+
A resolver reads the caller from `context.user`. This page is where that value
|
|
4
|
+
comes from, which sources are trusted, and how `@authenticated` refuses a
|
|
5
|
+
missing caller or one of the wrong kind.
|
|
6
|
+
|
|
7
|
+
## The rule: a verified source, never the body alone
|
|
8
|
+
|
|
9
|
+
The GraphQL request's `extensions` travel in the request body, which any
|
|
10
|
+
client writes. So the caller comes from one of two places:
|
|
11
|
+
|
|
12
|
+
| Source | Plugin | Verified by |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| a Kratos session or a Hydra access token | `useOryAuth(ory)` | Ory, server-side, on every request |
|
|
15
|
+
| what a gateway resolved and put in `extensions` | `useAuth()` / `extractJwtPlugin()` | the gateway's proof — `trustedGateway` — on every request |
|
|
16
|
+
|
|
17
|
+
Anything else leaves the context without a caller.
|
|
18
|
+
|
|
19
|
+
## `useOryAuth(ory)` — an API that authenticates its own callers
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createOry } from '@nxgt/ory-sdk';
|
|
23
|
+
import { useAuthenticated, useOryAuth } from '@nxgt/shared-graphql';
|
|
24
|
+
|
|
25
|
+
const ory = createOry({ /* … */ });
|
|
26
|
+
|
|
27
|
+
createYoga({ plugins: [useOryAuth(ory), useAuthenticated()] });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
It reads `Authorization: Bearer`, then `X-Session-Token`, then the Kratos
|
|
31
|
+
cookie, and puts `user`, `claims`, `token` and `ory` on the context. An Ory
|
|
32
|
+
outage throws a 503 `SERVICE_UNAVAILABLE`, never an anonymous caller.
|
|
33
|
+
|
|
34
|
+
## `useAuth()` and `extractJwtPlugin()` — behind a gateway
|
|
35
|
+
|
|
36
|
+
A gateway that authenticated the caller can forward it to subgraphs in the
|
|
37
|
+
request's `extensions`: `user` and `token` for `useAuth()` (Yoga),
|
|
38
|
+
`payload` for `extractJwtPlugin()` (Apollo Server). Both read it **only** when
|
|
39
|
+
`trustedGateway` holds for the request:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { extractJwtPlugin, gatewaySecret, useAuth } from '@nxgt/shared-graphql';
|
|
43
|
+
|
|
44
|
+
const trustedGateway = gatewaySecret({ secret: process.env.GATEWAY_SECRET! });
|
|
45
|
+
|
|
46
|
+
createYoga({ plugins: [useAuth({ trustedGateway })] });
|
|
47
|
+
new ApolloServer({ plugins: [extractJwtPlugin({ trustedGateway })] });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `gatewaySecret({ secret, header? })`
|
|
51
|
+
|
|
52
|
+
Holds for a request whose `header` — `x-gateway-secret` by default — carries
|
|
53
|
+
`secret`, compared in constant time. The gateway adds it to every subgraph
|
|
54
|
+
request; a client that reaches the service directly does not know it, and its
|
|
55
|
+
`extensions` are ignored.
|
|
56
|
+
|
|
57
|
+
| Option | Default | Meaning |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `secret` | — | shared with the gateway only; **at least 16 characters**, or `gatewaySecret` throws when it is built |
|
|
60
|
+
| `header` | `x-gateway-secret` | the header the gateway sends it in |
|
|
61
|
+
|
|
62
|
+
The length check is there for the day `GATEWAY_SECRET` is unset: the server
|
|
63
|
+
stops at boot instead of trusting an empty header.
|
|
64
|
+
|
|
65
|
+
### Another proof
|
|
66
|
+
|
|
67
|
+
`trustedGateway` is any function of the request's headers:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
type GatewayTrust = (headers: { get(name: string): string | null | undefined }) => boolean | Promise<boolean>;
|
|
71
|
+
|
|
72
|
+
// Your proxy terminates mTLS and sets this header only for the gateway's certificate.
|
|
73
|
+
useAuth({ trustedGateway: (headers) => headers.get('x-client-cert-subject') === 'CN=gateway' });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
It receives a fetch `Headers` under Yoga and Apollo's `HeaderMap` under
|
|
77
|
+
Apollo Server; both answer `get`.
|
|
78
|
+
|
|
79
|
+
### What is read
|
|
80
|
+
|
|
81
|
+
- `useAuth()`: `extensions.user` when it is an object with a non-empty string
|
|
82
|
+
`sub`, and `extensions.token` when it is a string. Otherwise nothing.
|
|
83
|
+
- `extractJwtPlugin()`: `extensions.payload`, under the same `sub` rule, on
|
|
84
|
+
`context.jwt.payload`.
|
|
85
|
+
- A request that fails the proof leaves `user`, `token` and `jwt` as they
|
|
86
|
+
were — another plugin's caller is not erased.
|
|
87
|
+
- Over WebSocket, `useAuth()` reads no caller: the proof needs a fetch
|
|
88
|
+
`Request`, which a graphql-ws context does not hold. Resolve the caller per
|
|
89
|
+
connection with `resolveWsUser` instead.
|
|
90
|
+
|
|
91
|
+
Without a `trustedGateway`, both throw a `TypeError` when called — see
|
|
92
|
+
[troubleshooting](../troubleshooting.md).
|
|
93
|
+
|
|
94
|
+
## `@authenticated(type: [String!])`
|
|
95
|
+
|
|
96
|
+
```graphql
|
|
97
|
+
directive @authenticated(type: [String!]) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Declared in `SHARED_TYPE_DEFS` and in `AUTHENTICATED_DIRECTIVE_SDL`; enforced
|
|
101
|
+
by `useAuthenticated()`, or `applyAuthenticated(schema)` on a built schema.
|
|
102
|
+
|
|
103
|
+
```graphql
|
|
104
|
+
type Query {
|
|
105
|
+
me: User @authenticated # any caller
|
|
106
|
+
webhooks: [Webhook!]! @authenticated(type: ["token"]) # a machine client, not a session
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface Node @authenticated { id: ID! }
|
|
110
|
+
type Staff implements Node @authenticated(type: ["session"]) { id: ID!, name: String }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Caller | Answer |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| none (`context.user` unset) | `UNAUTHENTICATED`, HTTP 401 |
|
|
116
|
+
| of a type no applicable `type:` names | `FORBIDDEN`, HTTP 403 |
|
|
117
|
+
| otherwise | the resolver runs |
|
|
118
|
+
|
|
119
|
+
- **The caller's type** is `context.ory.kind` — `session` for a Kratos
|
|
120
|
+
session, `token` for an access token — or `context.user.tokenType` when no
|
|
121
|
+
Ory principal is on the context.
|
|
122
|
+
- **Every directive that applies is AND-ed**: the field's, its type's, its
|
|
123
|
+
type's interfaces', and those interfaces' same field. `Staff.name` above
|
|
124
|
+
needs a caller (`Node`) who is a session (`Staff`).
|
|
125
|
+
- **A scalar's or an enum's directive guards every field returning it** —
|
|
126
|
+
`scalar Secret @authenticated` refuses `Query.secret: Secret` to an
|
|
127
|
+
anonymous caller, as federation's router reads it.
|
|
128
|
+
- **A subscription is refused before its stream opens**: the check runs
|
|
129
|
+
before the field's `subscribe`, and again before each event is resolved.
|
|
130
|
+
- **A type's directive guards its fields, not the field returning it.**
|
|
131
|
+
`Query.staff` runs for an anonymous caller; the error lands on
|
|
132
|
+
`staff.name`. Put `@authenticated` on the field too when the lookup must not
|
|
133
|
+
run.
|
|
134
|
+
|
|
135
|
+
### Other caller types
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { CALLER_TYPES, useAuthenticated } from '@nxgt/shared-graphql';
|
|
139
|
+
|
|
140
|
+
useAuthenticated({ types: [...CALLER_TYPES, 'service'] });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`types` lists every value a `type:` may name; `CALLER_TYPES` —
|
|
144
|
+
`['session', 'token']` — is the default. Behind a gateway, it is the set
|
|
145
|
+
of `tokenType`s your gateway writes.
|
|
146
|
+
|
|
147
|
+
### Refused at build
|
|
148
|
+
|
|
149
|
+
`applyAuthenticated` throws a `TypeError` for a directive no request could
|
|
150
|
+
pass, naming where it sits — `Query.me` on a field, `Staff` on a type,
|
|
151
|
+
`Secret` on a scalar:
|
|
152
|
+
|
|
153
|
+
| Mistake | Message starts |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `type: []` | ``@authenticated on Query.me: `type: []` admits no caller`` |
|
|
156
|
+
| a type outside `types` | `@authenticated on Query.me: unknown type "staff" — known: session, token` |
|
|
157
|
+
| restrictions with no type in common | ``@authenticated on Note.body: the field's, its type's and its interfaces' `type`s have none in common`` |
|
|
158
|
+
|
|
159
|
+
### Federation's `@authenticated`
|
|
160
|
+
|
|
161
|
+
Federation defines its own `@authenticated`, with **no argument**, and a
|
|
162
|
+
subgraph imports it:
|
|
163
|
+
|
|
164
|
+
```graphql
|
|
165
|
+
extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@authenticated"])
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`useAuthenticated` accepts that shape: an `@authenticated` whose declaration
|
|
169
|
+
has no `type` means "any caller", and is enforced as such. So in a subgraph,
|
|
170
|
+
keep federation's declaration — `FEDERATION_DIRECTIVES` carries it unchanged
|
|
171
|
+
— and do not load `SHARED_TYPE_DEFS` beside it. `type:` is for a schema that
|
|
172
|
+
declares `@authenticated` through `SHARED_TYPE_DEFS` or
|
|
173
|
+
`AUTHENTICATED_DIRECTIVE_SDL`.
|
|
174
|
+
|
|
175
|
+
### Beside `useKetoChecks`
|
|
176
|
+
|
|
177
|
+
Both plugins replace the schema. Each marks the fields it guarded and leaves a
|
|
178
|
+
marked field alone, so the two settle on one schema in either order — and a
|
|
179
|
+
schema merged from a guarded half and an unguarded one gets the second half
|
|
180
|
+
guarded when it is transformed again, as is a field whose resolver was
|
|
181
|
+
replaced since. On a field carrying both,
|
|
182
|
+
`@authenticated` is checked before `@permission` whatever the plugin order,
|
|
183
|
+
so a caller of the wrong type is `FORBIDDEN` before Keto is asked:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
plugins: [useOryAuth(ory), useAuthenticated(), useKetoChecks(ory)]
|
|
187
|
+
```
|