|
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).
|