Skip to content

Commit 46a3b4a

Browse files
committed
Ask for authentication when credentials are expired instead of redirecting
See gssapi/mod_auth_gssapi#316 Signed-off-by: Aurélien Bompard <[email protected]>
1 parent aa1cc0d commit 46a3b4a

File tree

3 files changed

+66
-41
lines changed

3 files changed

+66
-41
lines changed

flask_mod_auth_gssapi/ext.py

+19-13
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33

44
import gssapi
5-
from flask import abort, current_app, g, redirect, request
5+
from flask import abort, current_app, g, make_response, request
66

77
_log = logging.getLogger(__name__)
88

@@ -41,7 +41,7 @@ def _gssapi_check(self):
4141
ccache_type, _sep, ccache_location = ccache.partition(":")
4242
if ccache_type == "FILE" and not os.path.exists(ccache_location):
4343
_log.warning("Delegated credentials not found: %r", ccache_location)
44-
return self._clear_session()
44+
return self._authenticate()
4545

4646
gss_name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
4747
try:
@@ -59,21 +59,27 @@ def _gssapi_check(self):
5959
lifetime = 0
6060
if lifetime <= 0:
6161
_log.info("Credential lifetime has expired.")
62-
return self._clear_session()
62+
if ccache_type == "FILE":
63+
try:
64+
os.remove(ccache_location)
65+
except OSError as e:
66+
_log.warning(
67+
"Could not remove expired credential at %s: %s",
68+
ccache_location,
69+
e,
70+
)
71+
return self._authenticate()
6372

6473
g.gss_name = gss_name
6574
g.gss_creds = creds
6675
g.principal = gss_name.display_as(gssapi.NameType.kerberos_principal)
6776
g.username = g.principal.split("@")[0]
6877

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"]] = (
77-
"MagBearerToken="
78-
)
78+
def _authenticate(self):
79+
"""Unset mod_auth_gssapi's session cookie and restart GSSAPI authentication"""
80+
_log.debug("Clearing the session and asking for re-authentication.")
81+
response = make_response("Re-authentication is necessary.", 401)
82+
response.headers["WWW-Authenticate"] = "Negotiate"
83+
session_header = current_app.config["MOD_AUTH_GSSAPI_SESSION_HEADER"]
84+
response.headers[session_header] = "MagBearerToken="
7985
return response

tests/conftest.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from types import SimpleNamespace
2+
13
import pytest
24
from flask import Flask
35

@@ -15,3 +17,16 @@ def app():
1517
@pytest.fixture
1618
def wsgi_env():
1719
return {"KRB5CCNAME": "/tmp/ignore", "GSS_NAME": "[email protected]"} # noqa: S108
20+
21+
22+
@pytest.fixture
23+
def credential(mocker):
24+
creds_factory = mocker.patch("gssapi.Credentials")
25+
cred = creds_factory.return_value = SimpleNamespace(lifetime=10)
26+
return cred
27+
28+
29+
@pytest.fixture
30+
def expired_credential(credential):
31+
credential.lifetime = 0
32+
return credential

tests/test_ext.py

+32-28
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import logging
2-
from types import SimpleNamespace
2+
import os
3+
import stat
34

45
import pytest
56
from flask import Flask, g
67
from gssapi.raw.exceptions import ExpiredCredentialsError
7-
from werkzeug.exceptions import Forbidden, InternalServerError, Unauthorized
8+
from werkzeug.exceptions import Forbidden, InternalServerError
89

910
from flask_mod_auth_gssapi import FlaskModAuthGSSAPI
1011

@@ -45,29 +46,17 @@ def test_no_cache(app, wsgi_env):
4546
assert g.username is None
4647

4748

48-
def test_expired(app, wsgi_env, caplog, mocker):
49-
creds_factory = mocker.patch("gssapi.Credentials")
50-
creds_factory.return_value = SimpleNamespace(lifetime=0)
49+
def test_expired(app, wsgi_env, caplog, expired_credential, tmp_path):
50+
ccache_path = tmp_path.joinpath("ccache")
51+
open(ccache_path, "w").close() # Create the file
52+
wsgi_env["KRB5CCNAME"] = f"FILE:{ccache_path.as_posix()}"
5153
caplog.set_level(logging.INFO)
5254
client = app.test_client()
5355
response = client.get("/someplace", environ_base=wsgi_env)
54-
assert response.status_code == 302
55-
assert response.headers["location"] == "http://localhost/someplace"
56+
assert response.status_code == 401
57+
assert response.headers["www-authenticate"] == "Negotiate"
5658
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):
63-
with pytest.raises(Unauthorized) as excinfo:
64-
app.preprocess_request()
65-
assert g.principal is None
66-
assert g.username is None
67-
assert (
68-
excinfo.value.description
69-
== "Re-authentication is necessary, please try your request again."
70-
)
59+
assert not os.path.exists(ccache_path)
7160

7261

7362
def test_expired_exception(app, wsgi_env, mocker, caplog):
@@ -85,14 +74,29 @@ def lifetime(self):
8574
response = client.get("/someplace", environ_base=wsgi_env)
8675
except ExpiredCredentialsError:
8776
pytest.fail("Did not catch ExpiredCredentialsError on cred.lifetime")
88-
assert response.status_code == 302
89-
assert response.headers["location"] == "http://localhost/someplace"
77+
assert response.status_code == 401
78+
assert response.headers["www-authenticate"] == "Negotiate"
9079
assert caplog.messages == ["Credential lifetime has expired."]
9180

9281

93-
def test_nominal(app, wsgi_env, mocker):
94-
creds_factory = mocker.patch("gssapi.Credentials")
95-
creds_factory.return_value = SimpleNamespace(lifetime=10)
82+
def test_expired_cant_remove(app, wsgi_env, caplog, expired_credential, tmp_path):
83+
ccaches_dir = tmp_path.joinpath("ccaches")
84+
os.makedirs(ccaches_dir)
85+
ccache_path = ccaches_dir.joinpath("ccache")
86+
open(ccache_path, "w").close() # Create the file
87+
os.chmod(ccaches_dir, stat.S_IRUSR | stat.S_IXUSR) # Prevent writing to this dir
88+
wsgi_env["KRB5CCNAME"] = f"FILE:{ccache_path.as_posix()}"
89+
client = app.test_client()
90+
response = client.get("/someplace", environ_base=wsgi_env)
91+
assert response.status_code == 401
92+
assert response.headers["www-authenticate"] == "Negotiate"
93+
assert caplog.messages == [
94+
f"Could not remove expired credential at {ccache_path.as_posix()}: "
95+
f"[Errno 13] Permission denied: '{ccache_path.as_posix()}'"
96+
]
97+
98+
99+
def test_nominal(app, wsgi_env, credential):
96100
with app.test_request_context("/", environ_base=wsgi_env):
97101
app.preprocess_request()
98102
assert g.principal == "[email protected]"
@@ -122,6 +126,6 @@ def test_ccache_not_found(app, wsgi_env, caplog, mocker):
122126
# caplog.set_level(logging.INFO)
123127
client = app.test_client()
124128
response = client.get("/someplace", environ_base=wsgi_env)
125-
assert response.status_code == 302
126-
assert response.headers["location"] == "http://localhost/someplace"
129+
assert response.status_code == 401
130+
assert response.headers["www-authenticate"] == "Negotiate"
127131
assert caplog.messages == ["Delegated credentials not found: '/tmp/does-not-exist'"]

0 commit comments

Comments
 (0)