Skip to main content

Token policy

The policy block is the trust domain's token policy. It decides every request to the OAuth server, whatever the flow, and shapes the tokens that Defakto issues: their scopes, their lifetime, and any custom claims.

The services that receive tokens still decide what each caller may do. The token policy adds a central control in front of them. A caller that the policy doesn't allow never receives a token, so it can't make the call at all.

Example​

policy:
authorization:
unmatched: deny
deny:
- name: report-generator-never-acts-for-users
match:
caller:
spiffeID: spiffe://example.com/ns/batch/sa/report-generator
requestedTokenType: urn:defakto:security:token-type:jwt-svid-obo
allow:
- name: agents-call-helpdesk
match:
caller:
spiffeID: spiffe://example.com/ns/agents/**
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
audience: https://helpdesk.example.com/mcp
token:
allowedScopes: [tickets.read]
maxTTL: 2m

Fields​

FieldRequiredDescriptionExample
refuseAllNo. Default false.true refuses every request to the OAuth server, whatever the rules say, including sign-ins and external service tokens. Use it as an emergency stop.false
authorizationNoThe rules.
authorization.unmatchedYes, when authorization is presentWhat happens to a request that no rule decides: deny or allow.deny
authorization.denyNoDeny rules.
authorization.allowNoAllow rules.
authorization.externalAuthorizeNoExternal authorizer rules.

The three rule lists can hold up to 100 rules in total. Rule order doesn't matter, within a list or across lists.

warning

When the configuration has no authorization block, every request that the configuration supports is allowed, except requests for external service tokens, described below: any caller, any audience, and any configured identity provider. To allow only what you choose, set unmatched: deny and add allow rules for every flow you use.

A request for an external service token is refused unless an allow or external authorizer rule matches it, and that rule's match.audience names the service's resource. This applies even without an authorization block, and with unmatched: allow. Every workload gets tokens through Defakto's one registration at the service, so the service can't tell your workloads apart. The rule is where you decide which workloads can use it.

Rules​

Every rule has a name and a match block. Allow and external authorizer rules can also have a token block, and an external authorizer rule names its authorizer.

FieldApplies toDescriptionExample
nameAll rulesA DNS label, unique across all three rule lists.agents-call-helpdesk
matchAll rulesWhich requests the rule applies to. See Matching requests.
tokenAllow and external authorizer rulesHow the rule shapes the issued token. See Token settings.
authorizerExternal authorizer rulesThe name of the authorizer to call.corp-pdp
Rule kindEffect
DenyRefuses matching requests.
AllowPermits matching requests, and applies its token settings.
External authorizerSends matching requests to your external authorizer webhook, which makes the decision. Its token settings apply when the webhook allows the request.

Matching requests​

A rule matches a request when every field in its match block matches. A field that's absent matches any value, including a request with no value for it, such as a request with no user. Every field except requestedTokenType takes one value or a list, and a list matches any of its values.

  • Allow and external authorizer rules must name a requestedTokenType, so a kind of token added later never joins existing rules.
  • A deny rule must name at least one field. To refuse everything, use refuseAll.
FieldMatchesRule kinds
callerWho is askingAll
userWho the caller acts for, if anyoneAll
requestedTokenTypeThe kind of token requestedAll
audienceThe service the token is forAll
delegatedByThe workloads that acted for the user before the callerDeny

caller​

The caller is whoever will use the issued token. A caller entry names exactly one of these keys. caller takes one entry or a list, and a list matches any of its entries.

KeyMatchesValuesExample
spiffeIDA workload in your trust domain, by its SPIFFE IDOne or a list of SPIFFE IDs in your trust domain, exact or as patterns. * matches one path segment, and ** matches any number of segments and can appear only at the end.spiffe://example.com/ns/agents/**
typeEvery caller of a kindspiffeID for workloads in your trust domain, or oauthClientID for OAuth clients that people sign in throughoauthClientID
oauthClientIDAn OAuth client, by its Client ID Metadata Document URLOne or a list of exact URLshttps://app.example.com/oauth/client-metadata

The caller's SPIFFE ID comes from its X.509-SVID over mutual TLS, from a JWT-SVID client assertion, or from the JWT-SVID it presents in the JWT bearer grant. An OAuth client, such as Claude, is matched only when it requests a token for a person.

warning

An OAuth client's client ID isn't verified, so another application can present the same value. Use oauthClientID to tailor settings for an application, not to restrict access.

To match several kinds of callers in one rule, list one entry for each:

caller:
- spiffeID: spiffe://example.com/ns/reporting/**
- type: oauthClientID

user​

The user is the person the caller acts for, if anyone. Every user is vouched for by an identity provider, and reaches Defakto in one of three ways, which rules name with via:

viaHow the user arrived
idTokenA workload presented an ID token from the identity provider.
accessTokenA workload presented an access token from the identity provider.
loginThe user signed in through Defakto, which sent them to the identity provider.

Defakto records the identity provider and via in the tokens it issues for the user, so a rule several hops into a chain of services matches the user the same way as a rule at the first hop.

A user entry has these fields. user takes one entry or a list, and a list matches any of its entries.

FieldMatchesValues
noneOnly requests with no usertrue. Can't be combined with other fields in the same entry.
identityProviderThe identity provider that vouched for the userOne or a list of identity provider names
viaHow the user arrivedOne or a list of idToken, accessToken, and login. Without it, the rule matches users however they arrive, including through token types added to the identity provider later.
subThe user's identifierOne or a list of exact values. Requires exactly one identityProvider in the same entry.

Common shapes:

userMatches
{ identityProvider: corp-idp }Any user from corp-idp, however they arrive
{ identityProvider: corp-idp, via: login }Users who signed in through Defakto
{ identityProvider: corp-idp, via: [idToken, accessToken] }Users whose tokens a workload presented
{ identityProvider: corp-idp, sub: alice-8f3k2 }One user
{ none: true }Only requests where the caller acts as itself

requestedTokenType​

ValueKind of token
urn:ietf:params:oauth:token-type:access_tokenAn OAuth access token, including an external service's token
urn:defakto:security:token-type:jwt-svid-oboAn on-behalf-of token
urn:ietf:params:oauth:token-type:txn_tokenA Transaction Token

A rule names exactly one value. Requests in flows that don't send a requested token type, such as the JWT bearer grant and user sign-in, request an access token.

audience​

The service the token is for, compared exactly with the audience or resource the caller sent. One or a list of exact values. * isn't accepted.

  • For an external service, the audience is the service's resource value.
  • For a Transaction Token, the audience is your trust domain name, such as example.com.
  • A rule without audience matches any audience, including external services.

A deny rule on an audience is only as strong as the receiving service's own check of the token's aud claim.

delegatedBy​

FieldMatchesValues
delegatedBy.containsA re-exchanged on-behalf-of token whose chain includes a workload, not counting the current callerOne or a list of SPIFFE IDs or patterns, as for caller.spiffeID

delegatedBy applies only to deny rules.

Token settings​

The token block of an allow or external authorizer rule shapes the token that Defakto issues. A rule that omits a setting imposes nothing.

FieldDescriptionExample
maxTTLThe token's lifetime, as a duration such as 90s, 2m, or 24h, longer or shorter than the 5-minute default, up to 168h (7 days). A configuration with a longer maxTTL is rejected. The token's actual lifetime can be shorter: It's capped at the maximum for its kind of token, and never outlives the credential it was issued from.2m
allowedScopesThe scopes the token may carry. A caller that requests more scopes receives only these. A caller that requests only scopes outside the list is refused with invalid_scope. Not allowed on rules for on-behalf-of tokens, which carry no scopes.[tickets.read]
customClaimsClaims to add to the token. See Custom claims.dept: "{{custom.department}}"

For external service tokens, only allowedScopes applies, within the service's grant.clientCredentials.scopes. The service's authorization server sets the token's lifetime and contents.

Custom claims​

customClaims adds your own claims to the tokens a rule allows. Only on-behalf-of tokens carry custom claims. Each key is the name of a claim to add, and each value is a template that refers to a source:

token:
customClaims:
email: "{{jwt.claim.email}}"
caller: "{{caller.id}}"
dept: "{{custom.department}}"
SourceValueAvailable
jwt.claim.<name>A claim from the JWT the request presented. See below.All rules
caller.<name>An attribute of the calling workload, such as caller.id, its SPIFFE IDAll rules
custom.<name>A value from your external authorizer's responseExternal authorizer rules

jwt.claim.<name> reads the JWT that the request presented:

FlowThe JWT that jwt.claim reads
On-behalf-of token, first exchangeThe user's ID token or access token
On-behalf-of token, re-exchangeThe on-behalf-of token being re-exchanged

For example, if a user's ID token contains these claims, {{jwt.claim.email}} renders as alex.morgan@example.com:

{
"iss": "https://idp.example.com",
"sub": "user_987654321",
"aud": "web-app",
"name": "Alex Morgan",
"email": "alex.morgan@example.com",
"email_verified": true
}

jwt.claim can read only string claims. A template that is exactly one custom reference copies the value as it is, of any JSON type, including objects and lists. A template that combines text and references produces a string.

If a referenced value is missing, the request is refused, rather than issuing a token without the claim:

  • A missing jwt.claim value refuses the request with invalid_request.
  • A custom value that the external authorizer's response doesn't include makes the response invalid, and the caller receives server_error.

Custom claims aren't carried forward automatically when an on-behalf-of token is re-exchanged. To carry a claim through a chain, add a rule for the re-exchange that copies it from the presented token, such as email: "{{jwt.claim.email}}".

Custom claims appear at the top level of the on-behalf-of token. These names are reserved, and can't be used for custom claims: iss, sub, aud, exp, nbf, iat, jti, cnf, obo, txn, auth_time, and nonce.

How rules are evaluated​

Defakto checks every rule against each request, and the matching rules decide together:

  1. If any deny rule matches, the request is refused.
  2. Otherwise, if any external authorizer rule matches, Defakto calls the authorizer once, with the names of every matching external authorizer rule, and the authorizer's response decides. Matching allow rules don't bypass the authorizer, but their token settings still apply.
  3. Otherwise, if any allow rule matches, the request is allowed.
  4. Otherwise, unmatched decides, and no token settings apply.

Token settings from every matching allow and external authorizer rule, and from the authorizer's response, combine:

SettingCombined as
maxTTLThe lowest value
allowedScopesOnly the scopes that every rule allows
customClaimsEvery claim that any matching rule sets is added. If two matching rules set the same claim with different templates, the request fails with server_error.

Rules that don't set a setting take no part in combining it.

For example, take these two allow rules:

allow:
- name: workloads-act-as-themselves
match:
caller:
spiffeID: spiffe://example.com/**
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
- name: reporting-reads-invoices
match:
caller:
spiffeID: spiffe://example.com/ns/reporting/**
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
audience: https://invoices.example.com
token:
allowedScopes: [invoices.read]

A workload under spiffe://example.com/ns/reporting/ requests an access token for https://invoices.example.com, with the scopes invoices.read invoices.write. Both rules match, and no deny or external authorizer rule does, so the request is allowed. The token carries only invoices.read, from reporting-reads-invoices, and lives for 5 minutes, because no matching rule sets maxTTL.

Whatever the rules say, Defakto never:

  • Issues a token with more scopes than the caller requested
  • Accepts an identity provider's token as asserting a SPIFFE ID in your trust domain
  • Turns a workload's own access token into an on-behalf-of token
  • Issues a token whose audience is the trust domain's own OAuth server

Examples​

Each example shows only the rules for its goal. Add them to the authorization block alongside your other rules.

Allow only specific workloads to call a service​

allow:
- name: agents-call-helpdesk
match:
caller:
spiffeID: spiffe://example.com/ns/agents/**
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
audience: https://helpdesk.example.com/mcp

Let people sign in to a service​

allow:
- name: people-call-helpdesk
match:
caller:
type: oauthClientID
user:
identityProvider: corp-idp
via: login
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
audience: https://helpdesk.example.com/mcp
token:
allowedScopes: [tickets.read]

Let a workload act for users​

allow:
- name: backend-acts-for-users
match:
caller:
spiffeID: spiffe://example.com/ns/web/sa/backend
user:
identityProvider: corp-idp
requestedTokenType: urn:defakto:security:token-type:jwt-svid-obo
audience: spiffe://example.com/orders

Let workloads start Transaction Tokens​

allow:
- name: gateway-starts-transactions
match:
caller:
spiffeID: spiffe://example.com/ns/web/sa/gateway
requestedTokenType: urn:ietf:params:oauth:token-type:txn_token
audience: example.com

Block a workload from acting for users​

deny:
- name: report-generator-never-acts-for-users
match:
caller:
spiffeID: spiffe://example.com/ns/batch/sa/report-generator
requestedTokenType: urn:defakto:security:token-type:jwt-svid-obo

Require agents to act for a user​

deny:
- name: agents-never-call-payments-as-themselves
match:
caller:
spiffeID: spiffe://example.com/ns/agents/**
user:
none: true
audience: https://payments.example.com

Block a user​

deny:
- name: offboarded-users
match:
user:
- identityProvider: corp-idp
sub: alice-8f3k2
- identityProvider: corp-idp
sub: bob-2m7q9

Block requests that passed through a workload​

deny:
- name: nothing-through-ingest
match:
delegatedBy:
contains: spiffe://example.com/ns/edge/sa/ingest

Send decisions to your own policy engine​

externalAuthorize:
- name: pdp-decides-delegations
match:
requestedTokenType: urn:defakto:security:token-type:jwt-svid-obo
authorizer: corp-pdp
token:
customClaims:
dept: "{{custom.department}}"

Refuse every request​

policy:
refuseAll: true
authorization:
# your rules, unchanged

Validation​

When you apply the configuration, Defakto also rejects:

  • A rule that names an identity provider or authorizer that doesn't exist
  • Duplicate rule names
  • unmatched: deny with no allow or external authorizer rules. To refuse everything, use refuseAll.
  • A caller entry that can never match under the rest of the rule. For example, an oauthClientID or type: oauthClientID entry needs a rule for access tokens, and a user that admits people who sign in. A spiffeID entry in a rule for access tokens can't require a user, because a workload that acts for a user gets an on-behalf-of token or a Transaction Token.
  • A via value that none of the entry's identity providers supports, or sub without exactly one identityProvider
  • A rule for Transaction Tokens whose audience isn't your trust domain name
  • allowedScopes on a rule for on-behalf-of tokens
  • A rule for external services that names a user, sets maxTTL or customClaims, or requests anything other than an access token
  • A custom claim with a reserved name, or a template that refers to an unknown source or to a source the rule can't provide