Skip to content

Commit f9cc367

Browse files
James Groffensimo5
authored andcommitted
Add option to not send a Negotiate headers
If negotiation was attempted but failed do not send a new Negotiate header. Useful when only one single sign on mechanism is allowed and to avoid misleading login prompts in some browsers. Added a test of the GssapiDontReauth option to the test suite. Also added SPNEGO no auth test. [SS: reworded and fixed commit subject/comment] [SS: fixed whitespace errors and 80 column wrappings] Reviewed-by: Simo Sorce <simo@redhat.com> Close #65
1 parent f29a157 commit f9cc367

File tree

7 files changed

+152
-5
lines changed

7 files changed

+152
-5
lines changed

README

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,29 @@ underscores for environment variable names.
264264
#### Example
265265
GssapiNameAttributes json
266266
GssapiNameAttributes RADIUS_NAME urn:ietf:params:gss:radius-attribute_1
267+
268+
269+
### GssapiNegotiateOnce
270+
271+
When this option is enabled the Negotiate header will not be resent if
272+
Negotiation has already been attempted but failed.
273+
274+
Normally when a client fails to use Negotiate authentication, a HTTP 401
275+
response is returned with a WWW-Authenticate: Negotiate header, implying that
276+
the client can retry to use Negotiate with different credentials or a
277+
different mechanism.
278+
279+
Consider enabling GssapiNegotiateOnce when only one single sign on mechanism
280+
is allowed, or when GssapiBasicAuth is enabled.
281+
282+
**NOTE:** if the initial Negotiate attempt fails, some browsers will fallback
283+
to other Negotiate mechanisms, prompting the user for login credentials and
284+
reattempting negotiation. This situation can mislead users - for example if
285+
krb5 authentication failed and no other mechanisms are allowed, a user could
286+
be prompted for login information even though any login information provided
287+
cannot succeed. When this occurs, some browsers will not fall back to a Basic
288+
Auth mechanism. Enable GssapiNegotiateOnce to avoid this situation.
289+
290+
- **Enable with:** GssapiNegotiateOnce On
291+
- **Default:** GssapiNegotiateOnce Off
292+

src/mod_auth_gssapi.c

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ static int mag_auth(request_rec *req)
674674
gss_buffer_desc lname = GSS_C_EMPTY_BUFFER;
675675
struct mag_conn *mc = NULL;
676676
int i;
677+
bool send_auth_header = true;
677678

678679
type = ap_auth_type(req);
679680
if ((type == NULL) || (strcasecmp(type, "GSSAPI") != 0)) {
@@ -765,6 +766,9 @@ static int mag_auth(request_rec *req)
765766
auth_header_type = ap_getword_white(req->pool, &auth_header);
766767
if (!auth_header_type) goto done;
767768

769+
/* We got auth header, sending auth header would mean re-auth */
770+
send_auth_header = !cfg->negotiate_once;
771+
768772
for (i = 0; auth_types[i] != NULL; i++) {
769773
if (strcasecmp(auth_header_type, auth_types[i]) == 0) {
770774
auth_type = i;
@@ -957,11 +961,14 @@ static int mag_auth(request_rec *req)
957961
apr_table_add(req->err_headers_out, req_cfg->rep_proto, reply);
958962
}
959963
} else if (ret == HTTP_UNAUTHORIZED) {
960-
apr_table_add(req->err_headers_out, req_cfg->rep_proto, "Negotiate");
961-
962-
if (is_mech_allowed(desired_mechs, gss_mech_ntlmssp,
963-
cfg->gss_conn_ctx)) {
964-
apr_table_add(req->err_headers_out, req_cfg->rep_proto, "NTLM");
964+
if (send_auth_header) {
965+
apr_table_add(req->err_headers_out,
966+
req_cfg->rep_proto, "Negotiate");
967+
if (is_mech_allowed(desired_mechs, gss_mech_ntlmssp,
968+
cfg->gss_conn_ctx)) {
969+
apr_table_add(req->err_headers_out, req_cfg->rep_proto,
970+
"NTLM");
971+
}
965972
}
966973
if (cfg->use_basic_auth) {
967974
apr_table_add(req->err_headers_out, req_cfg->rep_proto,
@@ -1229,6 +1236,14 @@ static const char *mag_allow_mech(cmd_parms *parms, void *mconfig,
12291236
return NULL;
12301237
}
12311238

1239+
static const char *mag_negotiate_once(cmd_parms *parms, void *mconfig, int on)
1240+
{
1241+
struct mag_config *cfg = (struct mag_config *)mconfig;
1242+
1243+
cfg->negotiate_once = on ? true : false;
1244+
return NULL;
1245+
}
1246+
12321247
#define GSS_NAME_ATTR_USERDATA "GSS Name Attributes Userdata"
12331248

12341249
static apr_status_t mag_name_attrs_cleanup(void *data)
@@ -1360,6 +1375,8 @@ static const command_rec mag_commands[] = {
13601375
#endif
13611376
AP_INIT_ITERATE("GssapiAllowedMech", mag_allow_mech, NULL, OR_AUTHCFG,
13621377
"Allowed Mechanisms"),
1378+
AP_INIT_FLAG("GssapiNegotiateOnce", mag_negotiate_once, NULL, OR_AUTHCFG,
1379+
"Don't resend negotiate header on negotiate failure"),
13631380
AP_INIT_RAW_ARGS("GssapiNameAttributes", mag_name_attrs, NULL, OR_AUTHCFG,
13641381
"Name Attributes to be exported as environ variables"),
13651382
{ NULL }

src/mod_auth_gssapi.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ struct mag_config {
7474
bool use_basic_auth;
7575
gss_OID_set_desc *allowed_mechs;
7676
gss_OID_set_desc *basic_mechs;
77+
bool negotiate_once;
7778
struct mag_name_attributes *name_attributes;
7879
};
7980

tests/httpd.conf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ CoreDumpDirectory /tmp
137137
Require valid-user
138138
</Location>
139139

140+
<Location /spnego_negotiate_once>
141+
AuthType GSSAPI
142+
AuthName "Login Negotiate Once"
143+
GssapiSSLonly Off
144+
GssapiUseSessions On
145+
Session On
146+
SessionCookieName gssapi_session path=/spnego_negotiate_once;httponly
147+
GssapiCredStore ccache:${HTTPROOT}/tmp/httpd_krb5_ccache
148+
GssapiCredStore client_keytab:${HTTPROOT}/http.keytab
149+
GssapiCredStore keytab:${HTTPROOT}/http.keytab
150+
GssapiBasicAuth Off
151+
GssapiAllowedMech krb5
152+
GssapiNegotiateOnce On
153+
Require valid-user
154+
</Location>
155+
140156
<Location /basic_auth_krb5>
141157
Options +Includes
142158
AddOutputFilter INCLUDES .html

tests/magtests.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,34 @@ def test_spnego_auth(testdir, testenv, testlog):
283283
else:
284284
sys.stderr.write('SPNEGO Proxy Auth: SUCCESS\n')
285285

286+
with (open(testlog, 'a')) as logfile:
287+
spnego = subprocess.Popen(["tests/t_spnego_no_auth.py"],
288+
stdout=logfile, stderr=logfile,
289+
env=testenv, preexec_fn=os.setsid)
290+
spnego.wait()
291+
if spnego.returncode != 0:
292+
sys.stderr.write('SPNEGO No Auth: FAILED\n')
293+
else:
294+
sys.stderr.write('SPNEGO No Auth: SUCCESS\n')
295+
296+
297+
def test_spnego_negotiate_once(testdir, testenv, testlog):
298+
299+
spnego_negotiate_once_dir = os.path.join(testdir, 'httpd', 'html',
300+
'spnego_negotiate_once')
301+
os.mkdir(spnego_negotiate_once_dir)
302+
shutil.copy('tests/index.html', spnego_negotiate_once_dir)
303+
304+
with (open(testlog, 'a')) as logfile:
305+
spnego = subprocess.Popen(["tests/t_spnego_negotiate_once.py"],
306+
stdout=logfile, stderr=logfile,
307+
env=testenv, preexec_fn=os.setsid)
308+
spnego.wait()
309+
if spnego.returncode != 0:
310+
sys.stderr.write('SPNEGO Negotiate Once: FAILED\n')
311+
else:
312+
sys.stderr.write('SPNEGO Negotiate Once: SUCCESS\n')
313+
286314

287315
def test_basic_auth_krb5(testdir, testenv, testlog):
288316

@@ -358,6 +386,7 @@ def test_basic_auth_krb5(testdir, testenv, testlog):
358386

359387
test_spnego_auth(testdir, testenv, testlog)
360388

389+
test_spnego_negotiate_once(testdir, testenv, testlog)
361390

362391
testenv = {'MAG_USER_NAME': USR_NAME,
363392
'MAG_USER_PASSWORD': USR_PWD,

tests/t_spnego_negotiate_once.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/python
2+
# Copyright (C) 2015 - mod_auth_gssapi contributors, see COPYING for license.
3+
4+
import os
5+
import requests
6+
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
7+
8+
9+
if __name__ == '__main__':
10+
sess = requests.Session()
11+
url = 'http://%s/spnego_negotiate_once/' % (
12+
os.environ['NSS_WRAPPER_HOSTNAME'])
13+
14+
# ensure a 401 with the appropriate WWW-Authenticate header is returned
15+
# when no auth is provided
16+
r = sess.get(url)
17+
if r.status_code != 401:
18+
raise ValueError('Spnego Negotiate Once failed - 401 expected')
19+
if not (r.headers.get("WWW-Authenticate") and
20+
r.headers.get("WWW-Authenticate").startswith("Negotiate")):
21+
raise ValueError('Spnego Negotiate Once failed - WWW-Authenticate '
22+
'Negotiate header missing')
23+
24+
# test sending a bad Authorization header with GssapiNegotiateOnce enabled
25+
r = sess.get(url, headers={"Authorization": "Negotiate badvalue"})
26+
if r.status_code != 401:
27+
raise ValueError('Spnego Negotiate Once failed - 401 expected')
28+
if r.headers.get("WWW-Authenticate"):
29+
raise ValueError('Spnego Negotiate Once failed - WWW-Authenticate '
30+
'Negotiate present but GssapiNegotiateOnce is '
31+
'enabled')
32+
33+
# ensure a 200 is returned when valid auth is provided
34+
r = sess.get(url, auth=HTTPKerberosAuth())
35+
if r.status_code != 200:
36+
raise ValueError('Spnego Negotiate Once failed')
37+

tests/t_spnego_no_auth.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/python
2+
# Copyright (C) 2015 - mod_auth_gssapi contributors, see COPYING for license.
3+
4+
import os
5+
import requests
6+
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
7+
8+
9+
if __name__ == '__main__':
10+
sess = requests.Session()
11+
url = 'http://%s/spnego/' % os.environ['NSS_WRAPPER_HOSTNAME']
12+
13+
r = sess.get(url)
14+
if r.status_code != 401:
15+
raise ValueError('Spnego failed - 401 expected')
16+
17+
if not (r.headers.get("WWW-Authenticate") and
18+
r.headers.get("WWW-Authenticate").startswith("Negotiate")):
19+
raise ValueError('Spnego failed - WWW-Authenticate Negotiate header '
20+
'missing')
21+

0 commit comments

Comments
 (0)