Files
jdescopingtool/openspec/specs/web-api-auth/spec.md
T
Joseph Doherty 26ff8d9b4f Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
2026-01-02 07:43:29 -05:00

32 KiB

Web API and Authentication Specification

Purpose

This specification defines the REST API endpoints, SignalR real-time communication hub, and LDAP authentication system for the JDE Scoping Tool. The system provides a single .NET 10 service that exposes REST APIs for Blazor WebAssembly clients to manage searches, perform lookup operations, upload/download files, and receive real-time status updates via SignalR. Authentication is performed against an LDAP directory server with group membership verification, with support for a development-mode bypass.

Source Reference

Legacy Files NEW/ Target Purpose
OLD/WebInterface/Controllers/SearchController.cs NEW/ScopingTool.Api/Controllers/SearchController.cs Search management API - create, view, copy, save searches and download results
OLD/WebInterface/Controllers/AccountController.cs NEW/ScopingTool.Api/Controllers/AuthController.cs User authentication - login, logout, authorization
OLD/WebInterface/Controllers/LookupController.cs NEW/ScopingTool.Api/Controllers/LookupController.cs Autocomplete lookup APIs for items, profit centers, work centers, operators
OLD/WebInterface/Controllers/FileIOController.cs NEW/ScopingTool.Api/Controllers/FileController.cs Excel file upload/download for bulk data import
OLD/WebInterface/Controllers/CrudController.cs NEW/ScopingTool.Api/Controllers/ApiControllerBase.cs Base controller with current user context
OLD/WebInterface/Hubs/StatusHub.cs NEW/ScopingTool.Api/Hubs/StatusHub.cs SignalR hub for real-time status and search updates
OLD/WebInterface/Helpers/LDAPHelper.cs NEW/ScopingTool.Api/Services/LdapAuthService.cs LDAP server authentication and user lookup
OLD/WebInterface/Security/UserIdentity.cs NEW/ScopingTool.Api/Security/UserIdentity.cs Claims-based user identity from LDAP
OLD/WebInterface/Models/LogonRequest.cs NEW/ScopingTool.Api/Models/LoginRequest.cs Login request model
OLD/DataModel/Models/LDAPEntry.cs NEW/ScopingTool.Domain/Models/UserInfo.cs User information model (renamed from LDAPEntry)
OLD/DataModel/Models/StatusUpdate.cs NEW/ScopingTool.Domain/Models/StatusUpdate.cs Process status update model
OLD/DataModel/Models/SearchUpdate.cs NEW/ScopingTool.Domain/Models/SearchUpdate.cs Search status update model

Requirements

Requirement: Authentication Service Interface

The system SHALL provide an abstraction for authentication to support LDAP authentication in production and fake authentication in development mode.

Interface Definition

public interface IAuthService
{
    Task<AuthResult> AuthenticateAsync(string username, string password, CancellationToken ct = default);
    Task<UserInfo?> GetUserInfoAsync(string username, CancellationToken ct = default);
}

public record AuthResult(bool Success, UserInfo? User, string? ErrorMessage);

Implementations

  • LdapAuthService - Production LDAP authentication using System.DirectoryServices.Protocols
  • FakeAuthService - Development mode bypass that accepts any credentials

Business Rules

  • The system SHALL register IAuthService in the DI container based on configuration
  • The system SHALL use LdapAuthService when AuthOptions.UseFakeAuth is false
  • The system SHALL use FakeAuthService when AuthOptions.UseFakeAuth is true (development only)
  • FakeAuthService SHALL return a predefined UserInfo for any username/password combination

Scenario: Production mode uses LDAP authentication

  • WHEN the application starts with AuthOptions.UseFakeAuth = false
  • THEN LdapAuthService is registered as IAuthService and all authentication flows use LDAP

Scenario: Development mode uses fake authentication

  • WHEN the application starts with AuthOptions.UseFakeAuth = true
  • THEN FakeAuthService is registered as IAuthService and any credentials are accepted

Requirement: Configuration Options

The system SHALL use strongly-typed configuration options for LDAP and authentication settings.

LdapOptions

public class LdapOptions
{
    public const string SectionName = "Ldap";

    public string[] ServerUrls { get; set; } = Array.Empty<string>();
    public string GroupDn { get; set; } = string.Empty;
    public string SearchBase { get; set; } = string.Empty;
    public int ConnectionTimeoutSeconds { get; set; } = 30;
}

AuthOptions

public class AuthOptions
{
    public const string SectionName = "Auth";

    public bool UseFakeAuth { get; set; } = false;
    public string CookieName { get; set; } = "ScopingTool.Auth";
    public int CookieExpirationMinutes { get; set; } = 480; // 8 hours
    public string[] AdminBypassUsers { get; set; } = Array.Empty<string>();  // Usernames that bypass group check
    public long MaxUploadSizeBytes { get; set; } = 10 * 1024 * 1024;  // 10MB default
}

LDAP Connection Management

The system SHALL use a connection-per-request pattern for LDAP connections. Each authentication request creates a new LdapConnection, authenticates, and disposes. Connection pooling is NOT used due to LDAP bind credential requirements.

Business Rules

  • The system SHALL bind LdapOptions from the Ldap configuration section
  • The system SHALL bind AuthOptions from the Auth configuration section
  • The system SHALL use IOptions<LdapOptions> and IOptions<AuthOptions> for injection

Requirement: LDAP Authentication

The system SHALL authenticate users against an LDAP directory server and verify group membership before granting access.

Inputs

  • Username (sAMAccountName format)
  • Password (plain text, transmitted over HTTPS)
  • LDAP server URLs (from IOptions<LdapOptions>, supports multiple URLs for failover)
  • LDAP group distinguished name (from IOptions<LdapOptions>)

Outputs

  • AuthResult containing:
    • Success/failure indicator
    • UserInfo on success (see Data Models section)
    • Error message on failure

Business Rules

  • The system SHALL use System.DirectoryServices.Protocols.LdapConnection for cross-platform compatibility
  • The system SHALL attempt authentication against each configured LDAP server URL sequentially until one succeeds
  • The system SHALL verify the user is a member of the configured LDAP group after successful bind
  • The system SHALL use the sAMAccountName LDAP filter format (sAMAccountName={0}) for user lookup
  • The system SHALL extract user properties: distinguishedName, givenName, sn, mail, title
  • The system SHALL compute DisplayName as "{FirstName} {LastName}".Trim() or fall back to Username if both are empty
  • The system SHALL sign out any existing session before creating a new one
  • The system SHALL use non-persistent (session-only) authentication cookies

Scenario: Successful LDAP authentication with group membership

  • WHEN a user submits valid credentials for an LDAP user who is a member of the required group
  • THEN the system authenticates against the LDAP server, verifies group membership, creates a claims-based identity, signs in the user, and returns a success response with user info

Scenario: Failed LDAP authentication with invalid credentials

  • WHEN a user submits invalid credentials
  • THEN the system returns AuthResult { Success = false, ErrorMessage = "Incorrect username or password" }

Scenario: Valid credentials but user not in required group

  • WHEN a user submits valid credentials but is not a member of the required LDAP group
  • THEN the system returns AuthResult { Success = false, ErrorMessage = "User is not a member of the required security group" }

Scenario: LDAP server unavailable with failover

  • WHEN the primary LDAP server is unavailable but a secondary server is configured
  • THEN the system attempts authentication against each configured server in sequence until one succeeds or all fail

Scenario: All LDAP servers unavailable

  • WHEN all configured LDAP servers are unavailable
  • THEN the system returns AuthResult { Success = false, ErrorMessage = "Unable to connect to directory server" }

Requirement: User Session Management

The system SHALL maintain user session state using ASP.NET Core cookie authentication.

Inputs

  • Authenticated user identity (from LDAP or fake auth)
  • Session cookie

Outputs

  • User context available to all authorized controllers via HttpContext.User
  • Session termination on logout

Business Rules

  • The system SHALL use HttpContext.SignInAsync() with CookieAuthenticationDefaults.AuthenticationScheme for sign-in
  • The system SHALL use HttpContext.SignOutAsync() for sign-out
  • The system SHALL store user claims in a ClaimsPrincipal with cookie authentication scheme
  • The system SHALL provide access to current user's UserInfo through HttpContext.User claims
  • The system SHALL clear authentication cookies on logout
  • The system SHALL return HTTP 401 Unauthorized for unauthenticated API requests (no redirect for Blazor WASM)

Scenario: Access protected resource while authenticated

  • WHEN an authenticated user requests a protected resource
  • THEN the system provides the current user context and serves the requested resource

Scenario: Access protected resource while unauthenticated

  • WHEN an unauthenticated user requests a protected resource with [Authorize] attribute
  • THEN the system returns HTTP 401 Unauthorized

Scenario: User logs out

  • WHEN an authenticated user requests logout via POST /api/auth/logout
  • THEN the system calls HttpContext.SignOutAsync(), clears all authentication cookies, and returns HTTP 200 OK

Scenario: Parse user identity from claims

  • WHEN a controller accesses HttpContext.User
  • THEN the system provides access to user claims including Username, FirstName, LastName, Email, and Title

Requirement: Auth API Endpoints

The system SHALL provide REST API endpoints for authentication operations.

Inputs

Endpoint Method Parameters Description
/api/auth/login POST LoginRequest body Authenticate user
/api/auth/logout POST - Sign out current user
/api/auth/me GET - Get current user info

Outputs

  • JSON responses with UserInfo on success
  • HTTP 401 Unauthorized on authentication failure
  • HTTP 200 OK on successful logout

Business Rules

  • The system SHALL use [ApiController] attribute on the controller
  • The system SHALL use attribute routing with /api/auth prefix
  • The system SHALL return UserInfo JSON (not redirect) on successful login for Blazor WASM compatibility
  • The system SHALL call IAuthService.AuthenticateAsync() for login operations

Scenario: Successful login

  • WHEN a user posts valid credentials to /api/auth/login
  • THEN the system authenticates, creates a session, and returns HTTP 200 with UserInfo JSON

Scenario: Failed login

  • WHEN a user posts invalid credentials to /api/auth/login
  • THEN the system returns HTTP 401 Unauthorized with error message

Scenario: Get current user

  • WHEN an authenticated user requests /api/auth/me
  • THEN the system returns HTTP 200 with UserInfo JSON for the current user

Scenario: Get current user when not authenticated

  • WHEN an unauthenticated user requests /api/auth/me
  • THEN the system returns HTTP 401 Unauthorized

Requirement: Search API Endpoints

The system SHALL provide REST API endpoints for search management operations.

Inputs

Endpoint Method Parameters Description
/api/search GET - Get current user's searches
/api/search/queue GET - Get all queued searches
/api/search/{id} GET id (int) Get search by ID
/api/search/{id}/copy POST id (int) Copy search with reset status
/api/search POST SearchViewModel body Create/submit search
/api/search/{id}/results GET id (int) Download search results Excel file

Outputs

  • JSON responses using System.Text.Json serialization
  • File download for results (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
  • Search ID on successful create

Business Rules

  • The system SHALL use [ApiController] attribute on the controller
  • The system SHALL use attribute routing with /api/search prefix
  • The system SHALL require authorization for all search endpoints via [Authorize] attribute
  • The system SHALL filter GetSearches to only return searches owned by the current user
  • The system SHALL order user searches by StartDT descending (most recent first)
  • The system SHALL reset Status, UserName, SubmitDT, StartDT, and EndDT when copying a search
  • The system SHALL publish a SearchUpdate to the SignalR hub when a new search is saved
  • The system SHALL return the new search ID on successful create
  • The system SHALL set filename to "search_results.xlsx" for result downloads
  • The system SHALL inject IHubContext<StatusHub> via DI for SignalR notifications

Scenario: Get user's search history

  • WHEN an authenticated user requests GET /api/search
  • THEN the system returns a JSON array of SearchViewModel objects for searches owned by that user, ordered by most recent first
  • WHEN an authenticated user posts a SearchViewModel to POST /api/search
  • THEN the system converts the view model to a Search entity, submits it to the database, publishes a SignalR notification via IHubContext<StatusHub>, and returns HTTP 201 Created with the new search ID
  • WHEN an authenticated user requests POST /api/search/{id}/copy
  • THEN the system loads the original search, resets the status to New, clears timestamps, sets the current user as owner, saves the copy, and returns HTTP 201 Created with the new search ID

Scenario: Download search results

  • WHEN an authenticated user requests GET /api/search/{id}/results
  • THEN the system retrieves the binary Excel data from the database and returns it as a file download with Content-Type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

Scenario: Download results for search without results

  • WHEN an authenticated user requests results for a search that has not completed
  • THEN the system returns HTTP 404 Not Found

Requirement: Lookup API Endpoints

The system SHALL provide REST API endpoints for autocomplete/lookup operations.

Inputs

Endpoint Method Parameters Description
/api/lookup/items GET q (string) Search items by number
/api/lookup/profit-centers GET q (string) Search profit centers
/api/lookup/work-centers GET q (string) Search work centers
/api/lookup/operators GET q (string) Search operators/users

Outputs

  • JSON arrays of matching entities converted to view models
  • Results ordered alphabetically by primary identifier

Business Rules

  • The system SHALL use [ApiController] attribute on the controller
  • The system SHALL use attribute routing with /api/lookup prefix
  • The system SHALL NOT require authorization for lookup endpoints (public access)
  • The system SHALL order item results by ItemNumber
  • The system SHALL order profit center results by Code
  • The system SHALL order work center results by Code
  • The system SHALL order operator results by FullName

Scenario: Search for items by partial number

  • WHEN a user requests GET /api/lookup/items?q=ABC
  • THEN the system searches for items matching the partial number and returns matching ItemViewModel objects ordered by ItemNumber

Scenario: Search for profit centers

  • WHEN a user requests GET /api/lookup/profit-centers?q=100
  • THEN the system searches for profit centers matching the partial code and returns matching view models ordered by Code

Scenario: Search for work centers

  • WHEN a user requests GET /api/lookup/work-centers?q=MACH
  • THEN the system searches for work centers matching the partial code and returns matching view models ordered by Code

Scenario: Search for operators by name

  • WHEN a user requests GET /api/lookup/operators?q=Smith
  • THEN the system searches for users matching the partial name and returns matching view models ordered by FullName

Requirement: File API Endpoints

The system SHALL provide REST API endpoints for Excel file upload and download operations using ClosedXML.

Inputs

Endpoint Method Parameters Description
/api/file/work-orders/upload POST IFormFile file Upload work orders from Excel
/api/file/work-orders/template POST List<long> body Generate work order template, returns cache key
/api/file/work-orders/template/{key} GET key (Guid) Download cached template
/api/file/part-numbers/upload POST IFormFile file Upload part numbers from Excel
/api/file/part-numbers/template POST List<ItemViewModel> body Generate part number template
/api/file/part-numbers/template/{key} GET key (Guid) Download cached template
/api/file/component-lots/upload POST IFormFile file Upload component lots from Excel
/api/file/component-lots/template POST List<LotViewModel> body Generate component lot template
/api/file/component-lots/template/{key} GET key (Guid) Download cached template
/api/file/part-operations/upload POST IFormFile file Upload part operations from Excel
/api/file/part-operations/template POST List<PartOperationViewModel> body Generate part operations template
/api/file/part-operations/template/{key} GET key (Guid) Download cached template

Outputs

  • FileUploadResult<T> with WasSuccessful, ErrorMessage, and Data properties
  • GUID key for cached file downloads
  • Excel file stream for GET download requests

Business Rules

  • The system SHALL use [ApiController] attribute on the controller
  • The system SHALL use attribute routing with /api/file prefix
  • The system SHALL NOT require authorization for file endpoints (matches legacy behavior)
  • The system SHALL accept file uploads via IFormFile parameter
  • The system SHALL parse Excel files using ClosedXML library, reading from row 2 (skip header)
  • The system SHALL inject IMemoryCache via DI for template caching
  • The system SHALL cache generated templates with 1-minute absolute expiration
  • The system SHALL use GUID keys for cached file retrieval
  • The system SHALL return HTTP 404 if cached file not found or expired
  • The system SHALL remove cached data after successful download
  • The system SHALL deduplicate uploaded data using DistinctBy before returning (except part operations)

Scenario: Upload work orders from Excel

  • WHEN a user uploads an Excel file to POST /api/file/work-orders/upload
  • THEN the system parses work order numbers from column 1 using ClosedXML, looks up matching work orders in the database, deduplicates results, and returns a FileUploadResult with matching WorkOrderViewModel objects

Scenario: Generate and download work order template

  • WHEN a user posts work order numbers to POST /api/file/work-orders/template
  • THEN the system generates an Excel template using ClosedXML, caches it with a GUID key, and returns the key
  • WHEN the user requests GET /api/file/work-orders/template/{guid}
  • THEN the system retrieves the cached file and returns it as "work_order_template.xlsx"

Scenario: Upload component lots with item numbers

  • WHEN a user uploads an Excel file with lot numbers and item numbers to POST /api/file/component-lots/upload
  • THEN the system parses lot numbers from column 1 and item numbers from column 2, looks up matching lots, deduplicates, and returns matching LotViewModel objects

Scenario: Cached file expired

  • WHEN a user requests a download with an expired or invalid cache key
  • THEN the system returns HTTP 404 Not Found

Scenario: No file uploaded

  • WHEN a user posts to an upload endpoint without a file
  • THEN the system returns FileUploadResult { WasSuccessful = false, ErrorMessage = "No file uploaded" }

Requirement: SignalR Real-Time Updates

The system SHALL provide real-time status updates to connected clients via ASP.NET Core SignalR hub.

Inputs

  • Status updates from worker service (via IHubContext<StatusHub>)
  • Search status changes (via IHubContext<StatusHub>)
  • Client requests for cached status (GetCachedStatus hub method)

Outputs

  • Broadcasts to all connected clients via SendAsync("statusUpdate", ...) and SendAsync("searchUpdate", ...)

Business Rules

  • The system SHALL use ASP.NET Core SignalR (Microsoft.AspNetCore.SignalR)
  • The system SHALL maintain a cached StatusUpdate with message and timestamp
  • The system SHALL broadcast status updates to ALL connected clients using Clients.All.SendAsync()
  • The system SHALL inject IHubContext<StatusHub> into controllers and services that need to publish updates
  • The system SHALL NOT use static GlobalHost pattern - use DI exclusively
  • The system SHALL initialize cached status with "Unknown" message and current timestamp
  • The system SHALL map the hub endpoint to /hubs/status

Scenario: Worker service publishes status update

  • WHEN the worker service publishes a StatusUpdate via IHubContext<StatusHub>
  • THEN the system caches the update and broadcasts it to all connected clients via Clients.All.SendAsync("statusUpdate", statusUpdate)

Scenario: New search submitted via controller

  • WHEN the SearchController saves a new search
  • THEN the controller uses injected IHubContext<StatusHub> to call Clients.All.SendAsync("searchUpdate", searchUpdate)

Scenario: Client requests current status

  • WHEN a newly connected client invokes the GetCachedStatus hub method
  • THEN the system returns the most recent cached StatusUpdate (or the default "Unknown" status if none set)

Scenario: Multiple clients receive broadcast

  • WHEN a status update is broadcast
  • THEN all connected SignalR clients receive the update simultaneously via their statusUpdate or searchUpdate event handlers

Requirement: Blazor Client Integration

The system SHALL support Blazor WebAssembly client authentication and real-time updates.

Authentication Flow

  1. Blazor client calls POST /api/auth/login with credentials
  2. Server validates credentials, creates cookie-based session
  3. Server returns UserInfo JSON
  4. Client stores user info in memory and sets authenticated state
  5. Subsequent API calls include auth cookie automatically

SignalR Connection

  1. Blazor client creates HubConnection to /hubs/status
  2. Client registers handlers for statusUpdate and searchUpdate events
  3. Client calls GetCachedStatus() on connection to get initial state
  4. Client receives real-time updates via registered handlers

Business Rules

  • The system SHALL use cookie authentication (not JWT) for browser-based Blazor WASM
  • The system SHALL configure CORS for same-origin only by default; cross-origin support is NOT required for single-domain deployment
  • The system SHALL return JSON responses (not redirects) for all API errors
  • The system SHALL support reconnection for SignalR clients with automatic retry
  • The SignalR hub SHALL remain open without requiring per-request authentication (cookies validated on connection)
  • The Blazor client SHALL handle SPA navigation behaviors (return URL handling, 401 interception for redirect to login)

Requirement: Authorization Patterns

The system SHALL enforce authorization using ASP.NET Core attribute-based access control.

Inputs

  • Authorization attributes on controllers and actions
  • User authentication status from cookie

Outputs

  • Access granted or denied
  • HTTP 401 Unauthorized for unauthenticated API requests
  • HTTP 403 Forbidden for authenticated but unauthorized requests

Business Rules

  • The system SHALL apply [Authorize] at controller level for SearchController
  • The system SHALL apply [AllowAnonymous] for AuthController.Login action
  • The system SHALL NOT require authorization for LookupController or FileController endpoints
  • The system SHALL return HTTP 401 (not redirect) for unauthorized API requests to support Blazor WASM
  • The system SHALL configure cookie authentication to suppress redirect on 401

Scenario: Authorized user accesses protected controller

  • WHEN an authenticated user accesses a controller with [Authorize] attribute
  • THEN the system allows the request to proceed to the action method

Scenario: Anonymous user accesses protected API

  • WHEN an anonymous user accesses a controller with [Authorize] attribute
  • THEN the system returns HTTP 401 Unauthorized (no redirect)

Scenario: User accesses public lookup endpoint

  • WHEN any user (authenticated or not) accesses a LookupController endpoint
  • THEN the system allows the request without authentication check

Data Models

UserInfo (formerly LDAPEntry)

public class UserInfo
{
    public string DN { get; set; } = string.Empty;           // Distinguished name
    public string Username { get; set; } = string.Empty;    // sAMAccountName (lowercase)
    public string FirstName { get; set; } = string.Empty;   // givenName
    public string LastName { get; set; } = string.Empty;    // sn
    public string DisplayName => string.IsNullOrWhiteSpace(FirstName) && string.IsNullOrWhiteSpace(LastName)
        ? Username
        : $"{FirstName} {LastName}".Trim();                 // Computed
    public string Title { get; set; } = string.Empty;       // title
    public string EmailAddress { get; set; } = string.Empty; // mail
}

StatusUpdate

public class StatusUpdate
{
    public string Message { get; set; } = string.Empty;     // Update message to display
    public DateTime Timestamp { get; set; }                  // When message was sent
}

SearchUpdate

public class SearchUpdate
{
    public int ID { get; set; }                             // Search primary key
    public string UserName { get; set; } = string.Empty;    // Username who submitted
    public string Name { get; set; } = string.Empty;        // Search name

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public SearchStatus Status { get; set; }                // Enum: New, Submitted, Started, Ended, Error

    public DateTime? SubmitDT { get; set; }                 // When submitted (required for UI grid)
    public DateTime? StartDT { get; set; }                  // When processing started (required for UI grid)
    public DateTime? EndDT { get; set; }                    // When processing ended (required for UI grid)
    public DateTime Timestamp { get; set; }                 // When update was generated (required for ordering)
}

FileUploadResult

public class FileUploadResult<T>
{
    public bool WasSuccessful { get; set; }
    public string? ErrorMessage { get; set; }
    public T[]? Data { get; set; }
}

LoginRequest

public class LoginRequest
{
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
}

AuthResult

public record AuthResult(bool Success, UserInfo? User, string? ErrorMessage);

Migration Notes

Legacy Pattern New Pattern Rationale
ASP.NET MVC 5 Controllers ASP.NET Core Controllers with [ApiController] Modern framework, automatic model binding and validation
JsonNetResult custom result Results.Ok(data) or return Ok(data) Built-in JSON support with System.Text.Json
Newtonsoft.Json [JsonConverter(typeof(StringEnumConverter))] System.Text.Json [JsonConverter(typeof(JsonStringEnumConverter))] Built-in serialization
Legacy SignalR (Microsoft.AspNet.SignalR) ASP.NET Core SignalR (Microsoft.AspNetCore.SignalR) Built-in, cross-platform
GlobalHost.ConnectionManager.GetHubContext<T>() IHubContext<T> via dependency injection Standard DI pattern, testable
Clients.All.statusUpdate(...) Clients.All.SendAsync("statusUpdate", ...) Async-first API
OWIN Authentication middleware ASP.NET Core Authentication with CookieAuthenticationDefaults Modern authentication stack
HttpContext.GetOwinContext().Authentication.SignIn() HttpContext.SignInAsync() No OWIN abstraction layer
DefaultAuthenticationTypes.ApplicationCookie CookieAuthenticationDefaults.AuthenticationScheme Standard scheme name
System.DirectoryServices.DirectoryEntry System.DirectoryServices.Protocols.LdapConnection Cross-platform LDAP support
WebConfigurationManager.AppSettings IOptions<LdapOptions> / IOptions<AuthOptions> Strongly-typed configuration
MemoryCache.Default IMemoryCache via dependency injection Standard caching abstraction
Request.Files collection IFormFile parameter Modern file upload handling
EPPlus library ClosedXML library MIT license (EPPlus changed to non-commercial)
Route-based MVC URLs (/Search/GetSearches) Attribute routing (/api/search) REST conventions, clear API structure
Login redirect for unauthorized HTTP 401 response Blazor WASM SPA compatibility
NLog for logging ILogger<T> injected + BeginScope() for context Built-in logging abstraction
LDAPEntry model UserInfo model Clearer naming, not tied to LDAP implementation
Hardcoded user exception in code Configurable via AuthOptions.AdminBypassUsers Production-safe configuration; empty array by default

Codex Review Findings (Status)

The following issues were identified during review and their resolution status:

Finding Status Resolution
Missing Account Endpoints Resolved New /api/auth/* endpoints documented
Controller Name Typo (SessionController vs SessionsController) Resolved Legacy session endpoints not needed - Blazor WASM handles client-side routing
InvalidUA Route Resolved Not needed - Blazor WASM handles user agent detection client-side
LDAP DisplayName Fallback Clarified Spec now correctly documents computed DisplayName logic
File Upload Error Handling Documented Per-row parse errors swallowed, specific error messages documented
SignalR Best-Effort Documented SignalR publish is best-effort, exceptions logged but swallowed

Open Questions (Resolved)

Question Decision Rationale
LDAP Failover Strategy Keep simple sequential Simple sequential failover is sufficient for this use case. Health checks and circuit breaker add complexity without significant benefit given the expected load.
Authorization Granularity Group membership only The legacy system only requires group membership. More granular RBAC is not needed for this tool.
SignalR Connection Management Broadcast to all Broadcasting to all clients is acceptable since the status updates are not user-specific and the number of concurrent users is expected to be small.
API Authentication Cookie-based only JWT tokens add complexity without benefit for this internal browser-based tool. Cookie auth is simpler and more secure for browser clients.
File Upload Security Keep anonymous Maintain legacy behavior. File uploads only parse Excel data; they don't access protected resources.
Rate Limiting Not implemented initially Can be added later if needed. Current user base is small and internal.
Hardcoded User Exception Configurable admin bypass Move to AuthOptions.AdminBypassUsers configuration array (empty by default). Allows dev flexibility without hardcoding.