security-architect
Designs the security of an already-exposed servlet REST use case with Spring Security — who may call each endpoint (public, authenticated, role, group, scope, or only the resource's owner), how the ca
Files16
View on GitHubSKILL.md.claude/skills/security-architect/SKILL.md
GitHubAvailable specs
!find "${CLAUDE_PROJECT_DIR:-.}/docs/use-cases" -mindepth 1 -maxdepth 1 -type d -name 'UC-*' 2>/dev/null | sort
Empty above → none yet, run /use-case-design first. (find, not an ls glob: under zsh an unmatched glob
aborts the command before any fallback runs.)
Target
$ARGUMENTS
Security Architect
Designs who may call each endpoint and how the caller proves who it is: the authentication
mechanism of the project, the request rules and method-security annotations of each endpoint,
the claims-to-authorities mapping, the ownership check a use case runs, and what the API
answers when the proof or the permission is missing. What use-case-design recorded as the
case's Access and rest-api-architect exposed as endpoints, this skill puts behind the
right door.
Scope: servlet applications. Spring MVC on the servlet filter chain. A reactive
application (WebFlux, SecurityWebFilterChain) has another filter model and is out of scope
here — say so and stop when the project's web starter is the reactive one.
Entry rule: without 30-rest.md or 31-graphql.md there is nothing to protect. This skill reads
docs/use-cases/UC-NNN-<slug>/00-use-case.md, 10-domain.md and whichever inbound partials the
Trigger names — 30-rest.md, 31-graphql.md, or both — and treats them as a contract. Without the
partial its trigger names it stops and tells the caller to run /rest-api-architect or
/graphql-api-architect — access rules written before the operations exist protect a guess.
It runs when at least one holds, and only then:
00-use-case.md § Access |
Project on disk | What it designs |
|---|---|---|
| A role, a group, a scope, or the owner | any | The rules of this case — plus the project-wide setup when none exists yet |
| Authenticated | no SecurityFilterChain yet |
The project-wide setup — the first secured case |
| Public | a SecurityFilterChain exists |
The permitAll line and its recorded reason |
None holds — public in an unsecured project, or authenticated in a project whose chain already
denies by default — stop and say which: there is nothing to add, and the consolidated spec
records that the case is covered. An 00-use-case.md with no Access row (written before
the row existed) is not a reason to stop: ask the question in step 3, and record the answer as
a divergence.
Exit rule: writes no code. It emits 32-security.md. The configuration classes, the
annotations on the controllers and the handlers come from the executor agent, which reads the
partial and the exemplars in templates/.
Rule rule: rules don't live here. Deny by default, where each check lives, the mechanism
requirements, stateless and CSRF, CORS, the failure bodies and the three-way endpoint proof are
@.claude/rules/authorization.md. The statuses are @.claude/rules/api-rest.md. This skill
applies and cites them; it doesn't reproduce them.
How it's invoked
Two paths: /security-architect by hand, or chained by /new-feature right after REST and GraphQL and
before messaging, jobs and persistence. That's why it does not carry
disable-model-invocation — a skill the model can't see is a skill the orchestrator can't call
(@.claude/decisions/0007-pipeline-skills-invocation.md). The guard against firing out of
order is the entry rule above.
Why this is a skill and not a subagent
Form 1, motivated by axes 2 and 9 of the designer's interview: access is decided per use case,
behind a mechanism decided once per project, and no piece owned either — the authorization.md
norm sat in the planned list with no reader. The closest rejected form was a security block
inside rest-api-architect: it would re-interview a project-wide mechanism inside a
per-endpoint skill and load security content into every REST run, public endpoints included. A
subagent fails the counter-test on all three points: the interview is the task, the reference
fits in references/ and templates/, and the partial is short.
Pinned to opus: the partial is what the executor implements verbatim, and an access rule
designed wrong opens an endpoint in silence (@.claude/decisions/0081-skill-model-required-per-class.md).
Design record: @.claude/decisions/0112-security-architect-skill.md (this repository only).
Boundary with neighboring skills
| Piece | Decides | Never decides |
|---|---|---|
use-case-design |
The business fact Access: anyone, any authenticated caller, named roles or groups, only the owner (and who may override it) |
Mechanism, annotation, status |
domain-modeling |
When Access is the owner: the actor the command carries, the owner field on the aggregate, the invariant and the not-found exception it raises |
Where the actor comes from |
rest-api-architect |
Endpoints, DTOs, the domain error map | Who may call them; the 401/403 rows |
| this skill | Mechanism, claims-to-authorities mapping, request rules and method-security per endpoint, the permitAll list with reasons, project-wide setup, 401/403 bodies, CORS, Actuator and OpenAPI exposure, how the controller resolves the actor |
Tables and columns, the ownership invariant, test shape |
persistence-architect |
The final form of a credentials or API-key table this skill asks for | Who may read it |
test-architect |
The test shape for every case § 10 lists | Which cases exist |
Two facts cross that line, and each has one owner:
- The actor in the command is
10-domain.md's. This skill names where the controller reads it from (the principal's claim) in § 5; if the domain partial has no actor whileAccessis the owner, that is a requirement to report upstream, never a command field added here. - The 401 and 403 rows of the error map are this partial's § 6.
30-rest.md's error map keeps the domain rows; consolidation merges both, and this partial wins on the two statuses.
Procedure
Read the specs.
00-use-case.md(itsAccessrow and trigger),10-domain.md(command fields, the ownership invariant when there is one),30-rest.md(endpoints,operationIds, error map, the OpenAPI block) and31-graphql.md(root fields, § 8's sensitive fields, § 9's access rows) — each when the trigger names it. Without the one it names, stop. Apply the entry rule.Survey — every answer the project already gave is inherited, never re-asked.
# Stack and mechanism already in the build grep -nE "spring-boot-starter-(security|oauth2-resource-server|webflux|web\b|webmvc)" pom.xml build.gradle* 2>/dev/null # Security wiring already in the code grep -rnE "SecurityFilterChain|@EnableMethodSecurity|oauth2ResourceServer|opaqueToken|httpBasic|UserDetailsService|ApiKey" --include='*.java' src/main 2>/dev/null grep -rnE "@PreAuthorize|@PostAuthorize|@Secured|@RolesAllowed" --include='*.java' src/main 2>/dev/null # Decisions earlier use cases recorded find docs/use-cases -name '32-security.md' 2>/dev/null grep -rnE "spring\.security|springdoc|management\.endpoints|app\.security" src/main/resources 2>/dev/nullAnd the active blueprint's
packages.map: the entry ending in.configis where the wiring goes; with none (hexagonal, onion), asecuritysubpackage of the entry ending in.rest. Never a package chosen inside this use case.Fact Inherited from Asked only when Mechanism The starters in the build, the chain on disk, or an earlier 32-security.md§ 2None exists — first secured case of the project Where roles and groups live in the token, prefixes, audience The converter on disk or an earlier § 3 Same Role and group catalog Constants on disk ( Roles,Groups) or earlier § 3A name Accessuses is not in it yet — added, not askedOpenAPI / Swagger exposure, Actuator operator role, CORS origins, headers The chain on disk or an earlier § 4 Same as mechanism The first secured case owns every endpoint already on disk. Deny by default turns every endpoint an earlier case published as public into a 401 on the next deploy. List each controller mapping step 2 found with no rule, and decide with the user, endpoint by endpoint,
permitAllwith a reason or protected from now on — rows in## Impact on approved use cases, never a silent change of behavior. The same goes for their web tests: once the security starter is on the classpath, every existing web-slice test runs behind a filter chain it never knew about and answers 401. Each existing controller test is an impact row too — it imports the project's chain and sends a credential, or proves the public row public; § 10 lists them sotest-architectplans the change.Interview — only what step 2 did not settle.
AskUserQuestion, at most 4 questions per call and never fewer than 2 real options per question; an axis with one sensible answer is decided and recorded (@CLAUDE.md§ Known pitfalls). Every option states its cost, the wayjobs-architectputs the tables a tool adds inside the option text.Axis Decides Skip when Mechanism — who issues the credential: an external identity provider (signed token), a provider that only issues opaque tokens or must revoke instantly, the application itself (users and passwords here), or a machine with a static key § 2, through references/mechanism-decision-matrix.mdInherited Token shape — where roles and groups sit (a flat rolesclaim,realm_access.roles,groups,scope), the audience this API expects§ 3 Inherited, or mechanism is not a token Roles per endpoint — which named roles or groups Accessmeans, per operation§ 1 Accessnames them one to oneOwner override — may an administrator act on another user's resource? Whether the actor's roles travel in the command Accessis not the ownerAPI docs — OpenAPI document, Swagger UI and, when the project has Kafka, the AsyncAPI document public, or protected too — one answer for all § 4 Inherited Browser client — does a browser on another origin call this API, and from which origins? CORS in § 4 Inherited Operator role — which authority reads the actuator endpoints beyond health and info § 4 Inherited Never ask about statuses (
@.claude/rules/api-rest.mddecides), annotations, or class names — the same checklistuse-case-designapplies before every question.Choose the mechanism. Apply
references/mechanism-decision-matrix.md§ 2: the first row that matches wins. A second mechanism for the same callers is a divergence to report and ask about, never added because one endpoint looked easier with it (@.claude/rules/authorization.md§ Mechanism). Record the winner, the row that decided it, one line per rejected mechanism, and the exemplar it maps to:Mechanism Exemplars Resource server, signed token templates/SecurityConfig.java.example+templates/JwtResourceServerConfig.java.exampleResource server, opaque token templates/SecurityConfig.java.example+templates/OpaqueTokenResourceServerConfig.java.exampleUsers owned by the application templates/SecurityConfig.java.example+templates/LocalUsersSecurityConfig.java.exampleAPI key (machines) templates/SecurityConfig.java.example+templates/ApiKeyAuthenticationConfig.java.exampleFix the access of each endpoint — § 1. One row per operation of
30-rest.md: method, path,operationId, the rule (public · authenticated · role · group · scope · owner), where it is enforced (request rule in the chain, or method security on the controller), the meta-annotation when the expression repeats (templates/MethodSecurityAnnotations.java.example), and the three outcomes the rule's § Tests requires. A public row carries its reason. Ownership rows say "authenticated in the chain, owner in the use case" — never a SpEL bean call (@.claude/rules/authorization.md§ Boundary).GraphQL rows (
@.claude/rules/api-graphql.md§ Access). One row per root field of31-graphql.md§ 1 (Query.order,Mutation.cancelOrder) and one per sensitive field § 9 asks for (Customer.taxId), each listing every path through the graph that reaches it. The chain only saysPOST /graphqlauthenticated; a role, group or scope is always method security on the resolver method — the same meta-annotation the REST row over the same port uses, so the role is spelled once and checked in each adapter. A denial is aFORBIDDEN/UNAUTHORIZEDGraphQL error with HTTP 200 (Spring Boot'sSecurityDataFetcherExceptionResolver), which § 6 records per row. An operation callable without authentication stops the design: opening/graphqlis not this case's decision. When the meta-annotations live in the REST adapter'ssecuritysubpackage, the GraphQL adapter imports them from there — never a second copy of a role.Where the actor comes from. For every endpoint whose command carries an actor, name the principal attribute the controller reads (the token's subject, a claim, the username) and the value object it becomes — shape in
templates/CurrentActorResolution.java.example. The controller resolves it; the command receives it; no use case reads the security context.Fix the project-wide setup — § 4. Once per project: NEW in the first secured case, REUSE afterwards, CHANGE when this case adds a matcher, a role constant or an origin. Each component with its exemplar:
Component Exemplar Filter chain: stateless, CSRF justified, headers, CORS, request rules in specific-to-general order, deny by default, OpenAPI and Actuator matchers SecurityConfig.java.exampleMechanism wiring the row of step 4 401 entry point and 403 access-denied handler writing Problem Details ProblemDetailSecurityHandlers.java.exampleTranslation of a method-security denial ahead of the 500 fallback SecurityExceptionHandler.java.exampleMethod security and the role/group catalog MethodSecurityAnnotations.java.exampleDenial and authentication-failure log and metric SecurityAuditListener.java.exampleBearer scheme in the OpenAPI document OpenApiSecurityConfig.java.exampleProperties application-security.yml.exampleFix ownership — § 5. When
Accessis the owner: the aggregate's owner field and the invariant from10-domain.md(cited, not redesigned), the outcome when the actor is not the owner — the not-found exception the domain already raises for an absent aggregate, so existence is not revealed (@.claude/rules/authorization.md§ Failure responses) — and the override, when step 3 granted one, as the role the command carries. Missing in10-domain.md→ a requirement todomain-modelingin § 11, and the report says the folder is not consistent until that partial is updated.Fix failure responses — § 6. 401 and 403 per endpoint — and for a GraphQL row, 401 from the chain for an anonymous request and
FORBIDDEN/UNAUTHORIZEDerrors with HTTP 200 for a denied field — theWWW-Authenticateheader, the body shape (@.claude/rules/api-rest.md§ Error body), and the 404 that replaces 403 for ownership. These rows are merged into the error map at consolidation, and this partial wins on them.Hand persistence what it needs — § 7, requirements only. Users owned by the application → the credentials store (username, password hash, enabled flag, roles) and the outbound port the user-details service calls; API key → the key store (key id, hash, owner, created and revoked timestamps). Named by what they are, never DDL — the same discipline
jobs-architectkeeps.nonefor a token mechanism: the provider holds the credentials.Declare dependencies — § 8. The security starter, and the resource-server starter for a token mechanism. Version column empty: the Boot parent manages both — read
pom.xmlfirst, and never write a version from memory (@CLAUDE.mdinvariant 8). The artifact names change between Boot majors too (the resource-server starter gained asecurity-infix); take them from what the Initializr'ssecurityandoauth2-resource-serverdependency ids resolve to for the project's Boot version, never from an older project. The test support library istest-architect's § 5, not this list. The executor may writepom.xmlfor exactly these rows.Fix configuration — § 9. Every property the setup reads (issuer, audiences, introspection endpoint and client, CORS origins, the API-key header name), its value per profile, and the environment variable behind each secret — never a literal (
@.claude/rules/authorization.md§ Credentials and logs). Shape intemplates/application-security.yml.example.List the contract test cases — § 10. Per endpoint and per GraphQL row, the three outcomes of step 5 — a GraphQL row's at the HTTP level through the real chain, never in the GraphQL slice — plus the other-actor case at the use case for every ownership row.
test-architectturns them into tests with its own exemplar; this skill only fixes which cases exist.
12b. Decide the design patterns of this layer. Run
@.claude/skills/gof-design-patterns/SKILL.md § Design-time use over what this case adds —
a claims converter that composes several sources is the usual row (Composite), a growing
if over authentication types the usual symptom. The answer goes into ## Design patterns — none when nothing matches, and absence is not none.
Write the partial.
docs/use-cases/UC-NNN-<slug>/32-security.md, fromtemplates/security-spec.md.example. Eleven numbered blocks plus## Design patterns, all mandatory, eachnonewhen empty — absence is notnone.Report and stop. File path; the mechanism and whether it was inherited; the access table in one line per endpoint; every
permitAllwith its reason; § 7 and § 8 as lists; the requirements to other partials (§ 11). Don't invoke anyone: messaging, jobs and persistence run next, and persistence reads § 7 in its first pass.
What the partial contains
| Block | Fixes |
|---|---|
| 1 · Access per endpoint | Method, path, operationId, rule, where enforced, meta-annotation, the three outcomes; a reason on every public row. GraphQL: one row per root field and per sensitive field, with every path that reaches it |
| 2 · Mechanism | The mechanism, the matrix row that chose it, the rejected ones, inherited or not, exemplars |
| 3 · Authorities | Where roles, groups and scopes sit in the credential, the prefixes, the audience, the role and group catalog (NEW or REUSE constants) |
| 4 · Project-wide setup | Each component of step 6 with NEW / REUSE / CHANGE; default access; OpenAPI and Actuator exposure; CORS origins; CSRF and headers with their reasons |
| 5 · Ownership | Owner field, actor and its source, the not-owner outcome, the override role — or none |
| 6 · Failure responses | 401 / 403 / 404 per endpoint, header, body |
| 7 · Schema requirements | Credential or key store and its port — requirements, never DDL. none for a token mechanism |
| 8 · Declared dependencies | Coordinates, version empty when managed |
| 9 · Configuration | Every property, its value per profile, the environment variable behind each secret |
| 10 · Contract test cases | Per endpoint: anonymous, without authority, with authority; per ownership row, the other actor |
| 11 · Deferred and requirements to other partials | Same shape as every partial's Deferred, plus each requirement upstream (an actor 10-domain.md lacks) |
| Design patterns | Each pattern adopted — step 12b. none when none |
Plus ## Impact on approved use cases and ## Implementation order, as in every partial.
The exemplars in templates/ are a shape reference, not files to copy. They target the
servlet stack and the Spring Security generation the project's Boot parent manages; the
links they follow are in references/servlet-reference-links.md.
Contract
Class: design — the territory is skill_classes.design in
@.claude/schemas/extensions.json, and ArchHook.java guard enforces it. Writes inside the use
case folder and nothing else; a SecurityConfig written under src/ is refused with exit 2.
Reads docs/use-cases/UC-NNN-<slug>/00-use-case.md, 10-domain.md, and 30-rest.md or
31-graphql.md (mandatory — stops without the one the trigger names), earlier cases' 32-security.md, pom.xml (or the Gradle
build), src/main/resources/application*.yml, the security wiring on disk, the active
blueprint's packages.map, @.claude/rules/authorization.md, @.claude/rules/api-rest.md,
@.claude/rules/api-graphql.md (§ Access, when the trigger names GraphQL),
@.claude/rules/architecture-ddd.md, @.claude/rules/logging.md,
@.claude/rules/observability.md, and references/.
Writes docs/use-cases/UC-NNN-<slug>/32-security.md. Nothing else.
Owns the project's authentication mechanism, the claims-to-authorities mapping and the
role and group catalog, each endpoint's access rule and where it is enforced, the permitAll
list and its reasons, the project-wide filter chain, the 401 and 403 responses, CORS, and the
OpenAPI and Actuator exposure.
Does not decide the use case boundary or its Access fact (00-use-case.md), the
actor field, the owner field or the ownership invariant (10-domain.md), endpoints and domain
error rows (30-rest.md), tables and columns (20-persistence.md), or the test shape
(40-tests.md). Does not write Java, pom.xml or application.yml — the executor does, from
this partial. Doesn't touch .claude/rules/**. Does not design reactive (WebFlux) security.