Project

General

Profile

proposal-major-functional-1.txt

Sergey Ivanovskiy, 08/14/2026 05:13 AM

Download (9.41 KB)

 
1
================================================================================
2
PROPOSAL major-functional-1
3
SSO re-auth expiry tick destroys the server-driven logout redirect
4
================================================================================
5

    
6
Branch      : 11645a (task #11645), trunk base r16695
7
Files        : src/com/goldencode/p2j/ui/client/driver/web/res/p2j.sso_reauth.js
8
               src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js
9
Severity     : MAJOR (functional) - confirmed in the challenge pass
10
Category     : regression introduced by this branch
11

    
12

    
13
1. PROBLEM
14
--------------------------------------------------------------------------------
15
Nothing cancels the re-auth countdown when the session is ended by the server's
16
own force-logout. The expiry tick therefore fires *into* the shutdown handshake
17
and clobbers the logout redirect, leaving the user on a dead iframe instead of
18
the login page.
19

    
20
Before this branch the expiry path called cleanupReauthUi(true) and only replaced
21
the document body, leaving the socket module intact to complete the MSG_QUIT
22
redirect. The branch replaced that with beginInternalReload() +
23
window.location.reload(), which unloads the document mid-handshake.
24

    
25

    
26
2. EVIDENCE / TRIGGER
27
--------------------------------------------------------------------------------
28
The trigger is the ordinary "user ignores the overlay" case:
29

    
30
  a) SsoTokenManager.handleInvalidToken schedules the force-logout BEFORE it
31
     tells the client anything:
32

    
33
       SsoTokenManager.java:518   scheduler.schedule(() -> forceLogout(...),
34
                                     reauthTimeoutSec, SECONDS)
35
       SsoTokenManager.java:539   client.sendSsoReauth(reauthUrl, reauthTimeoutSec)
36

    
37
     The module sets reauthDeadline = Date.now() + timeoutSec * 1000 only on
38
     receipt, so the client deadline is always the server deadline plus delivery
39
     latency. The server therefore always wins, and the client tick always fires
40
     inside the shutdown window. reauthInFlight() is false when the user ignored
41
     the overlay, so the REAUTH_GRACE_MS branch does not apply.
42

    
43
  b) Nothing cancels countdownInterval on that path. cleanupReauthUi is the only
44
     thing that clears it, and it is reachable solely from this module's own
45
     success / failure listeners and btnLogout.onclick. p2j.socket.js touches the
46
     module only at :6037-6039 (handleReauth). There is no cleanup hook on
47
     MSG_SHUTTINGDOWN, MSG_QUIT or doRedirectToLogoutPage.
48

    
49
  c) The reload breaks the handshake two ways:
50

    
51
     - The browser's MSG_SHUTTINGDOWN ack is sent only from the .finally of
52
       requestDeleteAuthCookie (p2j.socket.js:3331-3338). The navigation discards
53
       it, so WebClientProtocol.quit()'s waitForResult burns the full
54
       clientResponseTimeout (default 5000 ms) and MSG_QUIT goes nowhere.
55
     - Even if MSG_QUIT arrived first, doRedirectToLogoutPage's
56
       window.top.location.replace(logoutPage) sits inside a fetch().then(); the
57
       reload aborts that pending top-level navigation. web_landing.html's
58
       'CLOSE' handler only resets state and never navigates.
59

    
60
  d) The reloaded iframe cannot reach the login page either. web_landing.html:123
61
     points it at the spawned client's own embedded web server, which
62
     shutdownServer() has just torn down, and beginInternalReload() deliberately
63
     suppresses exitTheApplication so isRequiredToRedirect() returns false and the
64
     reloaded page would not redirect either.
65

    
66
Net effect: browser network-error page inside a dead iframe, ~5 s of needless
67
server-side blocking, no logout redirect.
68

    
69

    
70
3. ROOT CAUSE
71
--------------------------------------------------------------------------------
72
Two independent expiry mechanisms - the server's forceLogout and the client's
73
countdown tick - race with no coordination, and the client one is now destructive
74
(it navigates) where before it was inert (it swapped the body). The server is the
75
authoritative one; the client tick must stand down when the server acts.
76

    
77

    
78
4. PROPOSED FIX
79
--------------------------------------------------------------------------------
80
Two parts. Part A is the fix; Part B is defence in depth for orderings A cannot
81
observe.
82

    
83
PART A - give the shutdown path an explicit way to cancel the re-auth UI.
84

    
85
  A1. p2j.sso_reauth.js: export a cancellation entry point alongside the existing
86
      handleSsoReauth export. It must be synchronous and idempotent, because it
87
      is called from the shutdown path which cannot await:
88

    
89
        /**
90
         * Stand down the re-auth overlay because the session is ending by the
91
         * server's decision. Clears every timer, closes the popup and drops the
92
         * overlay without triggering the expiry reload. Idempotent.
93
         */
94
        me.abortReauth = function()
95
        {
96
           reloadRequested = true;          // makes the tick a no-op if it races
97

    
98
           if (countdownInterval)
99
           {
100
              clearInterval(countdownInterval);
101
              countdownInterval = null;
102
           }
103
           if (popupCloseCheckInterval)
104
           {
105
              clearInterval(popupCloseCheckInterval);
106
              popupCloseCheckInterval = null;
107
           }
108
           closeReauthPopup();
109

    
110
           if (reauthMessageListener)
111
           {
112
              window.removeEventListener('message', reauthMessageListener);
113
              reauthMessageListener = null;
114
           }
115
        };
116

    
117
      Setting reloadRequested is what closes the last window: the tick already
118
      checks it, so even a tick that fired between the server's decision and this
119
      call becomes a no-op.
120

    
121
      Note the overlay element itself is deliberately NOT removed - the user
122
      should keep seeing "session ended" text while the redirect completes.
123
      If the existing overlay text is unsuitable, swap it for a terminal message
124
      here rather than removing it.
125

    
126
  A2. p2j.socket.js: call it from both shutdown entry points, before either does
127
      anything that navigates or awaits:
128

    
129
        - in the MSG_SHUTTINGDOWN handler, ahead of requestDeleteAuthCookie()
130
        - in the MSG_QUIT handler, ahead of doRedirectToLogoutPage()
131

    
132
      Guard the call so it is safe when the SSO module is not loaded (non-SSO
133
      deployments do not include p2j.sso_reauth.js):
134

    
135
        if (p2j.sso_reauth && p2j.sso_reauth.abortReauth)
136
        {
137
           p2j.sso_reauth.abortReauth();
138
        }
139

    
140
PART B - make the tick refuse to act on a session that is already gone.
141

    
142
  In the expiry branch of the countdown tick, before beginInternalReload() and
143
  window.location.reload(), bail out when the session is finished:
144

    
145
        if (exitTheApplication || !p2j.socket.isConnected())
146
        {
147
           // the server is already tearing the session down; its MSG_QUIT
148
           // redirect is authoritative, do not navigate over it
149
           return;
150
        }
151

    
152
  This needs a small read-only accessor on the socket module if none exists
153
  (isConnected() / getReadyState()); do not reach into its internals from the
154
  SSO module.
155

    
156

    
157
5. ALTERNATIVES CONSIDERED
158
--------------------------------------------------------------------------------
159
(i)  Add a grace margin to the client deadline so the server always fires first
160
     and the client tick never runs.
161
     Rejected as a primary fix: it makes the race quieter, not absent - the tick
162
     still runs and still navigates if the server's forceLogout is delayed or
163
     fails, which is exactly when the client-side expiry is supposed to help.
164
     Harmless as an addition to Part A if a margin is wanted for UX reasons.
165

    
166
(ii) Have the server arm forceLogout at reauthTimeoutSec plus a grace, so the
167
     client's own expiry UX gets a chance to run first.
168
     This is a legitimate design change and may well be desirable, but it belongs
169
     to SsoTokenManager, changes observable session-lifetime behaviour, and does
170
     not remove the need for the cancel hook (the server still force-logs-out
171
     eventually). Out of scope here; raise separately if wanted.
172

    
173
(iii) Revert the expiry path to the pre-branch showSessionExpiredPage behaviour.
174
     Rejected: the branch's stated goal (no prompt, no quit-on-reload, internal
175
     reload so a live session re-attaches) is sound. The defect is the missing
176
     coordination, not the internal reload itself.
177

    
178

    
179
6. RISK
180
--------------------------------------------------------------------------------
181
Low. Part A adds one idempotent function and two guarded calls on paths that are
182
already terminal. Part B only adds an early return.
183

    
184
Watch for: abortReauth() must not throw when no overlay was ever shown (all four
185
blocks are null-guarded), and must not be async - the MSG_SHUTTINGDOWN handler
186
cannot await it without reintroducing a race.
187

    
188

    
189
7. TEST PLAN
190
--------------------------------------------------------------------------------
191
1. SSO deployment, short reauthTimeoutSec (e.g. 15 s). Trigger an invalid token,
192
   let the overlay appear, and IGNORE it. Expect: overlay stands down, the browser
193
   lands on the login page, and the server log shows quit() completing promptly
194
   rather than waiting out clientResponseTimeout.
195
2. Same, but click Re-login and complete the flow inside the window. Expect: no
196
   regression - session re-attaches, overlay cleared by the existing success path.
197
3. Same, but click Logout. Expect: existing logout behaviour unchanged.
198
4. Non-SSO deployment: confirm the guarded call in p2j.socket.js is a no-op and
199
   normal MSG_QUIT logout still redirects.
200
5. Reload the page manually while the overlay is up. Expect: no stray timers left
201
   behind (check for orphaned intervals in the console).