Multi-Issuer Authorization Guide#
This guide provides a comprehensive overview of Cedarling's multi-issuer authorization feature, including concepts, implementation patterns, and real-world use cases.
Overview#
Multi-issuer authorization (authorize_multi_issuer) enables applications to make authorization decisions based on multiple JWT tokens from different identity providers in a single request. Unlike traditional authorization that creates User and Workload principals, multi-issuer authorization evaluates policies based purely on token entities themselves.
A batch variant, authorize_multi_issuer_batch, validates the token set once and evaluates N {resource, action, context} items against that shared snapshot. Same token contract as documented on this page (validation, entity creation, context.tokens naming, failure handling); the batching mechanics — request/response shape, batch_id correlation, and the batch-level vs per-item failure split — are covered in Batch Authorization.
Key Benefits#
- Federation Support: Native support for tokens from multiple identity providers
- Capability-Based Authorization: Make decisions based on capabilities asserted by different issuers
- Zero Trust Architecture: Each token represents verification from a different trust boundary
- Flexible Token Types: Support for custom token types beyond standard OAuth/OIDC tokens
- API Gateway Ready: Ideal for API gateways validating tokens from various upstream services
How It Works#
1. Request Structure#
A multi-issuer authorization request consists of:
{
"tokens": [
{
"mapping": "Jans::Access_Token",
"payload": "eyJhbGciOiJIUzI1NiIs..."
},
{
"mapping": "Jans::Id_Token",
"payload": "eyJhbGciOiJFZERTQSIs..."
},
{
"mapping": "Acme::DolphinToken",
"payload": "ey1b6cfMef21084633a7..."
}
],
"action": "Jans::Action::\"Read\"",
"resource": {
"cedar_entity_mapping": {
"entity_type": "Jans::Document",
"id": "doc-123"
},
"owner": "alice@example.com",
"classification": "confidential"
},
"context": {
"ip_address": "54.9.21.201",
"time": 1730000000
}
}
2. Token Processing Pipeline#
┌─────────────────────────────────────────────────────────┐
│ 1. Token Input │
│ Array of tokens with explicit type mappings │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. Token Validation │
│ - Signature verification │
│ - Time-based validation (exp, nbf) │
│ - Status validation (revocation check) │
│ - Trusted issuer verification │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. Entity Creation │
│ - Create Cedar entity for each valid token │
│ - Store token metadata (type, jti, issuer, exp) │
│ - Store JWT claims as entity tags (Set<String>) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. Token Collection Assembly │
│ - Organize tokens with predictable naming │
│ - Pattern: {issuer_name}_{token_type} │
│ - Example: acme_access_token, google_id_token │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 5. Policy Evaluation │
│ - Evaluate Cedar policies without principal │
│ - Policies reference context.tokens.{name} │
│ - Return authorization decision │
└─────────────────────────────────────────────────────────┘
3. Token Entity Structure#
Each validated token becomes a Cedar entity:
entity Token = {
"token_type"?: String, // e.g., "Jans::Access_Token"
"jti"?: String, // Token ID
"iss"?: Jans::TrustedIssuer, // JWT iss claim
"exp"?: Long, // Expiration timestamp
"validated_at"?: Long // Validation timestamp
} tags Set<String>;
All JWT claims are stored as tags and accessed using Cedar's tag operations:
context.tokens.acme_access_token.hasTag("scope")
context.tokens.acme_access_token.getTag("scope").contains("read:profile")
4. Token Collection Naming#
Tokens are organized in the context using a deterministic naming algorithm:
Pattern: {issuer_name}_{token_type}
Issuer Name Resolution:
- Look up issuer in trusted issuer metadata
- Use the
namefield from configuration - If no
namefield, extract hostname from JWTissclaim - Convert to lowercase, replace special characters with underscores
Token Type Resolution:
- Extract from
mappingfield (e.g., "Jans::Access_Token") - Split by namespace separator ("::"), and use the last segment.
- Convert to lowercase, preserve underscores
Examples:
| JWT Issuer | Trusted Issuer Name | Token Mapping | Result |
|---|---|---|---|
https://idp.acme.com/auth |
"Acme" |
Jans::Access_Token |
acme_access_token |
https://accounts.google.com |
"Google" |
Jans::Id_Token |
google_id_token |
https://idp.dolphin.sea/auth |
"Dolphin" |
Acme::DolphinToken |
dolphin_dolphintoken |
Use Cases#
Use Case 1: Federation Scenario#
Scenario: A collaborative platform accepts tokens from multiple corporate identity providers.
Requirements:
- Users can authenticate with their corporate IDP
- Authorization requires valid token from user's organization
- Different organizations have different permission structures
Implementation:
# User presents tokens from their corporate IDP and the platform IDP
tokens = [
TokenInput(
mapping="Jans::Access_Token",
payload="<acme_corp_token>" # From Acme Corp IDP
),
TokenInput(
mapping="Platform::Access_Token",
payload="<platform_token>" # From platform IDP
)
]
request = AuthorizeMultiIssuerRequest(
tokens=tokens,
action='Platform::Action::"ShareDocument"',
resource=document,
context={"ip_address": "192.168.1.100"}
)
result = cedarling.authorize_multi_issuer(request)
Policy:
// Allow sharing if user has valid tokens from both IDPs
permit(
principal,
action == Platform::Action::"ShareDocument",
resource in Platform::Document
) when {
// Verify corporate IDP token with employee status
context has tokens.acme_corp_access_token &&
context.tokens.acme_corp_access_token.hasTag("employee_status") &&
context.tokens.acme_corp_access_token.getTag("employee_status").contains("active") &&
// Verify platform token with sharing scope
context has tokens.platform_access_token &&
context.tokens.platform_access_token.hasTag("scope") &&
context.tokens.platform_access_token.getTag("scope").contains("share:documents")
};
Use Case 2: API Gateway with Multiple Upstream Services#
Scenario: An API gateway needs to validate tokens from various upstream microservices.
Requirements:
- Each microservice issues its own JWT tokens
- Gateway validates all tokens before forwarding requests
- Authorization based on combination of service capabilities
Implementation:
// Gateway receives tokens from multiple services
let tokens = [
{
mapping: "AuthService::Access_Token",
payload: authServiceToken,
},
{
mapping: "PaymentService::Access_Token",
payload: paymentServiceToken,
},
{
mapping: "UserService::Access_Token",
payload: userServiceToken,
},
];
let request = {
tokens: tokens,
action: 'Gateway::Action::"ProcessPayment"',
resource: {
cedar_entity_mapping: {
entity_type: "Gateway::Transaction",
id: "txn-12345",
},
amount: 1000.0,
currency: "USD",
},
context: {
ip_address: request.ip,
user_agent: request.headers["user-agent"],
},
};
// WASM binding: the request is passed as a JSON string
let result = await cedarling.authorize_multi_issuer(JSON.stringify(request));
Policy:
// Require tokens from all three services for payment processing
permit(
principal,
action == Gateway::Action::"ProcessPayment",
resource in Gateway::Transaction
) when {
// Auth service token with authenticated user
context has tokens.auth_service_access_token &&
context.tokens.auth_service_access_token.hasTag("authenticated") &&
// Payment service token with sufficient balance
context has tokens.payment_service_access_token &&
context.tokens.payment_service_access_token.hasTag("balance_verified") &&
// User service token with kyc_verified status
context has tokens.user_service_access_token &&
context.tokens.user_service_access_token.hasTag("kyc_verified") &&
context.tokens.user_service_access_token.getTag("kyc_verified").contains("true")
};
Use Case 3: Multi-Organization Voting System#
Scenario: A trade association requires tokens from both the association and member organizations for voting.
Requirements:
- User must have valid membership token from trade association
- User must have valid employee token from their organization
- Organization must be a corporate member
- User must be designated as voting representative
Implementation:
tokens := []cedarling_go.TokenInput{
{
Mapping: "TradeAssociation::MemberToken",
Payload: memberToken,
},
{
Mapping: "Jans::Access_Token",
Payload: employeeToken,
},
}
request := cedarling_go.AuthorizeMultiIssuerRequest{
Tokens: tokens,
Action: `TradeAssociation::Action::"Vote"`,
Resource: cedarling_go.EntityData{
CedarMapping: cedarling_go.CedarMapping{
EntityType: "TradeAssociation::Election",
ID: "election-2025",
},
Payload: map[string]any{
"election_type": "board",
"year": 2025,
},
},
}
result, err := instance.AuthorizeMultiIssuer(request)
Policy:
permit(
principal,
action == TradeAssociation::Action::"Vote",
resource in TradeAssociation::Election
) when {
// Require corporate membership token
context has tokens.trade_association_member_token &&
context.tokens.trade_association_member_token.hasTag("member_status") &&
context.tokens.trade_association_member_token.getTag("member_status").contains("Corporate Member") &&
// Require employee token with voting representative designation
context has tokens.company_access_token &&
context.tokens.company_access_token.hasTag("role") &&
context.tokens.company_access_token.getTag("role").contains("voting_representative")
};
Use Case 4: Healthcare HIPAA Compliance#
Scenario: Healthcare system requires multiple consent tokens for accessing medical records.
Requirements:
- Patient consent token required
- Provider credentials token required
- Facility authorization token required
- Purpose of use must match all tokens
Implementation:
tokens = [
TokenInput(
mapping="Healthcare::PatientConsent",
payload=patient_consent_token
),
TokenInput(
mapping="Healthcare::ProviderCredentials",
payload=provider_credentials_token
),
TokenInput(
mapping="Healthcare::FacilityAuth",
payload=facility_token
)
]
request = AuthorizeMultiIssuerRequest(
tokens=tokens,
action='Healthcare::Action::"AccessMedicalRecord"',
resource=medical_record,
context={
"purpose_of_use": "TREATMENT",
"emergency": False
}
)
Policy:
permit(
principal,
action == Healthcare::Action::"AccessMedicalRecord",
resource in Healthcare::MedicalRecord
) when {
// Patient consent token
context has tokens.patient_consent &&
context.tokens.patient_consent.hasTag("consent_status") &&
context.tokens.patient_consent.getTag("consent_status").contains("granted") &&
context.tokens.patient_consent.hasTag("expiry") &&
context.tokens.patient_consent.getTag("expiry").contains("2025-12-31") &&
// Provider credentials token
context has tokens.provider_credentials &&
context.tokens.provider_credentials.hasTag("license_status") &&
context.tokens.provider_credentials.getTag("license_status").contains("active") &&
context.tokens.provider_credentials.hasTag("specialty") &&
// Facility authorization token
context has tokens.facility_auth &&
context.tokens.facility_auth.hasTag("facility_type") &&
context.tokens.facility_auth.getTag("facility_type").contains("hospital") &&
// Purpose of use alignment
context has purpose_of_use &&
context.purpose_of_use == "TREATMENT"
};
Use Case 5: Zero Trust Network with Custom Token Types#
Scenario: A zero-trust architecture uses custom tokens for device attestation, network verification, and user authentication.
Requirements:
- Device attestation token from hardware TPM
- Network security token from network controller
- User authentication token from IDP
- All three required for accessing sensitive resources
Implementation:
let tokens = [
{
mapping: "Security::DeviceAttestation",
payload: deviceAttestationToken, // From device TPM
},
{
mapping: "Security::NetworkToken",
payload: networkToken, // From network controller
},
{
mapping: "Jans::Access_Token",
payload: userAccessToken, // From IDP
},
];
// WASM binding: the request is passed as a JSON string
let result = await cedarling.authorize_multi_issuer(JSON.stringify({
tokens: tokens,
action: 'Security::Action::"AccessClassified"',
resource: {
cedar_entity_mapping: {
entity_type: "Security::Document",
id: "classified-123",
},
classification: "SECRET",
compartment: "SPECIAL_ACCESS",
},
context: {
location: "secure_facility",
time: Date.now(),
},
}));
Policy:
permit(
principal,
action == Security::Action::"AccessClassified",
resource in Security::Document
) when {
// Device attestation with hardware-backed key
context has tokens.device_attestation &&
context.tokens.device_attestation.hasTag("tpm_verified") &&
context.tokens.device_attestation.getTag("tpm_verified").contains("true") &&
context.tokens.device_attestation.hasTag("encryption_level") &&
context.tokens.device_attestation.getTag("encryption_level").contains("FIPS-140-2") &&
// Network token from secure network
context has tokens.network_token &&
context.tokens.network_token.hasTag("network_type") &&
context.tokens.network_token.getTag("network_type").contains("CLASSIFIED") &&
context.tokens.network_token.hasTag("segment") &&
context.tokens.network_token.getTag("segment").contains("HIGH_SIDE") &&
// User access token with clearance
context has tokens.user_access_token &&
context.tokens.user_access_token.hasTag("clearance_level") &&
context.tokens.user_access_token.getTag("clearance_level").contains("SECRET") &&
// Location verification
context has location &&
context.location == "secure_facility"
};
Custom (Non-JWT) Token Processing#
Multi-issuer authorization is not limited to JWTs. A custom token processor lets Rust consumers authorize on non-JWT credentials like opaque tokens, API keys, vendor-specific formats, or tokens whose validation uses cryptography Cedarling does not implement while reusing the same entity builder, context.tokens.* machinery, and policies as JWTs.
Trust model: the processor's output is authoritative. Cedarling performs no signature or issuer verification on a custom token, validating the payload is entirely the processor's responsibility. Treat a registered processor as fully trusted code. The one exception is expiration: if the processor reports one (via
expirationor anexpclaim), Cedarling rejects the token once that time has passed.Availability: the processor is a Rust trait object registered on a live instance, so this feature is Rust-native only (native and the
blockingclient). It is not exposed through any of the bindings.
How routing works#
Routing is decided by the policy store alone: a token is sent to the custom path instead of the JWT pipeline whenever its mapping equals a token type declared by some custom issuer. Registering a CustomTokenProcessor does not change which path a token takes only whether that path can succeed.
So clearing the processor does not fall back to the JWT pipeline. If a mapping matches a custom issuer but no processor is registered, the token fails with NoProcessorRegistered (the whole request fails) for a required issuer, otherwise the token is skipped. A custom mapping therefore cannot equal a JWT trusted issuer's token type; that collision is rejected at instance startup.
1. Configure the custom issuer#
Add a custom_issuers map to the policy store, keyed by issuer name (mirrors trusted_issuers). For the directory format, use per-file custom-issuers/*.json, see Custom Issuer Files.
{
"custom_issuers": {
"CustomIssuerName": {
"tokens_mappings": {
"Custom::ApiKey": {
"required": true,
"required_claims": ["sub"]
},
"Custom::SessionKey": {}
}
}
}
}
| Field | Type | Description |
|---|---|---|
tokens_mappings |
map (required) | Token types this issuer emits, keyed by Cedar entity type name. The key is matched against the request mapping to route a token to this issuer, so one issuer can emit several token types. Must declare at least one. |
tokens_mappings.<type>.required |
bool (default false) |
When true, a processing failure (error, timeout, or missing required claim) fails the whole request. When false, the token is dropped and authorization continues without it. Set per token type, so one issuer can mix required and optional tokens. |
tokens_mappings.<type>.required_claims |
string[] (default []) |
Claims that must be present in the processor output; a missing claim yields MissingRequiredClaim. |
Each token type lands under its own context.tokens.{issuer}_{token_type} key, so the example above yields customkeys_apikey and customkeys_sessionkey.
An entity type name may be declared by only one custom issuer; support for several issuers sharing a type is tracked in issue #14747. Custom issuer names must be unique after sanitization and must not collide with a JWT trusted-issuer name since both share the context.tokens key namespace, so a collision fails instance startup.
2. Implement the processor#
Implement CustomTokenProcessor::process, which turns a raw payload into ProcessedTokenClaims. One processor handles all custom mappings and dispatches internally on mapping.
use cedarling::{CustomTokenError, CustomTokenProcessor, ProcessedTokenClaims};
use async_trait::async_trait;
use std::collections::HashMap;
struct ApiKeyProcessor;
#[async_trait]
impl CustomTokenProcessor for ApiKeyProcessor {
async fn process(
&self,
mapping: &str,
payload: &str,
) -> Result<ProcessedTokenClaims, CustomTokenError> {
// Validate the opaque payload however you like (DB lookup, HMAC, vault, ...).
if payload != "secret-admin-key" {
return Err(CustomTokenError::Processing(format!(
"unknown API key for mapping '{mapping}'"
)));
}
let mut claims = HashMap::new();
claims.insert("sub".to_string(), serde_json::json!("api-key-user"));
claims.insert("scope".to_string(), serde_json::json!("admin"));
let mut processed = ProcessedTokenClaims::new(claims, "api-key-1");
processed.cacheable = false; // re-validate on every request (revocation-sensitive)
Ok(processed)
}
}
ProcessedTokenClaims fields:
| Field | Type | Description |
|---|---|---|
claims |
map |
Claims for the token entity. Stored as tags (Set<String>), exactly like JWT claims. |
token_id |
string | Entity id of the resulting token entity supplied directly, not read from a claim. |
issuer_id |
string? (None) |
Which custom issuer this token belongs to. None falls back to the sole issuer declaring the mapping (an explicit value is required when several issuers share one mapping). |
expiration |
i64? (None) |
Optional expiration (unix seconds). The token is rejected once it passes, and the value bounds the token-cache TTL. Falls back to an exp claim when None; an explicit value wins over the claim. |
cacheable |
bool (default true) |
Set false for revocation-sensitive tokens so every request re-runs process. |
ProcessedTokenClaims::new(claims, token_id) builds a cacheable result with no issuer hint or expiration.
3. Register the processor#
Register (or clear) the processor on a live instance. It survives policy-store refreshes.
use std::sync::Arc;
cedarling.set_custom_token_processor(Some(Arc::new(ApiKeyProcessor)));
// Later, to disable custom-token processing:
cedarling.set_custom_token_processor(None);
4. Write the policy#
A custom token becomes a Cedar entity at context.tokens.{issuer}_{token_type} issuer CustomIssuerName + mapping Custom::ApiKey → customissuername_apikey. Claims are tags; the token entity's iss attribute is the sanitized issuer name as a plain string (there is no TrustedIssuer entity for a custom issuer).
permit(
principal,
action == Custom::Action::"Read",
resource == Custom::Resource::"Doc"
) when {
context has tokens.customkeys_apikey &&
context.tokens.customkeys_apikey.hasTag("scope") &&
context.tokens.customkeys_apikey.getTag("scope").contains("admin")
};
The matching schema types customkeys_apikey into context.tokens as Custom::ApiKey (all attributes optional, tags Set<String>), following the same rules as Cedar Schema for Multi-Issuer Tokens.
Failure handling and caching#
| Situation | Behavior |
|---|---|
Processor returns Ok |
Claims flow into context.tokens.*. |
Processor returns Err, token required: true |
Whole request fails with the error. |
Processor returns Err, token required: false |
Token dropped, authorization continues. |
mapping is custom but no processor registered |
NoProcessorRegistered (fail-closed if required, else skipped). |
A required_claims entry is absent from the output |
MissingRequiredClaim. |
The reported expiration (expiration, else an exp claim) has already passed |
Expired. |
The processor returns an issuer_id that does not declare the requested type |
UnknownTokenType. |
process exceeds the configured timeout (> 0) |
Timeout. |
Set CEDARLING_CUSTOM_TOKEN_PROCESSOR_TIMEOUT_MILLIS to bound slow processors. 0 (the default) disables the timeout; see Cedarling Properties. Keep cacheable: true (the default) to skip re-running process for an identical payload and use false for revocation-sensitive tokens.
A complete, runnable example can be found in
cedarling/examples/custom_token_processor.rs.
Configuration Guide#
Schema Requirements#
IMPORTANT: Multi-issuer authorization requires specific Cedar schema modifications. Without these changes, authorization will fail with schema validation errors.
Required Schema Changes#
Multi-issuer authorization creates token entities dynamically and places them in the Cedar context. Your schema must support:
1. Token Entity Structure
Token entities must have these required attributes:
// Jans namespace: shared infrastructure types used across namespaces.
namespace Jans {
type Url = {
host: String,
path: String,
protocol: String
};
};
// Acme namespace: token entities for the Acme IDP.
namespace Acme {
entity TrustedIssuer = {
issuer_entity_id: Jans::Url
};
entity Access_token = {
token_type?: String, // Required for multi-issuer
jti?: String, // Required for multi-issuer
iss?: TrustedIssuer, // Required for multi-issuer
exp?: Long, // Required for multi-issuer
validated_at?: Long, // Required for multi-issuer
// Other JWT claims as optional attributes
aud?: String,
iat?: Long,
scope?: Set<String>,
// ...
} tags Set<String>; // Required for dynamic JWT claims
};
2. Context Structure
The Context type must include a tokens field:
type Context = {
network?: String,
// ... other context fields
tokens?: TokensContext, // Required for multi-issuer
};
type TokensContext = {
total_token_count: Long, // Required
// Individual token fields added dynamically
};
3. Making Attributes Optional
All token entity attributes (except the core multi-issuer fields) must be optional (?) to prevent schema validation errors. This is because multi-issuer tokens may not have all the claims that standard authorization tokens have.
Why These Changes Are Needed#
- Dynamic token entities: Multi-issuer authorization creates token entities on-the-fly without User/Workload principals
- Tag-based claims: JWT claims are stored as entity tags (
Set<String>by default) for flexible access - Context structure: Tokens are organized in
context.tokens.{issuer}_{token_type}format - Schema validation: Cedar validates entities against the schema; missing required fields cause errors
Updating Core Schema#
If you're using the default cedarling_core.cedarschema from Agama Lab, it has been updated to support multi-issuer authorization. If you have a custom schema, make sure to apply these changes.
Cedar Schema for Multi-Issuer Actions#
Multi-issuer authorization runs Cedar's partial evaluator with no principal — authorize_multi_issuer does not accept one, and none is constructed internally. Actions used in multi-issuer requests must declare this in their appliesTo, and policies referencing those actions must leave the principal unconstrained.
Declaring the action#
Cedar's schema validator rejects an empty principal list (for action '...', 'principal' is '[]', which is invalid). To declare an action that runs without a principal, declare a placeholder entity type purely to satisfy the schema and reference it in appliesTo:
entity Any;
action "ReadArtifact" appliesTo {
principal: [Any],
resource: [Artifact],
context: Context
};
No instance of Any is ever constructed at runtime. authorize_multi_issuer invokes the action with principal: None and Cedar's partial evaluator runs against the policies. The placeholder entity type exists only so the schema parses.
Writing the policy#
Policies for multi-issuer actions must use the unconstrained permit(principal, ...) head — do not add principal == ... or principal is ..., because no principal exists at evaluation time:
permit(
principal,
action == Action::"ReadArtifact",
resource == Artifact::"doc-1"
) when {
context.tokens.AcmeCorp_access_token.scope.contains("read")
};
If a policy does constrain the principal, Cedar's partial evaluator cannot fully evaluate it and emits a residual. Residual-dependent requests fail closed with Deny in Cedarling; the request's diagnostics list the residual policy ids so they can be located and fixed.
Policy Store Configuration#
Configure trusted issuers with the name field for predictable token naming:
{
"trusted_issuers": {
"acme_corp_issuer": {
"name": "AcmeCorp",
"description": "Acme Corporation Identity Provider",
"openid_configuration_endpoint": "https://idp.acme.com/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"entity_type_name": "AcmeCorp::Access_Token",
"token_id": "jti"
}
}
},
"google_issuer": {
"name": "Google",
"description": "Google Identity Provider",
"openid_configuration_endpoint": "https://accounts.google.com/.well-known/openid-configuration",
"token_metadata": {
"id_token": {
"entity_type_name": "Google::Id_Token",
"token_id": "jti"
}
}
},
"custom_service_issuer": {
"name": "CustomService",
"description": "Custom Service Provider",
"openid_configuration_endpoint": "https://service.example.com/.well-known/openid-configuration",
"token_metadata": {
"custom_token": {
"entity_type_name": "CustomService::ServiceToken",
"token_id": "jti"
}
}
}
}
}
Cedar Schema for Multi-Issuer Tokens#
Core Token Schema Structure#
Each token type (Access_Token, Id_Token, custom tokens) must follow this structure:
// Jans namespace: shared infrastructure types used across namespaces.
namespace Jans {
type Url = {
host: String,
path: String,
protocol: String
};
type email_address = {
domain: String,
uid: String
};
};
// Acme namespace: token entities for the Acme IDP.
namespace Acme {
entity TrustedIssuer = {
issuer_entity_id: Jans::Url
};
// Core token entity structure compatible with multi-issuer authorization
entity Access_token = {
// Required multi-issuer attributes
token_type?: String, // Entity type name (e.g., "Acme::Access_token")
jti?: String, // JWT ID - unique token identifier
iss?: TrustedIssuer, // Issuer entity reference
exp?: Long, // Token expiration timestamp
validated_at?: Long, // Timestamp when token was validated
// Optional JWT claims (make all optional for compatibility)
aud?: String, // Audience
iat?: Long, // Issued at
scope?: Set<String>, // OAuth scopes
client_id?: String, // Client identifier
sub?: String, // Subject
// Add other JWT claims as needed
} tags Set<String>; // Tags store dynamic JWT claims
entity id_token = {
// Required multi-issuer attributes
token_type?: String,
jti?: String,
iss?: TrustedIssuer,
exp?: Long,
validated_at?: Long,
// Optional JWT claims
aud?: Set<String>,
iat?: Long,
sub?: String,
email?: Jans::email_address,
name?: String,
phone_number?: String,
role?: Set<String>,
acr?: String,
amr?: Set<String>,
// Add other JWT claims as needed
} tags Set<String>;
entity Userinfo_token = {
// Required multi-issuer attributes
token_type?: String,
jti?: String,
iss?: TrustedIssuer,
exp?: Long,
validated_at?: Long,
// Optional JWT claims
aud?: String,
iat?: Long,
sub?: String,
email?: Jans::email_address,
name?: String,
birthdate?: String,
phone_number?: String,
role?: Set<String>,
// Add other JWT claims as needed
} tags Set<String>;
};
Custom Token Types#
For custom token types, follow the same pattern:
// Jans namespace: shared infrastructure types used across namespaces.
namespace Jans {
type Url = {
host: String,
path: String,
protocol: String
};
};
// Custom namespace: custom service token entities.
namespace Custom {
entity TrustedIssuer = {
issuer_entity_id: Jans::Url
};
entity ServiceToken = {
// Required multi-issuer attributes
token_type?: String,
jti?: String,
iss?: TrustedIssuer,
exp?: Long,
validated_at?: Long,
// Custom token-specific attributes
service_id?: String,
permissions?: Set<String>,
service_tier?: String,
} tags Set<String>;
};
Complete Context Schema#
Define the Context type to include the tokens field:
type Context = {
// Standard context fields
network?: String,
network_type?: String,
user_agent?: String,
operating_system?: String,
device_health?: Set<String>,
current_time?: Long,
geolocation?: Set<String>,
fraud_indicators?: Set<String>,
// Multi-issuer tokens context (required)
tokens?: TokensContext,
};
type TokensContext = {
total_token_count: Long,
// Individual token fields are added dynamically by Cedarling
// Pattern: {issuer_name}_{token_type} (e.g., acme_access_token)
};
Key Schema Principles#
- Optional Attributes: All token attributes must be optional (
?) to support both standard and multi-issuer authorization - Tags Declaration: All token entities must declare
tags Set<String>for dynamic JWT claim storage - Context Integration: The Context type must include an optional
tokensfield - Consistency: Use the same attribute names across all token types (token_type, jti, issuer, exp, validated_at)
Error Handling#
Token Validation Failures#
# Individual token validation failures are handled gracefully
tokens = [
TokenInput(mapping="Jans::Access_Token", payload="valid_token"),
TokenInput(mapping="Jans::Id_Token", payload="invalid_token"), # Will be ignored
TokenInput(mapping="Acme::CustomToken", payload="valid_custom_token")
]
# Authorization continues with valid tokens
# Invalid tokens are logged but don't block processing
result = cedarling.authorize_multi_issuer(request)
However, if every token is invalid, Cedarling will raise an error. It is important that users always handle errors gracefully.
Non-Deterministic Tokens#
# ERROR: Multiple tokens of same type from same issuer
tokens = [
TokenInput(mapping="Jans::Access_Token", payload="token1"), # From Jans::Access_Token
TokenInput(mapping="Jans::Access_Token", payload="token2"), # Also from Jans::Access_Token - ERROR!
]
# This non-deterministic.
# Which token should policies reference?
# Cedarling processes only the first item and writes log messages for all subsequent items that are skipped.
Trusted Issuer Validation#
# Tokens from unknown issuers are rejected
tokens = [
TokenInput(
mapping="Jans::Access_Token",
payload="token_from_unknown_issuer" # ERROR if issuer not in trusted issuers
)
]
# Only tokens from issuers configured in policy store are accepted
Best Practices#
1. Use Descriptive Issuer Names#
Configure clear, predictable issuer names in your policy store:
{
"name": "AcmeCorp", // Good - clear and predictable
"name": "Issuer1" // Bad - unclear what this represents
}
2. Start Schema-Less for Development#
Begin without Cedar schemas for rapid development:
- All claims stored in tags as
Set<String> - Flexible and forgiving during development
- Add schemas later for production type safety
3. Implement Comprehensive Logging#
Monitor token validation and policy evaluation:
result = cedarling.authorize_multi_issuer(request)
# Retrieve logs for debugging
logs = cedarling.get_logs_by_request_id(result.request_id)
for log in logs:
print(f"Log: {log}")
4. Handle Failed Tokens Gracefully#
Design policies to work with partial token sets:
// Allow if EITHER token is present
permit(
principal,
action == Jans::Action::"Read",
resource in Jans::Document
) when {
(context has tokens.acme_access_token &&
context.tokens.acme_access_token.hasTag("scope") &&
context.tokens.acme_access_token.getTag("scope").contains("read")) ||
(context has tokens.google_access_token &&
context.tokens.google_access_token.hasTag("scope") &&
context.tokens.google_access_token.getTag("scope").contains("read"))
};
5. Test with Multiple Issuer Combinations#
Test policies with various token combinations:
# Test with all tokens
all_tokens_result = cedarling.authorize_multi_issuer(all_tokens_request)
# Test with subset of tokens
partial_tokens_result = cedarling.authorize_multi_issuer(partial_tokens_request)
# Test with invalid tokens mixed in
mixed_tokens_result = cedarling.authorize_multi_issuer(mixed_tokens_request)