Skip to content

Commit 3a86cdd

Browse files
committed
Rework the logout handler to
1) Generally act in reverse order of login regarding gdp project logout, 2FA session clean up and local+remote IDP logout 2) Disable the OpenID 2.0 workaround with interleaved remote IDP logout when using OpenID Connect This should make logout more consistent and more importantly address the issue with missing local 2FA session termination during OpenID Connect logout we currently see in issue 222.
1 parent aa67bb3 commit 3a86cdd

2 files changed

Lines changed: 61 additions & 49 deletions

File tree

mig/shared/functionality/logout.py

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# --- BEGIN_HEADER ---
55
#
66
# logout - force-expire local login session(s)
7-
# Copyright (C) 2003-2023 The MiG Project lead by Brian Vinter
7+
# Copyright (C) 2003-2025 The MiG Project lead by Brian Vinter
88
#
99
# This file is part of MiG.
1010
#
@@ -43,6 +43,7 @@
4343
require_twofactor_setup, build_logout_url, extract_client_openid
4444
from mig.shared.init import initialize_main_variables, find_entry
4545
from mig.shared.useradm import expire_oid_sessions, find_oid_sessions
46+
from mig.shared.url import urlencode
4647

4748

4849
def signature():
@@ -115,13 +116,25 @@ def main(client_id, user_arguments_dict, environ=None):
115116
if i in ['setup', 'logout']]
116117
title_entry['user_menu'] = []
117118

118-
# OpenID requires logout on provider and in local mod-auth-openid database.
119+
# NOTE: actual logout is done in reverse order of login. Namely, start with
120+
# any gdp project logout, then 2FA session removal and finally complete any
121+
# IDP logout.
122+
123+
# NOTE: OpenID 2.0 requires logout on provider and in local mod-auth-openid
124+
# database.
119125
# IMPORTANT: some browsers like Firefox may inadvertently renew the local
120126
# OpenID session while loading the resources for this page (in parallel).
121127
# User clicks logout at OpenID provider, which sends her back here to
122128
# finish the local logout.
129+
# NOTE: OpenID Connect on the other hand cannot return after logout and
130+
# must complete gdp and 2FA session clean up before calling remote logout.
123131

124132
if do_logout:
133+
if configuration.site_enable_gdp:
134+
logger.debug("close any open GDP project for %s" % client_id)
135+
project_logout(configuration, 'https', environ['REMOTE_ADDR'],
136+
client_id)
137+
125138
if configuration.site_enable_twofactor:
126139
real_user = client_id
127140
addr_filter = environ['REMOTE_ADDR']
@@ -131,6 +144,7 @@ def main(client_id, user_arguments_dict, environ=None):
131144
real_user = get_client_id_from_project_client_id(configuration,
132145
client_id)
133146
addr_filter = None
147+
logger.debug("expire twofactor session for %s" % client_id)
134148
if real_user and not expire_twofactor_session(configuration,
135149
real_user, environ,
136150
allow_missing=True,
@@ -144,7 +158,28 @@ def main(client_id, user_arguments_dict, environ=None):
144158
the %s Admins if it happens repeatedly.
145159
</p>""" % configuration.short_title
146160
})
161+
162+
if auth_type == AUTH_OPENID_V2:
163+
if auth_flavor == AUTH_MIG_OID:
164+
reentry_page = configuration.migserver_https_mig_oid_url
165+
elif auth_flavor == AUTH_EXT_OID:
166+
reentry_page = configuration.migserver_https_ext_oid_url
167+
elif auth_type == AUTH_OPENID_CONNECT:
168+
if auth_flavor == AUTH_MIG_OIDC:
169+
reentry_page = configuration.migserver_https_mig_oidc_url
170+
elif auth_flavor == AUTH_EXT_OIDC:
171+
reentry_page = configuration.migserver_https_ext_oidc_url
172+
elif auth_type == AUTH_CERTIFICATE:
173+
if auth_flavor == AUTH_MIG_CERT:
174+
reentry_page = configuration.migserver_https_mig_cert_url
175+
elif auth_flavor == AUTH_EXT_CERT:
176+
reentry_page = configuration.migserver_https_ext_cert_url
177+
else:
178+
reentry_page = "https://%s" % configuration.server_fqdn
179+
147180
if auth_type == AUTH_OPENID_V2:
181+
# TODO: truncate open_id_session_id instead as suggested in the FAQ?
182+
# https://findingscience.com/mod_auth_openid/faq.html
148183
(oid_db, identity) = extract_client_openid(configuration, environ,
149184
lookup_dn=False)
150185
logger.info("expiring active sessions for %s in %s" % (identity,
@@ -155,6 +190,18 @@ def main(client_id, user_arguments_dict, environ=None):
155190
configuration, oid_db, identity)
156191
elif auth_type == AUTH_OPENID_CONNECT:
157192
# NOTE: mod auth oidc handles local logout automatically
193+
logger.debug("pass through local %s auth module for %s session end"
194+
% (auth_type, identity))
195+
# NOTE: OpenID Connect module handles remote logout on vanity url
196+
terminate_session_url = '/dynamic/redirect_uri'
197+
# NOTE: upstream currently requires fixed logout callback url
198+
# TODO: change return_page to reentry_page when upstream allows it
199+
return_page = build_logout_url(configuration, environ)
200+
terminate_query = urlencode({'logout': return_page})
201+
reentry_page = terminate_session_url + '?%s' % terminate_query
202+
logger.debug("send %s to %s for logout" % (client_id, reentry_page))
203+
success, found, remaining = True, True, []
204+
elif auth_type == AUTH_CERTIFICATE:
158205
logger.info("no manual cleanup needed for %s session of %s" %
159206
(auth_type, identity))
160207
success, found, remaining = True, True, []
@@ -170,34 +217,13 @@ def main(client_id, user_arguments_dict, environ=None):
170217
return (output_objects, status)
171218

172219
if success and found and not remaining:
173-
if configuration.site_enable_gdp:
174-
reentry_page = "https://%s" % configuration.server_fqdn
175-
if auth_type == AUTH_OPENID_V2:
176-
if auth_flavor == AUTH_MIG_OID:
177-
reentry_page = configuration.migserver_https_mig_oid_url
178-
elif auth_flavor == AUTH_EXT_OID:
179-
reentry_page = configuration.migserver_https_ext_oid_url
180-
elif auth_type == AUTH_OPENID_CONNECT:
181-
if auth_flavor == AUTH_MIG_OIDC:
182-
reentry_page = configuration.migserver_https_mig_oidc_url
183-
elif auth_flavor == AUTH_EXT_OIDC:
184-
reentry_page = configuration.migserver_https_ext_oidc_url
185-
project_logout(
186-
configuration, 'https', environ['REMOTE_ADDR'], client_id)
187-
html = """
188-
<a id='gdp_logout' href='%s'></a>
220+
# TODO: switch to 'window.location = reentry_page' ?
221+
html = """
222+
<a id='finish_logout' href='%s'></a>
189223
<script type='text/javascript'>
190-
document.getElementById('gdp_logout').click();
224+
document.getElementById('finish_logout').click();
191225
</script>""" % reentry_page
192-
output_objects.append(
193-
{'object_type': 'html_form', 'text': html})
194-
else:
195-
output_objects.append(
196-
{'object_type': 'text', 'text':
197-
"""You are now logged out of %s locally - you may want to
198-
close your web browser to finish""" % configuration.short_title})
199-
title_entry['skipmenu'] = True
200-
226+
output_objects.append({'object_type': 'html_form', 'text': html})
201227
else:
202228
logger.error("remaining active sessions for %s: %s" % (identity,
203229
remaining))
@@ -218,6 +244,7 @@ def main(client_id, user_arguments_dict, environ=None):
218244
{'object_type': 'link',
219245
'destination': 'javascript:history.back();',
220246
'class': 'genericbutton greenBtn', 'text': "No, go back"})
247+
221248
# sub-container end
222249
output_objects.append({'object_type': 'html_form', 'text': '''
223250
</div>

mig/shared/httpsclient.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -233,34 +233,19 @@ def build_logout_url(configuration, environ):
233233
(auth_type, auth_flavor) = detect_client_auth(configuration, environ)
234234
local_logout = '%(SCRIPT_URI)s?logout=true' % environ
235235
if auth_type == AUTH_OPENID_V2:
236+
_logger.debug("logout chaining needed for %s auth" % auth_type)
236237
# NOTE: OpenID 2.0 logout requires local session database scrubbing.
237238
# Logout at provider and return to local logout page for that.
238239
login = extract_plain_login(configuration, environ)
239240
logout_base = os.path.dirname(os.path.dirname(login))
240241
logout_query = urlencode({'return_to': local_logout})
241242
logout_url = logout_base + '/logout?%s' % logout_query
242243
elif auth_type == AUTH_OPENID_CONNECT:
243-
# NOTE: OpenID Connect module handles chained logout on vanity url.
244-
# Use it to first log out at provider if supported and then
245-
# return for local logout with 2fa session clean up.
246-
# TODO: test RP-initiated logout on provider with end_session endpoint!
247-
logout_base = '/dynamic/redirect_uri'
248-
logout_query = urlencode({'logout': local_logout})
249-
logout_url = logout_base + '?%s' % logout_query
250-
251-
# NOTE: the MicroFocus provider we know does not offer end_session
252-
# so the user currently needs to close browser upon logout.
253-
# TODO: can we implement an alterntive explicit logout?
254-
# Something like this might be a qualified guess based on
255-
# https://www.microfocus.com/documentation/access-manager/developer-documentation-5.0/oauth-application-developer-guide/logout-endpoint.html
256-
# if auth_flavor == AUTH_MIG_OIDC:
257-
# logout_base = configuration.migserver_https_mig_oidc_url
258-
# elif auth_flavor == AUTH_EXT_OIDC:
259-
# logout_base = configuration.migserver_https_ext_oidc_url
260-
# logout_query = urlencode({'post_logout_redirect_uri': local_logout})
261-
# logout_url = os.path.join(logout_base, 'nidp/oauth/v1/nam',
262-
# 'end_session?%s' % logout_query)
263-
244+
_logger.debug("no logout chaining needed for %s auth" % auth_type)
245+
logout_url = local_logout
246+
elif auth_type == AUTH_CERTIFICATE:
247+
_logger.debug("no logout chaining needed for %s auth" % auth_type)
248+
logout_url = local_logout
264249
else:
265250
_logger.warning("unknown logout chaining for %s" % auth_type)
266251
logout_url = local_logout

0 commit comments

Comments
 (0)