<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Flows on Heimdall API</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/</link><description>Recent content in Flows on Heimdall API</description><generator>Hugo</generator><language>en</language><atom:link href="https://artur-rios.github.io/heimdall-api/docs/flows/index.xml" rel="self" type="application/rss+xml"/><item><title>Login</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/login/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/login/</guid><description>&lt;p&gt;&lt;code&gt;POST /api/auth/login&lt;/code&gt; — anonymous, rate-limited.&lt;/p&gt;&#10;&lt;h2 id="the-sequence"&gt;The sequence&lt;a class="td-heading-self-link" href="#the-sequence" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;sequenceDiagram&#10; autonumber&#10; actor C as Client&#10; participant AC as AuthController&#10; participant M as CommandMediator&#10; participant AH as AuditingCommandHandler&#10; participant H as LoginCommandHandler&#10; participant V as LoginCommandValidator&#10; participant PR as Person repository&#10; participant TS as PersonAuthTokenService&#10; participant TI as JwtAuthTokenIssuer&#10; participant DB as PostgreSQL&#10;&#10; C-&amp;gt;&amp;gt;AC: POST /api/auth/login {email, password, scopeId?}&#10; AC-&amp;gt;&amp;gt;M: ExecuteCommandAsync(LoginCommand)&#10; M-&amp;gt;&amp;gt;AH: HandleAsync&#10; AH-&amp;gt;&amp;gt;H: HandleAsync&#10;&#10; H-&amp;gt;&amp;gt;V: ValidateAsync&#10; alt invalid shape (AF-11f)&#10; V--&amp;gt;&amp;gt;H: errors&#10; H--&amp;gt;&amp;gt;C: 400 validation errors&#10; end&#10;&#10; H-&amp;gt;&amp;gt;PR: find by the lookup the role implies&#10; PR-&amp;gt;&amp;gt;DB: SELECT person &amp;#43; role &amp;#43; scope navigations&#10; DB--&amp;gt;&amp;gt;PR: person or null&#10; Note over H: AF-11a unknown · AF-11b wrong password&amp;lt;br/&amp;gt;AF-11c deleted person · AF-11d/e scope not eligible&amp;lt;br/&amp;gt;locked out (FR-AU-09)&amp;lt;br/&amp;gt;→ all answer &amp;#34;invalid credentials&amp;#34;&#10;&#10; H-&amp;gt;&amp;gt;H: Hash.TextMatches(password, hash, salt)&#10; H-&amp;gt;&amp;gt;TS: TryBuildSubject(person)&#10;&#10; H-&amp;gt;&amp;gt;DB: SELECT active TwoFactorAuth for person&#10; alt 2FA active (AF-11g)&#10; H-&amp;gt;&amp;gt;H: retire live email codes, issue &amp;#43; mail a fresh one&#10; H-&amp;gt;&amp;gt;H: issue challenge token (MfaPending, ~5 min)&#10; H--&amp;gt;&amp;gt;C: 200 {requiresTwoFactor, challengeToken, availableMethods}&#10; else no 2FA&#10; H-&amp;gt;&amp;gt;TS: IssueAsync(subject)&#10; TS-&amp;gt;&amp;gt;TI: IssueAsync&#10; TI-&amp;gt;&amp;gt;DB: SELECT flagged scope permissions&#10; TI--&amp;gt;&amp;gt;TS: signed JWT &amp;#43; expiry&#10; H--&amp;gt;&amp;gt;AH: DataOutput{token, expiresAt, emailVerified}&#10; AH-&amp;gt;&amp;gt;DB: INSERT AuditLog(LoginCommand)&#10; AH--&amp;gt;&amp;gt;C: 200 {token, expiresAt, emailVerified}&#10; end&lt;/pre&gt;&#10;&lt;h2 id="which-lookup-and-why"&gt;Which lookup, and why&lt;a class="td-heading-self-link" href="#which-lookup-and-why" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;Person&lt;/code&gt; has no scope column, so &lt;em&gt;how&lt;/em&gt; the person is found depends on the role being sought:&lt;/p&gt;</description></item><item><title>Two-factor authentication</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/two-factor/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/two-factor/</guid><description>&lt;p&gt;Two-factor authentication is opt-in, per person, and available to &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;ScopeAdmin&lt;/code&gt;, and&#10;&lt;code&gt;SystemAdmin&lt;/code&gt; alike. It is &lt;strong&gt;not&lt;/strong&gt; available to Google Users, who are already subject to Google&amp;rsquo;s own&#10;account security.&lt;/p&gt;&#10;&lt;p&gt;Every 2FA endpoint acts on the caller themselves — the person comes from the bearer token, never from&#10;a path or body — so no caller can reach another person&amp;rsquo;s configuration.&lt;/p&gt;&#10;&lt;h2 id="the-lifecycle"&gt;The lifecycle&lt;a class="td-heading-self-link" href="#the-lifecycle" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;stateDiagram-v2&#10; [*] --&amp;gt; None: no TwoFactorAuth row&#10; None --&amp;gt; Pending: UC-36 enable&amp;lt;br/&amp;gt;(row created, IsActive = false)&#10; Pending --&amp;gt; Pending: UC-36 again&amp;lt;br/&amp;gt;(AF-36d — selection overwritten)&#10; Pending --&amp;gt; Active: UC-37 confirm&amp;lt;br/&amp;gt;(every selected method proven)&#10; Active --&amp;gt; Active: UC-40 regenerate&amp;lt;br/&amp;gt;(ten fresh recovery codes)&#10; Active --&amp;gt; None: UC-39 disable&#10; Active --&amp;gt; Active: UC-38 verify&amp;lt;br/&amp;gt;(completes a gated login)&lt;/pre&gt;&#10;&lt;h2 id="setup--uc-36-and-uc-37"&gt;Setup — UC-36 and UC-37&lt;a class="td-heading-self-link" href="#setup--uc-36-and-uc-37" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;sequenceDiagram&#10; autonumber&#10; actor P as Person (authenticated)&#10; participant AC as AuthController&#10; participant EH as EnableTwoFactorAuthCommandHandler&#10; participant CH as ConfirmTwoFactorAuthCommandHandler&#10; participant TP as ITotpSecretProtector&#10; participant ES as ITwoFactorEmailSender&#10; participant DB as PostgreSQL&#10;&#10; P-&amp;gt;&amp;gt;AC: POST /api/auth/2fa/enable {methods, currentPassword}&#10; AC-&amp;gt;&amp;gt;EH: HandleAsync&#10; EH-&amp;gt;&amp;gt;DB: SELECT TwoFactorAuth for caller&#10; alt already active (AF-36a)&#10; EH--&amp;gt;&amp;gt;P: rejected — configuration untouched&#10; end&#10; alt Email selected and the account-email budget would refuse (AF-36f, AF-36g)&#10; EH--&amp;gt;&amp;gt;P: 429 — nothing changed or sent, no password check&#10; end&#10; alt currentPassword missing or wrong (AF-36e)&#10; EH--&amp;gt;&amp;gt;P: 401 — nothing created, no account email charged&#10; end&#10; opt Email selected&#10; EH-&amp;gt;&amp;gt;DB: reserve one account email (FR-PE-16) — a burst gets one&#10; end&#10; EH-&amp;gt;&amp;gt;DB: UPSERT pending row (IsActive = false)&#10; Note over EH,DB: AF-36d — re-initiating overwrites&amp;lt;br/&amp;gt;the pending selection entirely&#10;&#10; opt App selected&#10; EH-&amp;gt;&amp;gt;TP: Protect(fresh TOTP secret)&#10; TP--&amp;gt;&amp;gt;EH: encrypted secret&#10; EH-&amp;gt;&amp;gt;DB: store TotpSecretEncrypted&#10; EH--&amp;gt;&amp;gt;P: secret returned once, for enrolment&#10; end&#10; opt Email selected&#10; EH-&amp;gt;&amp;gt;DB: retire outstanding codes, INSERT fresh 6-digit code (hash &amp;#43; salt, 10 min)&#10; EH-&amp;gt;&amp;gt;ES: SendAsync(email, code)&#10; end&#10;&#10; P-&amp;gt;&amp;gt;AC: POST /api/auth/2fa/confirm {appCode?, emailCode?}&#10; AC-&amp;gt;&amp;gt;CH: HandleAsync&#10; CH-&amp;gt;&amp;gt;DB: SELECT pending row&#10; CH-&amp;gt;&amp;gt;DB: charge the re-authentication budget (FR-AU-13)&#10; CH-&amp;gt;&amp;gt;CH: check appCode if AppEnabled (AF-37b)&#10; CH-&amp;gt;&amp;gt;CH: check emailCode if EmailEnabled (AF-37c)&#10; CH-&amp;gt;&amp;gt;DB: replace the person&amp;#39;s security stamp&#10; CH-&amp;gt;&amp;gt;DB: INSERT 10 recovery codes (SHA-256 digests only)&#10; CH-&amp;gt;&amp;gt;DB: UPDATE IsActive = true&#10; CH-&amp;gt;&amp;gt;DB: mark the confirming email code Used&#10; CH--&amp;gt;&amp;gt;P: 200 — ten recovery codes, shown exactly once&lt;/pre&gt;&#10;&lt;p&gt;&lt;strong&gt;Recovery codes are 80 bits&lt;/strong&gt; (&lt;strong&gt;FR-2F-20&lt;/strong&gt;): ten random bytes written as sixteen characters of&#10;Crockford&amp;rsquo;s base32 — the digits and the uppercase letters without I, L, O and U — and shown as&#10;&lt;code&gt;XXXX-XXXX-XXXX-XXXX&lt;/code&gt;. A code is accepted with or without its hyphens, with spaces, and in any case,&#10;and an O, I or L typed in one reads as 0, 1 and 1. Codes of the shorter &lt;code&gt;XXXX-XXXX&lt;/code&gt; shape issued&#10;before keep working until they are used or regenerated; regenerating (UC-40) is how a person gets&#10;the stronger ones.&lt;/p&gt;</description></item><item><title>Google Sign-In</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/google-sign-in/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/google-sign-in/</guid><description>&lt;p&gt;Google Sign-In is &lt;strong&gt;off by default&lt;/strong&gt; and enabled per scope. A Google identity is always&#10;&lt;code&gt;User&lt;/code&gt;-equivalent — it can never create or authenticate a Scope Admin or System Admin, whatever the&#10;Google account is (&lt;strong&gt;FR-GO-04&lt;/strong&gt;).&lt;/p&gt;&#10;&lt;h2 id="enabling-it--two-independent-switches"&gt;Enabling it — two independent switches&lt;a class="td-heading-self-link" href="#enabling-it--two-independent-switches" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;flowchart LR&#10; A[&amp;#34;PUT /api/scopes/{id}/google-signin&amp;lt;br/&amp;gt;UC-24 — per scope&amp;#34;] --&amp;gt; C{Both true?}&#10; B[&amp;#34;HEIMDALL_GOOGLE_CLIENT_IDS&amp;lt;br/&amp;gt;— per deployment&amp;#34;] --&amp;gt; C&#10; C --&amp;gt;|Yes| D[Google sign-in works]&#10; C --&amp;gt;|Scope off| E[&amp;#34;403 — sign-in unavailable&amp;#34;]&#10; C --&amp;gt;|No client IDs| F[&amp;#34;401 — every token refused&amp;#34;]&lt;/pre&gt;&#10;&lt;p&gt;With no client IDs configured, the API registers a verifier that &lt;strong&gt;refuses every token&lt;/strong&gt; and warns&#10;once at start-up. This is not a convenience fallback: verification needs an audience to check against&#10;(&lt;strong&gt;NFR-13&lt;/strong&gt;), and a verifier with no configured client could only reject everything or trust&#10;everything — so it rejects. Every other endpoint keeps working.&lt;/p&gt;</description></item><item><title>Person onboarding</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/person-onboarding/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/person-onboarding/</guid><description>&lt;p&gt;Creating a person is one use case (UC-06) with &lt;strong&gt;three paths&lt;/strong&gt;, decided by which endpoint is called&#10;rather than by a field in the body.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Endpoint&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Path&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Creates&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;POST /api/persons&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;b&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A &lt;code&gt;ScopeAdmin&lt;/code&gt; or &lt;code&gt;SystemAdmin&lt;/code&gt; with no scope&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;POST /api/scopes/{scopeId}/persons&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;a&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A &lt;code&gt;User&lt;/code&gt; in that scope, with a &lt;code&gt;SCOPE_USER&lt;/code&gt; row&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;POST /api/scopes/{scopeId}/owners&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;c&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A brand-new &lt;code&gt;ScopeAdmin&lt;/code&gt; directly as a co-owner, with a &lt;code&gt;SCOPE_OWNER&lt;/code&gt; row&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="creating-a-user--uc-06-path-a"&gt;Creating a User — UC-06 path a&lt;a class="td-heading-self-link" href="#creating-a-user--uc-06-path-a" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;sequenceDiagram&#10; autonumber&#10; actor A as Actor (SystemAdmin or ScopeAdmin)&#10; participant PC as PersonController&#10; participant AH as AuditingCommandHandler&#10; participant H as CreateUserCommandHandler&#10; participant OC as IScopeOwnershipChecker&#10; participant EV as IEmailVerificationService&#10; participant ES as IEmailVerificationSender&#10; participant DB as PostgreSQL&#10;&#10; A-&amp;gt;&amp;gt;PC: POST /api/scopes/{scopeId}/persons {name, email, password}&#10; PC-&amp;gt;&amp;gt;AH: ExecuteCommandAsync&#10; AH-&amp;gt;&amp;gt;H: HandleAsync&#10;&#10; H-&amp;gt;&amp;gt;H: validate input (AF-06d)&#10; H-&amp;gt;&amp;gt;DB: SELECT scope — must exist and not be deleted (AF-06b)&#10; H-&amp;gt;&amp;gt;OC: does the actor own this scope? (AF-06e)&#10; Note over OC: a SystemAdmin bypasses this check&#10; H-&amp;gt;&amp;gt;DB: is the email free among the scope&amp;#39;s Users? (AF-06a, case-insensitive)&#10; H-&amp;gt;&amp;gt;DB: INSERT person (Argon2id hash &amp;#43; salt) &amp;#43; SCOPE_USER row&#10;&#10; H-&amp;gt;&amp;gt;EV: IssueAndSendAsync(person)&#10; EV-&amp;gt;&amp;gt;DB: INSERT EmailVerificationToken (48 chars, time-limited)&#10; EV-&amp;gt;&amp;gt;ES: SendAsync(email, token)&#10; Note over EV,ES: a send failure does not undo the person —&amp;lt;br/&amp;gt;the token is persisted and can be re-sent (UC-15)&#10;&#10; H--&amp;gt;&amp;gt;AH: DataOutput{person}&#10; AH-&amp;gt;&amp;gt;DB: INSERT AuditLog(CreateUserCommand)&#10; AH--&amp;gt;&amp;gt;A: 201 person created&lt;/pre&gt;&#10;&lt;p&gt;Email uniqueness is scoped to the role: a &lt;code&gt;User&lt;/code&gt;&amp;rsquo;s address must be unique &lt;strong&gt;within their scope&lt;/strong&gt;&#10;(compared case-insensitively, &lt;code&gt;LOWER()&lt;/code&gt; in SQL), while a &lt;code&gt;ScopeAdmin&lt;/code&gt;&amp;rsquo;s or &lt;code&gt;SystemAdmin&lt;/code&gt;&amp;rsquo;s is unique&#10;system-wide. The same address can therefore be a User in several different scopes — which is exactly&#10;why login needs a scope identifier to find a User.&lt;/p&gt;</description></item><item><title>Password recovery</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/password-recovery/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/password-recovery/</guid><description>&lt;p&gt;Two anonymous, rate-limited endpoints: one asks for a link, the other spends it.&lt;/p&gt;&#10;&lt;h2 id="requesting-a-link--uc-12"&gt;Requesting a link — UC-12&lt;a class="td-heading-self-link" href="#requesting-a-link--uc-12" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;sequenceDiagram&#10; autonumber&#10; actor C as Anonymous caller&#10; participant AC as AuthController&#10; participant H as PasswordRecoveryCommandHandler&#10; participant Q as PasswordRecoveryQueue&#10; participant W as PasswordRecoveryDispatcher&#10; participant PR as PasswordRecoveryProcessor&#10; participant PS as IPasswordResetService&#10; participant S as IPasswordResetSender&#10; participant DB as PostgreSQL&#10;&#10; C-&amp;gt;&amp;gt;AC: POST /api/auth/password-recovery {email, scopeId?}&#10; AC-&amp;gt;&amp;gt;H: HandleAsync&#10; H-&amp;gt;&amp;gt;H: validate input shape (NFR-10)&#10; Note right of H: the only rejection this endpoint ever issues&#10; H-&amp;gt;&amp;gt;Q: TryEnqueue(email, scopeId)&#10; H--&amp;gt;&amp;gt;C: 200 — identical response, in the same time, either way&#10;&#10; Q--&amp;gt;&amp;gt;W: next request (background, own DI scope)&#10; W-&amp;gt;&amp;gt;PR: ProcessAsync&#10; PR-&amp;gt;&amp;gt;DB: find by the lookup the role implies&#10; alt person found and eligible&#10; PR-&amp;gt;&amp;gt;PS: issue token&#10; PS-&amp;gt;&amp;gt;DB: INSERT PasswordResetToken (time-limited)&#10; PS-&amp;gt;&amp;gt;S: SendAsync(email, token)&#10; else nobody, deleted or restricted person, or deleted scope (AF-12a)&#10; Note over PR,DB: no row written, no email sent&#10; end&lt;/pre&gt;&#10;&lt;p&gt;&lt;strong&gt;Every path returns the same success output.&lt;/strong&gt; AF-12a — the address belongs to nobody — is not an&#10;error flow but the &lt;em&gt;absence&lt;/em&gt; of one: the handler issues no token and answers exactly as it would&#10;have. A logically deleted person, and a &lt;code&gt;User&lt;/code&gt; whose scope is deleted, are treated the same way.&lt;/p&gt;</description></item><item><title>Scope permission claims</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/scope-permission-claims/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/scope-permission-claims/</guid><description>&lt;p&gt;A &lt;strong&gt;scope permission&lt;/strong&gt; is a named permission defined inside one scope. It has no owner of its own —&#10;owning the scope is the whole of the authorization to manage it — and it is a leaf in the data model:&#10;nothing carries a foreign key to it.&lt;/p&gt;&#10;&lt;p&gt;Its one moving part is the &lt;code&gt;IncludeAsJwtClaim&lt;/code&gt; flag.&lt;/p&gt;&#10;&lt;h2 id="from-definition-to-claim"&gt;From definition to claim&lt;a class="td-heading-self-link" href="#from-definition-to-claim" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;sequenceDiagram&#10; autonumber&#10; actor SA as Scope Admin&#10; participant SPC as ScopePermissionController&#10; participant CH as CreateScopePermissionCommandHandler&#10; participant DB as PostgreSQL&#10; actor U as User in that scope&#10; participant AC as AuthController&#10; participant LH as LoginCommandHandler&#10; participant TI as JwtAuthTokenIssuer&#10;&#10; SA-&amp;gt;&amp;gt;SPC: POST /api/scopes/{scopeId}/permissions&amp;lt;br/&amp;gt;{name, description, includeAsJwtClaim: true}&#10; SPC-&amp;gt;&amp;gt;CH: HandleAsync&#10; CH-&amp;gt;&amp;gt;DB: is the actor an owner of this scope?&#10; CH-&amp;gt;&amp;gt;DB: INSERT ScopePermission&#10; CH--&amp;gt;&amp;gt;SA: 201&#10;&#10; Note over U: later — an ordinary login&#10; U-&amp;gt;&amp;gt;AC: POST /api/auth/login&#10; AC-&amp;gt;&amp;gt;LH: HandleAsync&#10; LH-&amp;gt;&amp;gt;TI: IssueAsync(subject)&#10; TI-&amp;gt;&amp;gt;DB: SELECT DISTINCT name&amp;lt;br/&amp;gt;WHERE scope in subject&amp;#39;s scopes&amp;lt;br/&amp;gt;AND NOT scope.IsDeleted&amp;lt;br/&amp;gt;AND NOT permission.IsDeleted&amp;lt;br/&amp;gt;AND IncludeAsJwtClaim&#10; DB--&amp;gt;&amp;gt;TI: permission names&#10; TI-&amp;gt;&amp;gt;TI: fold into the scopePermissions claim&#10; TI--&amp;gt;&amp;gt;U: JWT carrying the permission names&lt;/pre&gt;&#10;&lt;h2 id="which-permissions-a-caller-claims"&gt;Which permissions a caller claims&lt;a class="td-heading-self-link" href="#which-permissions-a-caller-claims" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;flowchart TD&#10; S[Subject being issued a token] --&amp;gt; R{Role}&#10; R --&amp;gt;|User| A[&amp;#34;The permissions of&amp;lt;br/&amp;gt;the one scope they belong to&amp;#34;]&#10; R --&amp;gt;|ScopeAdmin| B[&amp;#34;The union over&amp;lt;br/&amp;gt;every scope they own&amp;#34;]&#10; R --&amp;gt;|SystemAdmin| C[&amp;#34;None — they belong&amp;lt;br/&amp;gt;to no scope&amp;#34;]&#10; A &amp;amp; B --&amp;gt; F[&amp;#34;Filtered: scope not deleted,&amp;lt;br/&amp;gt;permission not deleted,&amp;lt;br/&amp;gt;IncludeAsJwtClaim set&amp;#34;]&#10; F --&amp;gt; G[&amp;#34;Distinct names →&amp;lt;br/&amp;gt;scopePermissions claim (JSON array)&amp;#34;]&lt;/pre&gt;&#10;&lt;p&gt;The fold happens &lt;strong&gt;at issuance&lt;/strong&gt;, reading the database once. A permission created, flagged, or&#10;deleted after a token was issued does not change that token — the holder picks up the change on their&#10;next login. This is the same trade the rest of the authentication design makes: no database read per&#10;request, in exchange for claims that are a snapshot rather than a live view.&lt;/p&gt;</description></item><item><title>Audit logging</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/audit-logging/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/audit-logging/</guid><description>&lt;p&gt;&lt;strong&gt;NFR-09&lt;/strong&gt;: all write operations shall produce audit log entries.&lt;/p&gt;&#10;&lt;p&gt;Rather than a line in every handler — forgettable, and duplicated forty times — this is a decorator&#10;applied at registration.&lt;/p&gt;&#10;&lt;h2 id="how-a-handler-gets-audited"&gt;How a handler gets audited&lt;a class="td-heading-self-link" href="#how-a-handler-gets-audited" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;pre class="mermaid"&gt;sequenceDiagram&#10; autonumber&#10; participant DI as DI container&#10; participant M as CommandMediator&#10; participant A as AuditingCommandHandler&#10; participant H as Concrete handler&#10; participant W as AuditLogWriter&#10; participant AA as IActorAccessor&#10; participant DB as PostgreSQL&#10;&#10; M-&amp;gt;&amp;gt;DI: resolve ICommandHandlerAsync&amp;lt;TCommand, TOutput&amp;gt;&#10; DI--&amp;gt;&amp;gt;M: AuditingCommandHandler wrapping the concrete handler&#10; M-&amp;gt;&amp;gt;A: HandleAsync(command, cancellationToken)&#10; A-&amp;gt;&amp;gt;H: HandleAsync(command, cancellationToken)&#10; H-&amp;gt;&amp;gt;DB: the actual write&#10; H--&amp;gt;&amp;gt;A: DataOutput{success, data}&#10;&#10; alt success&#10; A-&amp;gt;&amp;gt;A: resolve target from output.Id / output.PublicId&#10; A-&amp;gt;&amp;gt;W: WriteAsync(commandTypeName, targetId)&#10; W-&amp;gt;&amp;gt;AA: ActorPersonId, ActorRole&#10; AA--&amp;gt;&amp;gt;W: from the current request&amp;#39;s IdentityUser&#10; W-&amp;gt;&amp;gt;DB: INSERT AuditLog&#10; alt the insert fails&#10; W--xA: exception&#10; A-&amp;gt;&amp;gt;A: log a warning — the request still succeeds&#10; end&#10; else failure&#10; Note over A: nothing written — only successful writes are audited&#10; end&#10;&#10; A--&amp;gt;&amp;gt;M: the unchanged DataOutput&lt;/pre&gt;&#10;&lt;p&gt;Registration is the whole mechanism:&lt;/p&gt;</description></item><item><title>Application authentication</title><link>https://artur-rios.github.io/heimdall-api/docs/flows/application-authentication/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://artur-rios.github.io/heimdall-api/docs/flows/application-authentication/</guid><description>&lt;p&gt;An application authenticates to Heimdall as itself: an administrator issues it a secret (UC-53), the&#10;application exchanges the secret for a short-lived token (UC-56), and a consumer service checks that&#10;token live (UC-57). This page is written for the people building that consumer. The authoritative&#10;descriptions live in the&#10;&lt;a href="../../requirements/use-case-specification-document/"&gt;Use Case Specification Document&lt;/a&gt;&#10;.&lt;/p&gt;&#10;&lt;h2 id="who-this-is-for"&gt;Who this is for&lt;a class="td-heading-self-link" href="#who-this-is-for" aria-label="Heading self-link"&gt;&lt;/a&gt;&#10;&lt;/h2&gt;&#10;&lt;p&gt;A service that must know &lt;em&gt;which registered application&lt;/em&gt; is calling it, not &lt;em&gt;which person&lt;/em&gt;. Cerberus&#10;is the first. Such a service never sees a person&amp;rsquo;s token for this purpose and never holds a role:&#10;an application token carries the application&amp;rsquo;s identity and nothing else.&lt;/p&gt;</description></item></channel></rss>