Immutable. This exact content is served forever at /api/v1/blob/7645c1af467aaaff.
---
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`:
```yaml
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:
```yaml
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:**
1. No policy files loaded — check `cosec.authorization.local-policy.enabled=true`
2. Policy files don't match the location pattern — default is `classpath:cosec-policy/*-policy.json`
3. Policy JSON syntax error — check startup logs for deserialization errors
**Fix:**
```yaml
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:
```json
{
"name": "NewPublicEndpoint",
"action": "/api/new-endpoint"
}
```
### Request allowed when it should be denied
**Symptoms:** A request that should be blocked gets through.
**Likely causes:**
1. DENY statement doesn't match — check action pattern and condition
2. Another ALLOW statement matches first (but DENY should take precedence)
3. 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:**
1. `cosec.jwt.secret` doesn't match the token issuer's secret
2. `cosec.jwt.algorithm` doesn't match the token's algorithm
3. Token is expired
4. Token format is wrong (not a standard JWT)
**Check:**
```yaml
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:**
1. File location: `src/main/resources/cosec-policy/` (not `resources/main/...`)
2. File naming: must match `*-policy.json` pattern
3. Property: `cosec.authorization.local-policy.enabled=true`
4. JSON validity: parse errors are logged at startup
5. 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:**
1. Use `{varName}` not `:varName` (Spring WebFlux style)
2. Access the variable via `request.path.var.varName` in conditions
3. 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 variable
- `request.remoteIp` — client IP address
- `request.origin` — Origin header
- `request.method` — HTTP method
- `request.attributes.{key}` — request attributes
- `request.headers.{name}` — request header
- `context.principal.id` — user ID
- `context.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
```kotlin
@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
```kotlin
@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 context
- `request.attributes.ipRegion` — IP geolocation (if `cosec-ip2region` is enabled)
In a WebFlux handler:
```kotlin
@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 |