Skip to content

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.

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.

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 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 LegacyAuthCookieService.cs
Encoding UTF-8 plaintext and entropy; Base64 ciphertext. See BR-09 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.

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) 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
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 Required for legacy reader compatibility; bypasses Response.Cookies.Append encoding.

Clearing: ClearCookie calls Response.Cookies.Delete("SAIFAUTH") with default CookieOptions (BR-14). 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)
Scope KeyType.MachineKey → flags CRYPTPROTECT_UI_FORBIDDEN (1) \| CRYPTPROTECT_LOCAL_MACHINE (4) (see BR-04, BR-11)
Entropy context.Connection.RemoteIpAddress.ToString() (see BR-03)
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, BR-13). 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 — fails closed).
  6. Instance default differs. new DPAPI().Encrypt(text) uses user scope (BR-07), 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.

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