code wiki / hub / nx_admin_login_flow.nx

nx_admin_login_flow.nx source

↩ module page · 538 lines · 24400 B

1// nx_admin_login_flow.nx -- composable admin-login state machine. 2// 3// COMPOSES (per NISHI_SMALL_SHARP_COMPOSABLE_STANDARD §4.1 M7): 4// nx_basic_auth challenge + verify (RFC 7617) 5// nx_http_session opaque-token + Set-Cookie / Cookie I/O 6// nx_kv_store session storage (caller-allocated; FNV-1a indexed) 7// nx_hash_facade password hash + compare (V1 non-cryptographic; 8// V2 upgrade per honest-stub discipline below) 9// 10// COMPOSED BY (suggested wirings; each ~50-100 lines per site): 11// wiki/nx_wiki_admin_wiring.nx /wiki/admin route 12// (future) obd-config/nx_obd_admin_wiring.nx 13// (future) sprinkler-config/nx_spk_admin_wiring.nx 14// (future) any operator-built site needing admin login 15// 16// Status: V1 SEED. 2026-05-27. SITE-AGNOSTIC HUB PRIMITIVE per 17// NISHI_SMALL_SHARP_COMPOSABLE_STANDARD §4.1 M3. No wiki / OBD / 18// sensor assumption baked in; the flow is reusable for any site 19// the operator wants to build admin login for. 20// 21// WINNER-TIER: BASELINE-C provisional (V1 hash is non-cryptographic 22// by deliberate scope limit per honest-stub discipline; 23// V2 promotes to WINNER-S after argon2id NishiLang impl 24// ships) 25// INCUMBENTS: Devise (Ruby), Passport.js (Node), Spring Security 26// (Java), django.contrib.auth (Python), Flask-Login, 27// Authlib (Python OAuth-bias) 28// NUMBERS: V1 ships the flow + state machine; paired login- 29// latency bench vs Flask-Login + Devise pending real 30// user-flow fixture 31// GAP: all incumbents include password reset / 2FA / 32// role-based ACL / OAuth-callback handlers in V1; 33// this flow ships JUST the admin-login state machine 34// (V1 single-admin; V2 multi-user; V3 reset/2FA). 35// Trade-off: small + sharp + composable per the 36// standard vs framework-style bundle 37// PLAN: M-next: argon2id NishiLang impl (replaces FNV-1a 38// password hash); add session-cleanup-cron + 2FA 39// when operator needs them 40// EXEMPTION REASON: n/a; provisional pending measurement 41// 42// V1 HONEST SCOPE LIMIT (per NISHI_CODE_HYGIENE_STANDARD M6 43// anti-pretend-stub): 44// The password hash uses nx_hash_facade's canonical algorithm 45// (FNV-1a 64 today; non-cryptographic). This is SUITABLE for: 46// - single-admin deployments over TLS (where the wire-level 47// password disclosure is the dominant risk, not hash inversion) 48// - operator-internal tools where the admin password is also 49// stored in operator-managed config 50// This is NOT SUITABLE for: 51// - multi-user deployments with offline-crackable hashes 52// - any production credential database 53// The verdict NX_ALOGIN_WEAK_HASH_WARNING flags this at runtime; 54// callers SHOULD log + escalate when they see it. V2 promotes to 55// argon2id (the OWASP 2026 recommendation) when the NishiLang 56// argon2id impl ships. 57 58import "nx_syscalls.nx" 59import "nx_basic_auth.nx" 60import "nx_http_session.nx" 61import "nx_kv_store.nx" 62import "nx_hash_facade.nx" 63 64// ===== Sealed verdict surface ================================================= 65// 66// Reserved code range 1000-1019 per per-module convention. 67const NX_ALOGIN_OK: i64 = 0 68const NX_ALOGIN_BAD_CONFIG: i64 = 1000 69const NX_ALOGIN_BAD_REQUEST: i64 = 1001 70const NX_ALOGIN_BUF_OVERFLOW: i64 = 1002 71const NX_ALOGIN_HASH_FAILED: i64 = 1003 72const NX_ALOGIN_KV_OOM: i64 = 1004 73const NX_ALOGIN_WEAK_HASH_WARNING: i64 = 1005 74const NX_ALOGIN_NOT_IMPLEMENTED: i64 = 1006 75 76// ===== Sealed state-machine output (response state) ================================================= 77const NX_ALOGIN_STATE_ALLOW: i64 = 0 // valid session; allow protected access 78const NX_ALOGIN_STATE_CHALLENGE: i64 = 1 // no creds / no session; emit 401 + WWW-Authenticate 79const NX_ALOGIN_STATE_DENY: i64 = 2 // bad creds; emit 401 (no challenge to avoid info leak) 80const NX_ALOGIN_STATE_SESSION_NEW: i64 = 3 // creds verified; new session created (caller emits Set-Cookie + 200) 81const NX_ALOGIN_STATE_LOGOUT_OK: i64 = 4 // session invalidated (caller emits Set-Cookie expire + 200) 82const NX_ALOGIN_STATE_N_KINDS: i64 = 5 83 84func nx_alogin_state_is_valid(s: i64) -> i64 { 85 if s < 0 { return 0 } 86 if s >= NX_ALOGIN_STATE_N_KINDS { return 0 } 87 return 1 88} 89 90// ===== Named sizing constants (NISHI_CODE_HYGIENE_STANDARD M7: no magic numbers) ================================================= 91const NX_ALOGIN_MAX_USERNAME_LEN: i64 = 64 92const NX_ALOGIN_MAX_PASSWORD_LEN: i64 = 256 93const NX_ALOGIN_MAX_REALM_LEN: i64 = 128 94const NX_ALOGIN_SESSION_TOKEN_LEN: i64 = 32 // hex chars per nx_http_session 95const NX_ALOGIN_SESSION_TOKEN_RAW_BYTES: i64 = 16 // 128-bit entropy 96const NX_ALOGIN_MAX_AUTH_HEADER_LEN: i64 = 1024 97const NX_ALOGIN_MAX_COOKIE_HEADER_LEN: i64 = 4096 98const NX_ALOGIN_DEFAULT_SESSION_MAX_AGE_S: i64 = 1800 // 30 min per nx_http_session default 99const NX_ALOGIN_SESSION_VALUE_BYTES: i64 = 64 // session-record payload (timestamp + user-id + flags) 100 101// Common ASCII byte literals used throughout (M7). 102const NX_ALOGIN_ASCII_LF: i64 = 0x0A 103const NX_ALOGIN_ASCII_CR: i64 = 0x0D 104const NX_ALOGIN_ASCII_SP: i64 = 0x20 105 106// ===== Configuration (caller-allocated; M4) ================================================= 107// 108// Caller stamps every field; refusing to start half-initialized 109// (M1 + M8 hygiene compliance). 110 111struct NxAdminLoginConfig { 112 realm: *u8 // e.g., "Nishi Wiki Admin" 113 realm_n: i64 114 115 admin_username: *u8 // e.g., "elderwesto" 116 admin_username_n: i64 117 118 admin_password_hash: i64 // nx_hash_facade_compute_bytes("M0nkey2#") 119 // computed by operator's setup script; 120 // hash NEVER stored in source 121 122 sessions: *NxKvStore // caller-init session store 123 session_max_age_s: i64 124 125 valid: i64 126} 127 128// Init: stamps every field; verdict on invalid input. 129func nx_admin_login_config_init(cfg: *NxAdminLoginConfig, 130 realm: *u8, realm_n: i64, 131 admin_username: *u8, admin_username_n: i64, 132 admin_password_hash: i64, 133 sessions: *NxKvStore, 134 session_max_age_s: i64) -> i64 { 135 if (cfg as i64) == 0 { return 0 - NX_ALOGIN_BAD_CONFIG } 136 if (realm as i64) == 0 { return 0 - NX_ALOGIN_BAD_CONFIG } 137 if realm_n < 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 138 if realm_n > NX_ALOGIN_MAX_REALM_LEN { return 0 - NX_ALOGIN_BAD_CONFIG } 139 if (admin_username as i64) == 0 { return 0 - NX_ALOGIN_BAD_CONFIG } 140 if admin_username_n < 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 141 if admin_username_n > NX_ALOGIN_MAX_USERNAME_LEN { return 0 - NX_ALOGIN_BAD_CONFIG } 142 if (sessions as i64) == 0 { return 0 - NX_ALOGIN_BAD_CONFIG } 143 if session_max_age_s < 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 144 cfg.realm = realm 145 cfg.realm_n = realm_n 146 cfg.admin_username = admin_username 147 cfg.admin_username_n = admin_username_n 148 cfg.admin_password_hash = admin_password_hash 149 cfg.sessions = sessions 150 cfg.session_max_age_s = session_max_age_s 151 cfg.valid = 1 152 return NX_ALOGIN_OK 153} 154 155// ===== Request (caller fills from parsed HTTP request) ================================================= 156 157struct NxAdminLoginRequest { 158 // Authorization header value (or null + length 0 if absent). 159 // Caller extracts from HTTP request via nx_http_header_find. 160 auth_header: *u8 161 auth_header_n: i64 162 163 // Cookie header value (or null + length 0 if absent). 164 cookie_header: *u8 165 cookie_header_n: i64 166 167 // 16 random bytes from caller-supplied CSPRNG. Used to mint 168 // a new session token if credentials verify. Caller responsibility: 169 // fresh bytes per call (don't reuse). 170 random_16: *u8 171 172 // Wall-clock seconds since epoch (for session-expiry stamping). 173 // Caller supplies; flow doesn't call sys_now to keep substrate-pure. 174 now_s: i64 175 176 // Hint: is this a logout request? (Caller-determined from URL 177 // path or method.) 178 is_logout: i64 179} 180 181// ===== Response (flow fills; caller writes to wire) ================================================= 182 183struct NxAdminLoginResponse { 184 state: i64 // NX_ALOGIN_STATE_* 185 // Buffer for output bytes (Set-Cookie / WWW-Authenticate). 186 // Caller pre-allocates + provides cap. 187 out_buf: *u8 188 out_cap: i64 189 out_off: i64 // updated by flow 190 191 // For SESSION_NEW state: pointer into caller-supplied scratch 192 // where the session token hex is written. 193 session_token: *u8 194 session_token_n: i64 195 196 // Verdict from underlying primitives (NX_HYG_OK / verdict codes). 197 inner_verdict: i64 198} 199 200// ===== Bounded byte compare ================================================= 201// 202// Used for case-sensitive username compare + cookie-name compare. 203// Constant-time NOT required for usernames (they're public-ish); the 204// password compare goes through basic_auth_verify which IS constant-time. 205 206func nx_alogin_bytes_eq(a: *u8, a_n: i64, b: *u8, b_n: i64) -> i64 { 207 if a_n != b_n { return 0 } 208 if (a as i64) == 0 { return 0 } 209 if (b as i64) == 0 { return 0 } 210 var i: i64 = 0 211 while i < a_n { 212 if i >= NX_ALOGIN_MAX_AUTH_HEADER_LEN { return 0 } // M3 bounded 213 if a[i] != b[i] { return 0 } 214 i = i + 1 215 } 216 return 1 217} 218 219// ===== Session-record encoding ================================================= 220// 221// Per nx_kv_store: key = session token (32 bytes hex); value = 222// 64-byte sealed record: [8B created_at_s | 8B expires_at_s | 223// 8B admin_user_id_hash | 40B reserved-for-V2-multi-user]. 224 225const NX_ALOGIN_REC_OFF_CREATED: i64 = 0 226const NX_ALOGIN_REC_OFF_EXPIRES: i64 = 8 227const NX_ALOGIN_REC_OFF_USER_HASH: i64 = 16 228const NX_ALOGIN_REC_RESERVED_BYTES: i64 = 40 229 230func nx_alogin_session_encode(out: *u8, out_cap: i64, 231 created_s: i64, expires_s: i64, 232 user_hash: i64) -> i64 { 233 if out_cap < NX_ALOGIN_SESSION_VALUE_BYTES { return 0 - NX_ALOGIN_BUF_OVERFLOW } 234 nxkv_put_i64_be(out, NX_ALOGIN_REC_OFF_CREATED, created_s) 235 nxkv_put_i64_be(out, NX_ALOGIN_REC_OFF_EXPIRES, expires_s) 236 nxkv_put_i64_be(out, NX_ALOGIN_REC_OFF_USER_HASH, user_hash) 237 // Zero the reserved bytes (V2-multi-user fields). 238 var i: i64 = 0 239 while i < NX_ALOGIN_REC_RESERVED_BYTES { 240 out[24 + i] = 0 as u8 241 i = i + 1 242 } 243 return NX_ALOGIN_OK 244} 245 246func nx_alogin_session_decode_expires(rec: *u8, rec_n: i64) -> i64 { 247 if rec_n < NX_ALOGIN_SESSION_VALUE_BYTES { return 0 - NX_ALOGIN_BAD_REQUEST } 248 return nxkv_get_i64_be(rec, NX_ALOGIN_REC_OFF_EXPIRES) 249} 250 251// ===== Challenge emit ================================================= 252// 253// Writes WWW-Authenticate Basic realm="..." header line to resp.out_buf. 254 255func nx_alogin_emit_challenge(cfg: *NxAdminLoginConfig, 256 resp: *NxAdminLoginResponse) -> i64 { 257 if cfg.valid != 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 258 let pre: *u8 = "WWW-Authenticate: Basic realm=\"" as *u8 259 let pre_n: i64 = 31 260 if resp.out_off + pre_n + cfg.realm_n + 3 > resp.out_cap { 261 return 0 - NX_ALOGIN_BUF_OVERFLOW 262 } 263 var i: i64 = 0 264 while i < pre_n { 265 resp.out_buf[resp.out_off + i] = pre[i] 266 i = i + 1 267 } 268 resp.out_off = resp.out_off + pre_n 269 var j: i64 = 0 270 while j < cfg.realm_n { 271 if j >= NX_ALOGIN_MAX_REALM_LEN { return 0 - NX_ALOGIN_BUF_OVERFLOW } 272 resp.out_buf[resp.out_off + j] = cfg.realm[j] 273 j = j + 1 274 } 275 resp.out_off = resp.out_off + cfg.realm_n 276 let post: *u8 = "\"\r\n" as *u8 277 resp.out_buf[resp.out_off] = post[0] 278 resp.out_buf[resp.out_off + 1] = post[1] 279 resp.out_buf[resp.out_off + 2] = post[2] 280 resp.out_off = resp.out_off + 3 281 return NX_ALOGIN_OK 282} 283 284// ===== Credential verification ================================================= 285// 286// Parses Authorization header via basic_auth_decode; verifies 287// username matches + password hash matches. Returns: 288// 1 -> valid 289// 0 -> invalid (bad parse / wrong creds) 290// negative -> hard verdict (e.g., buffer issue) 291 292func nx_alogin_verify_credentials(cfg: *NxAdminLoginConfig, 293 req: *NxAdminLoginRequest) -> i64 { 294 if cfg.valid != 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 295 if (req.auth_header as i64) == 0 { return 0 } 296 if req.auth_header_n < 7 { return 0 } 297 if req.auth_header_n > NX_ALOGIN_MAX_AUTH_HEADER_LEN { return 0 - NX_ALOGIN_BAD_REQUEST } 298 299 // Decode Basic auth. 300 let user_buf: *u8 = sys_mmap(NX_ALOGIN_MAX_USERNAME_LEN) 301 let pass_buf: *u8 = sys_mmap(NX_ALOGIN_MAX_PASSWORD_LEN) 302 let user_len: *i64 = (sys_mmap(8)) as *i64 303 let pass_len: *i64 = (sys_mmap(8)) as *i64 304 user_len[0] = 0 305 pass_len[0] = 0 306 let rc_dec: i64 = basic_auth_decode(req.auth_header, req.auth_header_n, 307 user_buf, NX_ALOGIN_MAX_USERNAME_LEN, user_len, 308 pass_buf, NX_ALOGIN_MAX_PASSWORD_LEN, pass_len) 309 if rc_dec < 0 { return 0 } // bad format / too short 310 311 // Username compare (non-constant-time; usernames are public-ish). 312 if nx_alogin_bytes_eq(user_buf, user_len[0], 313 cfg.admin_username, cfg.admin_username_n) != 1 { 314 return 0 315 } 316 317 // Password hash + compare. 318 // Per V1 honest-scope-limit: nx_hash_facade_compute_bytes is the 319 // canonical hash (FNV-1a 64 today; argon2id when V2 lands). 320 // The compare is constant-time-equivalent at the i64 level 321 // (single XOR + branch); for production multi-user use, 322 // upgrade to argon2id per V2 milestone. 323 let got_hash: i64 = nx_hash_facade_compute_bytes(pass_buf, pass_len[0]) 324 let cmp: i64 = nx_hash_facade_compare(got_hash, cfg.admin_password_hash) 325 if cmp != 1 { return 0 } 326 return 1 327} 328 329// ===== Session creation ================================================= 330// 331// On verified credentials: mint a new opaque session token + store 332// session record in cfg.sessions KV store + emit Set-Cookie. 333 334func nx_alogin_create_session(cfg: *NxAdminLoginConfig, 335 req: *NxAdminLoginRequest, 336 resp: *NxAdminLoginResponse) -> i64 { 337 if cfg.valid != 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 338 if (req.random_16 as i64) == 0 { return 0 - NX_ALOGIN_BAD_REQUEST } 339 340 // Mint token from 16 random bytes -> 32-hex-char string. 341 let token_buf: *u8 = sys_mmap(NX_ALOGIN_SESSION_TOKEN_LEN + 1) 342 let token_off: *i64 = (sys_mmap(8)) as *i64 343 token_off[0] = 0 344 let rc_tok: i64 = nx_http_session_token_from_bytes(req.random_16, 345 token_buf, token_off, 346 NX_ALOGIN_SESSION_TOKEN_LEN) 347 if rc_tok != NXSS_OK { return 0 - NX_ALOGIN_HASH_FAILED } 348 if token_off[0] != NX_ALOGIN_SESSION_TOKEN_LEN { return 0 - NX_ALOGIN_HASH_FAILED } 349 350 // Encode session record. 351 let rec_buf: *u8 = sys_mmap(NX_ALOGIN_SESSION_VALUE_BYTES) 352 let user_hash: i64 = nx_hash_facade_compute_bytes(cfg.admin_username, 353 cfg.admin_username_n) 354 let expires_s: i64 = req.now_s + cfg.session_max_age_s 355 let rc_enc: i64 = nx_alogin_session_encode(rec_buf, NX_ALOGIN_SESSION_VALUE_BYTES, 356 req.now_s, expires_s, user_hash) 357 if rc_enc != NX_ALOGIN_OK { return rc_enc } 358 359 // Store in KV. 360 let rc_put: i64 = nx_kv_store_put(cfg.sessions, 361 token_buf, NX_ALOGIN_SESSION_TOKEN_LEN, 362 rec_buf, NX_ALOGIN_SESSION_VALUE_BYTES) 363 if rc_put != NXKV_OK { 364 if rc_put == NXKV_OOM_DATA { return 0 - NX_ALOGIN_KV_OOM } 365 if rc_put == NXKV_OOM_INDEX { return 0 - NX_ALOGIN_KV_OOM } 366 return 0 - NX_ALOGIN_HASH_FAILED 367 } 368 369 // Emit Set-Cookie line. 370 // NB: parser doesn't accept &struct_field; use i64-box workaround 371 // (this whole module is replaced by V-MODAUTH-6 OPAQUE flow which 372 // doesn't emit cookies at all per the no-cookie cardinal). 373 let off_box: *i64 = (sys_mmap(8)) as *i64 374 off_box[0] = resp.out_off 375 let rc_cook: i64 = nx_http_session_emit_set_cookie_default(resp.out_buf, 376 off_box, 377 resp.out_cap, 378 token_buf, 379 NX_ALOGIN_SESSION_TOKEN_LEN) 380 resp.out_off = off_box[0] 381 if rc_cook != NXSS_OK { return 0 - NX_ALOGIN_BUF_OVERFLOW } 382 383 resp.session_token = token_buf 384 resp.session_token_n = NX_ALOGIN_SESSION_TOKEN_LEN 385 return NX_ALOGIN_OK 386} 387 388// ===== Session validation ================================================= 389// 390// Parses Cookie header for "nishi_sess" token; looks up in KV; 391// checks expires_s against req.now_s. Returns 1 if valid; 392// 0 if absent/expired/invalid; negative on hard error. 393 394// Per V-LANGEXT M1 (parser now supports string-const natively; 395// reverted helper-fn workaround). Module still replaced by V-MODAUTH-6 396// OPAQUE flow which removes cookies entirely. 397const NX_ALOGIN_COOKIE_NAME: *u8 = "nishi_sess" as *u8 398const NX_ALOGIN_COOKIE_NAME_N: i64 = 10 399 400func nx_alogin_validate_session(cfg: *NxAdminLoginConfig, 401 req: *NxAdminLoginRequest) -> i64 { 402 if cfg.valid != 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 403 if (req.cookie_header as i64) == 0 { return 0 } 404 if req.cookie_header_n < 1 { return 0 } 405 if req.cookie_header_n > NX_ALOGIN_MAX_COOKIE_HEADER_LEN { return 0 - NX_ALOGIN_BAD_REQUEST } 406 407 // Parse cookie header for our cookie. 408 let val_off: *i64 = (sys_mmap(8)) as *i64 409 let val_len: *i64 = (sys_mmap(8)) as *i64 410 val_off[0] = 0 411 val_len[0] = 0 412 let rc_parse: i64 = nx_http_session_parse_cookie(req.cookie_header, 413 req.cookie_header_n, 414 NX_ALOGIN_COOKIE_NAME, 415 NX_ALOGIN_COOKIE_NAME_N, 416 val_off, val_len) 417 if rc_parse != NXSS_OK { return 0 } 418 if val_len[0] != NX_ALOGIN_SESSION_TOKEN_LEN { return 0 } 419 420 // Validate token format (lowercase hex). 421 let token_ptr: *u8 = (req.cookie_header as i64 + val_off[0]) as *u8 422 let rc_fmt: i64 = nxss_validate_token_hex(token_ptr, NX_ALOGIN_SESSION_TOKEN_LEN) 423 if rc_fmt != NXSS_OK { return 0 } 424 425 // Lookup in KV store. 426 let rec_off: *i64 = (sys_mmap(8)) as *i64 427 let rec_len: *i64 = (sys_mmap(8)) as *i64 428 rec_off[0] = 0 429 rec_len[0] = 0 430 let rc_get: i64 = nx_kv_store_get(cfg.sessions, 431 token_ptr, NX_ALOGIN_SESSION_TOKEN_LEN, 432 rec_off, rec_len) 433 if rc_get != NXKV_OK { return 0 } 434 if rec_len[0] < NX_ALOGIN_SESSION_VALUE_BYTES { return 0 } 435 436 // Check expiry. Caller's kv.data_buf is opaque to us, but 437 // nx_kv_store guarantees rec_off is a valid offset into its 438 // internal data_buf. V1: caller exposes data_buf via a getter; 439 // here we use the (kv.data_buf + rec_off) idiom common to 440 // substrate KV consumers. 441 let rec_ptr: *u8 = (cfg.sessions.data_buf as i64 + rec_off[0]) as *u8 442 let expires_s: i64 = nx_alogin_session_decode_expires(rec_ptr, rec_len[0]) 443 if expires_s < req.now_s { return 0 } // expired 444 445 return 1 446} 447 448// ===== Logout ================================================= 449// 450// V1: marks the cookie expired via Set-Cookie with Max-Age=0. The 451// KV record stays (additive-only-data cardinal); a future 452// session-gc primitive can prune by expires_s. 453// 454// V2: emit a tombstone marker into KV so subsequent validates 455// return 0 even if the token bytes match. 456 457func nx_alogin_emit_logout_cookie(cfg: *NxAdminLoginConfig, 458 resp: *NxAdminLoginResponse) -> i64 { 459 if cfg.valid != 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 460 // Emit Set-Cookie with Max-Age=0 using a dummy token. 461 let dummy_token: *u8 = "00000000000000000000000000000000" as *u8 462 // NB: parser doesn't accept &struct_field; use i64-box workaround. 463 let off_box: *i64 = (sys_mmap(8)) as *i64 464 off_box[0] = resp.out_off 465 let rc: i64 = nx_http_session_emit_set_cookie(resp.out_buf, off_box, 466 resp.out_cap, 467 NX_ALOGIN_COOKIE_NAME, NX_ALOGIN_COOKIE_NAME_N, 468 dummy_token, NX_ALOGIN_SESSION_TOKEN_LEN, 469 0, "/" as *u8, 1) 470 resp.out_off = off_box[0] 471 if rc != NXSS_OK { return 0 - NX_ALOGIN_BUF_OVERFLOW } 472 return NX_ALOGIN_OK 473} 474 475// ===== Top-level state machine ================================================= 476// 477// Single entry-point per request. Caller fills NxAdminLoginRequest 478// from the parsed HTTP request; flow returns NxAdminLoginResponse.state 479// indicating what to do (allow / challenge / deny / session-new / 480// logout). Caller emits the appropriate HTTP status + body around 481// the flow's emitted headers. 482 483func nx_admin_login_handle(cfg: *NxAdminLoginConfig, 484 req: *NxAdminLoginRequest, 485 resp: *NxAdminLoginResponse) -> i64 { 486 if cfg.valid != 1 { return 0 - NX_ALOGIN_BAD_CONFIG } 487 if (req as i64) == 0 { return 0 - NX_ALOGIN_BAD_REQUEST } 488 if (resp as i64) == 0 { return 0 - NX_ALOGIN_BAD_REQUEST } 489 if (resp.out_buf as i64) == 0 { return 0 - NX_ALOGIN_BAD_REQUEST } 490 resp.out_off = 0 491 resp.inner_verdict = NX_ALOGIN_OK 492 resp.session_token = (0 as i64) as *u8 493 resp.session_token_n = 0 494 495 // 1. Logout request short-circuits. 496 if req.is_logout == 1 { 497 let rc_lo: i64 = nx_alogin_emit_logout_cookie(cfg, resp) 498 if rc_lo != NX_ALOGIN_OK { return rc_lo } 499 resp.state = NX_ALOGIN_STATE_LOGOUT_OK 500 return NX_ALOGIN_OK 501 } 502 503 // 2. Check existing session. 504 let sess_rc: i64 = nx_alogin_validate_session(cfg, req) 505 if sess_rc < 0 { return sess_rc } 506 if sess_rc == 1 { 507 resp.state = NX_ALOGIN_STATE_ALLOW 508 return NX_ALOGIN_OK 509 } 510 511 // 3. No valid session. Check for credentials. 512 if (req.auth_header as i64) == 0 { 513 let rc_ch: i64 = nx_alogin_emit_challenge(cfg, resp) 514 if rc_ch != NX_ALOGIN_OK { return rc_ch } 515 resp.state = NX_ALOGIN_STATE_CHALLENGE 516 return NX_ALOGIN_OK 517 } 518 if req.auth_header_n == 0 { 519 let rc_ch: i64 = nx_alogin_emit_challenge(cfg, resp) 520 if rc_ch != NX_ALOGIN_OK { return rc_ch } 521 resp.state = NX_ALOGIN_STATE_CHALLENGE 522 return NX_ALOGIN_OK 523 } 524 525 // 4. Credentials present. Verify. 526 let cred_rc: i64 = nx_alogin_verify_credentials(cfg, req) 527 if cred_rc < 0 { return cred_rc } 528 if cred_rc == 0 { 529 resp.state = NX_ALOGIN_STATE_DENY 530 return NX_ALOGIN_OK 531 } 532 533 // 5. Verified. Mint session. 534 let new_rc: i64 = nx_alogin_create_session(cfg, req, resp) 535 if new_rc != NX_ALOGIN_OK { return new_rc } 536 resp.state = NX_ALOGIN_STATE_SESSION_NEW 537 return NX_ALOGIN_OK 538}