Auth API#

Legacy password verify + Cube-2.0-owned rehash + JWT issue + login audit.

  • Endpoint: POST /api/v1/auth/login
  • Auth: none (this issues the token)
  • Jira: BFC-13
  • ADRs: 0001 (auth model, amended), 0005 (claim mapping), 0006 (rehash persistence)
  • Legacy source: aspnet_Membership + aspnet_Users + aspnet_Profile + MnxUserAuth (new)

Flow#

sequenceDiagram
    autonumber
    participant Client
    participant AuthCtrl as AuthController
    participant Svc as UserAuthService
    participant Hasher as PasswordHasher
    participant Rec as LoginActivityRecorder
    participant Tok as TokenService
    participant DB as SQL

    Client->>AuthCtrl: POST /api/v1/auth/login {username, password}
    AuthCtrl->>Svc: AuthenticateAsync(ctx)

    Svc->>DB: SELECT MnxUserAuth WHERE username
    alt MnxUserAuth row exists
        Svc->>Hasher: VerifyPbkdf2(password, hash)
    else no MnxUserAuth row
        Svc->>DB: SELECT aspnet_Membership WHERE username
        Svc->>Hasher: VerifyEncrypted(password, storedPw)
        Note over Svc,DB: On success:<br/>INSERT MnxUserAuth (Pbkdf2 hash)
    end

    alt IsLockedOut or IsSuspended (checked AFTER verify)
        Svc->>Rec: Record(LockedOut / Suspended)
        Svc-->>AuthCtrl: LoginOutcome.LockedOut / Suspended
        AuthCtrl-->>Client: 401
    else Verify failed
        Svc->>Rec: Record(BadPassword)
        Svc-->>AuthCtrl: LoginOutcome.BadPassword
        AuthCtrl-->>Client: 401
    else Success
        Svc->>Tok: IssueToken(userId)
        Tok->>DB: Resolve groups, roles, mod_admin, itar<br/>(per ADR 0005)
        Tok-->>Svc: JWT
        Svc->>Rec: Record(Success)
        Svc-->>AuthCtrl: LoginOutcome.Success + JWT
        AuthCtrl-->>Client: 200 { accessToken, claims }
    end

Request#

POST /api/v1/auth/login
Content-Type: application/json

{ "username": "compAdmin", "password": "***" }

Response — success#

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOi...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "claims": {
      "sub": "<user-guid>",
      "unique_name": "compAdmin",
      "role": ["SalesOrder.View", "..."],
      "grp": ["<group-guid>", "..."],
      "mod_admin": ["SGA", "MFG", "..."],
      "itar": true
    }
  }
}

Response — failure#

Always 401 Unauthorized regardless of why — no user enumeration. The reason is recorded in MnxLoginActivity (server-side only) for support triage.

Rules#

  • Order matters: password is verified before the suspended / locked-out check. This prevents timing side-channels that could enumerate usernames.
  • Rehash on login: legacy Encrypted passwords verified against aspnet_Membership trigger a one-time write to MnxUserAuth with a PBKDF2 hash. All subsequent logins use MnxUserAuth. Legacy rows are never modified (per ADR 0004).
  • CompanyAdmin expansion: at token-issue time, CompanyAdmin=true expands to mod_admin=[SGA, MFG, SCM, CRM, ENG, ACCT] so downstream authorization doesn’t need special-case logic.
  • itar claim: sourced from aspnet_Profile.IsItarAuthorized. Data-scope filter, not a role.
  • BFC-31 — machine keys still in appsettings.json (blocked on secrets store)
  • BFC-34 — failed-attempt tracking + auto-lockout (blocked on product)