security-architect

Designs the security of an already-exposed servlet REST or GraphQL use case with Spring Security — who may call each endpoint or GraphQL field (public, authenticated, role, group, scope, or only the r

SKILL.md

GitHub

Available 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 while Access is 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

  1. Read the specs. 00-use-case.md (its Access row and trigger), 10-domain.md (command fields, the ownership invariant when there is one), 30-rest.md (endpoints, operationIds, error map, the OpenAPI block) and 31-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.

  2. 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/null
    

    And the active blueprint's packages.map: the entry ending in .config is where the wiring goes; with none (hexagonal, onion), a security subpackage 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 § 2 None 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 § 3 A name Access uses is not in it yet — added, not asked
    OpenAPI / 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, permitAll with 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 so test-architect plans the change.

  3. 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 way jobs-architect puts 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.md Inherited
    Token shape — where roles and groups sit (a flat roles claim, 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 Access means, per operation § 1 Access names them one to one
    Owner override — may an administrator act on another user's resource? Whether the actor's roles travel in the command Access is not the owner
    API 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.md decides), annotations, or class names — the same checklist use-case-design applies before every question.

  4. 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.example
    Resource server, opaque token templates/SecurityConfig.java.example + templates/OpaqueTokenResourceServerConfig.java.example
    Users owned by the application templates/SecurityConfig.java.example + templates/LocalUsersSecurityConfig.java.example
    API key (machines) templates/SecurityConfig.java.example + templates/ApiKeyAuthenticationConfig.java.example
  5. Fix 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 of 31-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 says POST /graphql authenticated; 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 a FORBIDDEN / UNAUTHORIZED GraphQL error with HTTP 200 (Spring Boot's SecurityDataFetcherExceptionResolver), which § 6 records per row. An operation callable without authentication stops the design: opening /graphql is not this case's decision. When the meta-annotations live in the REST adapter's security subpackage, 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.

  6. 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.example
    Mechanism wiring the row of step 4
    401 entry point and 403 access-denied handler writing Problem Details ProblemDetailSecurityHandlers.java.example
    Translation of a method-security denial ahead of the 500 fallback SecurityExceptionHandler.java.example
    Method security and the role/group catalog MethodSecurityAnnotations.java.example
    Denial and authentication-failure log and metric SecurityAuditListener.java.example
    Bearer scheme in the OpenAPI document OpenApiSecurityConfig.java.example
    Properties application-security.yml.example
  7. Fix ownership — § 5. When Access is the owner: the aggregate's owner field and the invariant from 10-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 in 10-domain.md → a requirement to domain-modeling in § 11, and the report says the folder is not consistent until that partial is updated.

  8. Fix failure responses — § 6. 401 and 403 per endpoint — and for a GraphQL row, 401 from the chain for an anonymous request and FORBIDDEN / UNAUTHORIZED errors with HTTP 200 for a denied field — the WWW-Authenticate header, 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.

  9. 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-architect keeps. none for a token mechanism: the provider holds the credentials.

  10. 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.xml first, and never write a version from memory (@CLAUDE.md invariant 8). The artifact names change between Boot majors too (the resource-server starter gained a security- infix); take them from what the Initializr's security and oauth2-resource-server dependency ids resolve to for the project's Boot version, never from an older project. The test support library is test-architect's § 5, not this list. The executor may write pom.xml for exactly these rows.

  11. 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 in templates/application-security.yml.example.

  12. 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-architect turns 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.

  1. Write the partial. docs/use-cases/UC-NNN-<slug>/32-security.md, from templates/security-spec.md.example. Eleven numbered blocks plus ## Design patterns, all mandatory, each none when empty — absence is not none.

  2. Report and stop. File path; the mechanism and whether it was inherited; the access table in one line per endpoint; every permitAll with 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.