Skip to content

Commit f8092b5

Browse files
committed
🐛(oidc) fix session persistence with Redis backend for OIDC flows
1 parent c771bae commit f8092b5

File tree

1 file changed

+87
-7
lines changed

1 file changed

+87
-7
lines changed

src/lasuite/oidc_login/views.py

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def persist_state(request, state):
4747
request.session["oidc_states"] = {}
4848

4949
request.session["oidc_states"][state] = {}
50+
51+
# Force immediate session save for cache-based backends
52+
request.session.modified = True
5053
request.session.save()
5154

5255
def construct_oidc_logout_url(self, request):
@@ -102,6 +105,12 @@ def post(self, request):
102105
# If the user is not redirected to the OIDC provider, ensure logout
103106
if logout_url == self.redirect_url:
104107
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()
105114

106115
return HttpResponseRedirect(logout_url)
107116

@@ -131,52 +140,123 @@ def get(self, request):
131140

132141
state = request.GET.get("state")
133142

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+
134149
if state not in request.session.get("oidc_states", {}):
135150
msg = "OIDC callback state not found in session `oidc_states`!"
136151
raise SuspiciousOperation(msg)
137152

153+
# Clean up the state from session
138154
del request.session["oidc_states"][state]
139155
request.session.save()
140156

157+
# Perform Django logout
141158
auth.logout(request)
142159

143160
return HttpResponseRedirect(self.redirect_url)
144161

145162

146163
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)
148194

149195
@property
150196
def failure_url(self):
151197
"""
152198
Override the failure URL property to handle silent login flow.
153199
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.
156203
"""
157204
if self.request.session.get("silent", None):
205+
# Clean up silent flag
158206
del self.request.session["silent"]
159207
self.request.session.save()
208+
# Redirect to success URL (homepage) instead of failure page
160209
return self.success_url
161210
return super().failure_url
162211

163212

164213
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
166240

167241
def get_extra_params(self, request):
168242
"""
169243
Handle 'prompt' extra parameter for the silent login flow.
170244
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
173251
"""
174252
extra_params = self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", None)
175253
if extra_params is None:
176254
extra_params = {}
255+
256+
# Check if this is a silent login attempt
177257
if request.GET.get("silent", "").lower() == "true":
178258
extra_params = copy.deepcopy(extra_params)
179259
extra_params.update({"prompt": "none"})
180260
request.session["silent"] = True
181-
request.session.save()
261+
182262
return extra_params

0 commit comments

Comments
 (0)