Skip to content

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 token
  • POST /api/auth/login - Authenticate a returning user via OAuth token
  • POST /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