Audit trail for Write Operations
Capture user_id from JWT Access Token
April 2026
Table of Contents
Executive Summary
This design document specifies a middleware approach to automatically capture the user_id from JWT access tokens and inject it into all write operations across the OpenG2P Registry Gen2 system. The solution eliminates the need for UI clients to explicitly send user_id, reduces code duplication, and maintains complete audit trails across all registry operations.
Key Benefits
Zero UI Changes Required – The token is already being sent; user_id is extracted server-side
Leverages Existing IAM Infrastructure – Reuses established authentication and token validation
Single Point of Configuration – All write operations use the same dependency; change once, affects everywhere
Maintains Backwards Compatibility – Non-authenticated endpoints and existing reads are unaffected
Consistent with FastAPI Best Practices – Follows standard dependency injection patterns
Architecture Overview
The Core Idea
Extract the user_id from the authenticated JWT token (already validated by existing IAM middleware) and store it in a request-scoped context that all write operations can access without explicit UI transmission. FastAPI's dependency injection system combined with request state provides the mechanism for this pattern.
The user_id comes from the subject claim in the OIDC token, which is the unique identifier for the authenticated user and is guaranteed to be present in all validated tokens.
Existing Infrastructure
The registry already implements:
AuthMiddleware that validates JWT tokens and populates
request.state.auth_principalIAM Service Integration for OIDC/OAuth2.0 token generation and validation
Token Claims with the subject claim containing the user's unique identifier
FastAPI Framework with built-in dependency injection capabilities
This design builds on top of these existing components without requiring additional authentication infrastructure.
Implementation Strategy
Layer 1: Enhance AuthMiddleware
Extend the existing AuthMiddleware to extract and store the user_id in request state after token validation.
Implementation Location:
In the middleware's post-token-validation step, after request.state.auth_principal is populated:
Key Point: The subject claim in OIDC tokens is the unique identifier for the authenticated user and is guaranteed to be present in all validated tokens by the OIDC specification.
Layer 2: Create User ID Dependency
Define a FastAPI dependency that retrieves the user_id from request state and makes it available to all write operation handlers.
Purpose: This dependency handles the defensive check that user_id is present, raising an authentication error if the middleware didn't populate it correctly. In normal operation, all authenticated requests will have user_id populated.
Layer 3: Add Dependency to Write Operations
Modify all write operation endpoints to include the user_id dependency. The following operations require modification:
Change Request Operations:
POST /change-request→create_change_request()PATCH /change-request/{id}/verify→verify_change_request()PATCH /change-request/{id}/approve→approve_change_request()
Intake Form Operations:
POST /intake-form→create_intake_form()PATCH /intake-form/{id}/verify→verify_intake_form()PATCH /intake-form/{id}/approve→approve_intake_form()
Verification Log Operations (CRITICAL - Fixes Existing TODO):
POST /verifications/add_verification→add_verification()Note: Currently hardcodes
verified_by="system"in the codebase (TODO comment present)Our middleware will inject actual user_id from JWT token
This will populate
verified_byfield correctly with authenticated user
Example Endpoint Modification
Before:
After:
Key Point
The UI client does not send user_id. The FastAPI framework automatically calls get_current_user_id() and passes the result to the handler. This is zero additional work for the UI layer – the token it already sends is sufficient.
Benefits:
UI developers don't need to be aware of user_id handling
No opportunity for user ID spoofing at the UI level
User_id is always from a validated JWT token
Layer 4: Service Layer Implementation
Modify your service methods to accept and use the user_id parameter. The service layer receives user_id from the controller and stores it in the appropriate database field.
Design Pattern: The created_by, verified_by, and approved_by fields are already part of the audit trail design. The service layer simply fills them from the JWT token rather than relying on the UI to send them.
Database Schema
Overview of Affected Tables
The middleware will inject user_id into write operations affecting:
G2PChangeRequest – For create/verify/approve operations
G2PIntakeForm – For create/verify/approve operations
G2PRegisterVerification – Verification log table (critical for this design)
G2PRegisterVerification Table (Verification Log)
This existing table stores audit trail of all verifications performed on change requests and intake forms. Multiple verifications can exist per change_request or intake_form.
Current Structure:
verification_id
String (UUID)
No
Unique identifier for each verification record
register_id
String
No
References the register being verified
change_request_id
String
Yes
Links to G2PChangeRequest (mutually exclusive with submission_id)
submission_id
String
Yes
Links to G2PIntakeForm (mutually exclusive with change_request_id)
verified_by
String
No
User ID who performed verification ← Middleware injects this
verified_at
DateTime
No
Timestamp when verification occurred
verification_observations
Text
Yes
Optional verification notes
is_approved
Boolean
No
Whether this verification is approved
Parent Entity Tracking:
Both G2PChangeRequest and G2PIntakeForm track verification progress:
no_of_verifications_required– How many verifications neededno_of_verifications_done– Incremented when new verification added
Critical: Resolving Existing TODO
Current Codebase State: The add_verification() service currently has a TODO comment:
Our Solution: The middleware will inject the actual authenticated user_id from the JWT token into the add_verification() operation, replacing the hardcoded "system" string with the real user who performed the verification.
Service Operations Requiring user_id
G2PChangeRequest/G2PIntakeForm:
G2PRegisterVerification (Add Verification):
Security Considerations
5.1 Token Validation
The user_id is extracted from a validated JWT token signed by the IAM service. No additional validation is required because:
Signature Verified: The token signature has already been verified by AuthMiddleware
Expiry Checked: The token expiry has been checked during validation
Cannot Be Forged: The subject claim (user_id) cannot be forged without access to IAM's private key
Authentication Boundary Established: The trust boundary is already established by the IAM service's token generation
The entire security model relies on the validity of the JWT signature, which is the standard for OAuth2.0 and OIDC implementations.
5.2 Request Scope Isolation
Each HTTP request in FastAPI has its own request state object (request.state). This provides natural request-scoped isolation:
No Cross-Request Leakage: User ID from Request A cannot leak into Request B
Concurrent Request Safety: Concurrent requests maintain separate state objects
Automatic Cleanup: State is automatically cleaned up after request completion
FastAPI's request handling is thread-safe and async-safe, so this isolation is guaranteed by the framework.
5.3 Audit Trail Integrity
The user_id stored in created_by, verified_by, and approved_at fields provides:
Immutable Audit Record: Documents who performed each operation
Accountability: All mutations to registrant data are attributable to a specific user
Compliance Ready: Data for compliance and regulatory reporting
Operational Traceability: Enables audits of who made what changes and when
The audit trail is created at the moment of operation, making it tamper-evident (if properly protected at the database level).
5.4 Defensive Programming
The get_current_user_id() dependency includes defensive error handling:
Defensive Benefits:
Returns 401 Unauthorized if user_id is missing from request state
Catches configuration errors or unexpected middleware failures
Prevents silent failures that could corrupt audit records
Fails fast with clear error messages
5.5 No UI Transmission
By not requiring the UI to send user_id, this design eliminates a vector for user ID spoofing or manipulation at the client level:
Server-Authoritative: The user_id comes solely from the authenticated token
Cryptographically Protected: The token is signed by the IAM service's private key
No Client Manipulation: The UI cannot forge or alter the user_id
Standard Practice: This is the recommended approach in OAuth2.0 and OIDC specifications
Implementation Checklist
Use this checklist to ensure complete and systematic implementation:
Middleware Layer
[ ] Update AuthMiddleware to extract
user_idfromauth_principal.subject[ ] Store extracted user_id in
request.state.user_id[ ] Verify middleware populates user_id for all authenticated requests
Dependency & Routing Layer
[ ] Create
get_current_user_id()dependency function[ ] Add user_id dependency to
POST /change-requestendpoint[ ] Add user_id dependency to
PATCH /change-request/{id}/verifyendpoint[ ] Add user_id dependency to
PATCH /change-request/{id}/approveendpoint[ ] Add user_id dependency to
POST /intake-formendpoint[ ] Add user_id dependency to
PATCH /intake-form/{id}/verifyendpoint[ ] Add user_id dependency to
PATCH /intake-form/{id}/approveendpoint[ ] Add user_id dependency to
POST /verifications/add_verificationendpoint (CRITICAL - fixes TODO)
Service Layer
[ ] Update
ChangeRequestService.create()to accept and useuser_idparameter[ ] Update
ChangeRequestService.verify()to accept and useuser_idparameter[ ] Update
ChangeRequestService.approve()to accept and useuser_idparameter[ ] Update
IntakeFormService.create()to accept and useuser_idparameter[ ] Update
IntakeFormService.verify()to accept and useuser_idparameter[ ] Update
IntakeFormService.approve()to accept and useuser_idparameter[ ] Update
G2PRegisterVerificationService.add_verification()to acceptuser_idparameter (CRITICAL)[ ] Replace hardcoded
verified_by="system"withverified_by=user_idin add_verification()
Database Schema
[ ] Verify existing
G2PRegisterVerificationtable has all required audit fields (already exists)[ ]
verification_id(UUID primary key)[ ]
change_request_id(foreign reference)[ ]
submission_id(foreign reference)[ ]
verified_by(currently will be populated from JWT)[ ]
verified_at(timestamp)
[ ] Add
created_byandcreated_atfields toG2PChangeRequestmodel (if not already present)[ ] Add
approved_byandapproved_atfields toG2PChangeRequestmodel (if not already present)[ ] Add audit fields to
G2PIntakeFormmodel (if not already present)[ ] Create database migration for any new audit fields on
G2PChangeRequest[ ] Create database migration for any new audit fields on
G2PIntakeForm[ ] No migration needed for G2PRegisterVerification (table already exists with correct structure)
[ ] Apply migrations to development database
[ ] Apply migrations to staging database
[ ] Apply migrations to production database
Testing
[ ] Test authenticated request with valid JWT populates all audit fields correctly
[ ] Test unauthenticated requests are rejected with 401 status
[ ] Test multiple concurrent authenticated requests maintain separate audit trails
[ ] Test expired tokens are rejected
[ ] Test invalid tokens are rejected
[ ] Verify UI does NOT send user_id in request payloads
[ ] Verify user_id is always sourced from the JWT token
[ ] Load test: verify audit fields are populated under high concurrency
Documentation
[ ] Document API changes: user_id is now always captured from JWT
[ ] Update API specification (OpenAPI/Swagger) if applicable
[ ] Document audit trail fields in database schema documentation
[ ] Update developer guide for new endpoints
[ ] Add examples showing user_id is NOT required in client requests
[ ] Document the security model for user_id capture
Solving Existing Codebase TODO
This middleware design directly addresses an existing TODO in the registry codebase:
Location: G2PRegisterVerificationService.add_verification() method
Current Code (TODO):
After Middleware Implementation:
Impact:
Verification audit trail will now correctly attribute verifications to actual users
Replaces generic "system" entries with real user identifiers
Enables proper accountability for verification operations
Completes the audit trail for change requests and intake forms
Last updated
Was this helpful?