Authentication & Multi-Tenancy¶
Petfolio uses OAuth-only authentication (no passwords) with account-based multi-tenancy. Users sign in with Google (with Microsoft and Facebook planned), and all their data is scoped to the account they belong to.
This page covers what the auth flow looks like for users, and how it works technically.
Design decisions
For the full rationale, alternatives considered, and security analysis, see ADR-001: Authentication and Multi-Tenancy.
User Flow¶
Signing Up (New User)¶
A new user arrives at Petfolio and creates an account through their OAuth provider.
sequenceDiagram
actor User
participant FE as Petfolio Frontend
participant Google as Google OAuth
participant API as Petfolio API
participant DB as Database
User->>FE: Clicks "Sign up with Google"
FE->>Google: Redirects to Google consent screen
Google->>User: "Allow Petfolio to access your profile?"
User->>Google: Grants consent
Google->>FE: Returns OAuth token
FE->>API: POST /api/auth/signup with Google token + account details
API->>Google: Validates token (using Google's public keys)
API->>DB: Creates Account + User record
API->>FE: Returns JWT access token + refresh token
FE->>User: Redirected to dashboard
What happens behind the scenes:
- Google authenticates the user and confirms their identity
- The API validates the Google token locally (no network call to Google on every request)
- A new Account is created (the tenant boundary for all their data)
- A new User is created and linked to the Account, storing:
- Their name and email (from Google's profile)
- Their Google provider ID (so we can recognise them next time)
- The API issues a JWT access token (short-lived, 15 minutes) and a refresh token (7 days)
Logging In (Returning User)¶
A returning user authenticates with their OAuth provider to get a new session.
sequenceDiagram
actor User
participant FE as Petfolio Frontend
participant Google as Google OAuth
participant API as Petfolio API
participant DB as Database
User->>FE: Clicks "Log in with Google"
FE->>Google: Redirects to Google
Google->>FE: Returns OAuth token
FE->>API: POST /api/auth/login with Google token
API->>Google: Validates token
API->>DB: Looks up User by provider + provider ID
DB->>API: Returns existing User + Account
API->>FE: Returns JWT access token + refresh token
FE->>User: Redirected to dashboard
Key difference from signup: No new records are created. The API looks up the user by their OAuth provider and provider user ID to find their existing account.
Making Authenticated Requests¶
Once logged in, every API request includes the JWT token. The user never interacts with auth again until the token expires.
sequenceDiagram
actor User
participant FE as Petfolio Frontend
participant API as Petfolio API
participant DB as Database
User->>FE: Views their animals
FE->>API: GET /api/animals (with JWT in header)
API->>API: Validates JWT signature
API->>API: Extracts UserId, AccountId, Email from claims
API->>DB: Queries animals WHERE AccountId = user's account
DB->>API: Returns only this account's animals
API->>FE: Animal list
FE->>User: Shows their animals
Multi-tenancy in action: The database automatically filters results by the authenticated user's AccountId. A user in Account A can never see data belonging to Account B.
Token Refresh¶
Access tokens expire after 15 minutes. The frontend silently refreshes them using the refresh token.
sequenceDiagram
actor User
participant FE as Petfolio Frontend
participant API as Petfolio API
FE->>API: GET /api/animals (with expired JWT)
API->>FE: 401 Unauthorised
FE->>API: POST /api/auth/refresh (with refresh token)
API->>API: Validates refresh token
API->>FE: New JWT access token + new refresh token
FE->>API: GET /api/animals (with new JWT)
API->>FE: Animal list
Note over User: User sees no interruption
The user never notices this happening. If the refresh token itself has expired (after 7 days), the user is redirected to log in again.
Technical Overview¶
How the Pieces Fit Together¶
graph TB
subgraph "Browser"
FE[Frontend App]
end
subgraph "OAuth Provider"
G[Google / Microsoft / Facebook]
end
subgraph "Petfolio API"
direction TB
MW[JWT Middleware]
HCA[HttpContextClaimsPrincipalAccessor]
CUS[CurrentUserService]
MED[MediatR Pipeline]
HAN[Command / Query Handlers]
DB[DbContext + Global Query Filters]
end
subgraph "Database"
MySQL[(MySQL)]
end
FE -- "Bearer token in header" --> MW
FE -- "OAuth token at login" --> G
G -- "Validated identity" --> FE
MW -- "Validates JWT, populates ClaimsPrincipal" --> HCA
HCA -- "Extracts ClaimsPrincipal from HttpContext" --> CUS
CUS -- "Provides UserId, AccountId, Email" --> MED
MED -- "Validates + dispatches" --> HAN
HAN --> DB
DB -- "Filters by AccountId automatically" --> MySQL
style MW fill:#e3f2fd
style CUS fill:#e8f5e9
style DB fill:#fff3e0
JWT Claims¶
When a user is authenticated, the JWT token contains three claims that Petfolio uses:
| Claim | Example | Used For |
|---|---|---|
sub (NameIdentifier) |
google\|123456789 |
Identifying which user is making the request |
accountId |
550e8400-e29b-41d4-a716-446655440000 |
Filtering data to the correct tenant |
email |
alice@example.com |
Display and communication |
The accountId claim is the linchpin of multi-tenancy -- it determines which data the user can see and modify.
Request Lifecycle¶
Here is what happens for every authenticated API request, from HTTP to database:
Multi-Tenancy: How Data Isolation Works¶
Every entity that needs tenant isolation implements the IMultiTenant interface, which requires an AccountId property. This is used in two ways:
On read: EF Core Global Query Filters automatically append WHERE AccountId = @currentAccountId to every query. Developers do not need to add this manually -- it is impossible to forget.
On write: The SaveChangesAsync override checks that every new or modified IMultiTenant entity has the correct AccountId set. If an entity is inserted without an AccountId, it throws an exception rather than saving bad data.
graph LR
subgraph "Account A"
UA[User: Alice] --> AA[Account A]
A1[Animal: Max] --> AA
A2[Animal: Bella] --> AA
end
subgraph "Account B"
UB[User: Bob] --> AB[Account B]
B1[Animal: Rex] --> AB
end
style AA fill:#e8f5e9
style AB fill:#e3f2fd
Alice can only see Max and Bella. Bob can only see Rex. The database enforces this boundary -- it is not dependent on application code remembering to filter.
Key Components¶
| Component | Layer | Responsibility |
|---|---|---|
ICurrentUserService |
Domain | Interface exposing UserId, AccountId, Email, IsAuthenticated |
IClaimsPrincipalAccessor |
Domain | Abstraction over how the ClaimsPrincipal is accessed |
CurrentUserService |
Infrastructure | Parses JWT claims into typed properties |
HttpContextClaimsPrincipalAccessor |
API | Bridges ASP.NET Core's IHttpContextAccessor to IClaimsPrincipalAccessor |
IMultiTenant |
Domain | Interface marking entities that are scoped to an account |
PetfolioDbContext |
Infrastructure | Applies global query filters and sets AccountId on save |
| JWT Middleware | API | Validates token signatures and populates HttpContext.User |
OAuth Providers¶
Petfolio stores which OAuth provider a user signed up with as a value object on the User entity:
| Provider | Status | Enum Value |
|---|---|---|
| Active | AuthenticationProvider.Google |
|
| Microsoft | Planned | AuthenticationProvider.Microsoft |
| Planned | AuthenticationProvider.Facebook |
Each user is linked to exactly one OAuth provider. A user who signs up with Google must always log in with Google. See ADR-001 for the rationale and trade-offs of this decision.
Security Summary¶
| Concern | Approach |
|---|---|
| Token validation | Asymmetric keys (RSA/ECDSA) -- public key validates, private key signs |
| Token lifetime | 15-minute access tokens, 7-day refresh tokens |
| Password storage | None -- OAuth only |
| Data isolation | EF Core Global Query Filters on AccountId |
| Rate limiting | ASP.NET Core AddRateLimiter() on auth endpoints |
| Secrets | User Secrets (dev), Azure Key Vault / AWS KMS (prod) |
For the full security analysis, including identified risks and mitigations, see the Security section of ADR-001.
Related Resources¶
- ADR-001: Authentication and Multi-Tenancy -- design decisions, alternatives, security, and GDPR
- ADR-002: Role-Based Access Control -- authorisation model (who can do what)
- Architecture -- Clean Architecture overview and CQRS pipeline