From cosec-troubleshoot
Debug and troubleshoot CoSec authorization issues. Use this skill when users report unexpected 403/401 errors, requests being denied when they should be allowed, policies not taking effect, JWT token issues, or need to understand why a specific request was authorized or denied.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cosec-troubleshoot:cosec-troubleshootThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
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
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.
Symptoms: Every endpoint returns 403, even public ones.
Likely causes:
cosec.authorization.local-policy.enabled=trueclasspath:cosec-policy/*-policy.jsonFix:
cosec:
authorization:
local-policy:
enabled: true
locations: classpath:cosec-policy/*-policy.json
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"
}
Symptoms: A request that should be blocked gets through.
Likely causes:
Debug: Enable debug logging and check which statement matched.
Symptoms: Requests with valid JWT tokens return 401.
Likely causes:
cosec.jwt.secret doesn't match the token issuer's secretcosec.jwt.algorithm doesn't match the token's algorithmCheck:
cosec:
jwt:
algorithm: hmac256 # must match the signing algorithm
secret: exact-same-secret-used-by-issuer
Symptoms: Startup succeeds but policies don't take effect.
Checklist:
src/main/resources/cosec-policy/ (not resources/main/...)*-policy.json patterncosec.authorization.local-policy.enabled=true"global" for the policy to apply to all requestsSymptoms: 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.
Symptoms: /user/123 doesn't match /user/{id}.
Check:
{varName} not :varName (Spring WebFlux style)request.path.var.varName in conditionsSymptoms: #{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}.
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 attributeCommon mistakes:
request.ip (wrong) → request.remoteIp (correct)principal.id (wrong) → context.principal.id (correct)request.pathVariable.id (wrong) → request.path.var.id (correct)@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 { [email protected] } returns principal
}
val result = evaluator.evaluate(request, context)
assertThat(result.authorized).isTrue()
}
@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()
}
When debugging, inspect the request attributes that CoSec sets:
COSEC_SECURITY_CONTEXT — the parsed security contextrequest.attributes.ipRegion — IP geolocation (if cosec-ip2region is 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
))
}
| 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 |
Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub ahoo-wang/skills