---
title: SAIF.NetCore.LegacyAuth business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: Framework
last_reviewed: 2026-10-02
library: SAIF.NetCore.LegacyAuth
reference_type: package
---

# SAIF.NetCore.LegacyAuth business rules

This page captures the 14 business rules enforced by `SAIF.NetCore.LegacyAuth`'s cookie-issuance and DPAPI encryption logic. Rule IDs use a plain, sequentially-numbered scheme for this library (see the rule headings below).

## BR-01 Fixed cookie name SAIFAUTH

| Field | Value |
| --- | --- |
| **Statement** | The legacy authentication cookie is always named `SAIFAUTH`. |
| **Trigger** | `SetCookie`, `ClearCookie` |
| **Enforcement** | `LegacyAuthCookieService.COOKIE_NAME` constant, used by both `SetCookie` and `ClearCookie`. |
| **Outcome** | The name is fixed and non-configurable; it matches the name `LegacyBolAuthMiddleware` in `SAIF.NetCore.ServiceExtensions` checks for. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

---

## BR-02 Tab-delimited credential payload

| Field | Value |
| --- | --- |
| **Statement** | The cookie payload is `user` + TAB + `password` + TAB (a trailing separator is included). |
| **Trigger** | `SetCookie` |
| **Enforcement** | `LegacyAuthCookieService.Encrypt`, using the private `PW_SEPARATOR = "\t"` constant. |
| **Outcome** | The legacy reader is expected to split on tab; a tab character embedded in the user name or password corrupts parsing*. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

---

## BR-03 Client IP as encryption entropy

| Field | Value |
| --- | --- |
| **Statement** | The client's remote IP address is used as DPAPI entropy, binding the cookie to the issuing client IP. |
| **Trigger** | `SetCookie` |
| **Enforcement** | `LegacyAuthCookieService.Encrypt` reads `context.Connection.RemoteIpAddress.ToString()` as the entropy value. |
| **Outcome** | Decryption succeeds only when the identical IP string is presented*. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

---

## BR-04 Machine-scope DPAPI encryption

| Field | Value |
| --- | --- |
| **Statement** | Legacy cookie credentials are encrypted with the machine DPAPI scope. |
| **Trigger** | `SetCookie` → `DPAPI.Encrypt` |
| **Enforcement** | `LegacyAuthCookieService.Encrypt` passes `KeyType.MachineKey`; `DPAPI.Encrypt(KeyType, byte[], byte[], string)` maps it to the `CRYPTPROTECT_LOCAL_MACHINE` flag. |
| **Outcome** | The ciphertext can be decrypted by any process on the same machine, and only on that machine. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs`; `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs` |

---

## BR-05 No URL-encoding on cookie value

| Field | Value |
| --- | --- |
| **Statement** | The cookie value is written without URL-encoding so that the legacy .NET Framework SecurityModule can decrypt it. |
| **Trigger** | `AddCookieToHeader` |
| **Enforcement** | `LegacyAuthCookieService.AddCookieToHeader` builds a `SetCookieHeaderValue` and appends its rendered string directly to the `Set-Cookie` header, bypassing `Response.Cookies.Append`'s automatic encoding. |
| **Outcome** | Raw Base64 (including `+`, `/`, `=` characters) is sent in the `Set-Cookie` header. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

---

## BR-06 Cookie attributes omit Secure and SameSite

| Field | Value |
| --- | --- |
| **Statement** | Cookies written by the service are `HttpOnly` with `Path=/`; no `Secure`, `SameSite`, `Domain`, or expiry is set. |
| **Trigger** | `AddCookieToHeader` |
| **Enforcement** | `LegacyAuthCookieService.AddCookieToHeader` constructs a `SetCookieHeaderValue` with only `Path` and `HttpOnly` populated. |
| **Outcome** | A host-only session cookie that JavaScript cannot read, but that is sent on every path and can travel over plain HTTP or in cross-site requests. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

---

## BR-07 Instance Encrypt defaults to user key scope

| Field | Value |
| --- | --- |
| **Statement** | The instance `DPAPI.Encrypt(string)` defaults to the user key scope; every other path chooses the scope explicitly. |
| **Trigger** | `new DPAPI().Encrypt(text)` |
| **Enforcement** | `DPAPI.Encrypt(string)` calls through with `KeyType.UserKey`. |
| **Outcome** | User-scoped ciphertext, which differs from the machine scope the cookie service uses ([BR-04](#br-04-machine-scope-dpapi-encryption)). |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs` |

---

## BR-08 Null inputs coerced to empty values

| Field | Value |
| --- | --- |
| **Statement** | Null plaintext, entropy, and description are treated as empty strings. |
| **Trigger** | Any `Encrypt` call |
| **Enforcement** | `DPAPI.Encrypt` overloads coerce `null` arguments to `""` before encoding. |
| **Outcome** | No `ArgumentNullException` is thrown; an empty value is encrypted instead. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs` |

---

## BR-09 UTF-8 and Base64 encoding for string encryption

| Field | Value |
| --- | --- |
| **Statement** | String encryption uses UTF-8 for plaintext and entropy, and Base64 for the returned ciphertext. |
| **Trigger** | String `Encrypt` overloads |
| **Enforcement** | `DPAPI.Encrypt(KeyType, string, string, string)` encodes with `Encoding.UTF8` and `Convert.ToBase64String`. |
| **Outcome** | The legacy reader must use the same encodings to decrypt successfully*. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs` |

---

## BR-10 Active HTTP request required for cookie operations

| Field | Value |
| --- | --- |
| **Statement** | `SetCookie` requires both an active ambient `HttpContext` (resolved from `IHttpContextAccessor`) and a known remote IP on that context; `ClearCookie` requires only an active ambient `HttpContext` and never reads the remote IP. `AddCookieToHeader` is unaffected — it takes an explicit `HttpContext` parameter rather than resolving one from the accessor. |
| **Trigger** | `SetCookie`, `ClearCookie` |
| **Enforcement** | Neither `SetCookie` nor `ClearCookie` guards against a `null` `HttpContext` from `_httpContextAccessor.HttpContext` before use; `SetCookie` additionally dereferences `context.Connection.RemoteIpAddress.ToString()` without a null guard. `ClearCookie` only accesses `context.Response.Cookies`, so it never touches `RemoteIpAddress`. |
| **Outcome** | A `NullReferenceException` is thrown when `HttpContext` is `null` for either method, and additionally when `RemoteIpAddress` is `null` for `SetCookie` only. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

---

## BR-11 No DPAPI UI prompts permitted

| Field | Value |
| --- | --- |
| **Statement** | Every encrypt/decrypt call made through the managed `DPAPI` wrapper methods must never prompt or show UI. This guarantee applies only to the managed wrapper: a consumer calling the public `CryptProtectData`/`CryptUnprotectData` externs directly supplies its own `dwFlags` and is not subject to it. |
| **Trigger** | Every managed `Encrypt`/`Decrypt` call |
| **Enforcement** | `DPAPI.cs`'s managed `Encrypt`/`Decrypt` overloads always include the `CRYPTPROTECT_UI_FORBIDDEN` flag and initialize the prompt struct with no window handle or prompt text before calling the native primitive. |
| **Outcome** | Calls made through the managed wrapper are suitable for non-interactive server processes; direct calls to the raw externs can enable a UI prompt if the caller chooses to omit the flag. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs` |

---

## BR-12 Generic exception wrapping on encryption failure

| Field | Value |
| --- | --- |
| **Statement** | Every failure raised inside the managed byte-array `Encrypt(KeyType, byte[], byte[], string)` overload's try block surfaces as a generic `System.Exception("DPAPI was unable to encrypt data.")` wrapping the original cause (a `Win32Exception` for native API failures). A direct call to the public `CryptProtectData` extern is not wrapped; it returns the raw native `bool` result and leaves failure handling to the caller. |
| **Trigger** | Any exception raised inside the byte-array `Encrypt(KeyType, byte[], byte[], string)` overload's try block — not `InitPrompt` (called before the try block starts), the string overload's own `Encoding.UTF8.GetBytes`/`Convert.ToBase64String` calls (which run outside the byte overload entirely), or the `finally` block's cleanup |
| **Enforcement** | A single `catch`/rethrow wraps every failure path inside that overload's try block, including a `CryptProtectData` return of `false`. |
| **Outcome** | For the exceptions scoped by the Trigger above (those raised inside the byte-array overload's try block), callers cannot catch a specific exception type; the Win32 error code is only available in the inner exception. Preparation, UTF-8/Base64 conversion, and cleanup failures outside that try block — and any failure from a direct call to the raw extern — propagate unwrapped. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs` |

---

## BR-13 Windows-only DPAPI DLL path

| Field | Value |
| --- | --- |
| **Statement** | DPAPI is loaded only from the fixed path `c:\windows\system32\crypt32.dll`. |
| **Trigger** | First `CryptProtectData` call |
| **Enforcement** | `DllImportAttribute` on `DPAPI.CryptProtectData` and `CryptUnprotectData` hard-codes the absolute DLL path. |
| **Outcome** | Windows-only behavior; it fails on other operating systems or non-default Windows installation directories. A call through the managed `Encrypt`/`Decrypt` wrapper has the resulting `DllNotFoundException` wrapped per [BR-12](#br-12-generic-exception-wrapping-on-encryption-failure); a direct call to the raw externs receives the unwrapped `DllNotFoundException` since [BR-12](#br-12-generic-exception-wrapping-on-encryption-failure) does not apply to that path. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs` |

---

## BR-14 Default cookie delete options on clear

| Field | Value |
| --- | --- |
| **Statement** | Clearing the cookie uses framework-default delete options (path `/`). |
| **Trigger** | `ClearCookie` |
| **Enforcement** | `LegacyAuthCookieService.ClearCookie` calls `Response.Cookies.Delete("SAIFAUTH")` with no explicit `CookieOptions`. |
| **Outcome** | The default delete path matches the issued `Path=/`, so the browser removes the cookie. |
| **Source** | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

---

## Notes
- There are no business rules originating in dependency packages: `IOidcSettings` is injected but never invoked.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.md)
- [API reference](api/index.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Sources
- `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs`
- `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs`
- `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/ILegacyAuthCookieService.cs`
