name: cosec-troubleshoot description: "Use when diagnosing CoSec authentication or authorization failures such as unexpected 401/403 responses, denied requests that should be allowed, policies not loading, JWT token rejection, matcher mismatches, or unclear access decisions."
CoSec Troubleshooting Guide
This skill helps you debug authorization issues in CoSec. When a request gets an unexpected result (403, 401, or is allowed when it shouldn't be), follow this systematic approach.
Step 1: Enable Debug Logging
The fastest way to understand authorization decisions is debug logging on SimpleAuthorization:
logging:
level:
me.ahoo.cosec.authorization.SimpleAuthorization: debug
This logs the full evaluation chain: root check → blacklist → global policies → principal policies → role permissions → final result.
For more granular tracing:
logging:
level:
me.ahoo.cosec.policy: debug
me.ahoo.cosec.authentication: debug
me.ahoo.cosec.jwt: debug
Step 2: Understand the Evaluation Order
SimpleAuthorization evaluates in this order, stopping at the first definitive result:
1. Root user check
└─ If principal.id == "cosec" → ALLOW (bypass everything)
2. Blacklist check
└─ If principal is blacklisted → EXPLICIT_DENY
3. Global policies (type: "global")
└─ For each global policy:
a. Check policy-level condition → skip if no match
b. Check DENY statements → EXPLICIT_DENY if any matches
c. Check ALLOW statements → ALLOW if any matches
└─ First definitive result wins
4. Principal-specific policies
└─ Policies attached to the user (via policy IDs on the principal)
└─ Same evaluation as global policies
5. Role-based app permissions
└─ Evaluate role permissions for the request's appId/spaceId
└─ Only applies when request has an appId
6. Default → IMPLICIT_DENY
Each step uses switchIfEmpty to fall through to the next if no match is found.
Step 3: Common Issues and Fixes
All requests return 403
Symptoms: Every endpoint returns 403, even public ones.
Likely causes:
- No policy files loaded — check
cosec.authorization.local-policy.enabled=true - Policy files don't match the location pattern — default is
classpath:cosec-policy/*-policy.json - Policy JSON syntax error — check startup logs for deserialization errors
Fix:
cosec:
authorization:
local-policy:
enabled: true
locations: classpath:cosec-policy/*-policy.json
Specific endpoint returns 403 when it should be public
Symptoms: Most endpoints work, but a new public endpoint returns 403.
Cause: No ALLOW statement matches the endpoint. By default, CoSec uses implicit deny — anything not explicitly allowed is denied.
Fix: Add a statement for the endpoint:
{
"name": "NewPublicEndpoint",
"action": "/api/new-endpoint"
}
Request allowed when it should be denied
Symptoms: A request that should be blocked gets through.
Likely causes:
- DENY statement doesn't match — check action pattern and condition
- Another ALLOW statement matches first (but DENY should take precedence)
- Root user bypass — check if the user ID is "cosec"
Debug: Enable debug logging and check which statement matched.
JWT token rejected
Symptoms: Requests with valid JWT tokens return 401.
Likely causes:
cosec.jwt.secretdoesn't match the token issuer's secretcosec.jwt.algorithmdoesn't match the token's algorithm- Token is expired
- Token format is wrong (not a standard JWT)
Check:
cosec:
jwt:
algorithm: hmac256 # must match the signing algorithm
secret: exact-same-secret-used-by-issuer
Policies not loading from local files
Symptoms: Startup succeeds but policies don't take effect.
Checklist:
- File location:
src/main/resources/cosec-policy/(notresources/main/...) - File naming: must match
*-policy.jsonpattern - Property:
cosec.authorization.local-policy.enabled=true - JSON validity: parse errors are logged at startup
- Policy type: must be
"global"for the policy to apply to all requests
Rate limiter not working
Symptoms: Rate limiting conditions are ignored.
Cause: Rate limiters require a shared state. In a distributed setup, you need Redis-backed caching (cosec-cocache).
Fix: Add the cocache dependency and configure Redis.
Path variables not matching
Symptoms: /user/123 doesn't match /user/{id}.
Check:
- Use
{varName}not:varName(Spring WebFlux style) - Access the variable via
request.path.var.varNamein conditions - Ensure the path pattern is correct (no trailing slash mismatch)
SpEL template not evaluating
Symptoms: #{principal.id} is treated as a literal string.
Cause: SpEL templates use #{} syntax. {} alone is a path variable, not SpEL.
Fix: Use #{principal.id} not {principal.id}.
Condition part path is wrong
Symptoms: Condition always returns false.
Valid part paths:
request.path.var.{name}— path variablerequest.remoteIp— client IP addressrequest.origin— Origin headerrequest.method— HTTP methodrequest.attributes.{key}— request attributesrequest.headers.{name}— request headercontext.principal.id— user IDcontext.principal.attributes.{key}— principal attribute
Common mistakes:
request.ip(wrong) →request.remoteIp(correct)principal.id(wrong) →context.principal.id(correct)request.pathVariable.id(wrong) →request.path.var.id(correct)
Step 4: Testing Policies Locally
Unit test with SimpleAuthorization
@Test
fun `test policy evaluation`() {
val policyLoader = LocalPolicyLoader("classpath:cosec-policy/test-policy.json")
val policies = policyLoader.load()
val evaluator = DefaultPolicyEvaluator(policies)
val request = mockk<Request> {
every { path } returns "/api/users/123"
every { method } returns "GET"
every { remoteIp } returns "192.168.1.1"
}
val principal = mockk<CoSecPrincipal> {
every { id } returns "user-123"
every { authenticated } returns true
every { roles } returns setOf("user")
}
val context = mockk<SecurityContext> {
every { this@mockk.principal } returns principal
}
val result = evaluator.evaluate(request, context)
assertThat(result.authorized).isTrue()
}
Test specific matcher
@Test
fun `test path action matcher`() {
val factory = PathActionMatcherFactory()
val matcher = factory.create(Configuration.of("pattern" to "/api/users/*"))
val request = mockk<Request> {
every { path } returns "/api/users/123"
every { method } returns "GET"
}
assertThat(matcher.match(request, mockk())).isTrue()
}
Step 5: Request Attributes for Debugging
When debugging, inspect the request attributes that CoSec sets:
COSEC_SECURITY_CONTEXT— the parsed security contextrequest.attributes.ipRegion— IP geolocation (ifcosec-ip2regionis enabled)
In a WebFlux handler:
@GetMapping("/debug/whoami")
fun whoami(exchange: ServerWebExchange): Mono<Map<String, Any?>> {
val context = exchange.getAttribute<SecurityContext>(COSEC_SECURITY_CONTEXT)
return Mono.just(mapOf(
"principal" to context?.principal?.id,
"authenticated" to context?.principal?.authenticated,
"roles" to context?.principal?.roles,
"tenant" to context?.tenant?.tenantId
))
}
Quick Reference: Authorization Results
| Result | authorized |
Meaning |
|---|---|---|
ALLOW |
true |
Explicitly allowed by a policy statement |
EXPLICIT_DENY |
false |
Explicitly denied by a DENY statement |
IMPLICIT_DENY |
false |
No statement matched (default deny) |
TOKEN_EXPIRED |
false |
JWT token has expired |
TOO_MANY_REQUESTS |
false |
Rate limiter exceeded |