M1-34: identity: sessions, access tokens, refresh families and logout¶
Status: ready | Depends on: M1-32, M1-33 | Milestone: M1
Goal¶
After this ticket a person's app holds a session with a short-lived access token (JWS with
cnf.jkt and sid, no permissions inside) and a rotating refresh token in a family bound to the
device's DPoP key. Reuse of a rotated refresh token revokes family and session; extensions get
child sessions with their own family; logout ends a session and its children. The identity
module implements kernel/actor.Authenticator, so every REST request of a person is
authenticated with token, DPoP proof, session and device state before authorize runs. This is
the token model of 08 "7. Token model" that all login tickets (M1-37 to M1-43), the gateway
(M1-64) and step-up (M1-35) build on.
Read first¶
docs/spec/08-identity.md: "1. Goals", "5.2 Approval", "7.1 Overview", "7.2 Access token", "7.3 DPoP", "7.4 Refresh tokens: rotation, families, reuse", "8. Session duration", "13. Login protection", "15. Tests" (ID-T14 Bearer case, ID-T15, ID-T16, ID-T17, ID-T27), "16. Decisions of this chapter" (R-ID-05), "18. Configuration values of this chapter".docs/spec/03-data-model.md: "1. Principles" (items 1, 2, 9, 11, 13), "4.2 Identity" tablesauth_sessions,refresh_token_families,refresh_tokens(including the rotation rules underrefresh_tokens),devices,device_keys; "4.1 Instance" tableserver_signing_keys; "4.10" tableevent_log; "4.12" tableaudit_events; "6.3 Lock order"; "9. Configuration values of this chapter" (auth.session_max_age,auth.refresh_token_ttl,auth.refresh_reuse_grace,identity.last_active_granularity).docs/spec/04-sync-protocol.md: "2.2 Envelope", "8.5 Revocation of a session, device or principal".docs/spec/05-actions.md: "2.1 Column Permission", "2.4 Columns Owner, Step-up, Audit, Permissions", "2.5 Decision", "4.1 Public and login" (auth.token.refresh,auth.logout), "4.3" (session.list), "6. Durable event types and receive action" (session.revoked).docs/spec/07-permissions.md: "3. Principal context", "4.1 Decisions and reasons", "4.2 Flow" (check_active), "SR5", "SR7".docs/spec/06-api.md: "3. Authentication per principal kind", "3.1 DPoP", "5. Errors", "6. Pagination", "7. Idempotency" item 3 (noIdempotency-Keyon refresh).docs/spec/02-architecture.md: "4.3 Ports, events and sources per module" (identity:Auth,SessionEnded), "4.4 Allowed dependencies" (identityusesinstance,authz), "4.5 Cross-cutting ports" (kernel/actor), "8. Transactions and Unit of Work", "9. Request path".docs/spec/19-security.md: "9. Logs without secrets and without content", "10. Secrets".docs/spec/15-client-architecture.md: "8. Network and login" (single-flight refresh per family; extensions refresh only their own child session).- ADR 0020, ADR 0021, ADR 0017.
docs/api/openapi.yaml: operationsrefreshToken,logout,listMySessions,listUserSessions,createExtensionSession; schemasTokenRefreshRequest,TokenResponse,ExtensionSessionCreate,Session,SessionPage,Device,SessionRevokedPayload,SessionRevokedEvent,EventEnvelope,Problem; parameterDpopProof; security schemesdpopAuth,bearerAuth.docs/security/threat-model.md: T-TK-01, T-DV-01.- The merged code of M1-16 (
kernel/eventlog.Appender), M1-24 (kernel/auditing.Recorder), M1-28 (internal/authz,check_active), M1-32 (internal/instanceportKeys, purposeaccess_token), M1-33 (identity.Devices,identity.DPoP,kernel/jose,identitytest,ExpectedHTU), M1-18 (platform/httpserver,kernel/actor, problem mapping) and M1-15 (instance record withinstance_id). services/server/internal/identity/README.md,docs/quality/gates.md,CONTRIBUTING.md,docs/PLAN.md"3. Way of working per ticket".
This ticket also writes session.revoked (M1-16) and audit entries (M1-24); both are merged
through the dependencies (M1-32 needs M1-20, which needs M1-16; M1-28 needs M1-24).
Fixed requirements that apply¶
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
- Test first: every behavior starts with a failing test (red output goes into the report).
- No en dash and no em dash in any text you write.
- White label: only example.com, example.org and the names Alex, Sam, Robin in examples.
- Configuration values only through named keys from
docs/operations/configuration.md. - Access token: JWS signed with a server key of purpose
access_token; claimsiss(instance_id),aud,sub,sid,did,jti,iat,nbf,exp,cnf.jkt,amr; no permissions in the token (08 "7.2"). Verification withkidamong keys in stateactiveorretiring,iss,aud,expandnbfwithauth.clock_leeway, then DPoP, then session and device (08 "7.2"). - A DPoP-bound token used as
Beareris rejected (08 "7.3", RFC 9449 section 7.2). - Refresh token: 256 random bits, stored only as HMAC-SHA-256 with the server pepper; lifetime
auth.refresh_token_ttl, never beyondauth_sessions.expires_at(08 "7.1"); one active family per session, bound todpop_jkt; every refresh needs a proof with exactly that key (08 "7.4", 03refresh_token_families). - Rotation, grace and reuse exactly as 03
refresh_tokensitems 1 to 3 withauth.refresh_reuse_grace; reuse revokes family and session (refresh_reuse), writes audit andsession.revoked(08 "7.4"). - Child sessions for the share extension (
share_ext) with their own family on the same device; revoking the parent revokes all children with the same reason (08 "7.4", R-ID-05, 03auth_sessions). The notification service extension has no session; it uses the preview token of its push registration (13 "6.4", ADR 0056). - Session absolute end
auth.session_max_age(03, 08 "8"). auth.logoutends the own session;session.listis self or owner (05 "4.1", "4.3").- A pending device may refresh and log out but nothing else except
self.readand its owndevice.list(05 "2.5"). - Tokens and token hashes never appear in logs, events, audit details, telemetry or errors (SEC-LOG-1, SEC-SEC-3).
Scope¶
- Migration
services/server/migrations/<next free number>_identity_sessions.sql: tablesauth_sessions,refresh_token_families,refresh_tokensexactly as 03 "4.2" (columns, defaults, CHECKs including the three table checks ofauth_sessions, FKs, unique and partial indexes, indexes). Exception: the FKauth_sessions.identity_id -> user_identities(id)is not created here becauseuser_identitiesarrives with M1-36; the column exists and a comment says that M1-36 adds the constraint. - sqlc queries
internal/identity/adapters/postgres/queries/sessions.sqlandrefresh.sql, repositoryadapters/postgres/sessions.go; the authentication lookup is one query: session by id joined with its device and the device's activedpopkey. - Port in
internal/identity/auth.go(names fixed):ClientComponent(app,share_ext,console),AuthMethod(the six values of 03),SessionRevokeReason(the values of 03auth_sessions.revoke_reason).StartSession{UserID, DeviceID, AuthMethod, IdentityID *ID}(componentapp;consolecomes with M6-01).TokenSet{AccessToken, RefreshToken secrets-typed values; ExpiresIn int; RefreshExpiresAt time.Time; Session Session; Device Device}; token values use the redacting secret type ofinternal/platform/secrets(M1-09) or an identity-local equivalent that prints[redacted].SessionDTO with the members of the API schemaSession(currentis set by the handler).type Auth interface:StartSession(ctx, StartSession) (TokenSet, error)for the login tickets, joins the ambient unit of work;StartChildSession(ctx, parentSessionID ID, component ClientComponent) (TokenSet, error)(used by the handlercreateExtensionSession);Refresh(ctx, refreshToken string, proof VerifiedProof) (TokenSet, error);Logout(ctx, actor.Actor) error;RevokeSession(ctx, sessionID ID, reason SessionRevokeReason, by *ID) error(also used by M1-35 and M1-45);ListSessions(ctx, actor.Actor, userID ID, Page) (SessionPage, error).- In-transaction event
SessionEnded{SessionIDs []ID; UserID ID; DeviceID ID; Reason SessionRevokeReason}published on every revocation (02 "4.3";notifysubscribes in M3).
- Domain
internal/identity/domain/session: the pure rotation decisionDecide(family, token, now, grace) Outcomewith outcomesrotate,grace_rotate,reuse,expired,family_revoked; claim building and validity capping (expand refreshexpires_atnever beyond the session end). - App
internal/identity/app:sessions.go(start, child, revoke, list),refresh.go,logout.go,authenticator.go(implementskernel/actor.Authenticator),accesstoken.go(issue and verify throughinstance.Keys). - Adapters
internal/identity/adapters/httpapi: handlersrefreshToken,logout,listMySessions,listUserSessions,createExtensionSession; registration of theAuthenticatorin the HTTP chain (step 7 of 02 "9.1") throughinternal/wire. kernel/actor: if the mergedActorlacks any ofSessionID,SessionActive,DeviceID,DeviceState,DeviceKeyActive,StepUpAt,StepUpMethod,AuthMethod,ClientComponent,KeyThumbprint, add them with doc comments (the package is implemented byidentity, 02 "4.5").internal/authzcheck_active: if M1-28 does not yet decide session and device state from the actor, add exactly this rule with tests: principal notactivegivesprincipal_inactive;SessionActivefalse givessession_revoked; devicerevokedorDeviceKeyActivefalse givesdevice_inactive; all three are decisionunauthenticated. Do not duplicate the rule inidentity.- Config keys in the identity
Config:auth.access_token_ttl,auth.refresh_token_ttl,auth.session_max_age,auth.refresh_reuse_grace,auth.clock_leeway,auth.token_pepper_ref,identity.last_active_granularity,server.public_url,api.page_size_default,api.page_size_max; register missing keys in the schema and runmake config-reference. - Enable
refreshToken,logout,listMySessions,listUserSessions,createExtensionSessioninservices/server/api/operations.yamland regenerate (command fromservices/server/README.md, M1-17). - Lint: a depguard rule in
services/server/.golangci.ymlforbidsmath/randandmath/rand/v2ininternal/identity/...(part of ID-T27), unless a repo-wide rule already does. - Docs:
docs/spec/08-identity.md"7.2" already statesaud, headertyp,amrand the cap ofexpat the session end, "7.4" the child sessions and "13" the refresh answers ("Responses that this chapter defines"); change them only where the implementation proves them wrong (spec first). Rulings indocs/DECISIONS.md; identity README;services/server/CHANGELOG.md. authz.CredentialSource, sessions part (02 "Ports, events and sources per module", 07 "Flow"check_active, PLAN row M1-34): implementSessionActiveandSessionExpiresAtof the request's session in the identity adapter of the source port that M1-28 declared, and replace the corresponding empty source ininternal/wire(fail closed until then). Test:TestCredentialSource_Sessionsagainst PostgreSQL 18 (active, revoked, expired, foreign principal).
Out of scope¶
- Login flows that call
StartSession(M1-37 to M1-43); tests callStartSessiondirectly with devices registered throughidentity.Devices. - API tokens (
Bearer bothy_...) (M1-44): theAuthenticatorhas the dispatch branch and answers401 unauthenticateduntil M1-44 fills it. - Step-up challenges and
step_up_atwrites (M1-35); this ticket only readsstep_up_atinto the actor. session.revokeover the API,device.revoke,user.disable, raisingpermission_versionfor those actions (M1-45).- Closing gateway connections with
4012(M1-65) and the gateway variant of authentication (M1-64). - Console cookie sessions (M6-01).
- Re-login after
auth.idp_recheck_intervalfor OIDC sessions (M1-41 adds that refresh rule). - The job
identity.auth_cleanup(M1-35 creates it, M1-38 adds device expiry; 02 "12.3") and retention deletes; expiry is enforced at request time without a job.
Boundaries¶
- A new migration needs its line in
migrations.sha256: runmake migrations-lockafter creating the file and beforemake check; never edit a merged migration (M1-01, ADR 0007). - Work in a dedicated git worktree on the branch
feat/M1-34-identity-sessions. - Conventional Commits with scope
identity(orauthz,kernel,docswhere fitting), each signed off (git commit -s). - Never push, tag, open pull requests or create remote repositories.
- Do not modify files outside the scope above except
services/server/CHANGELOG.md,docs/DECISIONS.mdand generated references. - Follow the host rules supplied by the launcher.
- Tests bind only
127.0.0.1and port 0; PostgreSQL 18 runs as a throwaway container through the M1-12 helper; no Docker runtime means the test fails, never skips. - Stop only processes you started, by PID; temporary directories only via
mktemp -d, removed at the end. - Never read or print secrets; tests generate their own pepper and signing keys through the test helpers of M1-09 and M1-32.
- No network calls except package downloads for pinned tools.
- A gap in the core contract (00, 03, 04, 05) ends the ticket with
Verdict: BLOCKED(P-11).
Design notes¶
Access token (issue):
- Header
{"alg": <alg of the active access_token key>, "kid": <kid>, "typ": "at+jwt"};typfollows RFC 9068 section 2.1 and keeps access tokens apart from every other JWS. Signing goes through the signer ofinstance.Keysfor purposeaccess_token. - Claims:
iss=instance_idas UUID string (M1-15 instance record);aud= origin ofserver.public_urlplus/api/v1(for examplehttps://chat.example.com/api/v1);sub= user id;sid= session id;did= device id;jti= new UUIDv7;iat=nbf= now (seconds);exp= min(now +auth.access_token_ttl,auth_sessions.expires_at);cnf={"jkt": <family dpop_jkt>};amr=[<auth_sessions.auth_method>]. No other claims (08 "7.2"). TokenResponse.expires_in= seconds from now toexp;token_type=DPoP.
Authenticator (Authenticate(r) (actor.Actor, error)), in this order:
- No
Authorizationheader: anonymous actor. - More than one
Authorizationheader or a scheme other thanDPoPandBearer:401 unauthenticated,WWW-Authenticate: DPoP algs="ES256 EdDSA". Bearer: a token starting withbothy_goes to the API token branch (M1-44; until then401 unauthenticated). A token that parses as this server's access token (headertyp=at+jwtand a knownkid) gives401 invalid_dpop_proof(ID-T14 Bearer case). Anything else401 unauthenticated.DPoP: parse withkernel/jose, allowlist = the algorithms of the access-token keys; headertypmust beat+jwt;kidmust belong to a key of purposeaccess_tokenin stateactiveorretiring; signature,iss,audmust match;nbf<= now + leeway;exp> now - leeway. Malformed, unknownkid, bad signature, wrongissoraud:401 unauthenticated; expired:401 token_expired; headerWWW-Authenticate: DPoP error="invalid_token", algs="ES256 EdDSA".identity.DPoP.VerifywithMethod= request method,URL=ExpectedHTU(server.public_url, r),AccessToken= the raw token,ExpectedJKT=cnf.jkt. Errors as in M1-33.- One query by
sid: session, device, activedpopkey. Build the actor (kinduser, idsub, all fields of Scope item 7).SessionActiveis true only if the row exists,revoked_atis null,expires_at> now,user_id=subanddevice_id=did;DeviceKeyActiveis true only if the activedpopkey'sjktequalscnf.jkt. The Authenticator does not reject on these flags:authorize(check_active) does, so the denial is audited with its reason (07 "4.2", "4.1"). The HTTP chain mapsprincipal_inactive,session_revokedanddevice_inactiveto401problem codesession_revoked(06 "5": "Session, device or principal revoked"),unauthenticatedto401 unauthenticated. - If the proof used the previous nonce (
NonceStale), the response carriesDPoP-Noncewith the current nonce (06 "3.1" item 2). - Activity: when
auth_sessions.last_seen_atordevices.last_seen_atis older thanidentity.last_active_granularity(or null), update both andauth_sessions.last_ip(client IP from the trusted-proxy logic of M1-18) in one short statement outside the request's unit of work; a failure is logged and does not fail the request.
Session state is read per request, not cached. Reason: revocation then works across processes at
once (AZ-P9 for sessions, 04 "8.5") without raising permission_version for logout and refresh
reuse; the cost is one indexed query per authenticated request. Record as ruling.
Session start (StartSession): the device belongs to the user and is not revoked (pending is
allowed, 08 "5.2"); expires_at = now + auth.session_max_age; family with dpop_jkt = jkt
of the device's active dpop key, current_generation 0; refresh token generation 0 with
expires_at = min(now + auth.refresh_token_ttl, session expires_at); access token as above.
No audit here; the calling login ticket audits auth.login.complete.
Child session (StartChildSession): parent active, client_component = 'app', not itself a
child; component share_ext; same user, device, auth_method,
identity_id; parent_session_id = parent; expires_at = parent expires_at; own family with
the parent family's dpop_jkt (extensions share the device key through the keychain group, 15
"9"). An active child of the same parent and component is revoked (logout, with its family and
session.revoked) in the same unit of work, so at most one exists (08 "7.4"). Any other input:
400 validation_failed or not_found.
createExtensionSession (POST /api/v1/me/sessions/extensions, action
session.extension.create, permission self, class rl.write, audit, x-idempotency-exempt:
auth_flow): body ExtensionSessionCreate (client_component), caller's session is the parent,
answer 201 TokenResponse of the child. A repeated call replaces the child again; that is the
reason for the exemption.
Refresh (refreshToken, action auth.token.refresh, no Idempotency-Key), one unit of work:
- A missing
DPoPheader gives401 invalid_dpop_proof(map the generated decoder's missing header error for this parameter, do not answer400). Verify the proof withAccessTokenempty andExpectedJKTempty; the result'sJKTis compared in step 4. token_hash= HMAC-SHA-256(pepper, token); unknown hash:401 unauthenticated.SELECT ... FOR UPDATEon the family, then read the token row, the session, the device and the principal status. Family revoked, session revoked or pastexpires_at, devicerevoked, principal notactive:401 session_revoked, nothing changes.- Proof
JKTdiffers fromfamily.dpop_jkt:401 invalid_dpop_proof, nothing changes, audit deny (ID-T17). Reason for no revocation: a caller without the device key must not be able to end someone's session. - Token
expires_at<= now:401 unauthenticated, nothing changes. - Decide per 03:
generation = current_generationandused_atnull: rotate (setused_at, insert generation + 1, raisecurrent_generation,rotated_at= now).generation = current_generation - 1and now -rotated_at<auth.refresh_reuse_grace: rotate again from the current generation and mark the never received current tokenused_at= now. Otherwise reuse. - Reuse: family
revoke_reason = 'reuse_detected', sessionrevoke_reason = 'refresh_reuse', child sessions with the same reason and their familiessession_revoked; onesession.revokedevent per revoked session;SessionEnded; audit deny. This work commits; then the request ends with401 session_revoked. - Success:
200 TokenResponsewith the new pair,sessionanddevice.
The authorize call of the handler uses the anonymous principal, action auth.token.refresh and
resource auth_session (permission public), so the proof of check of 02 "9.3" holds. A pending device
refreshes like an active one.
Logout (logout, action auth.logout, resource auth_session = the actor's session, permission
self): revoke the session (logout), its children (logout), all their families (logout);
one session.revoked per session; SessionEnded; audit allow; 204. An actor without a session
gets what authorize decides for a missing resource (404).
Revocation helper (RevokeSession): locks the session rows in ascending id (03 "6.3"), sets
revoked_at and revoke_reason on the session and its children, revokes their active families,
appends the events and publishes SessionEnded. Already revoked sessions stay unchanged
(idempotent). The event session.revoked: audience = the session's user, resource
{kind: "auth_session", id}, empty scope, actor = acting principal or absent, payload
{session_id, reason} with reason in the enum of SessionRevokedPayload.
Session lists: listMySessions (resource user = caller) and listUserSessions (resource user
= path id; self or owner, others 404); only sessions with revoked_at null and expires_at >
now, children included, order id DESC, cursor helper from M1-33, current true for the
caller's session; absent members instead of null.
Audit (via kernel/auditing.Recorder, denials in their own transaction after a rollback):
auth.logout allow; auth.token.refresh denials with deny_reason from 07 "4.1" only:
unknown token unauthenticated; revoked state and reuse session_revoked (details
{"cause":"refresh_reuse","family_id":...} for reuse); key mismatch unauthenticated (details
{"cause":"dpop_key_mismatch"}). Never a token or hash in details.
Randomness: refresh token = 32 bytes from crypto/rand, base64url without padding (43 chars).
Tests (write them first)¶
Unit tests:
TestSessionDecidetable: every outcome of the rotation decision, grace boundary exactly atauth.refresh_reuse_grace, generationcurrent - 2is reuse.TestAccessToken_Claims: exactly the claim set of "Design notes"; no permission claims;expcapped at the session end; headertypat+jwt.TestRefreshToken_IDT27: 32 bytes, 43 characters, distinct across many draws; plus the depguard rule (golangci fixture ormake lintfailing on a probe file in a temporary copy).
Integration tests against PostgreSQL 18 with real instance.Keys, authz, sync appender and
audit recorder:
TestSessionsMigration_Constraints: the three table checks ofauth_sessions, one active family per session, uniquetoken_hash, unique(family_id, generation), cascades.TestAuthenticate_Valid: actor fields filled from session, device and key.TestAuthenticate_IDT14_BearerRejected: access token asBearergives401 invalid_dpop_proof.TestAuthenticate_ProofOtherKey,TestAuthenticate_AthMismatch:401 invalid_dpop_proof.TestAuthenticate_KeyStates: token signed with anactiveand aretiringkey accepted,retiredor unknownkid401 unauthenticated.TestAuthenticate_ExpiryLeeway:expwithinauth.clock_leewayaccepted, beyond401 token_expired;nbfin the future beyond leeway rejected.TestAuthenticate_WrongIssOrAud:401 unauthenticated.TestAuthenticate_RevokedSessionBeforeExpiry: afterRevokeSessionthe still valid token gets401 session_revokedand an audit entry with reasonsession_revoked(AZ-M41 shape).TestAuthenticate_RevocationAcrossInstances: two app instances on one database; revocation through instance A is effective on instance B for the next request.TestAuthenticate_StaleNonceGetsFreshNonce:DPoP-Nonceheader on the response.TestAuthenticate_LastSeenGranularity: at most one write peridentity.last_active_granularity.TestRefresh_Rotates: new pair, old tokenused_at,current_generation+ 1.TestRefresh_IDT15_ReuseRevokes: familyreuse_detected, session and childrenrefresh_reuse, onesession.revokedper session inevent_log, audit deny,401 session_revoked, the old access token gets401 session_revoked.TestRefresh_GraceSameKey: previous generation within grace with the family key gives a new pair; after grace it is reuse.TestRefresh_ConcurrentSameTokenWithinGrace: two parallel refreshes with the same token and key: no revocation, both succeed, the first returned token is no longer valid.TestRefresh_IDT16_ParentAndChildConcurrent: app and extension refresh in parallel many times; no revocation, separate families.TestRefresh_IDT17_OtherKey:401 invalid_dpop_proof, family unchanged, audit deny.TestRefresh_MissingProof:401 invalid_dpop_proof, not400.TestRefresh_ExpiredandTestRefresh_SessionEnd: refreshexpires_atand accessexpnever after the session end; expired refresh token401 unauthenticated.TestRefresh_PendingDevice: allowed.TestRefresh_TokenNeverLogged: canary refresh and access tokens absent from captured logs, audit details and events.TestChildSession_RulesandTestRevokeParent_RevokesChildren.TestLogout: session, children, families revoked withlogout, events,SessionEndedpublished, audit allow,204; the same token afterwards401 session_revoked; pending device may log out.TestListSessions: self, owner, other person404, revoked and expired omitted,current, pagination, pending device403 device_pending, nonullmembers.
Commands that must pass:
cd services/server && go test -race ./internal/identity/... ./internal/authz/... ./internal/kernel/...
make arch
make codegen-check
make migrations-lint
make config-reference && git diff --exit-code docs/operations/configuration.md
make check
Acceptance criteria¶
- Migration creates the three tables exactly as 03 (FK on
identity_iddeferred to M1-36, documented in the migration). - Access tokens carry exactly the claims of 08 "7.2" plus header
typat+jwt, no permissions,expcapped at the session end. - Authenticator follows the order of "Design notes"; Bearer use of an access token is
rejected; revoked sessions are rejected by
authorizewith an audited reason. - Refresh implements rotation, grace and reuse of 03 with the codes of "Design notes"; ID-T15, ID-T16, ID-T17 green; a wrong key never revokes.
- Logout and
RevokeSessionrevoke children and families, appendsession.revoked, publishSessionEnded. -
listMySessions,listUserSessions,refreshToken,logout,createExtensionSessionenabled and conform to the schema; nonullmembers; a second child of the same component revokes the first (ID-T16 setup). - Refresh tokens are 256 random bits from
crypto/rand;math/randbanned in identity. - 08 "7.2", "7.4" and "13" match the implementation; rulings in DECISIONS.md.
-
make checkgreen.
Definition of done¶
Code, tests and docs complete: identity README (status, port Auth, Authenticator, tables,
actions, event session.revoked, in-transaction event SessionEnded, config keys, test ids), a doc
comment on every public symbol, comments explain why, an ADR only for an architecture decision
beyond ADR 0020, the CHANGELOG entry under Unreleased of services/server/CHANGELOG.md, generated
references current. make check green. Independent review (make review plus a code review of the
diff) and red team gate (docs/quality/red-team-gate.md) over the diff with both verdicts in the
report; merge locally into the base branch only if both are clean and make check is green; then
git worktree remove.
Report¶
Write to {{REPORT_PATH}}: first line Verdict: DONE or Verdict: BLOCKED <reason>; then the
commits, the files with line counts, the test results with counts (including the red output of
the first failing tests), what is not proven, and follow-ups that could not be done in scope, each
with its reason. Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.