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.
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 |
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.
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) |
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:
- 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.
- 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**.
- 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**.
- Platform coupling. The hard-coded DLL path and absence of
[SupportedOSPlatform("windows")]mean non-Windows hosts fail at the firstSetCookiecall with a wrappedDllNotFoundException(see BR-12, BR-13). This fails closed: no cookie is issued. - Null-dereference fail paths.
HttpContextorRemoteIpAddressbeingnullthrowsNullReferenceExceptionbefore any cookie is written (see BR-10 — fails closed). - Instance default differs.
new DPAPI().Encrypt(text)uses user scope (BR-07), unlike the cookie path. Consumers usingDPAPIdirectly 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
Decryptinternal does not restrict decryption: any consumer can call the publicDPAPI.CryptUnprotectDataextern directly with a captured cookie and the client IP, and recover the password on the issuing host. Theinternalvisibility onDecryptis 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
CryptUnprotectDatareturn offalse— surfaces as a genericException("DPAPI was unable to decrypt data."); messages do not leak payload content. This wrapping does not apply to the string overload's ownConvert.FromBase64Stringcall, which throws an unwrappedFormatExceptionon invalid Base64 before the byte-array overload is ever reached, nor to a direct call to the publicCryptUnprotectDataextern, which returns the raw nativeboolresult 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.
IOidcSettingsis injected but never read (verified directly in source); this may imply cookie settings (such asCookieName) 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¶
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.cssrc/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/ILegacyAuthCookieService.cssrc/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/LegacyBolAuthMiddleware.cs