Auditor Portal¶
Path: /auditor
The Auditor Portal gives an external auditor a scoped, read-only view of a single assessment without giving them an account, an org membership, or a third-party identity provider. An admin invites the auditor against one specific assessment, the auditor opens an accept link and receives a short-lived session token, and they land in a workspace that shows only that assessment's controls, evidence, and findings — nothing else in the tenant. The scoped token is the session; there is no user record to create, manage, or off-board.
Key Elements¶
- Auditors section on the assessment — Lives on the assessment detail page. An admin invites an auditor by email, picks a role, and gets back a copy-able accept URL. The same section lists outstanding grants with their status and a Revoke button.
- Accept link — A one-time URL (
/auditor?token=...) the auditor opens. Accepting it redeems the token for a scoped session token. - Auditor workspace (
/auditor) — A single read-only page showing the assessment summary, its controls/findings, and its evidence. It is pinned to one assessment and cannot reach any other data in the org.
Inviting an External Auditor¶
An invite (an access grant) is created against one assessment:
The body sets the auditor_email (required), an optional auditor_name, and the
role (defaults to auditor_readonly). The response returns the serialized grant
plus a one-time accept_token and a ready-to-send accept_url.
- Open the assessment and find the Auditors section.
- Enter the auditor's email and (optionally) name.
- Choose the role —
auditor_readonlyorauditor_commenter. - Invite. The accept URL appears in the panel; copy it and send it to the auditor.
Email is log-only on the demo — the accept URL is in the response
The demo has no SMTP relay, so the invite is not emailed. Instead the raw
accept_token and the full accept_url ({base}/auditor?token=...) are returned
in the API response and written to the audit log. This is intentional — it mirrors
an air-gapped invite flow where you hand the link to the auditor out-of-band.
The base URL prefers the APP_BASE_URL setting and falls back to the request origin.
The grant is created with status pending and an expires_at 14 days out, so the
auditor has a sensible window in which to accept.
Roles¶
| Role | Value | Meaning |
|---|---|---|
| Read-only | auditor_readonly |
View the scoped workspace. The default. |
| Commenter | auditor_commenter |
Read-only plus the intent to leave comments. |
The role is recorded on the grant and carried into the scoped token as a claim, so it travels with the auditor's session.
Accepting the Invite¶
The auditor opens the accept URL, which redeems the one-time token:
The body is just { "token": "..." }. On success the endpoint mints a short-lived,
assessment-scoped JWT and returns it as access_token (plus token_type, expires_in,
and the resolved assessment_id, organization_id, role, and auditor identity).
There is no third-party IdP and no user account — the scoped JWT is the session.
The frontend stores it for the browser session and uses it as a Bearer token for the
workspace call. Accepting consumes the one-time token; the grant flips to active and
records accepted_at.
The Scoped Workspace¶
The workspace is delivered as one read-only payload, pinned to the JWT's
assessment_id:
assessment— the framework, version, status, examination period, and the passing/failing control counts.controls/findings— each control's id, name, status, severity, disposition, evidence summary, remediation, and evidence count.evidence— the evidence references for the assessment, each flagged as downloadable or not.auditor— the auditor's email, name, and role, so the UI can label the session.
Because the response is built from the token's assessment_id, the auditor sees exactly
one assessment's data and has no path to anything else in the tenant.
Security Model¶
The portal uses a scoped JWT rather than reusing the normal user session, because an external auditor is not an org member.
- Distinct scope — The minted token carries
scope: "auditor_grant". The workspace dependency rejects any token whose scope is not this value, so a normal user token cannot be used on the auditor endpoints and vice-versa. - Assessment pin — The token carries
assessment_id(andorg_id,grant_id,grant_role) as claims. The workspace is built strictly from thatassessment_id, which is how per-assessment scoping is enforced — there is no parameter the auditor can change to widen access. - Re-validation on every call — The workspace does not trust the JWT alone. On each
request it re-loads the backing grant and rejects the token unless the grant is still
activeand not pastexpires_at. A revoked or expired grant cuts access immediately, even within the JWT's lifetime.
Two clocks: a 14-day grant and a 12-hour session
The grant (and its one-time accept token) is valid for 14 days — long enough to
work through an audit. The scoped JWT minted on accept lives for 12 hours, so a
leaked session token ages out quickly. Pending grants that pass their expiry are
lazily marked expired so the grant list reflects reality.
Revoking Access¶
Revoking sets the grant's status to revoked. Because the workspace re-validates the
grant on every request, the auditor's scoped JWT stops working immediately — there
is no waiting for the token to expire.
Who Can Do What¶
| Action | Required permission |
|---|---|
| List auditor grants on an assessment | Assessment read |
| Invite / revoke an auditor | Assessment update |
| Accept an invite | None — the one-time token authorizes it |
| Open the scoped workspace | The scoped auditor JWT (not an org login) |
Inviting and revoking are admin/manager actions gated on the assessment-update permission. The accept and workspace endpoints are a separate, public auth path: they are authorized solely by the one-time token and the scoped JWT, so an external auditor never needs an org login.