# Security

## Overview

*Shopfoo* implements a simplified but representative **claims-based access control** model. There is no real authentication layer (no Keycloak, no OAuth, no JWT verification): the *Login* page simply lets the user pick a persona/role for manual testing. This is sufficient for demo purposes while still exercising the same security patterns that a production app would use — claims checked both client-side and server-side.

## Domain model

The security types are defined in `Domain.Types/Security.fs`.

A `User` is either `Anonymous` or `LoggedIn` with a name and a set of `Claims`. Claims map a `Feat` (feature area) to an `Access` level:

```fsharp
type Access = View | Edit

type Feat = About | Admin | Catalog | Sales | Warehouse

type Claims = Map<Feat, Access>

type User =
    | Anonymous
    | LoggedIn of userName: string * claims: Claims
```

The `User` type exposes helpers to check access:

```fsharp
member user.CanAccess feat = ...    // bool — has any access to the feature
member user.AccessTo feat = ...     // Access option — the specific access level

// Active patterns for pattern matching
let (|UserCanAccess|_|) feat (user: User) = ...
let (|UserCanNotAccess|_|) feat (user: User) = ...
```

## Login page — persona selection

Since there is no real authentication, the Login page (`Pages/Login.fs`) displays a table of predefined personas. The user clicks a row to "log in" as that persona.

The `Persona` type is a discriminated union defined in `Domain.Types/Security.fs`, alongside the other security types:

| Persona         | About | Catalog | Sales | Warehouse | Admin |
| --------------- | ----- | ------- | ----- | --------- | ----- |
| Guest           | View  | View    | —     | —         | —     |
| Catalog Editor  | View  | Edit    | View  | View      | —     |
| Sales           | View  | View    | Edit  | Edit      | —     |
| Product Manager | View  | Edit    | Edit  | Edit      | —     |
| Administrator   | View  | Edit    | Edit  | Edit      | Edit  |

```fsharp
// Domain.Types/Security.fs
[<RequireQualifiedAccess>]
type Persona =
    | Guest
    | CatalogEditor
    | Sales
    | ProductManager
    | Administrator

    member this.Name: string = ...
    member this.Claims: Claims = ...

    static member All = [ Guest; CatalogEditor; Sales; ProductManager; Administrator ]
```

Each case declares its claims explicitly as a `Map` literal — there is no incremental building.

## Client-side access control

### Page routing (`View.fs`)

The main `AppView` determines which page to render based on the current route **and** the user's authentication state. Protected pages require a logged-in user; if the user is anonymous, the *Login* page is displayed inline (without URL redirection):

```fsharp
let pageToDisplayInline, featAccessToCheck =
    match model.Page, fullContext.User with
    // Public pages
    | Page.About, _
    | Page.Login, User.Anonymous -> model.Page, None

    // Protected pages — require login + feature access
    | Page.ProductIndex _, User.LoggedIn _
    | Page.ProductDetail _, User.LoggedIn _ -> model.Page, Some Feat.Catalog
    | Page.Admin, User.LoggedIn _ -> model.Page, Some Feat.Admin

    // Logged in but on Login/Home → redirect to default page
    | Page.Home, User.LoggedIn _
    | Page.Login, User.LoggedIn _ -> Page.ProductIndexDefaults, Some Feat.Catalog

    // Anonymous on protected page → show Login inline
    | Page.Admin, User.Anonymous
    | Page.Home, User.Anonymous
    | Page.ProductIndex _, User.Anonymous
    | Page.ProductDetail _, User.Anonymous -> Page.Login, None
```

After rendering, a `React.useEffect` hook checks that the user has the required feature access. If not, it redirects to a "Not Found" page:

```fsharp
React.useEffect (fun () ->
    match featAccessToCheck with
    | Some feat when not (fullContext.User.CanAccess feat) ->
        Router.navigatePage (Page.CurrentNotFound())
    | _ -> ()
)
```

### Conditional rendering in components

Individual components adapt their UI based on user claims.

**Product Details page** — the *Actions* column is only displayed when the user has `Sales` or `Warehouse` access and the product type supports it:

```fsharp
// Product/Details/Page.fs
let hasActions =
    match fullContext.User, sku.Type with
    | (UserCanAccess Feat.Sales | UserCanAccess Feat.Warehouse),
      (SKUType.FSID _ | SKUType.ISBN _) -> true
    | _ -> false
```

**Actions form** — within the *Actions* column, each action group uses `AccessTo` to determine the access level (`View` = read-only display, `Edit` = interactive actions):

```fsharp
// Product/Details/Actions.fs
// Price actions — requires Sales access
ActionsDropdown "list-price" ... (fullContext.User.AccessTo Feat.Sales) ...

// Stock actions — requires Warehouse access
ActionsDropdown "stock" ... (fullContext.User.AccessTo Feat.Warehouse) ...
```

## Server-side authorization

Claims are also verified server-side on every Remoting API call. The mechanism relies on several types working together — from the client-side `FullContext` down to the server-side `authorizeHandler`. The full Remoting pipeline is detailed in the [Remoting](/safe-clean-architecture/front-end/remoting.md) page; here we focus on the security aspects.

### Passing the token with each request

The `FullContext` record (`Shared/Remoting.fs`) holds the current `User`, an optional `AuthToken`, and the loaded `Translations` (which carry their own `Lang`):

```fsharp
type FullContext = {
    User: User
    Token: AuthToken option       // ← encrypted User (AES-256-GCM)
    Translations: AppTranslations
} with
    member this.Lang = this.Translations.Translations.Lang
```

Every API call in `Shared/Remoting.fs` is typed as a function taking a `Request<'t>`:

```fsharp
type Request<'t> = {
    Token: AuthToken option
    Lang: Lang
    Body: 't
}

type Command<'command>        = Request<'command> -> Async<Response<unit>>
type Query<'query, 'response> = Request<'query>   -> Async<Response<'response>>
```

The `FullContext.PrepareRequest` extension method (declared in `Client/Remoting.fs`) builds a `Request` by copying the `Token` from the context:

```fsharp
member this.PrepareRequest body =
    let secureRequest: Request<'a> = {
        Token = this.Token
        Lang = this.Lang
        Body = body
    }
    this.Cmder, secureRequest
```

A convenience variant, `PrepareQueryWithTranslations`, wraps the query together with the translation pages to reload.

In practice, every page calls `fullContext.PrepareRequest` (or `PrepareQueryWithTranslations`) before invoking any API endpoint — for example:

```fsharp
Cmd.loadHomeData (fullContext.PrepareQueryWithTranslations())
Cmd.loadPrices   (fullContext.PrepareRequest sku)
Cmd.saveProduct  (fullContext.PrepareRequest product)
```

### Token issuance — server-side encryption

The `AuthToken` is a simple wrapper: `type AuthToken = AuthToken of string`.

When the *Login* page loads, it calls the `HomeApi.Index` endpoint which returns a list of `AuthPersona` records — each pairing a `Persona` with a pre-computed `AuthToken`:

```fsharp
// Shared/Remoting.fs
type AuthPersona = { Persona: Persona; Token: AuthToken }
type HomeIndexResponse = { Personas: AuthPersona list }
```

The token is produced **server-side** in the `IndexHandler` by serializing the corresponding `User` to JSON and then **encrypting it with AES-256-GCM**:

```fsharp
for persona in personas ->
    { Persona = persona
      Token = tokenFor (User.LoggedIn(persona.Name, persona.Claims)) }
```

`tokenFor` is defined in Security.fs:

```fsharp
let internal tokenFor user = user |> JsonFSharp.serialize |> Crypto.encrypt |> AuthToken
```

The `Crypto` module (private to `Security.fs`) uses `System.Security.Cryptography.AesGcm` with an **ephemeral 256-bit key** generated at server startup. Each encrypted token is a Base64 string containing `nonce (12 bytes) + cipherText + tag (16 bytes)`. Since the key lives only in memory, all tokens become invalid when the server restarts (users must re-login).

{% hint style="info" %}
The token is opaque to the client — it never decodes or inspects it. Only the server can decrypt it.
{% endhint %}

When the user selects a persona, the client reconstructs the `User` and stores it alongside the `Token` in the `FullContext` via `WithPersona`:

```fsharp
member this.WithPersona(persona: AuthPersona) =
    { this with
        User = User.LoggedIn(persona.Name, persona.Claims)
        Token = Some persona.Token }
```

From that point on, every API call made through `PrepareRequest` includes this token.

### Authorization handler (`Server/Remoting/Security.fs`)

On the server, `checkToken` **decrypts** the token with `Crypto.decrypt`, then deserializes the JSON back into a `User` and compares its claims against the required ones. If decryption fails (tampered or forged token), the request is rejected with `TokenInvalid`. The `authorizeHandler` function wraps every API handler — it checks the token first, then delegates to the handler with the decoded `User`:

```fsharp
let authorizeHandler
    (claims: Claims)
    (handler: SecureRequestHandler<'requestBody, 'response>)
    request =
    async {
        match checkToken claims request.Token with
        | Error authError -> return Error(ServerError.AuthError authError)
        | Ok authorizedUser -> return! handler.Handle request.Lang request.Body authorizedUser
    }
```

### API endpoint authorization

Each API builder declares the required claims for its endpoints. For example:

```fsharp
// Catalog API — requires Catalog claims
GetProducts     = GetProductsHandler(...)     |> Security.authorizeHandler (claim Access.View)
SaveProduct     = SaveProductHandler(...)     |> Security.authorizeHandler (claim Access.Edit)

// Prices API — requires Sales or Warehouse claims depending on the operation
GetPrices       = GetPricesHandler(...)       |> Security.authorizeHandler (salesClaim Access.View)
SavePrices      = SavePricesHandler(...)      |> Security.authorizeHandler (salesClaim Access.Edit)
AdjustStock     = AdjustStockHandler(...)     |> Security.authorizeHandler stockClaims  // Sales.View + Warehouse.View
ReceiveSupply   = ReceiveSupplyHandler(...)   |> Security.authorizeHandler (warehouseClaim Access.Edit)

// Home API — no claims required (accessible to everyone, including Anonymous)
Index           = IndexHandler(...)           |> Security.authorizeHandler Claims.none
GetTranslations = GetTranslationsHandler(...) |> Security.authorizeHandler Claims.none
```

This ensures that even if a user bypasses client-side checks, the server rejects unauthorized requests.

### Testing Remoting API security

The `RemotingApi/XxxApiSecurityTests` classes verify that each endpoint of the related `XxxApi` is correctly secured. Unlike the lower-level `AuthorizeHandlerShould` tests (which test `authorizeHandler` in isolation with a stub handler), these tests build the real `RootApi` via Dependency Injection — the same way the server does — and call each endpoint with tokens from the actual personas.

**Test fixture setup (`RemotingApi/Helpers.fs`):**

```fsharp
let api =
    let emptyConfig = ConfigurationBuilder().Build() :> IConfiguration
    let services =
        ServiceCollection()
            // Production dependencies
            .AddRemotingApi(emptyConfig)
            // Dependencies overrides for testing
            .AddSingleton<IWorkMonitors, WorkMonitorsMock>()
            .AddSingleton<IFakeStoreClient>(Substitute.For<IFakeStoreClient>())
            .AddSingleton<IOpenLibraryClient>(Substitute.For<IOpenLibraryClient>())
            .AddSingleton(BooksRepository.ofList Fakes.allBooks)
            // ... other repos seeded empty

    let provider = services.BuildServiceProvider()
    provider.GetRequiredService<RootApiBuilder>().Build()
```

**Test helpers:**

The `Helpers.fs` module provides a `PersonaOrAnonymousEnum` char enum used as `[<Arguments>]` in tests (char values avoid F# DU serialization issues in test runners):

```fsharp
type PersonaOrAnonymousEnum =
    | Anonymous = 'a'
    | Guest = 'g'
    | CatalogEditor = 'c'
    | Sales = 's'
    | ProductManager = 'p'
    | Administrator = 'A'

type ExpectedResult =
    | Accepted = 'a'
    | Rejected = 'r'
```

The `PersonaOrAnonymousEnumToken` active pattern resolves an enum value to an `AuthToken option`, and `expect` dispatches to `assertAccepted` or `assertRejected`.

**Test structure:**

Tests are split into one class per API — `AdminApiSecurityTests`, `CatalogApiSecurityTests`, `HomeApiSecurityTests`, `PricesApiSecurityTests`. Each class uses `[<MethodDataSource>]` for sets of accepted/rejected personas, or `[<Arguments>]` for single-endpoint cases:

```fsharp
// CatalogApiSecurityTests.fs
[<Test; MethodDataSource(nameof PersonasWithCatalogViewAccepted)>]
member _.``GetProducts accepts only personas with Catalog View``(PersonaOrAnonymousEnumToken token, result) =
    expect result (api.Catalog.GetProducts(makeQueryWithTranslations Provider.OpenLibrary token))
```

**Test strategy:**

* **Reject** — a persona with insufficient claims gets `Error(ServerError.AuthError UserUnauthorized)`.
* **Accept** — a persona with sufficient claims gets a result that is *not* an `AuthError`. The handler may succeed or return a business error; only the auth layer matters here.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://rdeneau.gitbook.io/safe-clean-architecture/front-end/security.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
