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.
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, iat) │
│ - 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_acme_dolphin_token |
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"],
},
};
let result = await cedarling.authorize_multi_issuer(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
},
];
let result = await cedarling.authorize_multi_issuer({
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"
};
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:
namespace Jans{
entity Access_token = {
token_type?: String, // Required for multi-issuer
jti?: String, // Required for multi-issuer
iss?: Jans::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
entity TrustedIssuer = {
issuer_entity_id: Url
};
}
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.
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:
namespace Jans {
// Core token entity structure compatible with both standard and multi-issuer authorization
entity Access_token = {
// Required multi-issuer attributes
token_type?: String, // Entity type name (e.g., "Jans::Access_Token")
jti?: String, // JWT ID - unique token identifier
iss?: TrustedIssuer, // Issuer entity reference (for standard authz)
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?: 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?: email_address,
name?: String,
birthdate?: String,
phone_number?: String,
role?: Set<String>,
// Add other JWT claims as needed
} tags Set<String>;
entity TrustedIssuer = {
issuer_entity_id: Url
};
}
Custom Token Types#
For custom token types, follow the same pattern:
namespace Custom {
entity ServiceToken = {
// Required multi-issuer attributes
token_type?: String,
jti?: String,
iss?: Custom::TrustedIssuer,
exp?: Long,
validated_at?: Long,
// Custom token-specific attributes
service_id?: String,
permissions?: Set<String>,
service_tier?: String,
} tags Set<String>;
entity TrustedIssuer = {
issuer_entity_id: Url
};
}
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)
Migration from Standard Authorization#
Migrating from authorize() to authorize_multi_issuer():
Before (Standard Authorization)#
request = Request(
tokens={
"access_token": access_token,
"id_token": id_token
},
action='Jans::Action::"Read"',
resource=resource,
context=context
)
result = cedarling.authorize(request)
if result.is_allowed():
# Access granted
Policy:
permit(
principal is Jans::User,
action == Jans::Action::"Read",
resource in Jans::Document
) when {
principal.email == resource.owner
};
After (Multi-Issuer Authorization)#
request = AuthorizeMultiIssuerRequest(
tokens=[
TokenInput(mapping="Jans::Access_Token", payload=access_token),
TokenInput(mapping="Jans::Id_Token", payload=id_token)
],
action='Jans::Action::"Read"',
resource=resource,
context=context
)
result = cedarling.authorize_multi_issuer(request)
if result.decision:
# Access granted
Policy:
permit(
principal,
action == Jans::Action::"Read",
resource in Jans::Document
) when {
context has tokens.jans_id_token &&
context.tokens.jans_id_token.hasTag("email") &&
context.tokens.jans_id_token.getTag("email").contains(resource.owner)
};