Optional Kerberos Authentication
2015-05-22
Table of Contents
Abstract
We look at ways of enabling Kerberos authentication for Web application with Apache HTTP server while not making the authentication via Kerberos mandatory. Users should still be able to log in using application's standard logon form if they did not have Kerberos ticket or browser configured or supporting Kerberos authentication.
Form-based authentication
Assume we have a Web application which has a logon form at /app/login
:
Its HTML source code looks typically something like
<form id="logon-form" method="POST"> <dl> <dt><label for="login">Username</label></dt> <dd><input type="text" name="login" id="login"></input></dd> <dt><label for="password">Password</label></dt> <dd><input type="password" name="password" id="password"></input></dd> </dl> <button>Log in</button> </form>
and it is backed (and often generated) by an application which will be able
to accept the HTTP POST
request and process the parameters login
and password
, validating the credentials against user table in
relational database, via PAM, external LDAP, or other means. When authentication passes,
the application then creates session for the user and using HTTP response
Set-Cookie
header passes back identifier of that session, ensuring that
user's browser will send it with each subrequent request and thus keep the user
authenticated from application's point of view even for the following page visits.
Figure 1. Form-based authentication, HTTP exchange
Browser | HTTP Server | Application | |||||
---|---|---|---|---|---|---|---|
User accesses login page | |||||||
↳ | GET /app/login HTTP/1.0 | ⟶ | Static page, or generated by application | ||||
⟵ | 200 OK HTML source of the logon form | ↲ | |||||
User enters login and password | |||||||
↳ | POST /app/login HTTP/1.0 login=bob&password=Bobovo+heslo | ⟶ | Validates credentials | ||||
When password is valid: | |||||||
⟵ | 303 See Other Set-Cookie: session_id=339248bf820034234 Location: /app | ↲ | |||||
↳ | GET /app HTTP/1.0 Cookie: session_id=339248bf820034234 | ⟶ | Application generates content. | ||||
⟵ | 200 OK Application page | ↲ | |||||
User sees /app content. | |||||||
When password is not valid: | |||||||
⟵ | 200 OK Logon form again, with error “The password or username you entered is incorrect.” | ↲ | |||||
User tries to enter correct login and password |
Configuring Kerberos/GSSAPI/Negotiate HTTP authentication
When we want to enable Kerberos authentication, using for example Apache modules
mod_auth_gssapi or mod_auth_kerb,
we configure this authentication module and the authentication will be done by the Apache HTTP Server, before the
application gets a chance to process the input or display the logon form. When the
authentication in Apache is successful, the module will internally set r->user
field
of the request structure to hold the username, which then gets propagated to the
Web application via REMOTE_USER
environment variable or other mechanism.
The application handling the logon form should then just assume the user was authenticated
and use that REMOTE_USER
value as the username of the authenticated user.
If we enable the Kerberos/GSSAPI authentication using
<Location /app/login> AuthType GSSAPI AuthName "Kerberos Login" GssapiCredStore keytab:/etc/http.keytab require valid-user </Location>
or
<Location /app/login> AuthType Kerberos AuthName "Kerberos Login" KrbMethodNegotiate On Krb5KeyTab /etc/http.keytab require valid-user </Location>
and the user has a valid ticket-granting ticket and the browser is able to obtain
service ticket and authenticate using the Negotiate authentication schema, user
is authenticated, REMOTE_USER
set, and the application can
proceed directly to create the session.
However, what if the Kerberos authentication fails? Then the user is presented with the content of the last response that the browser got, which is the 401 Unauthorized page. With Apache default settings, that page has simple text information:
Unauthorized
This server could not verify that you are authorized to access the document requested. Either you supplied the wrong credentials (e.g., bad password), or your browser doesn't understand how to supply the credentials required.
Figure 2. Kerberos/GSSAPI/Negotiate HTTP authentication, HTTP exchange
Browser | HTTP Server | Application | |||||
---|---|---|---|---|---|---|---|
User accesses login page | |||||||
↳ | GET /app/login HTTP/1.0 | ⟶ | mod_auth_gssapi or mod_auth_kerb | ||||
⟵ | 401 Unauthorized WWW-Authenticate: Negotiate | ↲ | |||||
User has service ticket or the browser is able to obtain one using the ticket-granting ticket: | |||||||
↳ | GET /app/login HTTP/1.0 Authorization: Negotiate YIIEHwYGKw... | ⟶ | Verifies the service ticket | ||||
Ticket is valid: | |||||||
Authenticated user is put to REMOTE_USER or passed to application via other mechanism | |||||||
⟵ | 303 See Other Set-Cookie: session_id=b029382ffa9c82112 Location: /app | ↲ | |||||
↳ | GET /app HTTP/1.0 Cookie: session_id=b029382ffa9c82112 | ⟶ | Application generates content. | ||||
⟵ | 200 OK Application page | ↲ | |||||
User sees /app content. | |||||||
Ticket is not valid: | |||||||
⟵ | 401 Unauthorized WWW-Authenticate: Negotiate OqCWbAadcGec | ↲ | |||||
When service ticket cannot be obtained: | |||||||
User sees the content of the last (401 Unauthorized) HTTP response. |
Falling back to logon form
If we want the Kerberos authentication to be optional, we need
a way to display the logon form for unsuccessful Negotiate
authentication and make it possible for the user to enter their
credentials and authenticate using application's standard form-based
method. In other words, the content of the 401 Unauthorized
page needs to be the logon form or a redirect to location showing
the logon form, and the HTTP POST
request of the
logon form submission needs to reach the application for verification,
without the Kerberos modules (mod_auth_gssapi, mod_auth_kerb)
getting involved.
Display the logon form in the 401 response
With Apache HTTP Server, the way to alter the body of the error
HTTP response or any response based on its HTTP status is the
ErrorDocument
directive. Sadly, when we
extend our Kerberos configuration with
ErrorDocument 401 /app/login
failed authentication will still show the default Unauthorized message, now with an additional note
Additionally, a 401 Unauthorized error was encountered while trying to use an ErrorDocument to handle the request.
The issue is caused by the
require valid-user
directive that we have configured for the very same location
/app/login
. When Apache is about to return
the status 401 for /app/login
, based on the
ErrorDocument
configuration it goes to
fetch the content — from /app/login
again.
One solution is to make the logon screen also accessible via
different URL, without GSSAPI authentication, let's say
/app/login2
. Then
ErrorDocument 401 /app/login2
will generate the correct content.
Another possibility is to configure the same URL but with an extra query string which can then be tested in the configuration:
<If "%{QUERY_STRING} !~ /^noext=1/"> ... require valid-user ErrorDocument 401 /app/login?noext=1 </If>
This way the GSSAPI authentication will only be invoked
when the URL /app/login
is accessed by the user
from their browser without the noext=1
query string.
The internal fetch of the content for the 401 Unauthorized response
will then go to /app/login?noext=1
, thus avoid
the GSSAPI authentication.
Getting the logon form submission to the application
We have the application correctly displaying the logon form when
the Kerberos/GSSAPI authentication fails. What happens when the
user enters their credentials and submits the form? We will see
that the HTTP response status is 401 Unauthorized, application
does not authenticate the user, and the form is displayed again. Why?
Since the action (target) of the form submission is likely
/app/login
itself, the configured AuthType GSSAPI
causes the request to be handled by mod_auth_gssapi again, and
return 401 Unauthorized with its ErrorDocument
content, the logon form.
One possible solution is to change the application to use
action="/app/login?noext=1"
or action="/app/login2"
attribute on the <form>
element to drive the submission
to location not covered by the GSSAPI authentication. That however likely
requires modification of the Web application itself which might not
be feasible.
Easier, Apache-only approach to GSSAPI-protect only the first access
to the logon page but not the fallback communication is to notice
that the first access will likely always be HTTP method
GET
, while the logon form is typically
submitted using POST
. We can use Limit GET
to only enable the optional Kerberos/GSSAPI authentication for the
first, GET
access, only:
<Limit GET> ... require valid-user </Limit>
The complete setup
The whole configuration which will cause the
browser to attempt Kerberos/GSSAPI authentication upon the
first (GET
) access to the logon form,
will display the logon form for failed HTTP authentication,
and will allow the POST
submission
can thus be the following:
<Location /app/login> <If "%{QUERY_STRING} !~ /^noext=1/"> <Limit GET> AuthType GSSAPI AuthName "Kerberos Login" GssapiCredStore keytab:/etc/http.keytab require valid-user ErrorDocument 401 /app/login?noext=1 </Limit> </If> </Location>
Redirect to different location
If we are able to use separate location / URI for the Kerberos
authentication and the form-based logon page, for example
/app/login2
, we can redirect
the browser to this second location. However, we cannot use
HTTP redirection status (302 or 303) to do that because we
need the HTTP response status to be 401 Unauthorized, for the
HTTP authentication to happen.
The solution is to use placeholder page which will just refresh
to the real logon-form location. The ErrorDocument
can take the direct content and the HTML needed is really minimal:
ErrorDocument 401 '<html> \ <meta http-equiv="refresh" content="0; URL=/app/login2"> \ <body>Kerberos authentication did not pass.</body></html>'
We can also store this HTML page in a static file and configure
ErrorDocument
to serve content from
this external file:
ErrorDocument 401 /app/logon-redirect.html
In the external file used for displaying the redirection, we can more comfortably design the “Kerberos failed” notification, especially if we want that information to be more prominent and we will use longer timeout for the refresh and perhaps show instructions for enabling Kerberos or obtaining the ticket-granting ticket.