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}