Document Version: 1.0
Created: 2025-12-23
This document outlines the implementation plan for adding Role-Based Access Control (RBAC) and Secure Authentication to MedStory. The goal is to create a secure, MVP-ready authentication system that allows patients to share their medical timeline with relatives and doctors via secure, time-limited access links.
- ✅ User registration (self-register + invitation-style via shared links)
- ✅ Email verification with OTP option
- ✅ Login with Email/Password + OAuth (Google)
- ✅ JWT-based session management with refresh tokens
- ✅ Password complexity rules & reset functionality
- ✅ Two-Factor Authentication (2FA)
- ✅ Session timeout & management
- ✅ Shareable access links:
- Authenticated links - Recipient must register/login to view
- One-time public links - Single-use, no account required (for doctors)
- ✅ Relative connections with delegated permissions (view + create links)
- ✅ Revocable access permissions
- ✅ Audit logging for data access
- ✅ Alembic database migrations
All users are Patients who own their medical timeline. There is no separate "relative" or "doctor" user type.
block-beta
columns 1
block:user["User (Patient)"]:1
columns 1
A["• Owns their medical timeline"]
B["• Can connect with other users as 'relatives'"]
C["• Can generate shareable access links"]
D["• Can grant/revoke access to their data"]
end
Patients can connect with each other as relatives (bidirectional relationship). Relatives receive delegated permissions to help manage access when the patient is incapacitated.
flowchart LR
subgraph UserA["Patient A (Owner)"]
A1["Owns Timeline A"]
A2["Can create links"]
end
subgraph UserB["Patient B (Relative)"]
B1["Owns Timeline B"]
B2["Can view Timeline A"]
B3["Can create links for A"]
end
UserA <-- "Relative Connection" --> UserB
Relative Permissions:
| Permission | Description |
|---|---|
| View Timeline | Read access to the patient's medical timeline |
| Create Shareable Links | Can generate access links on behalf of the patient |
| Cannot Revoke Links | Only the timeline owner can revoke access |
erDiagram
User ||--o{ RelativeConnection : "has"
User ||--o{ AccessLink : "creates"
User ||--o{ RefreshToken : "has"
User ||--o{ AuditLog : "generates"
User ||--o{ OTPVerification : "receives"
User {
int id PK
string email UK
string password_hash
string full_name
boolean is_verified
boolean is_active
boolean mfa_enabled
string mfa_secret
string oauth_provider
string oauth_id
datetime created_at
datetime updated_at
datetime last_login_at
}
RelativeConnection {
int id PK
int user_id FK "Timeline owner"
int relative_id FK "Connected relative"
string relationship_type
string status "pending/accepted/rejected"
boolean can_create_links "Delegated permission"
datetime created_at
}
AccessLink {
int id PK
int owner_id FK "Timeline owner"
int created_by_id FK "Creator - owner or relative"
string token UK
string access_type "authenticated/one_time_public"
int max_uses "1 for one-time or null"
int use_count
datetime expires_at
boolean is_revoked
string label
datetime created_at
datetime last_accessed_at
}
RefreshToken {
int id PK
int user_id FK
string token UK
datetime expires_at
boolean is_revoked
string device_info
datetime created_at
}
AuditLog {
int id PK
int user_id FK
string action
string resource_type
int resource_id
string ip_address
string user_agent
json metadata
datetime created_at
}
OTPVerification {
int id PK
int user_id FK
string otp_code
string purpose
datetime expires_at
boolean is_used
datetime created_at
}
class User(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
email: str = Field(unique=True, index=True, max_length=255)
password_hash: Optional[str] = None # Nullable for OAuth users
full_name: str = Field(max_length=100)
# Verification & Status
is_verified: bool = Field(default=False)
is_active: bool = Field(default=True)
# MFA
mfa_enabled: bool = Field(default=False)
mfa_secret: Optional[str] = None
# OAuth
oauth_provider: Optional[str] = None # 'google'
oauth_id: Optional[str] = None
# Timestamps
created_at: datetime
updated_at: datetime
last_login_at: Optional[datetime] = Noneclass AccessType(str, Enum):
AUTHENTICATED = "authenticated" # Recipient must register/login
ONE_TIME_PUBLIC = "one_time_public" # Single-use, no account needed
class AccessLink(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
owner_id: int = Field(foreign_key="user.id", index=True) # Timeline owner
created_by_id: int = Field(foreign_key="user.id") # Creator (owner or relative)
token: str = Field(unique=True, index=True) # UUID or secure random
access_type: AccessType
max_uses: Optional[int] = None # 1 for one-time, None for unlimited
use_count: int = Field(default=0)
expires_at: Optional[datetime] = None # None = permanent
is_revoked: bool = Field(default=False)
# Metadata
label: Optional[str] = None # "Dr. Smith's link", "Family access"
created_at: datetime
last_accessed_at: Optional[datetime] = NonesequenceDiagram
participant User
participant Frontend
participant Backend
participant Email
User->>Frontend: Fill registration form
Frontend->>Backend: POST /register
Note over Backend: Validate password complexity
Note over Backend: Create user (unverified)
Note over Backend: Generate OTP
Backend->>Email: Send verification email
Backend-->>Frontend: 201 Created
Frontend-->>User: Show OTP input
User->>Frontend: Enter OTP
Frontend->>Backend: POST /verify-email
Note over Backend: Verify OTP
Note over Backend: Mark user verified
Backend-->>Frontend: JWT Tokens
Frontend-->>User: Logged In! Redirect to Dashboard
sequenceDiagram
participant User
participant Frontend
participant Backend
User->>Frontend: Enter Email + Password
Frontend->>Backend: POST /login
Note over Backend: Verify credentials
Note over Backend: Check if MFA enabled
Backend-->>Frontend: {mfa_required: true}
Frontend-->>User: Show TOTP input
User->>Frontend: Enter TOTP code
Frontend->>Backend: POST /login/mfa
Note over Backend: Verify TOTP
Note over Backend: Create session
Note over Backend: Log audit event
Backend-->>Frontend: JWT Tokens
Frontend-->>User: Redirect to Dashboard
flowchart TB
subgraph JWT["JWT Token Structure"]
subgraph Access["Access Token (Short-lived: 15 minutes)"]
A1["Header: { alg: HS256, typ: JWT }"]
A2["Payload: { sub, email, type: access, exp, iat }"]
end
subgraph Refresh["Refresh Token (Long-lived: 7 days)"]
R1["• Stored in HttpOnly cookie"]
R2["• Hashed in database"]
R3["• Can be revoked individually"]
R4["• Tracks device/session info"]
end
end
| Type | Description | Use Case |
|---|---|---|
| Authenticated | Recipient must register/login to view | Long-term access for relatives, new doctors |
| One-Time Public | Single-use, no account needed | Quick share with doctor who won't create account |
Who can create links:
- ✅ Timeline owner (patient)
- ✅ Connected relatives (with
can_create_linkspermission)
flowchart TD
subgraph Form["Create Access Link Form"]
L["Label: Dr. Smith's Access Link"]
T["Link Type:"]
T1["○ Authenticated (requires login)"]
T2["● One-Time Public (single use)"]
E["Expires: 2025-01-23 14:00 (optional)"]
B["[Create Link]"]
end
Form --> Generated["Generated URL: https://medstory.app/share/abc123xyz..."]
When a user clicks an authenticated link without being logged in, they must register first.
sequenceDiagram
participant Recipient
participant Frontend
participant Backend
participant Email
Recipient->>Frontend: Click shared link
Frontend->>Backend: GET /api/share/:token
Note over Backend: Validate token & check expiry
alt Recipient is logged in
Backend-->>Frontend: Return timeline data
Frontend-->>Recipient: Display timeline
else Recipient not logged in
Backend-->>Frontend: {requires_auth: true, invitation: true}
Frontend-->>Recipient: Show Register/Login page
Recipient->>Frontend: Register with email
Frontend->>Backend: POST /api/auth/register
Backend->>Email: Send verification OTP
Recipient->>Frontend: Verify OTP
Frontend->>Backend: POST /api/auth/verify-email
Backend-->>Frontend: JWT Tokens + Redirect to shared timeline
Frontend-->>Recipient: Display timeline
end
Note over Backend: Log audit event
For doctors who don't want to create an account. Link becomes invalid after first use.
sequenceDiagram
participant Doctor
participant Frontend
participant Backend
Doctor->>Frontend: Click one-time link
Frontend->>Backend: GET /api/share/:token
Note over Backend: Validate token
Note over Backend: Check use_count < max_uses
Note over Backend: Check if not expired/revoked
alt Link is valid (not used)
Backend-->>Frontend: Timeline data
Note over Backend: Increment use_count to 1
Note over Backend: Log audit event
Frontend-->>Doctor: Display timeline (read-only)
else Link already used
Backend-->>Frontend: {error: "Link has expired"}
Frontend-->>Doctor: Show "Link no longer valid" message
end
flowchart TB
subgraph Creation["Link Creation"]
Owner["Patient (Owner)"]
Relative["Relative (Delegated)"]
end
subgraph Types["Link Types"]
Auth["Authenticated Link"]
OneTime["One-Time Public Link"]
end
subgraph Access["Access Flow"]
AuthFlow["Register/Login Required"]
PublicFlow["Immediate Access (once)"]
end
Owner --> Auth
Owner --> OneTime
Relative --> Auth
Relative --> OneTime
Auth --> AuthFlow
OneTime --> PublicFlow
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/register |
Register new user |
| POST | /api/auth/verify-email |
Verify email with OTP |
| POST | /api/auth/resend-otp |
Resend verification OTP |
| POST | /api/auth/login |
Login with email/password |
| POST | /api/auth/login/mfa |
Complete MFA verification |
| POST | /api/auth/refresh |
Refresh access token |
| POST | /api/auth/logout |
Logout (revoke refresh token) |
| POST | /api/auth/forgot-password |
Request password reset |
| POST | /api/auth/reset-password |
Reset password with token |
| GET | /api/auth/oauth/google |
Initiate Google OAuth |
| GET | /api/auth/oauth/google/callback |
Google OAuth callback |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/users/me |
Get current user profile |
| PATCH | /api/users/me |
Update profile |
| POST | /api/users/me/change-password |
Change password |
| POST | /api/users/me/enable-mfa |
Enable 2FA |
| POST | /api/users/me/disable-mfa |
Disable 2FA |
| GET | /api/users/me/sessions |
List active sessions |
| DELETE | /api/users/me/sessions/:id |
Revoke a session |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/access-links |
Create access link (owner or relative) |
| GET | /api/access-links |
List access links for my timeline |
| GET | /api/access-links/created-by-me |
List links I created (including for relatives) |
| DELETE | /api/access-links/:id |
Revoke access link (owner only) |
| GET | /api/share/:token |
Access shared timeline via link |
| GET | /api/share/:token/info |
Get link metadata (without accessing) |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/relatives/invite |
Invite user as relative |
| GET | /api/relatives |
List my relatives |
| GET | /api/relatives/requests |
Pending connection requests |
| POST | /api/relatives/accept/:id |
Accept connection |
| DELETE | /api/relatives/:id |
Remove relative connection |
flowchart TB
subgraph Rules["Password Complexity Rules"]
direction TB
R1["✓ Minimum 8 characters"]
R2["✓ At least 1 uppercase letter (A-Z)"]
R3["✓ At least 1 lowercase letter (a-z)"]
R4["✓ At least 1 digit (0-9)"]
R5["✓ At least 1 special character (!@#$%^&*...)"]
R6["✗ Cannot contain email or name"]
R7["✗ Cannot be a common password"]
end
| Setting | Value | Notes |
|---|---|---|
| Access Token TTL | 15 minutes | Short-lived for security |
| Refresh Token TTL | 7 days | Configurable via env |
| Idle Timeout | 30 minutes | Configurable via env |
| Max Active Sessions | 5 | Per user |
| Session Storage | HttpOnly Cookie | Refresh token |
| Endpoint | Limit | Window |
|---|---|---|
/api/auth/login |
5 attempts | 15 minutes |
/api/auth/register |
3 attempts | 1 hour |
/api/auth/forgot-password |
3 attempts | 1 hour |
/api/auth/verify-email |
5 attempts | 15 minutes |
| General API | 100 requests | 1 minute |
All security-relevant events are logged:
class AuditAction(str, Enum):
LOGIN_SUCCESS = "login_success"
LOGIN_FAILED = "login_failed"
LOGOUT = "logout"
PASSWORD_CHANGED = "password_changed"
PASSWORD_RESET = "password_reset"
MFA_ENABLED = "mfa_enabled"
MFA_DISABLED = "mfa_disabled"
ACCESS_LINK_CREATED = "access_link_created"
ACCESS_LINK_REVOKED = "access_link_revoked"
TIMELINE_ACCESSED = "timeline_accessed"
RELATIVE_CONNECTED = "relative_connected"
RELATIVE_REMOVED = "relative_removed"gantt
title Authorization & Security Implementation
dateFormat YYYY-MM-DD
section Phase 1
User model & migrations :p1a, 2025-01-06, 3d
Password hashing :p1b, after p1a, 2d
Registration endpoint :p1c, after p1b, 2d
Email verification (OTP) :p1d, after p1c, 2d
Login with JWT :p1e, after p1d, 2d
Refresh token mechanism :p1f, after p1e, 2d
Logout & password reset :p1g, after p1f, 3d
section Phase 2
Two-Factor Auth (TOTP) :p2a, after p1g, 3d
Rate limiting :p2b, after p2a, 2d
Session management :p2c, after p2b, 2d
Audit logging :p2d, after p2c, 2d
section Phase 3
Google OAuth :p3a, after p2d, 3d
Account linking :p3b, after p3a, 2d
section Phase 4
Access link generation :p4a, after p3b, 2d
Authenticated link flow :p4b, after p4a, 2d
One-time public link :p4c, after p4b, 2d
Relative delegation :p4d, after p4c, 2d
section Phase 5
Invite system :p5a, after p4d, 2d
Connection requests :p5b, after p5a, 2d
Relative timeline access :p5c, after p5b, 2d
Delegated link creation :p5d, after p5c, 2d
section Phase 6
Login/Register screens :p6a, after p5d, 3d
Token storage & refresh :p6b, after p6a, 2d
OAuth flow integration :p6c, after p6b, 3d
MFA & Access link UI :p6d, after p6c, 3d
- User model & Alembic database migrations
- Password hashing (bcrypt/argon2)
- Registration endpoint with validation
- Email verification with OTP
- Login endpoint with JWT generation
- Refresh token mechanism
- Logout & session invalidation
- Password reset flow
- Two-Factor Authentication (TOTP)
- Rate limiting middleware
- Session management (list/revoke)
- Audit logging system
- Password complexity validation
- Google OAuth implementation
- Account linking (OAuth + password)
- Access link model & Alembic migrations
- Authenticated link flow (invitation-style)
- One-time public link flow
- Link expiry & revocation
- Use count tracking
- Relative connection model & migrations
- Invite system with email
- Connection request handling
- Relative timeline access (view)
- Delegated link creation permission
- Login/Register screens (Flutter)
- Token storage & refresh logic
- OAuth flow integration
- MFA setup screens
- Access link management UI
- Relative connection UI
This project uses Alembic for database schema migrations. All schema changes must be managed through Alembic migrations.
flowchart LR
A["Modify models.py"] --> B["Generate migration"]
B --> C["Review migration file"]
C --> D["Apply migration"]
D --> E["Commit changes"]
# Navigate to backend directory
cd backend
# Generate a new migration after modifying models
alembic revision --autogenerate -m "Add user and auth models"
# Apply all pending migrations
alembic upgrade head
# Rollback last migration
alembic downgrade -1
# View current migration status
alembic current
# View migration history
alembic history --verbosebackend/
├── alembic/
│ ├── versions/
│ │ ├── 001_initial_schema.py
│ │ ├── 002_add_user_model.py
│ │ ├── 003_add_access_link_model.py
│ │ ├── 004_add_relative_connection.py
│ │ └── ...
│ ├── env.py
│ └── script.py.mako
├── alembic.ini
└── app/
└── models.py
| Practice | Description |
|---|---|
| Descriptive names | Use clear names like add_user_mfa_fields not update_user |
| Review before apply | Always review generated migrations for correctness |
| Atomic changes | One logical change per migration |
| Backward compatible | Consider rollback scenarios |
| Test migrations | Test on a copy of production data before deploying |
| Migration | Description | Phase |
|---|---|---|
add_user_model |
User table with auth fields | Phase 1 |
add_otp_verification |
OTP verification table | Phase 1 |
add_refresh_token |
Refresh token storage | Phase 1 |
add_audit_log |
Audit logging table | Phase 2 |
add_access_link |
Access link with types (authenticated/one-time) | Phase 4 |
add_relative_connection |
Relative connections with permissions | Phase 5 |
python-jose[cryptography] # JWT handling
passlib[bcrypt] # Password hashing
pyotp # TOTP for 2FA
httpx # Async HTTP for OAuth
slowapi # Rate limiting
aiosmtplib # Async email sending
dependencies:
flutter_secure_storage: ^9.0.0 # Secure token storage
google_sign_in: ^6.1.0 # Google OAuth
local_auth: ^2.1.0 # Biometric auth (future)# JWT Configuration
JWT_SECRET_KEY=<32+ character random string>
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=7
# Session Configuration
SESSION_IDLE_TIMEOUT_MINUTES=30
MAX_SESSIONS_PER_USER=5
# Email Configuration
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@medstory.app
SMTP_PASSWORD=<email password>
EMAIL_FROM=MedStory <noreply@medstory.app>
# OAuth Configuration (Google only)
GOOGLE_CLIENT_ID=<google client id>
GOOGLE_CLIENT_SECRET=<google client secret>
# Security
RATE_LIMIT_ENABLED=true
OTP_EXPIRE_MINUTES=10
PASSWORD_RESET_EXPIRE_HOURS=24
# Access Links
ONE_TIME_LINK_EXPIRY_HOURS=24 # Default expiry for one-time public links- Email Service: Which email provider to use? (SendGrid, AWS SES, Mailgun)
- Database: Stay with SQLite for MVP or migrate to PostgreSQL?
- Deployment: Session storage strategy for multiple instances?
- Mobile: Will there be native mobile apps needing different OAuth flows?
- HIPAA: Full compliance audit needed before production?
Please review this plan and confirm:
- Data models look correct
- Authentication flows are acceptable
- API endpoint design is approved
- Implementation phases are prioritized correctly
- Security measures are sufficient for MVP
Once approved, I will proceed with Phase 1: Core Authentication implementation.
Document maintained by: AI Assistant
Last updated: 2025-12-23