Auth and Multi-Tenancy¶
PetFolio uses OAuth-only authentication (no passwords) with account-based multi-tenancy. Users sign in with an OAuth provider (Google, with Microsoft and Facebook planned), and all their data is scoped to the account they belong to.
Auth spans the entire platform. The backend validates tokens and enforces data isolation; the frontend will handle the OAuth consent flow and token management. This page covers the cross-cutting concepts that both sides share.
| Section | What it covers |
|---|---|
| PetFolio Service (BE) auth | Backend implementation: CurrentUserService, multi-tenancy, testing |
| PetFolio FE auth | Frontend implementation (planned util-oauth-client library) |
Design decisions
For the full rationale, alternatives considered, and security analysis, see ADR-001: Authentication and Multi-Tenancy.
Key concepts¶
If you are new to authentication or multi-tenancy, these definitions will help you follow the rest of the docs.
| Concept | Overview |
|---|---|
| Multi-tenancy | A design where multiple customers (tenants) share the same application and database, but each can only see their own data. In PetFolio, each Account is a tenant. |
| Tenant | A single Account in PetFolio. All data belonging to that account (animals, users) is invisible to other accounts. |
| JWT (JSON Web Token) | A small, signed token that the frontend sends with every API request to prove who the user is. Think of it like a wristband at an event: you show it at each door and the staff can verify it is genuine without calling the box office every time. |
| Claim | A single piece of information inside a JWT. For example, email: alice@example.com is a claim. PetFolio reads three claims from each token (see Claim Contract below). |
| ClaimsPrincipal | The .NET object that ASP.NET Core creates from the JWT claims. It represents "the user making this request" and is available via HttpContext.User. |
| OAuth | A standard protocol that lets users sign in using an existing account (like Google) instead of creating a password. PetFolio never sees or stores the user's Google password. |
| OAuth Provider | The external identity service (e.g. Google) that authenticates the user. PetFolio stores which provider a user signed up with as the AuthProvider value object on the User entity, pairing the provider with its user ID. Each user is linked to exactly one provider and must always sign in with the same one. |
Claim contract
CurrentUserService reads three claims from the ClaimsPrincipal.
These are the claims that any authentication mechanism must provide for the system to work correctly.
| Claim Type | Example Value | Maps To | Purpose |
|---|---|---|---|
NameIdentifier / sub |
google\|123456789 |
ICurrentUserService.UserId |
Identifies which user is making the request. sub is the standard JWT field name; ASP.NET Core maps it to ClaimTypes.NameIdentifier. |
accountId (custom) |
550e8400-e29b-41d4-a716-446655440000 |
ITenantProvider.AccountId |
Determines which tenant's data is visible |
Email |
alice@example.com |
ICurrentUserService.Email |
Display and communication |
The accountId claim is the linchpin of multi-tenancy. It determines which data the user can see and modify.
How it all fits together¶
This diagram shows only the components that are currently implemented in the codebase.
graph TD
REQ["HTTP Request + ClaimsPrincipal"]
HCA["<b>ClaimsPrincipalAccessor</b>"]
CUS["<b>CurrentUserService</b><br/><i>UserId, Email, AccountId</i>"]
HAN["<b>Command / Query Handlers</b>"]
DBCTX["<b>PetfolioDbContext</b><br/><i>tenant-scoped</i>"]
DB[("MySQL<br/>WHERE AccountId = ?")]
REQ --> HCA
HCA -->|extracts claims| CUS
CUS -->|identity| HAN
CUS -->|AccountId| DBCTX
HAN --> DBCTX --> DB
classDef layer-api fill:#4a90d926,stroke:#4a90d9,stroke-width:2px
classDef layer-domain fill:#7ed32126,stroke:#7ed321,stroke-width:2px
classDef layer-infra fill:#f5a6232e,stroke:#f5a623,stroke-width:2px
classDef storage fill:#4a90d91f,stroke:#4a90d9,stroke-width:2px
class REQ,HCA layer-api
class CUS,HAN layer-domain
class DBCTX layer-infra
class DB storage
Diagram key
| Colour | Meaning |
|---|---|
| Blue border | API layer (HTTP request, ClaimsPrincipalAccessor) |
| Green border | Domain layer (CurrentUserService, Command/Query Handlers) |
| Orange border | Infrastructure layer (PetfolioDbContext) |
| Light blue border | Storage (MySQL database) |
No JWT middleware yet
The codebase does not currently include JWT Bearer middleware (AddAuthentication / UseAuthentication). HttpContext.User exists on every request, but no middleware validates tokens or populates it with verified claims. See Planned Features for what is coming.
Planned features¶
Not yet implemented
Everything in this section describes planned functionality from ADR-001. None of it exists in the codebase yet. Do not write code that depends on these features being present.
JWT bearer middleware¶
ASP.NET Core's AddAuthentication().AddJwtBearer() will validate token signatures and populate HttpContext.User with verified claims. Currently, no NuGet package for JWT Bearer is installed and no authentication middleware is configured.
Auth endpoints¶
Three API endpoints are planned:
POST /api/auth/signup- Create a new account and user via OAuth tokenPOST /api/auth/login- Authenticate a returning user via OAuth tokenPOST /api/auth/refresh- Exchange a refresh token for a new access token
Token lifecycle¶
- Access tokens: 15-minute expiry, signed with asymmetric keys (RSA/ECDSA)
- Refresh tokens: 7-day expiry, stored in the database, single-use with rotation
User flows¶
The signup and login flows involve the frontend, an OAuth provider (Google), and the API. These sequence diagrams show the planned behaviour:
Signup flow (planned)
graph TD
S1["User<br/><i>clicks 'Sign up with Google'</i>"]
S2["Google consent screen<br/><i>user grants access</i>"]
S3["Frontend<br/><i>receives OAuth token</i>"]
S4["POST /api/auth/signup<br/><i>Google token + account details</i>"]
S5["Validate token<br/><i>using Google's public keys</i>"]
S6["Create Account + User"]
S7["Return JWT + refresh token"]
S8["Redirected to dashboard"]
S1 --> S2 --> S3
S3 --> S4 --> S5
S5 --> S6 --> S7 --> S8
classDef layer-external fill:#9b9b9b2e,stroke:#9b9b9b,stroke-width:2px
classDef layer-api fill:#4a90d926,stroke:#4a90d9,stroke-width:2px
classDef layer-domain fill:#7ed32126,stroke:#7ed321,stroke-width:2px
classDef layer-infra fill:#f5a6232e,stroke:#f5a623,stroke-width:2px
class S1,S2,S5,S8 layer-external
class S3 layer-api
class S4,S7 layer-domain
class S6 layer-infra
Login flow (planned)
graph TD
L1["User<br/><i>clicks 'Log in with Google'</i>"]
L2["Google<br/><i>returns OAuth token</i>"]
L3["POST /api/auth/login<br/><i>sends Google token</i>"]
L4["Validate token<br/><i>with Google</i>"]
L5["Look up User<br/><i>by provider + ID</i>"]
L6["Return JWT + refresh token"]
L7["Redirected to dashboard"]
L1 --> L2 --> L3
L3 --> L4 --> L5
L5 --> L6 --> L7
classDef layer-external fill:#9b9b9b2e,stroke:#9b9b9b,stroke-width:2px
classDef layer-domain fill:#7ed32126,stroke:#7ed321,stroke-width:2px
classDef layer-infra fill:#f5a6232e,stroke:#f5a623,stroke-width:2px
class L1,L2,L4,L7 layer-external
class L3,L6 layer-domain
class L5 layer-infra
Token refresh flow (planned)
graph TD
R1["Request with expired JWT"]
R2["401 Unauthorised"]
R3["POST /api/auth/refresh<br/><i>sends refresh token</i>"]
R4["Validate refresh token"]
R5["Return new JWT + refresh token"]
R6["Retry original request"]
R7["No interruption"]
R1 --> R2 --> R3
R3 --> R4 --> R5
R5 --> R6 --> R7
classDef layer-api fill:#4a90d926,stroke:#4a90d9,stroke-width:2px
classDef layer-domain fill:#7ed32126,stroke:#7ed321,stroke-width:2px
classDef neutral fill:#c8c8c826,stroke:#999999,stroke-width:1.5px
classDef layer-external fill:#9b9b9b2e,stroke:#9b9b9b,stroke-width:2px
class R1,R3,R6 layer-api
class R4,R5 layer-domain
class R2 neutral
class R7 layer-external
Security configuration (planned)¶
| Concern | Planned Approach |
|---|---|
| Token signing | Asymmetric keys (RSA/ECDSA) |
| Token lifetime | 15-minute access tokens, 7-day refresh tokens |
| Rate limiting | AddRateLimiter() on auth endpoints |
| Secrets management | User Secrets (dev), Azure Key Vault / AWS KMS (prod) |
[Authorize] attributes |
On all controllers requiring authentication |
Related resources¶
- PetFolio Service (BE) auth - backend implementation (CurrentUserService, multi-tenancy, testing)
- PetFolio FE auth - frontend implementation (planned)
- ADR-001: Authentication and Multi-Tenancy - design decisions, alternatives, security, and GDPR
- ADR-002: Role-Based Access Control - authorisation model (who can do what)