PATTERN Cited by 1 source
Directive-based field authorization¶
Problem¶
A mixed-domain GraphQL graph — one where a single query may
fetch public product data alongside confidential customer
data — needs to enforce authentication and
authenticity level at
field granularity, not request granularity. Putting the
authz check in a middleware before GraphQL parsing is too
coarse (it would gate the whole query); scattering if
(!authenticated) throw ... through every resolver is
error-prone and invisible to schema readers.
Pattern¶
Declare a custom schema directive (Zalando: @isAuthenticated)
on FIELD_DEFINITION. The directive's implementation wraps
the resolver; before the resolver runs, it inspects the
request context for the authenticated user and the user's
authenticity level, and rejects the field with an
authorization error if either is missing or insufficient.
scalar ACRValue @specifiedBy(url: "https://example.com/zalando-acr-value")
directive @isAuthenticated(
acrValue: ACRValue
) on FIELD_DEFINITION
type Query {
customer: Customer @isAuthenticated
}
type Mutation {
updateCustomerInfo(
email: String
phoneNumber: String
): UpdateCustomerInfoResult @isAuthenticated(acrValue: HIGH)
}
Absent-argument ⇒ any-authenticated-user; present-argument ⇒ the session's ACR value must meet or exceed the declared level (Source: sources/2023-10-18-zalando-understanding-graphql-directives-practical-use-cases-at-zalando).
Why declarative beats imperative¶
- Schema-as-policy-source. A reviewer reads the schema
and sees immediately which fields require auth. An
if (!ctx.user)scattered through N resolvers does not aggregate into a policy view. - Single resolver-wrapper. The directive's implementation is written once (a schema-directive wrapper via graphql-tools) and applied everywhere by the graph layer at compile time — no risk of forgetting the check in a new resolver.
- Lint-friendly. A
schema linter can
check that every mutation touching customer data carries
@isAuthenticated, turning the policy into a CI gate. - Client-invisible. Clients never write the directive;
the policy can tighten without a client redeploy. Raising
acrValue: MEDIUM→HIGHfor a sensitive mutation is a schema change only.
Step-up semantics¶
The acrValue argument turns field-level authz into
step-up
authentication. A low-risk Query.customer resolves as
long as the user is authenticated at any level; a high-risk
Mutation.updateCustomerInfo(email, phoneNumber) requires
HIGH ACR, triggering a password / 2FA re-challenge if the
session is at a lower level.
The ACR value semantic comes from OpenID Connect's
authentication-context-class-reference taxonomy; Zalando
declares its own values (scale of their choosing —
LOW/MEDIUM/HIGH or OIDC-style URNs) and documents
them via @specifiedBy on the ACRValue scalar.
Trade-offs¶
- Resolver-wrap overhead. Every
@isAuthenticated- annotated field pays the wrapper's cost per resolution. For a graph with deep authenticated subtrees, the cost is non-trivial; mitigation is to put the directive at the top of an authenticated subtree and let child fields inherit (requires convention, since GraphQL directives don't structurally inherit — but Zalando's convention is to gate at the entry point to the customer subgraph). - Off-schema context dependency. The directive's implementation needs the authenticated-user object wired into the GraphQL context at request time. That plumbing is independent of the directive and is a per-deployment concern.
- Directive-only policy is a local maximum. Higher- assurance deployments combine the directive with cross- cutting enforcement: e.g. an outer edge proxy rejecting unauthenticated traffic before it hits the graph, so the directive serves as a defence-in-depth layer rather than the only gate.
Seen in¶
- Zalando UBFF — canonical instance.
@isAuthenticated(acrValue: ACRValue) on FIELD_DEFINITIONwith SSO + OIDC ACR values; public product data + private customer data share one graph, the directive gates the private subtree (sources/2023-10-18-zalando-understanding-graphql-directives-practical-use-cases-at-zalando, systems/zalando-graphql-ubff).
Related¶
- concepts/step-up-authentication — the authentication
discipline the
acrValueargument encodes. - concepts/graphql-schema-directive — the primitive that lets this pattern exist.
- concepts/sso-authentication — the Zalando-side auth substrate.
- concepts/zero-trust-authorization — adjacent session- level framework.
- patterns/directive-based-pii-redaction — a sibling schema-directive pattern addressing a different policy.
- patterns/directive-based-field-lifecycle — sibling schema-directive pattern for schema-evolution policy.
- systems/graphql · systems/zalando-graphql-ubff