@@ -47,6 +47,9 @@ def persist_state(request, state):
47
47
request .session ["oidc_states" ] = {}
48
48
49
49
request .session ["oidc_states" ][state ] = {}
50
+
51
+ # Force immediate session save for cache-based backends
52
+ request .session .modified = True
50
53
request .session .save ()
51
54
52
55
def construct_oidc_logout_url (self , request ):
@@ -102,6 +105,12 @@ def post(self, request):
102
105
# If the user is not redirected to the OIDC provider, ensure logout
103
106
if logout_url == self .redirect_url :
104
107
auth .logout (request )
108
+ else :
109
+ # Force final session save before redirect to SSO
110
+ # This ensures the logout state generated in construct_oidc_logout_url()
111
+ # is persisted in Redis before the browser redirects
112
+ request .session .modified = True
113
+ request .session .save ()
105
114
106
115
return HttpResponseRedirect (logout_url )
107
116
@@ -131,52 +140,123 @@ def get(self, request):
131
140
132
141
state = request .GET .get ("state" )
133
142
143
+ # Handle requests without state parameter
144
+ # Some SSO providers send a preflight request without state before the actual callback
145
+ # We should not raise an error in this case, just redirect gracefully
146
+ if not state :
147
+ return HttpResponseRedirect (self .redirect_url )
148
+
134
149
if state not in request .session .get ("oidc_states" , {}):
135
150
msg = "OIDC callback state not found in session `oidc_states`!"
136
151
raise SuspiciousOperation (msg )
137
152
153
+ # Clean up the state from session
138
154
del request .session ["oidc_states" ][state ]
139
155
request .session .save ()
140
156
157
+ # Perform Django logout
141
158
auth .logout (request )
142
159
143
160
return HttpResponseRedirect (self .redirect_url )
144
161
145
162
146
163
class OIDCAuthenticationCallbackView (MozillaOIDCAuthenticationCallbackView ):
147
- """Custom callback view for handling the silent login flow."""
164
+ """
165
+ Custom callback view for handling the silent login flow.
166
+
167
+ This view extends mozilla-django-oidc to properly handle silent login failures
168
+ without deleting OIDC states that might be needed by concurrent requests.
169
+ """
170
+
171
+ def get (self , request ):
172
+ """
173
+ Override callback to handle silent login failures gracefully.
174
+
175
+ When silent login fails (error=login_required), we don't delete the
176
+ OIDC state because the user might immediately try a normal login.
177
+ This prevents race conditions between silent and normal login flows.
178
+ """
179
+ error = request .GET .get ("error" )
180
+ state = request .GET .get ("state" )
181
+
182
+ # Handle silent login failure specifically
183
+ if error == "login_required" and request .session .get ("silent" ):
184
+ # Silent login failed (user not logged in to SSO)
185
+ # Clean up the 'silent' flag but KEEP the OIDC state
186
+ # This allows a subsequent normal login to succeed
187
+ del request .session ["silent" ]
188
+ request .session .save ()
189
+ return HttpResponseRedirect (self .success_url )
190
+
191
+ # For all other cases (normal login callback, other errors, etc.)
192
+ # let parent handle it (which will properly validate and clean up state)
193
+ return super ().get (request )
148
194
149
195
@property
150
196
def failure_url (self ):
151
197
"""
152
198
Override the failure URL property to handle silent login flow.
153
199
154
- A silent login failure (e.g., no active user session) should not be
155
- considered as an authentication failure.
200
+ A silent login failure (e.g., no active user session) should redirect
201
+ to success_url (homepage) instead of failure_url, because it's not
202
+ really a failure - it just means the user needs to log in manually.
156
203
"""
157
204
if self .request .session .get ("silent" , None ):
205
+ # Clean up silent flag
158
206
del self .request .session ["silent" ]
159
207
self .request .session .save ()
208
+ # Redirect to success URL (homepage) instead of failure page
160
209
return self .success_url
161
210
return super ().failure_url
162
211
163
212
164
213
class OIDCAuthenticationRequestView (MozillaOIDCAuthenticationRequestView ):
165
- """Custom authentication view for handling the silent login flow."""
214
+ """
215
+ Custom authentication view for handling the silent login flow.
216
+
217
+ This view extends mozilla-django-oidc to:
218
+ 1. Support silent login with prompt=none parameter
219
+ 2. Force session save before redirect (critical for Redis backend)
220
+ """
221
+
222
+ def get (self , request ):
223
+ """
224
+ Override GET to force session save after OIDC state initialization.
225
+ """
226
+ # Create session if it doesn't exist (new visitor)
227
+ if not request .session .session_key :
228
+ request .session .create ()
229
+
230
+ # Call parent method which generates OIDC state and stores it in session
231
+ response = super ().get (request )
232
+
233
+ # Force immediate session save to Redis before returning redirect response
234
+ # This ensures the OIDC state is persisted before the browser redirects to SSO
235
+ if hasattr (request , "session" ):
236
+ request .session .modified = True
237
+ request .session .save ()
238
+
239
+ return response
166
240
167
241
def get_extra_params (self , request ):
168
242
"""
169
243
Handle 'prompt' extra parameter for the silent login flow.
170
244
171
- This extra parameter is necessary to distinguish between a standard
172
- authentication flow and the silent login flow.
245
+ Silent login allows checking if user has an active SSO session
246
+ without displaying any UI. This is triggered by ?silent=true in URL.
247
+
248
+ If silent=true:
249
+ - Adds prompt=none to OIDC request (tells SSO to not show login page)
250
+ - Marks session with 'silent' flag for callback handling
173
251
"""
174
252
extra_params = self .get_settings ("OIDC_AUTH_REQUEST_EXTRA_PARAMS" , None )
175
253
if extra_params is None :
176
254
extra_params = {}
255
+
256
+ # Check if this is a silent login attempt
177
257
if request .GET .get ("silent" , "" ).lower () == "true" :
178
258
extra_params = copy .deepcopy (extra_params )
179
259
extra_params .update ({"prompt" : "none" })
180
260
request .session ["silent" ] = True
181
- request . session . save ()
261
+
182
262
return extra_params
0 commit comments