Skip to content

User Flow — Authentication

How a person signs in to the NKO ERP, how the app decides whether a request may proceed, and how the authenticated shell (navigation, culture, notifications) is assembled. Sign-in is handled by AccountController (in the NKO.Shared library) on top of ASP.NET Core Identity; every subsequent request is checked by the ClaimRequirementFilter authorization filter.

Source: NKO.Lib/NKO.Shared/Controllers/AccountController.cs, NKO.Lib/NKO.Shared/Views/Account/Login.cshtml, NKO.Main/NKO.Main/Startup.cs, NKO.Lib/NKO.Shared/Helpers/Sec/ClaimRequirementFilter.cs, NKO.Lib/NKO.Shared/Helpers/Sec/PermissionsHandler.cs, NKO.Lib/NKO.Shared/Controllers/TopbarViewComponent.cs, NKO.Main/NKO.Main/Views/Shared/_Layout.cshtml.

Actors

Actor Role in this flow
Employee Owns an ApplicationUser login linked to an AppUser employee record; signs in with username + password.
ASP.NET Core Identity Validates the password, issues the authentication cookie, enforces lockout and (optional) two-factor.
ClaimRequirementFilter Runs before every [ClaimRequirement] action: redirects anonymous users to login, loads the caller's permissions, blocks callers who lack the required key.
AccountController Renders the login page, processes the sign-in POST, handles forced password change and sign-out.

Preconditions

  • The person has an ApplicationUser record whose UserName is their login, linked to an AppUser by AppUser.IdentityUserId == ApplicationUser.Id.
  • ApplicationUser.IsEnableLogin == true. If it is false, the password is never checked as valid and the account resolves to an empty permission set.
  • The employee belongs to at least one enabled group (AppUserGroupMember.IsEnable) — otherwise they authenticate but can open nothing except the "General" endpoints.

Sign-in flow

sequenceDiagram
    actor U as Employee
    participant MW as Auth middleware
    participant F as ClaimRequirementFilter
    participant AC as AccountController
    participant ID as ASP.NET Core Identity
    participant TA as Target action

    U->>MW: GET /HumanResource without cookie
    MW->>F: authorize
    F-->>U: 302 to /Account/Login with ReturnUrl
    U->>AC: GET /Account/Login
    AC-->>U: Login page
    U->>AC: POST /Account/Login with Username and Password
    AC->>ID: FindByName then CheckPassword
    alt wrong password
        ID-->>AC: invalid
        AC->>ID: send Security Alert email to the account owner
        AC-->>U: generic error message
    else login disabled
        AC-->>U: generic error message
    else password ok and login enabled
        AC->>ID: PasswordSignIn persistent with lockout
        ID-->>U: set Identity auth cookie
        alt two-factor required
            AC-->>U: 302 to /Account/VerifyCode
        else initial password
            AC-->>U: 302 to /Account/Security
        else normal
            AC-->>U: 302 to ReturnUrl
        end
    end
    U->>MW: GET /HumanResource with cookie
    MW->>F: authorize
    F->>ID: load permissions for the user
    F->>F: check the action key is present
    alt has the key
        F->>TA: continue and set CurrentRequestData
        TA-->>U: page
    else missing
        F-->>U: 302 to /Error/UnAuthorised status 403
    end

The sign-in screen (/Account/Login):

NKO ERP sign-in screen

Where an authenticated user lands when they lack the action's permission key (/Error/UnAuthorised):

The "Unauthorized access" page

Step-by-step

# Actor Screen / route Code path Data touched Result
1 Employee any [ClaimRequirement] route while anonymous ClaimRequirementFilter.OnAuthorizationAsync — 302 → ~/Account/Login?ReturnUrl=<path>
2 Employee GET /Account/Login AccountController.Login(returnUrl) — Login.cshtml (Layout = null; AdminLTE/Metronic; "Sign In" form)
3 Employee POST /Account/Login AccountController.Login(LoginModel, returnUrl) — [ValidateAntiForgeryToken] ApplicationUsers, AppUser (read) see branches below
3a Identity — UserManager.CheckPasswordAsync fails — e-mail "Security Alert" to the account owner; generic error message; stay on page
3b Identity — user.IsEnableLogin == false — generic error; no sign-in
3c Identity — SignInManager.PasswordSignInAsync(user, pass, true, true) Identity cookie issued; AppUser tracking row via SetCustomTracking authenticated
4 AccountController — user.IsPasswordDefault == true — 302 → /Account/Security (forced change)
5 AccountController — 2FA required (Is2FAMandatory or TwoFactorEnabled) — 302 → /Account/VerifyCode (or Lockout view when locked)
6 AccountController — normal success — 302 → returnUrl (falls back to / → HomeController.Index)
7 ClaimRequirementFilter first authenticated request PermissionsHandler.GetUserPermissions(User.Identity.Name) ApplicationUsers, AppUser, AppUserGroupMember → AppUserGroup → AppUserGroupPermission → AppPermission (read) populates CurrentRequestData.Employee + .Permissions; 403 → /Error/UnAuthorised if the action's key is missing

Forced password change

ApplicationUser.IsPasswordDefault is set when an account is created with an initial password (see HR Onboarding). While it is true, a successful sign-in redirects to GET /Account/Security and a banner reads "You need to change your initial password, please consider changing your password as soon as possible." POST /Account/Security calls UserManager.ChangePasswordAsync; the new password must be at least 8 characters and match the confirmation field. On success IsPasswordDefault is cleared and the user continues to the app. Security itself is guarded by the AccountChangePassword permission key.

/Account/Security — change password (with the initial-password banner), plus the E-Signature PIN and per-signature security-email settings on the same page:

Change-password form E-Signature PIN and security-email settings

The E-Signature PIN set here is the 4-digit code checked by VerifyPin on the approval actions (e.g. Leave Management).

Two-factor and lockout

  • Two-factor — when ApplicationUser.Is2FAMandatory == true or TwoFactorEnabled == true, the controller uses SignInManager.PasswordSignInAsync and, on RequiresTwoFactor, redirects to GET /Account/VerifyCode?ReturnUrl=… for one-time-code entry.
  • Lockout — configured, but never triggered by a wrong password. Login validates the password with UserManager.CheckPasswordAsync first, and that method does not touch AccessFailedCount. On a wrong password the branch sends the Security Alert mail and returns the view, so PasswordSignInAsync — the only call that increments the failure counter — is never reached. The counter therefore stays at 0 and the account never locks, no matter how many attempts are made. Two further details: the non-2FA path signs in through SignIn(user, pass) with lockoutOnFailure: true, but only after the password has already been confirmed correct; the 2FA path calls PasswordSignInAsync(username, password, false, false) with lockout disabled outright. The Lockout view is reachable only via that 2FA path. Tracked as R6-7.
  • Password policy (Startup.cs → AddIdentity): minimum length 8; digits, uppercase, lowercase and non-alphanumeric characters are not required.

Session, culture, and the navigation shell

  • Scheme — AddAuthentication asks for JWT Bearer as the default scheme (secret JwtConfig:Secret), but AddIdentity runs immediately afterwards and resets the defaults to the Identity application cookie, which is what actually authenticates the browser UI. ConfigureApplicationCookie sets AccessDeniedPath = /Error/UnAuthorised. See Security & Permissions.
  • Immediate effect — the permission set is re-read from the database on every gated request, so disabling a login or changing a user's groups takes effect on their next request. It does not sign them out: a disabled login stays signed in but gets an empty permission set. (SecurityStampValidatorOptions.ValidationInterval = TimeSpan.Zero additionally re-validates the cookie's security stamp on every request.)
  • Per-request context — on every [ClaimRequirement] action the filter stores the caller's AppUser and permission list in the scoped CurrentRequestData; controllers, views and view components read from it rather than re-querying Identity.
  • Culture — UseRequestLocalization supports en (default), ar, tr via query string, then cookie, then Accept-Language. Arabic translates the text but does not switch the layout to right-to-left: _Layout.cshtml computes rtl, but its dir attribute and RTL stylesheet are commented out, and no RTL stylesheet ships — the only live effect is that toast notifications move to the left (R6-9).
  • Shell — authenticated pages render through _Layout.cshtml, a Metronic theme (set in _ViewStart.cshtml; _AppLayout.cshtml is used only by the public /Apps/* portal pages). One view component builds the chrome — the navigation is a top bar, not a sidebar:
  • TopbarViewComponent → the module menus, unread notifications (NotificationUser where !IsRead) and a single "requests waiting on me" badge (ViewBag.RequestCounts) summed across leaves, advances, overtime, timesheets, payroll, trainings, jobs, evaluations, pending e-signatures and unjustified attendance delays. Menu entries (78 HasPermission calls in the top bar alone) and in-page controls are shown or hidden with PermissionChecker.HasPermission("<key>") — see Permissions.

SidebarViewComponent also exists, but nothing invokes it, and Views/Base/SidebarUserPanel.cshtml is referenced by nothing — both are dead code.

The authenticated shell — the top navigation (filtered to the modules this account may open) and the top bar with the mail / notifications icons:

The authenticated navigation shell

Sign-out

AccountController.LogOut calls SignInManager.SignOutAsync() (which clears the Identity cookie) and redirects to /. The next request is anonymous and step 1 applies again.

Edge cases

Situation Behaviour
Unknown username / wrong password Generic message "اسم المستخدم أو كلمة السر خاطئة"; on a known user with a wrong password a "Security Alert" e-mail is sent to that user. The failure counter is not incremented — see Lockout above.
Known login with no linked AppUser row Login dereferences appUser.Firstname to compose the Security Alert before null-checking it, so a wrong password on such an account throws a NullReferenceException — which, per R1-1, renders the developer exception page. Tracked as R6-8.
IsEnableLogin == false Same generic message; never signs in.
Authenticated but missing the action's permission key ForbidResult + HTTP 403 → redirect to /Error/UnAuthorised.
Group membership disabled mid-session Stays signed in; that group's keys are gone on the next request, because permissions are re-read from the database every request. Nothing updates the security stamp, so the cookie itself stays valid.
Employee has no enabled group Signs in, but only "General" endpoints (profile, look-ups, dashboards) resolve; everything else 403s.
Direct hit on a deep URL while anonymous Preserved as ReturnUrl and reopened after a successful sign-in.
// doubled path segments Collapsed to / by a middleware shim before routing.
  • Permissions — groups, permission keys, data scoping, the org hierarchy.
  • HR Onboarding — how the ApplicationUser login and initial password are created.
  • Security & Permissions — the architectural view of the same model.
  • API Overview — authorization as it applies to controller actions.