Skip to content

SAIF.NetCore.LegacyAuth architecture

SAIF.NetCore.LegacyAuth is a narrow bridging library that lets an ASP.NET Core application issue the legacy SAIFAUTH credential cookie — the user's name and password, DPAPI-encrypted with the Windows machine key and client-IP entropy — so that a legacy .NET Framework component referred to in source as "SecurityModule" can authenticate the user on the same host; it is not itself an ASP.NET Core authentication handler, and it never reads or validates a request cookie.

Responsibilities

  • Build the tab-delimited user/password payload and encrypt it with the Windows Data Protection API (DPAPI), scoped to the local machine key and bound to the client's remote IP address as entropy.
  • Write the encrypted value as a raw, non-URL-encoded Set-Cookie: SAIFAUTH=<DPAPI-encrypted-Base64-value> response header so the legacy reader can decrypt it without first URL-decoding.
  • Provide a ClearCookie operation that deletes the SAIFAUTH cookie using framework-default delete options.
  • Expose a small, independently reusable DPAPI encrypt wrapper (DPAPI) over the native crypt32.dll P/Invoke surface, including Win32 interop structs.

Component boundaries

  • ILegacyAuthCookieService / LegacyAuthCookieService: the cookie service — builds the credential payload, encrypts it, and writes or deletes the Set-Cookie header. Depends on IHttpContextAccessor (framework) and IOidcSettings (SAIF.NetCore.ServiceContracts, injected but never read — verified directly in source).
  • DPAPI: a self-contained P/Invoke wrapper over CryptProtectData/CryptUnprotectData with UTF-8 + Base64 string helpers. Its managed Decrypt string/byte overloads are internal and unreachable from any consumer in this repository, but both raw native externs — CryptProtectData and CryptUnprotectData — are declared public static extern, so the raw decryption capability remains publicly reachable even though the convenience wrapper is not.
  • KeyType, DATA_BLOB, CRYPTPROTECT_PROMPTSTRUCT: supporting enum and Win32 interop structs used by DPAPI.
  • Contract boundary: IOidcSettings is declared in SAIF.NetCore.ServiceContracts and implemented by SAIF.NetCore.OIDC; this library only consumes the interface (and does not invoke any member on it).

Interaction model

  • A consuming web application (inferred to be a login application such as "OnlineLogin"**, since no consumer exists in this repository) registers ILegacyAuthCookieService, IHttpContextAccessor, and IOidcSettings itself — this library ships no IServiceCollection registration extension.
  • After the application authenticates a user through its normal login flow (inferred to be OIDC-based, per an excluded source comment referencing "OnlineLogin"), it calls SetCookie(user, password) while the plaintext password is still available, which encrypts the credential pair with DPAPI and appends the SAIFAUTH header to the current response.
  • The legacy "SecurityModule" reader, which lives outside this repository, must run on the same Windows machine as the issuing application (DPAPI machine-key scope) and must supply the same client IP as entropy to decrypt the cookie**.
  • SAIF.NetCore.ServiceExtensions' LegacyBolAuthMiddleware is a related but independent mechanism: it checks only for the presence of a SAIFAUTH/SAIF.BOL.Auth request cookie and redirects if absent, without referencing this package or decrypting the value — the two libraries share only the cookie-name convention.
  • This library makes no API (HTTP/SOAP) calls; its only outbound calls are to the native Windows DPAPI and to ASP.NET Core's own response-header and cookie APIs.

Architecture diagram

The following diagram shows the cookie-issuance flow from a consuming application's login handler through DPAPI encryption to the response header.

flowchart TD
    A["Consuming app:<br>user authenticates"] --> B["LegacyAuthCookieService.SetCookie(user, password)"]
    B --> C["Build payload:<br>user + TAB + password + TAB"]
    C --> D["Read client remote IP<br>as DPAPI entropy"]
    D --> E["DPAPI.Encrypt:<br>MachineKey scope + IP entropy"]
    E --> F["AddCookieToHeader:<br>raw Base64, no URL-encoding"]
    F --> G["Set-Cookie: SAIFAUTH=...<br>Path=/, HttpOnly"]
    G -.-> H["Legacy SecurityModule<br>(same machine, out of repo)"]

Sequence diagram

The following diagram shows the primary cookie-issuance sequence, including the unused IOidcSettings dependency resolved at construction.

sequenceDiagram
    participant App as "Consuming application"
    participant Service as LegacyAuthCookieService
    participant DPAPI
    participant Response as "HttpContext.Response"
    App->>Service: new LegacyAuthCookieService(httpContextAccessor, oidcSettings)
    App->>Service: SetCookie(user, password)
    Service->>Service: Build tab-delimited payload
    Service->>DPAPI: Encrypt(MachineKey, payload, remoteIp)
    DPAPI-->>Service: Base64 ciphertext
    Service->>Response: Append raw Set-Cookie SAIFAUTH header
    Response-->>App: void

Design constraints

  • The DPAPI P/Invoke path is hard-coded to c:\windows\system32\crypt32.dll with no [SupportedOSPlatform("windows")] annotation, even though the package targets cross-platform net8.0;net10.0 — the first encrypt call's native invocation throws DllNotFoundException on non-Windows hosts, but that exception is caught and rewrapped as a generic System.Exception("DPAPI was unable to encrypt data.") with the DllNotFoundException as its inner cause, rather than surfacing directly to the caller*.
  • The issued cookie sets no Secure, SameSite, Domain, or Expires attributes, so it is a host-only session cookie that can travel over plain HTTP and in cross-site requests.
  • IOidcSettings is constructor-injected but never read; callers must still register an implementation or DI construction fails.
  • There is no consumer of this package anywhere in this repository; its only known relationship is the shared SAIFAUTH cookie-name convention with LegacyBolAuthMiddleware in SAIF.NetCore.ServiceExtensions, which checks presence only and does not decrypt the value.

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/ILegacyAuthCookieService.cs
  • 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.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/LegacyBolAuthMiddleware.cs