Skip to content

Commit f87e6b5

Browse files
committed
Invalidate the session when the delegated credential has expired
See gssapi/mod_auth_gssapi#316 Signed-off-by: Aurélien Bompard <[email protected]>
1 parent 22947b3 commit f87e6b5

File tree

4 files changed

+64
-16
lines changed

4 files changed

+64
-16
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ A Flask extention to make use of the authentication provided by the
55
[mod_auth_gssapi](https://github.com/gssapi/mod_auth_gssapi) extention of
66
Apache's HTTPd. See [FASJSON](https://github.com/fedora-infra/fasjson) for a
77
usage example.
8+
9+
If you're using sessions from `mod_session` with `mod_auth_gssapi`, set your
10+
application's `MOD_AUTH_GSSAPI_SESSION_HEADER` configuration variable to the
11+
value you used in Apache's configuration file for `SessionHeader`. This will
12+
signal `mod_session` to invalidate the session when the authentication
13+
credential has expired.

config/httpd.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ WSGIScriptReloading Off
2121
GssapiUseSessions On
2222
Session On
2323
SessionCookieName foobar_session path=/foobar;httponly;secure;
24-
SessionHeader FOOBAR_SESSION
24+
SessionHeader X-Replace-Session
2525
GssapiSessionKey file:/run/foobar/session.key
2626

2727
GssapiImpersonate On

flask_mod_auth_gssapi/ext.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import logging
12
import os
23

34
import gssapi
4-
from flask import abort, g, request
5+
from flask import abort, current_app, g, redirect, request
6+
7+
_log = logging.getLogger(__name__)
58

69

710
class FlaskModAuthGSSAPI:
@@ -12,6 +15,7 @@ def __init__(self, app=None, abort=abort):
1215

1316
def init_app(self, app):
1417
app.before_request(self._gssapi_check)
18+
app.config.setdefault("MOD_AUTH_GSSAPI_SESSION_HEADER", "X-Replace-Session")
1519

1620
def _gssapi_check(self):
1721
g.gss_name = g.gss_creds = g.principal = g.username = None
@@ -34,6 +38,11 @@ def _gssapi_check(self):
3438
if not principal:
3539
return # Maybe the endpoint is not protected, stop here
3640

41+
ccache_type, _sep, ccache_location = ccache.partition(":")
42+
if ccache_type == "FILE" and not os.path.exists(ccache_location):
43+
_log.warning("Delegated credentials not found: %r", ccache_location)
44+
return self._clear_session()
45+
3746
gss_name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
3847
try:
3948
creds = gssapi.Credentials(
@@ -49,9 +58,20 @@ def _gssapi_check(self):
4958
except gssapi.exceptions.ExpiredCredentialsError:
5059
lifetime = 0
5160
if lifetime <= 0:
52-
self.abort(401, "Credential lifetime has expired")
61+
_log.info("Credential lifetime has expired.")
62+
return self._clear_session()
5363

5464
g.gss_name = gss_name
5565
g.gss_creds = creds
5666
g.principal = gss_name.display_as(gssapi.NameType.kerberos_principal)
5767
g.username = g.principal.split("@")[0]
68+
69+
def _clear_session(self):
70+
"""Unset mod_auth_gssapi's session cookie and redirect to the same URL"""
71+
if request.method in ("POST", "PUT", "DELETE"):
72+
self.abort(
73+
401, "Re-authentication is necessary, please try your request again."
74+
)
75+
response = redirect(request.url)
76+
response.headers[current_app.config["MOD_AUTH_GSSAPI_SESSION_HEADER"]] = "MagBearerToken="
77+
return response

tests/test_ext.py

+35-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from types import SimpleNamespace
23

34
import pytest
@@ -44,18 +45,29 @@ def test_no_cache(app, wsgi_env):
4445
assert g.username is None
4546

4647

47-
def test_expired(app, wsgi_env, mocker):
48+
def test_expired(app, wsgi_env, caplog, mocker):
4849
creds_factory = mocker.patch("gssapi.Credentials")
4950
creds_factory.return_value = SimpleNamespace(lifetime=0)
50-
with app.test_request_context("/", environ_base=wsgi_env):
51+
caplog.set_level(logging.INFO)
52+
client = app.test_client()
53+
response = client.get("/someplace", environ_base=wsgi_env)
54+
assert response.status_code == 302
55+
assert response.headers["location"] == "http://localhost/someplace"
56+
assert caplog.messages == ["Credential lifetime has expired."]
57+
58+
59+
def test_expired_unsafe_method(app, wsgi_env, mocker):
60+
creds_factory = mocker.patch("gssapi.Credentials")
61+
creds_factory.return_value = SimpleNamespace(lifetime=0)
62+
with app.test_request_context("/someplace", method="POST", environ_base=wsgi_env):
5163
with pytest.raises(Unauthorized) as excinfo:
5264
app.preprocess_request()
5365
assert g.principal is None
5466
assert g.username is None
55-
assert excinfo.value.description == "Credential lifetime has expired"
67+
assert excinfo.value.description == "Re-authentication is necessary, please try your request again."
5668

5769

58-
def test_expired_exception(app, wsgi_env, mocker):
70+
def test_expired_exception(app, wsgi_env, mocker, caplog):
5971
creds_factory = mocker.patch("gssapi.Credentials")
6072

6173
class MockedCred:
@@ -64,15 +76,15 @@ def lifetime(self):
6476
raise ExpiredCredentialsError(720896, 100001)
6577

6678
creds_factory.return_value = MockedCred()
67-
with app.test_request_context("/", environ_base=wsgi_env):
68-
with pytest.raises(Unauthorized) as excinfo:
69-
try:
70-
app.preprocess_request()
71-
except ExpiredCredentialsError:
72-
pytest.fail("Did not catch ExpiredCredentialsError on cred.lifetime")
73-
assert g.principal is None
74-
assert g.username is None
75-
assert excinfo.value.description == "Credential lifetime has expired"
79+
caplog.set_level(logging.INFO)
80+
client = app.test_client()
81+
try:
82+
response = client.get("/someplace", environ_base=wsgi_env)
83+
except ExpiredCredentialsError:
84+
pytest.fail("Did not catch ExpiredCredentialsError on cred.lifetime")
85+
assert response.status_code == 302
86+
assert response.headers["location"] == "http://localhost/someplace"
87+
assert caplog.messages == ["Credential lifetime has expired."]
7688

7789

7890
def test_nominal(app, wsgi_env, mocker):
@@ -100,3 +112,13 @@ def test_alt_abort(app, wsgi_env, mocker):
100112
call_args = mock_abort.call_args_list[0][0]
101113
assert call_args[0] == 403
102114
assert call_args[1].startswith("Invalid credentials ")
115+
116+
117+
def test_ccache_not_found(app, wsgi_env, caplog, mocker):
118+
wsgi_env["KRB5CCNAME"] = "FILE:/tmp/does-not-exist"
119+
#caplog.set_level(logging.INFO)
120+
client = app.test_client()
121+
response = client.get("/someplace", environ_base=wsgi_env)
122+
assert response.status_code == 302
123+
assert response.headers["location"] == "http://localhost/someplace"
124+
assert caplog.messages == ["Delegated credentials not found: '/tmp/does-not-exist'"]

0 commit comments

Comments
 (0)