adelton

Optional Kerberos Authentication

Jan Pazdziora

2015-05-22


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:

Logon form

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 ServerApplication
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 ServerApplication
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.