---
title: SAIF.NetCore.LegacyAuth security and authorization
summary: Credential-cookie issuance mechanism, cookie security attributes, and DPAPI binding; no authorization surface exists in this library.
status: draft
owner: Framework
last_reviewed: 2026-10-02
library: SAIF.NetCore.LegacyAuth
reference_type: package
---

# SAIF.NetCore.LegacyAuth security and authorization

This page documents the credential-cookie issuance mechanism implemented by this library. `SAIF.NetCore.LegacyAuth` is a framework component whose sole purpose is credential transport for a legacy authentication module; a scan of the library for `DirectoryServices`, `PrincipalContext`, `GroupPrincipal`, `LDAP`, `Authorize`, `IsInRole`, `Claim`, `Principal`, and `SecurityService` returned no matches, and no controller, action, attribute class, or policy/requirement/handler type exists in source. It has no authorization surface of its own, so the sections below report the authorization categories as empty and document the cookie mechanism itself as framework-level security behavior.

## Authorization architecture

**Architecture type: legacy credential-bridging, not itself an authorization mechanism.** A modern ASP.NET Core login application (inferred: "OnlineLogin"*, per an excluded source comment) calls `SetCookie(user, password)` after the user authenticates; the library DPAPI-encrypts the credentials and issues the `SAIFAUTH` cookie, which a legacy .NET Framework component referred to in source as "SecurityModule" decrypts to authenticate the user on legacy sites. There is no authorization surface to cover: the entire security surface is the cookie issuance/clearing path and the `DPAPI` encrypt wrapper. The library never reads or validates a request cookie, and it performs no `[Authorize]`, role, policy, or claim check of any kind.

The following diagram shows the credential-cookie issuance flow and the separate, presence-only check performed by `LegacyBolAuthMiddleware` in `SAIF.NetCore.ServiceExtensions`.

```mermaid
flowchart TD
    App["Consuming app:<br>user authenticates<br>(inferred OIDC-based)"] --> Service["LegacyAuthCookieService.SetCookie"]
    Service --> DPAPI["DPAPI.Encrypt:<br>MachineKey scope + client IP entropy"]
    DPAPI --> Cookie["Set-Cookie: SAIFAUTH<br>HttpOnly, Path=/"]
    Cookie -.-> Reader["Legacy SecurityModule<br>(same machine, out of repo)"]
    Cookie -.-> Middleware["LegacyBolAuthMiddleware<br>(SAIF.NetCore.ServiceExtensions)"]
    Middleware --> Presence{"SAIFAUTH or<br>SAIF.BOL.Auth cookie present?"}
    Presence -->|No| Redirect["Redirect to<br>LegacySessionProvider"]
    Presence -->|Yes| Continue["Continue pipeline<br>(value not decrypted or validated)"]
```

## AD resource catalog

**None found (verified).** A scan of the library for `DirectoryServices`, `PrincipalContext`, `GroupPrincipal`, `LDAP`, `Resource` literals, `Authorize`, `IsInRole`, `Claim`, `Principal`, and `SecurityService` returned no matches. The library has no `System.DirectoryServices*` references, and no domain (`corp.saif.com` / `external.saif.com`) is referenced.

| Resource name | Purpose | Enforcement point | Source |
| --- | --- | --- | --- |
| — | — | — | — (no matches) |

## Authorization enforcement points

**None. This library performs authentication credential transport, not authorization (verified).**

| Kind | Count | Notes |
| --- | --- | --- |
| Attribute-based (`[SAIFAuthorize]`, `[Authorize]`) | 0 | No controllers, actions, or attribute classes in the library. |
| Programmatic (`IsAuthorized`, `IsInRole`, `HasAccess`, `CheckAccess`) | 0 | — |
| Security manager implementations | 0 | — |
| ASP.NET Core policies / requirements / handlers | 0 | — |
| Authentication handlers / middleware that validate the cookie | 0 | The library writes `SAIFAUTH`; it never reads or validates a request cookie. |

### Attribute-based authorization

None in this library.

### Programmatic authorization checks

None in this library.

### Related enforcement point outside this library (cross-reference only)

| Location | Behavior | Relationship | Confidence |
| --- | --- | --- | --- |
| `SAIF.NetCore.ServiceExtensions` `LegacyBolAuthMiddleware.InvokeAsync` | If neither `SAIFAUTH` nor `SAIF.BOL.Auth` request cookie is present, redirects to the `LegacySessionProvider` endpoint resolved through `IEnvironmentClient`. It does not decrypt or validate the cookie value, and it still calls `next(context)` after redirecting. | Shares only the cookie-name convention; no package reference to this library. A presence-only check means any non-null `SAIFAUTH` value satisfies this gate — real validation is delegated to the legacy SecurityModule outside this repository. | verified (behavior); inferred (that validation is downstream)* |

This is a gating/redirect check owned by `SAIF.NetCore.ServiceExtensions` and documented there, not here.

## Data-level authorization

Not detected (verified). No ownership or context-validation checks (`CheckValidContext`, `IsValidPolicyForCurrentUser`, or equivalent) exist in this library.

## Segregation of duties

Not detected (verified). No SOD classes, rules, or privilege-combination queries exist in this library.

## Security interfaces and contracts

| Interface / Class | Role | Consumers | Source |
| --- | --- | --- | --- |
| `ILegacyAuthCookieService` | Defines the cookie-issuance contract (`SetCookie`, `ClearCookie`, `AddCookieToHeader`) implemented by `LegacyAuthCookieService`. | Consuming applications (none in this repository) | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/ILegacyAuthCookieService.cs` |
| `IOidcSettings` | Constructor-injected into `LegacyAuthCookieService` but never read by any member (verified directly in source). | `LegacyAuthCookieService` | `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/LegacyAuthCookieService.cs` |

## Credential-cookie issuance mechanism

### Credential payload

| Aspect | Finding | Source |
| --- | --- | --- |
| Content | `"{user}\t{password}\t"` — the plaintext password is placed in the payload (reversible encryption, not a hash or token). See [BR-02](business-rules.md#br-02-tab-delimited-credential-payload) | `LegacyAuthCookieService.cs` |
| Encoding | UTF-8 plaintext and entropy; Base64 ciphertext. See [BR-09](business-rules.md#br-09-utf-8-and-base64-encoding-for-string-encryption) | `DPAPI.cs` |
| Validation | None. Null values become empty strings; embedded tab characters are not escaped, which is a payload-ambiguity risk*. | `LegacyAuthCookieService.cs`; `DPAPI.cs` |

**Implication:** the cookie is a bearer credential that, once decrypted, yields the user's actual password — not a session reference*. Compromise of the cookie plus the ability to decrypt it compromises the account, not just a session.

### Cookie security attributes

The cookie is built with `SetCookieHeaderValue` and appended raw to the `Set-Cookie` header.

| Attribute | Value | Security implication |
| --- | --- | --- |
| Name | `SAIFAUTH` (hard-coded; see [BR-01](business-rules.md#br-01-fixed-cookie-name-saifauth)) | Not configurable; not read from `IOidcSettings.CookieName`. |
| `HttpOnly` | Set (`true`) | Mitigates theft via client-side script (XSS). |
| `Secure` | Not set | The encrypted credential can be sent over plain HTTP if any `http://` request reaches the host or path scope, unless infrastructure enforces HTTPS-only hosting or HSTS**. See [BR-06](business-rules.md#br-06-cookie-attributes-omit-secure-and-samesite) |
| `SameSite` | Not set | No explicit cross-site restriction; modern browsers default unspecified cookies to `Lax`*, but older browsers send it on all cross-site requests, increasing CSRF exposure for legacy sites that trust it. |
| `Path` | `/` | Sent on every path of the host. |
| `Domain` | Not set (host-only) | Only the issuing host receives it, consistent with the same-machine DPAPI requirement below. |
| `Expires` / `Max-Age` | Not set (session cookie) | Lives until the browser session ends; no server-side expiry. |
| Value encoding | Raw Base64 (no URL-encoding), deliberate. See [BR-05](business-rules.md#br-05-no-url-encoding-on-cookie-value) | Required for legacy reader compatibility; bypasses `Response.Cookies.Append` encoding. |

**Clearing:** `ClearCookie` calls `Response.Cookies.Delete("SAIFAUTH")` with default `CookieOptions` ([BR-14](business-rules.md#br-14-default-cookie-delete-options-on-clear)). The default delete path `/` matches the issued path, so deletion works. The delete header carries no `HttpOnly`/`Secure` flags (framework default)* — low impact because the value is empty. Clearing is client-side only; a copied cookie remains decryptable, since there is no server-side revocation*.

**Public raw-header writer:** `AddCookieToHeader(key, value, context)` is public on `ILegacyAuthCookieService`, so consumers can emit any cookie with the same attribute set (and the same `Secure`/`SameSite` gaps) without URL-encoding.

### DPAPI machine-scope and client-IP binding

| Aspect | Finding |
| --- | --- |
| Primitive | Windows DPAPI `CryptProtectData` via P/Invoke to a hard-coded `c:\windows\system32\crypt32.dll` path (see [BR-13](business-rules.md#br-13-windows-only-dpapi-dll-path)) |
| Scope | `KeyType.MachineKey` → flags `CRYPTPROTECT_UI_FORBIDDEN (1) \| CRYPTPROTECT_LOCAL_MACHINE (4)` (see [BR-04](business-rules.md#br-04-machine-scope-dpapi-encryption), [BR-11](business-rules.md#br-11-no-dpapi-ui-prompts-permitted)) |
| Entropy | `context.Connection.RemoteIpAddress.ToString()` (see [BR-03](business-rules.md#br-03-client-ip-as-encryption-entropy)) |
| Description | `string.Empty` |

Implications:

1. **Machine binding.** Machine-scope ciphertext can be decrypted by any process under any account on the same Windows host, and by no other host. The legacy SecurityModule must run on the same machine as the issuing ASP.NET Core app*. In a web farm, a cookie issued by one node cannot be decrypted on another unless sticky sessions or a shared host are used — any co-hosted application or compromised local account can recover passwords from captured cookies*.
2. **IP entropy is a weak, non-secret binding.** The client IP is not secret, so it adds little cryptographic strength; it mainly prevents replay from a different apparent IP. It creates availability failures when the client IP changes (mobile roaming, NAT changes). Behind a reverse proxy or load balancer without forwarded-headers processing, the entropy becomes the proxy's IP, removing per-client binding entirely**.
3. **No integrity/expiry metadata.** There is no timestamp, nonce, or issuance context in the payload. A copied ciphertext remains decryptable on the same host for as long as the required DPAPI key context (machine-scope key) and matching client-IP entropy are available — changing or clearing the user's account password does not cryptographically revoke the ciphertext, since DPAPI machine-key decryptability is independent of the account password*. Whether a legacy "SecurityModule" reader would still *accept* credentials recovered from such a captured cookie (for example, after the account password changed) is a separate question about downstream authentication validity that is outside this library's verified source — the reader lives outside this repository**.
4. **Platform coupling.** The hard-coded DLL path and absence of `[SupportedOSPlatform("windows")]` mean non-Windows hosts fail at the first `SetCookie` call with a wrapped `DllNotFoundException` (see [BR-12](business-rules.md#br-12-generic-exception-wrapping-on-encryption-failure), [BR-13](business-rules.md#br-13-windows-only-dpapi-dll-path)). This fails closed: no cookie is issued.
5. **Null-dereference fail paths.** `HttpContext` or `RemoteIpAddress` being `null` throws `NullReferenceException` before any cookie is written (see [BR-10](business-rules.md#br-10-active-http-request-required-for-cookie-operations) — fails closed).
6. **Instance default differs.** `new DPAPI().Encrypt(text)` uses user scope ([BR-07](business-rules.md#br-07-instance-encrypt-defaults-to-user-key-scope)), unlike the cookie path. Consumers using `DPAPI` directly may get a different binding than they expect.

### Dead decrypt code and public interop surface

| Aspect | Finding |
| --- | --- |
| `Decrypt` overloads | Three `internal static` overloads with no callers in the library and no `InternalsVisibleTo` declaration — unreachable from any consumer. |
| Raw P/Invoke | `CryptProtectData` **and** `CryptUnprotectData` are `public static extern`. |

Risks:

- **Unmaintained, untested code path.** The decrypt logic is unreachable and untested; it can drift from the legacy reader's expectations without detection*.
- **Decrypt capability is still exposed.** Making `Decrypt` internal does not restrict decryption: any consumer can call the public `DPAPI.CryptUnprotectData` extern directly with a captured cookie and the client IP, and recover the password on the issuing host. The `internal` visibility on `Decrypt` is not a security boundary.
- **Exception wrapping is scoped to the managed byte-array overload's try block.** Any failure raised there — BLOB initialization or a `CryptUnprotectData` return of `false` — surfaces as a generic `Exception("DPAPI was unable to decrypt data.")`; messages do not leak payload content. This wrapping does **not** apply to the string overload's own `Convert.FromBase64String` call, which throws an unwrapped `FormatException` on invalid Base64 before the byte-array overload is ever reached, nor to a direct call to the public `CryptUnprotectData` extern, which returns the raw native `bool` result with no wrapping at all.

### Other observations

- No logging or auditing of cookie issuance or clearing exists; there is no audit trail of credential-cookie issuance.
- No hard-coded secrets, passwords, or connection strings exist in source; DPAPI keys are OS-managed.
- `IOidcSettings` is injected but never read (verified directly in source); this may imply cookie settings (such as `CookieName`) were intended to be configurable but are not*.

## Notes
- This page documents a credential-transport mechanism, not an authorization system; the empty AD resource catalog, enforcement-point tables, data-level authorization, and SOD sections above are the correct and complete finding for this library, not an omission.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.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. Statements marked with `**` could not be confirmed; treat
> as unknown until verified.

## 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/ILegacyAuthCookieService.cs`
- `src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/LegacyBolAuthMiddleware.cs`
