API Access Policies control which clients can obtain access tokens for your APIs, under what conditions, and the settings applied to those tokens.
Every policy is attached to an API resource and evaluated before issuing an access token for that API:
A policy does two things:
Policies are written in, or compiled to Cedar: an open-source policy language for fast, analyzable, fine-grained authorization.
A Cedar policy either permits or forbids a request. Cedar evaluates each request using four parts, known as PARC:
| Part | Meaning | In MonoCloud |
|---|---|---|
| Principal | Who is making the request | The client requesting the access token |
| Action | What they are trying to do | IssueAccessToken |
| Resource | What they are acting on | The API resource for which the access token is requested |
| Context | Request-time data | Details such as the user, grant type, requested scopes, location, time, and client certificate |
A policy has the following general shape:
permit | forbid (principal, action, resource)
when {
// conditions that must be true
}
unless {
// conditions that must be false
};
Three rules govern every policy evaluation:
permit and a forbid policy match the same request, access is denied.Before issuing an access token for an API, MonoCloud builds a Cedar authorization request:
Client::"<client id>", including its client groups as parent entities.Action::"IssueAccessToken".Api::"<audience>", representing the API resource being evaluated.All enabled policies attached to the API resource are evaluated together.
| Outcome | Result |
|---|---|
At least one permit policy matches and no forbid policy matches | MonoCloud issues an access token for the API. Token settings from all matching permit policies are merged. |
Any forbid policy matches | The request is denied. Advanced policies can return a custom denial message; otherwise, MonoCloud returns a generic message. |
| No policy matches | The request is denied by default. |
When a request includes multiple API audiences, MonoCloud evaluates policies for each API independently. Policies for an API are evaluated only when that API is requested as an audience.
Policy conditions can reference the following entity types:
| Entity | Identifier format | Notes |
|---|---|---|
Client | Client::"client-id" | Belongs to its Group and has a client_type attribute (ClientType). |
Group | Group::"group-id" | A group assigned to clients and/or users. Use with principal in Group::"..." for clients or context.user in Group::"..." for users. |
User | User::"user-id" | Available as context.user for user-centric grants. Belongs to its Group. Has an optional email string attribute set to the user's primary email address. |
Api | Api::"audience" | Has an audience string attribute. |
Scope | Scope::"<audience>#<name>" for API scopes; Scope::"<name>" for identity scopes | Has a name attribute containing the raw scope name, such as "read". API scopes are members of their Api. |
NetworkZone | NetworkZone::"zone-id" | Represents a network zone matched by the request IP address. |
Country | Country::"US" | Uses an ISO 3166-1 alpha-2 country code. |
TrustStore | TrustStore::"store-id" | Represents the trust store that validated the client certificate. |
GrantType | GrantType::"client_credentials" | One of authorization_code, client_credentials, password, urn:ietf:params:oauth:grant-type:device_code, or refresh_token. |
CallerType | CallerType::"server" | Either server or browser. |
ClientType | ClientType::"application" | Either application or agent. Available on the principal as principal.client_type. |
DayOfWeek | DayOfWeek::"monday" | Lowercase day name. |
| Attribute | Type | Description |
|---|---|---|
context.user | User (optional) | The user for whom the access token is being issued. Absent for machine-to-machine grants; guard with context has user. |
context.user.email | String (optional) | The authenticated user's primary email address. Present only when the user has a primary email; guard with context.user has email (and context has user). |
context.grant_type | GrantType | The OAuth grant being executed. |
context.caller_type | CallerType | Whether the request originated from a server or browser. |
context.requested_audiences | Set<Api> | All API audiences requested in the current request. |
context.requested_api_scopes | Set<Scope> | Requested scopes that belong to the API currently being evaluated. |
context.requested_scopes | Set<Scope> | All requested scopes, including identity scopes such as Scope::"openid" and scopes for other APIs. |
context.requested_scope_names | Set<String> | The raw names of all requested scopes - both identity scopes and API scopes (for any audience) - without the <audience># prefix, de-duplicated. Use it to match scopes by name without specifying the audience. Because names are merged across APIs, it cannot distinguish the same scope name defined on two APIs; use context.requested_api_scopes for per-API matching. |
context.network_zones | Set<NetworkZone> | Network zones that match the request IP address. |
context.day_of_week | DayOfWeek | Current day of the week in UTC. |
context.time_of_day_seconds | Long | Seconds since midnight UTC, from 0 to 86399. |
context.now | datetime | The UTC timestamp at which the policy is evaluated. |
context.location.ip_address | ipaddr | The request IP address. |
context.location.country | Country | The country resolved from the request IP address. |
context.location.city | String | The city resolved from the request IP address. |
context.authentication | Record (optional) | Present when the request has an authenticated user. Includes auth_time (datetime) and amr (Set<String>, for example ["pwd"]). Guard with context has authentication. |
context.certificate | Record (optional) | Present when the client authenticates with mTLS. Includes trust_store (TrustStore), subject_dn, issuer_dn, san_uris (Set<String>), and thumbprint_sha256. Guard with context has certificate. |
context.spiffe_id | String (optional) | The SPIFFE ID used by the client for workload-identity requests. Guard with context has spiffe_id. |
You can create policies in two modes:
You do not need to write Cedar from scratch. In the dashboard's advanced rule editor, you can start with a sample policy for common scenarios or generate a Cedar policy with AI assistance. Describe the rule in plain English, such as “Allow billing admins during business hours from corporate IPs,” then review and refine the generated draft.
MonoCloud validates every advanced policy against the schema before it can be saved, catching invalid attribute names, entity types, and other schema errors before the policy is evaluated.
For example, a basic permit policy for web-client on https://api.example.com/billing, allowing the read and write scopes, compiles to:
permit (
principal == Client::"web-client",
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context.requested_audiences.contains(resource) &&
[
Scope::"https://api.example.com/billing#read",
Scope::"https://api.example.com/billing#write"
].containsAll(context.requested_api_scopes)
};
Each API resource defines default token settings. A permitting policy can override those settings for requests it matches.
When multiple permit policies match the same request, MonoCloud merges their effective settings using the restrictive rules below. Before merging, any setting left unset by a policy inherits the API resource default.
| Setting | Effect |
|---|---|
AccessTokenType | Issues self-contained JWTs or opaque reference tokens. |
AccessTokenLifetime | Sets the access token lifetime in seconds. |
AllowMultiAudience | Controls whether the token can include audiences in addition to this API. |
AllowUserInfoAccess | Controls whether the token can include identity scopes and be used at the UserInfo endpoint. |
BindTokensToSession | Binds the token to the user session, revoking it when the session ends. |
Multiple permit policies can match the same request, for example, when both a broad client-group policy and a more specific client policy apply. MonoCloud merges their settings so that the most restrictive effective value takes precedence.
| Setting | Merge rule |
|---|---|
AccessTokenLifetime | Uses the shortest lifetime. |
AccessTokenType | Uses a reference token if any matching policy resolves to reference; otherwise uses a JWT. |
AllowMultiAudience | Allowed only when every matching policy allows it. |
AllowUserInfoAccess | Allowed only when every matching policy allows it. |
BindTokensToSession | Enabled when any matching policy requires it. |
For example, when the API resource default is AllowUserInfoAccess=false, a permitting policy that leaves AllowUserInfoAccess unset inherits false. It therefore contributes false to the merge, and UserInfo access remains disabled unless every matching policy resolves to true.
Use a permit policy to allow a specific client to obtain access tokens for an API:
permit (
principal == Client::"backend-worker",
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
);
To allow all clients in a client group, match the principal against the group instead:
permit (
principal in Group::"Billing Admins",
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
);
Permit all clients in the First Party client group to request only the read and write scopes:
permit (
principal in Group::"First Party",
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
[
Scope::"https://api.example.com/billing#read",
Scope::"https://api.example.com/billing#write"
].containsAll(context.requested_api_scopes)
};
containsAll requires the requested API scopes to be a subset of the allowed scopes. If the request includes any scope outside this list, the policy does not match.
To match a scope by its raw name without specifying the audience, use context.requested_scope_names. For example, deny issuing a token whenever a privileged scope is requested anywhere in the call:
forbid (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context.requested_scope_names.contains("admin")
};
context.requested_scope_names holds the raw names of every requested scope (identity and API, for any audience), so this matches an admin scope no matter which API defines it. Because names are merged across APIs, use the audience-qualified Scope::"..." form above when you need to restrict a specific API's scopes.
context.requested_audiences is the set of all API audiences requested in the current call. Beyond the baseline context.requested_audiences.contains(resource) guard, you can use it to control which APIs may share a single token.
For example, deny issuing a token for this API whenever a more privileged API is requested in the same call:
forbid (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context.requested_audiences.contains(Api::"https://api.example.com/admin")
};
Because a matching forbid always takes precedence, the request is denied whenever both the billing and admin audiences are requested together, preventing the two from sharing an access token.
Permit access only to a specific user by their user id:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context has user &&
context.user == User::"3f9a8b7c-1e2d-4a5b-9c8d-7e6f5a4b3c2d"
};
Each user is identified as User::"<user id>", so match context.user against the user's id to scope access to an individual user.
Machine-to-machine requests do not include a user. A user may also be absent during an authorization request, before the user has authenticated. Guard conditions that reference context.user with context has user.
Permit access only to a specific user by their primary email address:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context has user &&
context.user has email &&
context.user.email == "admin@example.com"
};
context.user.email holds the user's primary email address. It is optional, so guard it with context.user has email (in addition to context has user). A user without a primary email has no email attribute, and email comparisons are case-sensitive.
Permit access only when the authenticated user belongs to a specific group:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context has user &&
context.user in Group::"Billing Admins"
};
Require that the user authenticated within the last 12 hours:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context has authentication &&
context.now.durationSince(context.authentication.auth_time) <= duration("12h")
};
Authentication context is available only after a user has authenticated. Guard conditions that reference context.authentication with context has authentication.
Permit access only when the request originates from a named network zone:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context.network_zones.contains(NetworkZone::"office")
};
Use containsAny to allow requests from any of several trusted zones, or containsAll to require that all specified zones match.
Permit access only when the request IP address falls within a specific range:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context.location.ip_address.isInRange(ip("203.0.113.0/24"))
};
Deny requests originating outside approved countries:
forbid (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
unless {
[Country::"US", Country::"CA"].contains(context.location.country)
};
unless means this policy forbids the request unless the resolved country is in the approved list.
Because a forbid policy overrides any matching permit policy, this rule denies the request even when another policy allows it. Use a custom denial message on the policy to help callers understand why access was denied.
Permit access only on weekdays between 09:00 and 17:00 UTC:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
[
DayOfWeek::"monday",
DayOfWeek::"tuesday",
DayOfWeek::"wednesday",
DayOfWeek::"thursday",
DayOfWeek::"friday"
].contains(context.day_of_week) &&
context.time_of_day_seconds >= 32400 && // 09:00 UTC
context.time_of_day_seconds < 61200 // 17:00 UTC
};
Grant access only until a specified date and time:
permit (
principal == Client::"contractor-app",
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context.now < datetime("2026-12-31T23:59:59Z")
};
All time-based context attributes are evaluated in UTC.
Permit access only to workloads that present a client certificate validated by a specific trust store and with the expected subject distinguished name:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/orders"
)
when {
context has certificate &&
context.certificate.trust_store == TrustStore::"mihpmRHX1ApWIZDyugVQ2" &&
context.certificate.subject_dn == "CN=orders-worker,OU=production,O=example.com"
};
For workloads using SPIFFE X.509 SVIDs, match the SPIFFE ID in the certificate’s URI SAN instead:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/orders"
)
when {
context has certificate &&
context.certificate.trust_store == TrustStore::"r2dzFrCfgU1aX5nTQUTS4" &&
context.certificate.san_uris.contains("spiffe://example.com/ns/orders/sa/worker")
};
The certificate context is available only when the client authenticates with mTLS. Guard conditions that reference context.certificate with context has certificate.
When a client authenticates using workload identity, the request includes its SPIFFE ID. This can apply to both application and agent clients.
To permit only a specific workload identity:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/orders"
)
when {
context has spiffe_id &&
context.spiffe_id == "spiffe://example.com/ns/orders/sa/worker"
};
context.spiffe_id is available only for workload-identity requests, so guard conditions that reference it with context has spiffe_id.
Permit only machine-to-machine access using the client credentials grant:
permit (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/internal"
)
when {
context.grant_type == GrantType::"client_credentials"
};
Use context.grant_type to control which OAuth grants can obtain access tokens for an API. For example, you can allow client_credentials for service-to-service access while requiring user-centric grants for APIs that act on behalf of a user.
To explicitly block a grant type, add a forbid policy. This example prevents password-grant requests from obtaining a token for the API:
forbid (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/internal"
)
when {
context.grant_type == GrantType::"password"
};
A matching forbid policy takes precedence over any matching permit policy.
A common pattern is to combine a broad permit policy with targeted forbid policies that enforce hard restrictions.
// Policy 1: Allow clients in the First Party group to access the API
permit (
principal in Group::"First Party",
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
);
// Policy 2: Deny requests from blocked network zones
forbid (
principal,
action == Action::"IssueAccessToken",
resource == Api::"https://api.example.com/billing"
)
when {
context.network_zones.contains(NetworkZone::"blocked")
};
A request from a client in the First Party group in the blocked network zone matches both policies. MonoCloud denies the request because a matching forbid policy always takes precedence over a matching permit policy.
A permitting policy can also apply token settings to the requests it matches.
For example, suppose the billing API defaults to one-hour JWT access tokens. To issue short-lived, session-bound reference tokens for browser-based requests, configure a permit policy with:
context.caller_type == CallerType::"browser"AccessTokenType=reference, AccessTokenLifetime=300, and BindTokensToSession=trueOther requests that match a different permit policy with no token-setting overrides inherit the API resource defaults and receive one-hour JWT access tokens.
When a request matches both policies, MonoCloud merges their settings using the restrictive merge rules. The resulting token is a 300-second, session-bound reference token.
Start with a baseline permit policy for the clients or client group that should normally access an API. Add more specific permit policies when a subset of requests needs stricter token settings, such as shorter lifetimes for browser requests or reference tokens for a sensitive integration.
Use forbid policies for hard boundaries that must apply regardless of other matching permits. Typical examples include blocked network zones, prohibited countries, revoked partner clients, or access outside an approved time window.
When multiple permit policies match, MonoCloud merges their token settings using the restrictive merge rules. A more specific permit can tighten the effective token settings, but it cannot make them less restrictive than another matching policy.
permit policy must match. Check that the request includes the API audience and that any scope condition, such as containsAll, allows every requested scope for that API.forbid policy may also match. A matching forbid policy always takes precedence over a matching permit policy. Advanced deny policies can return a custom denial message to help identify the restriction.user (along with its optional email attribute), authentication, certificate, and spiffe_id are not present on every request. For example, a user may be absent before authentication during an authorization request, certificates are present only for mTLS requests, and a user may not have a primary email. Guard conditions that reference optional context with context has ... (and context.user has email for the user's email). A missing optional attribute causes that policy not to match; the request is denied only when no other permit policy matches.day_of_week, time_of_day_seconds, and now are evaluated in UTC.