Example: API authentication and authorization¶
authentication-authorization.md records how a caller is identified, what gates
each endpoint, and what a caller receives when the gate refuses. Doc gen
produces one per API at docs/apis/<api-slug>/authentication-authorization.md.
What reviewers should check¶
- "None in code" is a legitimate finding and must be stated plainly with its
consequence. Authorization softened into an assumed protection — or borrowed
from a sibling repository that happened to have
[SaifAuthorize]— is the defect this document exists to prevent. - The authentication table separates what the code does from what the infrastructure does. When identity comes from IIS rather than from a handler, the Web.config element that shows it is named.
## Access requirements by endpointhas one row per endpoint in the catalog, including the unprotected ones. A short table usually means the unprotected endpoints were quietly dropped.- Each row states what happens on failure —
401,403, or "Request proceeds" — and401versus403is not used interchangeably. - Role names are the literal strings from the attributes, with a source path and line, not a paraphrase such as "agency staff".
- Secrets are named by their config key and retrieval mechanism, never by value, and the section is omitted when no secrets are used.
- Security rules use the
SEC-API-NNNscheme and each names the enforcement point; if the service has no authorization construct at all, the whole section is omitted and the absence recorded in the enforcement table instead.
Example¶
The document below is a complete, correct api-authentication-authorization.md
for a fictional API.
View rendered example — the same document as a real page, so you can compare rendering as well as source.
---
title: Authentication and authorization
summary: Identity establishment, authorization enforcement, and per-endpoint access requirements.
status: draft
owner: Agency Systems Team
last_reviewed: 2026-09-10
---
# Authentication and authorization
Callers are identified by IIS using Windows integrated authentication;
`SAIF.Agency.Api` contains no authentication code of its own. Authorization is
enforced by `[SaifAuthorize]` attributes, but only on two of the three
controllers: `AgenciesController` and `AgentsController` require the
`AgencyRead` role, the agency contact update additionally requires
`AgencyContactWrite`, and `AppointmentsController` — including its `POST` —
carries no authorization construct at all.
## Authentication
| Aspect | Finding |
| --- | --- |
| Mechanism | Windows/NTLM via IIS |
| Established by | IIS site configuration; `<authentication mode="Windows" />` in `Web.config` with `<anonymousAuthentication enabled="false" />` in the site's `system.webServer/security` section |
| Identity type | `WindowsIdentity`, projected onto `AgencyCaller` by `AgencyApiBase` |
| Configured in | `src/SAIF.Agency.Api/Web.config` |
| Anonymous access | Denied |
A caller proves identity before any managed code in this repository runs: IIS
negotiates Windows authentication, and a request that fails to authenticate is
rejected by the server with `401 Unauthorized` without reaching the routing
stage. `AgencyApiBase` reads `RequestContext.Principal.Identity` on every
request and resolves it into an `AgencyCaller` record, which supplies the
`CHANGED_BY` value written by the contact update. Consumers therefore call as
their own service identity — the agency portal's application pool account, the
`SAIF.Claims.Intake` application pool account, and the scheduled task account
that runs `SAIF.Policy.RenewalNotice` — not as an end user.
There is no data-level restriction anywhere in the service: a caller holding
`AgencyRead` can read every agency, not only the agency they belong to.
## Authorization enforcement
| Mechanism | Scope | Declared in | Effect |
| --- | --- | --- | --- |
| `[SaifAuthorize(Roles = "AgencyRead")]` | Controller | `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs:18` | Permits all four agency endpoints to callers in `AgencyRead` |
| `[SaifAuthorize(Roles = "AgencyRead")]` | Controller | `src/SAIF.Agency.Api/Controllers/v1/AgentsController.cs:16` | Permits all three agent endpoints to callers in `AgencyRead` |
| `[SaifAuthorize(Roles = "AgencyContactWrite")]` | Action | `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs:141` | Replaces the controller requirement on `UpdateAgencyContact`; `AgencyRead` alone is not sufficient |
| None in code | `AppointmentsController` | — | Every routed request reaches the action, including `POST /v1/appointments`; access relies entirely on network and IIS controls |
## Access requirements by endpoint
| Endpoint | Requirement | Enforced by | On failure |
| --- | --- | --- | --- |
| [`GET /v1/agencies/{agencyNumber}`](endpoints/agencies.md#get-v1agenciesagencynumber) | `AgencyRead` | Controller attribute on `AgenciesController` | `401 Unauthorized` |
| [`GET /v1/agencies`](endpoints/agencies.md#get-v1agencies) | `AgencyRead` | Controller attribute on `AgenciesController` | `401 Unauthorized` |
| [`GET /v1/agencies/{agencyNumber}/agents`](endpoints/agencies.md#get-v1agenciesagencynumberagents) | `AgencyRead` | Controller attribute on `AgenciesController` | `401 Unauthorized` |
| [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) | `AgencyContactWrite` | Action attribute on `UpdateAgencyContact` | `401 Unauthorized` |
| [`GET /v1/agents/{agentNumber}`](endpoints/agents.md#get-v1agentsagentnumber) | `AgencyRead` | Controller attribute on `AgentsController` | `401 Unauthorized` |
| [`GET /v1/agents`](endpoints/agents.md#get-v1agents) | `AgencyRead` | Controller attribute on `AgentsController` | `401 Unauthorized` |
| [`GET /v1/agents/{agentNumber}/appointments`](endpoints/agents.md#get-v1agentsagentnumberappointments) | `AgencyRead` | Controller attribute on `AgentsController` | `401 Unauthorized` |
| [`GET /v1/appointments/{appointmentId}`](endpoints/appointments.md#get-v1appointmentsappointmentid) | None in code | — | Request proceeds |
| [`GET /v1/appointments`](endpoints/appointments.md#get-v1appointments) | None in code | — | Request proceeds |
| [`POST /v1/appointments`](endpoints/appointments.md#post-v1appointments) | None in code | — | Request proceeds; any authenticated caller can create an appointment |
## Roles and resources
| Role / resource | Grants | Used by | Source |
| --- | --- | --- | --- |
| `AgencyRead` | Read access to agencies, agency rosters, and agents | [`GET /v1/agencies/{agencyNumber}`](endpoints/agencies.md#get-v1agenciesagencynumber), [`GET /v1/agencies`](endpoints/agencies.md#get-v1agencies), [`GET /v1/agencies/{agencyNumber}/agents`](endpoints/agencies.md#get-v1agenciesagencynumberagents), [`GET /v1/agents/{agentNumber}`](endpoints/agents.md#get-v1agentsagentnumber) | `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs:18`, `src/SAIF.Agency.Api/Controllers/v1/AgentsController.cs:16` |
| `AgencyContactWrite` | Replacing an agency's contact block | [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) | `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs:141` |
## Secrets handling
| Secret | Retrieved by | Consumed by | Notes |
| --- | --- | --- | --- |
| `agencyDb` password | `GetSecureConfigSetting` against the Environment API | `RegisterCommonDependencies` in `AutofacConfig.Register` | Read once at start-up and held by the NHibernate session factory; never present in `Web.config` |
## Security rules
### SEC-API-001 Every request carries an authenticated Windows identity
| Field | Value |
| --- | --- |
| **Statement** | The service accepts no anonymous request; IIS must have authenticated the caller before the request is routed. |
| **Trigger** | Evaluated by IIS on every request, before managed code runs. |
| **Enforcement** | `<authentication mode="Windows" />` and disabled anonymous authentication in `src/SAIF.Agency.Api/Web.config`. |
| **Applies to** | All ten endpoints, including [`POST /v1/appointments`](endpoints/appointments.md#post-v1appointments) |
| **Outcome** | IIS returns `401 Unauthorized` with no response body; nothing is logged by the application. |
| **Source** | `src/SAIF.Agency.Api/Web.config:34` |
### SEC-API-002 Agency contact changes require the AgencyContactWrite role
| Field | Value |
| --- | --- |
| **Statement** | Only a caller in `AgencyContactWrite` may change an agency's contact block; read access to agency data does not imply write access. |
| **Trigger** | Evaluated by the authorization filter before `UpdateAgencyContact` binds its body. |
| **Enforcement** | `[SaifAuthorize(Roles = "AgencyContactWrite")]` on the action, which replaces the controller-level `AgencyRead` requirement. |
| **Applies to** | [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) |
| **Outcome** | `401 Unauthorized` with an empty body; the action does not run and nothing is written. |
| **Source** | `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs:141` |
### SEC-API-003 Every write is attributed to the calling identity
| Field | Value |
| --- | --- |
| **Statement** | A contact change must record the Windows account that made it, so an incorrect address can be traced to the caller that supplied it. |
| **Trigger** | Evaluated when the contact update writes its audit row, inside the same unit of work as the change. |
| **Enforcement** | `AgencyApiBase.Caller` resolves `RequestContext.Principal.Identity.Name` and `AgencyService.UpdateContact` writes it to `AGENCY_CONTACT_AUDIT.CHANGED_BY`. |
| **Applies to** | [`PUT /v1/agencies/{agencyNumber}/contact`](endpoints/agencies.md#put-v1agenciesagencynumbercontact) |
| **Outcome** | On success the audit row carries the caller; if the identity cannot be resolved the service raises `AgencyValidationException` and the caller receives `400 Bad Request` rather than an unattributed write. |
| **Source** | `src/SAIF.Agency.Api.Services/AgencyService.cs:212` |
## Notes
- Mark inferred statements with `*` and unverifiable statements with `**`. Do not use bold inline confidence labels.
- The role memberships behind `AgencyRead` and `AgencyContactWrite` are managed
in Active Directory and are not visible from this repository.**
## Related documentation
- [API index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Endpoints](endpoints.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Data model](data-model.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)
## Sources
- `src/SAIF.Agency.Api/Web.config`
- `src/SAIF.Agency.Api/Controllers/AgencyApiBase.cs`
- `src/SAIF.Agency.Api/Controllers/v1/AgenciesController.cs`
- `src/SAIF.Agency.Api/Controllers/v1/AgentsController.cs`
- `src/SAIF.Agency.Api/Controllers/v1/AppointmentsController.cs`
- `src/SAIF.Agency.Api/App_Start/AutofacConfig.cs`
- `src/SAIF.Agency.Api.Services/AgencyService.cs`