Authorization
The EDK provides a flexible authorization framework that integrates with external Policy Decision Points (PDPs) via the OpenID AuthZEN specification. This enables fine-grained, policy-based access control without embedding authorization logic in your application.
Architecture
The authorization system follows a Policy Enforcement Point (PEP) / Policy Decision Point (PDP) architecture:
Key Components
PolicyEngine
The generic interface for authorization decisions. Abstracts different policy engines:
interface PolicyEngine {
val id: String
val engineType: PolicyEngineType // CEDAR, OPA, AUTHZEN, CUSTOM
suspend fun isHealthy(): Boolean
suspend fun evaluate(request: PolicyRequest): IdkResult<PolicyDecision, IdkError>
}
AuthZenPdp
The OpenID AuthZEN-specific PDP interface:
interface AuthZenPdp {
val id: String
suspend fun isHealthy(): Boolean
suspend fun evaluate(request: AuthZenEvaluationRequest): IdkResult<AuthZenEvaluationResponse, IdkError>
suspend fun getConfiguration(): IdkResult<AuthZenConfiguration, IdkError>
}
ResilientAuthZenPdp
Production-ready wrapper with resilience patterns:
- Circuit breaker - Prevents cascading failures when PDP is unavailable
- Response caching - Reduces PDP load for repeated requests
- Fallback handling - Configurable behavior when PDP fails
Supported Policy Engines
| Engine | Protocol | Description |
|---|---|---|
| Cedarling | AuthZEN | Cedar policy engine via Janssen sidecar |
| OPA | REST API | Open Policy Agent with Rego policies |
| Generic | AuthZEN | Any AuthZEN-compliant PDP |
Request/Response Models
PolicyRequest
The principal-action-resource model for authorization:
data class PolicyRequest(
val principal: PolicyPrincipal, // Who is requesting
val action: PolicyAction, // What operation
val resource: PolicyResource, // What target
val context: PolicyContext // Additional context
)
// Principal (the entity making the request)
data class PolicyPrincipal(
val type: String, // "User", "Workload", "Role"
val id: String,
val attributes: Map<String, JsonElement> = emptyMap()
)
// Action (the operation being requested)
data class PolicyAction(
val name: String, // Command ID like "party.create"
val attributes: Map<String, JsonElement> = emptyMap()
)
// Resource (the target of the request)
data class PolicyResource(
val type: String,
val id: String? = null,
val attributes: Map<String, JsonElement> = emptyMap()
)
// Context (environmental information)
data class PolicyContext(
val tenantId: String? = null,
val sessionId: String? = null,
val timestamp: Long? = null,
val attributes: Map<String, JsonElement> = emptyMap()
)
PolicyDecision
The result of policy evaluation:
data class PolicyDecision(
val decision: Decision, // PERMIT or DENY
val reasons: List<String> = emptyList(),
val diagnostics: Map<String, JsonElement> = emptyMap()
) {
val isPermitted: Boolean get() = decision == Decision.PERMIT
val isDenied: Boolean get() = decision == Decision.DENY
}
enum class Decision {
PERMIT,
DENY
}
AuthZEN Models
The OpenID AuthZEN protocol uses slightly different terminology:
// AuthZEN Subject (principal)
data class AuthZenSubject(
val type: String, // "user", "workload", "role"
val id: String,
val properties: Map<String, JsonElement> = emptyMap()
) {
companion object {
fun user(id: String) = AuthZenSubject("user", id)
fun workload(id: String) = AuthZenSubject("workload", id)
fun role(id: String) = AuthZenSubject("role", id)
}
}
// AuthZEN Evaluation Request
data class AuthZenEvaluationRequest(
val subject: AuthZenSubject,
val action: AuthZenAction,
val resource: AuthZenResource,
val context: AuthZenContext
)
// AuthZEN Evaluation Response
data class AuthZenEvaluationResponse(
val decision: Boolean, // true = permit, false = deny
val context: Map<String, JsonElement>? = null
)
Basic Usage
Evaluating Authorization
import com.sphereon.authz.policy.*
import jakarta.inject.Inject
import jakarta.inject.Singleton
@Singleton
class PartyService @Inject constructor(
private val policyEngine: PolicyEngine
) {
suspend fun createParty(userId: String, tenantId: String, input: PartyInput): Party {
// Build authorization request
val request = PolicyRequest(
principal = PolicyPrincipal(type = "User", id = userId),
action = PolicyAction(name = "party.create"),
resource = PolicyResource(type = "Party"),
context = PolicyContext(tenantId = tenantId)
)
// Evaluate policy
val decision = policyEngine.evaluate(request)
.getOrThrow()
if (decision.isDenied) {
throw AccessDeniedException("User $userId not authorized to create parties")
}
// Proceed with operation
return createPartyInternal(input)
}
}
Using AuthZEN Directly
import com.sphereon.authz.authzen.pdp.AuthZenPdp
import com.sphereon.authz.authzen.model.*
@Singleton
class AuthorizationService @Inject constructor(
private val pdp: AuthZenPdp
) {
suspend fun checkAccess(
userId: String,
action: String,
resourceType: String,
resourceId: String?,
tenantId: String,
sessionId: String
): Boolean {
val request = AuthZenEvaluationRequest(
subject = AuthZenSubject.user(userId),
action = AuthZenAction(name = action),
resource = AuthZenResource(type = resourceType, id = resourceId),
context = AuthZenContext(tenantId = tenantId, sessionId = sessionId)
)
val result = pdp.evaluate(request)
return result.getOrNull()?.decision ?: false
}
}
Configuration
AuthZenConfig
Configure the authorization subsystem:
data class AuthZenConfig(
val enabled: Boolean = false,
val pdp: AuthZenPdpConfig? = null,
val fallbackPolicy: FallbackPolicy = FallbackPolicy.DENY,
val cacheEnabled: Boolean = true,
val cacheTtlSeconds: Long = 300,
val excludePatterns: List<String> = listOf(
"health.**",
"discovery.**",
"actuator.**"
),
val includePatterns: List<String> = listOf("**"),
val resilience: ResilienceConfig = ResilienceConfig()
)
PDP Configuration
Configure the connection to your PDP:
// Cedarling (Cedar via Janssen)
val cedarlingConfig = AuthZenPdpConfig.Cedarling(
baseUrl = "http://cedarling-sidecar:8080",
evaluationPath = "/cedarling/evaluation",
healthPath = "/health",
timeoutMs = 5000
)
// Open Policy Agent
val opaConfig = AuthZenPdpConfig.Opa(
baseUrl = "http://opa:8181",
policyPath = "/v1/data/authz/allow",
timeoutMs = 5000
)
// Generic AuthZEN-compliant PDP
val genericConfig = AuthZenPdpConfig.Generic(
baseUrl = "https://pdp.example.com",
evaluationPath = "/access/v1/evaluation",
wellKnownPath = "/.well-known/authzen-configuration",
timeoutMs = 5000
)
Fallback Policies
Configure behavior when the PDP is unavailable:
| Policy | Behavior | Use Case |
|---|---|---|
DENY | Deny all requests (fail closed) | Production - secure default |
ALLOW | Allow all requests (fail open) | Development/testing only |
FAIL | Propagate error to caller | When explicit error handling needed |
Resilience Configuration
data class ResilienceConfig(
val circuitBreakerEnabled: Boolean = true,
val failureThreshold: Int = 5, // Failures before opening
val resetTimeMs: Long = 30000, // 30 seconds before retry
val retryEnabled: Boolean = false,
val maxRetries: Int = 2,
val retryDelayMs: Long = 100
)
Circuit Breaker
The circuit breaker prevents cascading failures when the PDP is unavailable:
Monitoring Circuit State
val resilientPdp: ResilientAuthZenPdp = // injected
// Check circuit breaker state
val state = resilientPdp.getCircuitBreakerState()
// State.CLOSED, State.OPEN, or State.HALF_OPEN
// Get cache statistics
val stats = resilientPdp.getCacheStats()
// Manual operations
resilientPdp.invalidateCache()
resilientPdp.resetCircuitBreaker()
Properties Configuration
Configure via application properties:
sphereon:
authz:
enabled: true
pdp:
type: cedarling
base-url: http://cedarling-sidecar:8080
timeout-ms: 5000
fallback-policy: deny
cache-enabled: true
cache-ttl-seconds: 300
exclude-patterns:
- "health.**"
- "actuator.**"
resilience:
circuit-breaker-enabled: true
failure-threshold: 5
reset-time-ms: 30000
Integration with Spring Boot
When using @EnableSphereonRestApi, authorization can be integrated via filters or interceptors:
import org.springframework.web.filter.OncePerRequestFilter
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
class AuthorizationFilter(
private val policyEngine: PolicyEngine,
private val config: AuthZenConfig
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
if (!config.enabled) {
filterChain.doFilter(request, response)
return
}
// Build policy request from HTTP request
val policyRequest = buildPolicyRequest(request)
// Evaluate
runBlocking {
val decision = policyEngine.evaluate(policyRequest).getOrThrow()
if (decision.isDenied) {
response.sendError(403, "Access denied")
return@runBlocking
}
}
filterChain.doFilter(request, response)
}
}
Best Practices
Use fail-closed in production. Configure fallbackPolicy: DENY to ensure security when the PDP is unavailable.
Enable caching for performance. Authorization decisions for the same request can be cached to reduce PDP load.
Monitor circuit breaker state. Expose metrics for OPEN/HALF_OPEN states to detect PDP issues early.
Exclude health endpoints. Don't require authorization for health checks and actuator endpoints.
Use command IDs as actions. Map your service operations to hierarchical command IDs for consistent policy authoring.