summaryrefslogtreecommitdiff
path: root/auth/auth_kerb.c
diff options
context:
space:
mode:
Diffstat (limited to 'auth/auth_kerb.c')
-rw-r--r--auth/auth_kerb.c358
1 files changed, 280 insertions, 78 deletions
diff --git a/auth/auth_kerb.c b/auth/auth_kerb.c
index f9be9a4..27fb968 100644
--- a/auth/auth_kerb.c
+++ b/auth/auth_kerb.c
@@ -17,7 +17,10 @@
#ifdef SERF_HAVE_KERB
-/*** Kerberos authentication ***/
+/** These functions implement SPNEGO-based Kerberos and NTLM authentication,
+ * using either GSS-API (RFC 2743) or SSPI on Windows.
+ * The HTTP message exchange is documented in RFC 4559.
+ **/
#include <serf.h>
#include <serf_private.h>
@@ -27,20 +30,13 @@
#include <apr_base64.h>
#include <apr_strings.h>
-/** These functions implements Kerberos authentication, using GSS-API
- * (RFC 2743). The message-exchange is documented in RFC 4559.
- *
- * Note: this implementation uses gssapi and only works on *nix.
- **/
-
/** TODO:
+ ** - This implements the SPNEGO mechanism, not Kerberos directly. Adapt
+ ** filename, functions & comments.
** - send session key directly on new connections where we already know
** the server requires Kerberos authn.
- ** - fix authn status, as the COMPLETE/CONTINUE status values
- ** are never used.
** - Add a way for serf to give detailed error information back to the
** application.
- ** - proxy support
**/
/* Authentication over HTTP using Kerberos
@@ -72,30 +68,95 @@
* WWW-Authenticate: Negotiate <Base64 encoded server
* authentication data>
*
- * -> The server returned a key to proof itself to us. We check this key
- * with the TGS again.
+ * -> The server returned an (optional) key to proof itself to us. We check this
+ * key with the TGS again. If it checks out, we can return the response
+ * body to the application.
*
- * Note: It's possible that the server returns 401 again in step 3, if the
- * Kerberos context isn't complete yet. Some (simple) tests with
- * mod_auth_kerb and MIT Kerberos 5 show this never happens.
+ * Note: It's possible that the server returns 401 again in step 2, if the
+ * Kerberos context isn't complete yet. This means there is 3rd step
+ * where we'll send a request with an Authorization header to the
+ * server. Some (simple) tests with mod_auth_kerb and MIT Kerberos 5 show
+ * this never happens.
*
- * This handshake is required for every new connection. If the handshake is
- * completed successfully, all other requests on the same connection will
- * be authenticated without needing to pass the WWW-Authenticate header.
+ * Depending on the type of HTTP server, this handshake is required for either
+ * every new connection, or for every new request! For more info see the next
+ * comment on authn_persistence_state_t.
*
* Note: Step 1 of the handshake will only happen on the first connection, once
* we know the server requires Kerberos authentication, the initial requests
- * on the other connections will include a session key, so we start at
+ * on the other connections will include a session key, so we start at
* step 2 in the handshake.
* ### TODO: Not implemented yet!
*/
+/* Current state of the authentication of the current request. */
typedef enum {
gss_api_auth_not_started,
gss_api_auth_in_progress,
gss_api_auth_completed,
} gss_api_auth_state;
+/**
+ authn_persistence_state_t: state that indicates if we are talking with a
+ server that requires authentication only of the first request (stateful),
+ or of each request (stateless).
+
+ INIT: Begin state. Authenticating the first request on this connection.
+ UNDECIDED: we haven't identified the server yet, assume STATEFUL for now.
+ Pipeline mode disabled, requests are sent only after the response off the
+ previous request arrived.
+ STATELESS: we know the server requires authentication for each request.
+ On all new requests add the Authorization header with an initial SPNEGO
+ token (created per request).
+ To keep things simple, keep the connection in one by one mode.
+ (otherwise we'd have to keep a queue of gssapi context objects to match
+ the Negotiate header of the response with the session initiated by the
+ mathing request).
+ This state is an final state.
+ STATEFUL: alright, we have authenticated the connection and for the server
+ that is enough. Don't add an Authorization header to new requests.
+ Serf will switch to pipelined mode.
+ This state is not a final state, although in practical scenario's it will
+ be. When we receive a 40x response from the server switch to STATELESS
+ mode.
+
+ We start in state init for the first request until it is authenticated.
+
+ The rest of the state machine starts with the arrival of the response to the
+ second request, and then goes on with each response:
+
+ --------
+ | INIT | C --> S: GET request in response to 40x of the server
+ -------- add [Proxy]-Authorization header
+ |
+ |
+ ------------
+ | UNDECIDED| C --> S: GET request, assume stateful,
+ ------------ no [Proxy]-Authorization header
+ |
+ |
+ |------------------------------------------------
+ | |
+ | C <-- S: 40x Authentication | C <-- S: 200 OK
+ | Required |
+ | |
+ v v
+ ------------- ------------
+ ->| STATELESS |<------------------------------| STATEFUL |<--
+ | ------------- C <-- S: 40x ------------ |
+ * | | Authentication | | 200 OK
+ | / Required | |
+ ----- -----/
+
+ **/
+typedef enum {
+ pstate_init,
+ pstate_undecided,
+ pstate_stateless,
+ pstate_stateful,
+} authn_persistence_state_t;
+
+
/* HTTP Service name, used to get the session key. */
#define KRB_HTTP_SERVICE "HTTP"
@@ -110,6 +171,9 @@ typedef struct
/* Current state of the authentication cycle. */
gss_api_auth_state state;
+ /* Current persistence state. */
+ authn_persistence_state_t pstate;
+
const char *header;
const char *value;
} gss_authn_info_t;
@@ -157,8 +221,7 @@ gss_api_get_credentials(char *token, apr_size_t token_len,
status = APR_SUCCESS;
break;
default:
- status = SERF_ERROR_AUTHN_FAILED;
- break;
+ return status;
}
/* Return the session key to our caller. */
@@ -168,65 +231,109 @@ gss_api_get_credentials(char *token, apr_size_t token_len,
return status;
}
-/* Read the header sent by the server (if any), invoke the gssapi authn
- code and use the resulting Server Ticket on the next request to the
+/* do_auth is invoked in two situations:
+ - when a response from a server is received that contains an authn header
+ (either from a 40x or 2xx response)
+ - when a request is prepared on a connection with stateless authentication.
+
+ Read the header sent by the server (if any), invoke the gssapi authn
+ code and use the resulting Server Ticket on the next request to the
server. */
static apr_status_t
-do_auth(int code,
+do_auth(peer_t peer,
+ int code,
gss_authn_info_t *gss_info,
serf_connection_t *conn,
const char *auth_hdr,
apr_pool_t *pool)
{
serf_context_t *ctx = conn->ctx;
- serf__authn_info_t *authn_info = (code == 401) ? &ctx->authn_info :
+ serf__authn_info_t *authn_info = (peer == HOST) ? &ctx->authn_info :
&ctx->proxy_authn_info;
const char *tmp = NULL;
char *token = NULL;
apr_size_t tmp_len = 0, token_len = 0;
- const char *space = NULL;
apr_status_t status;
- /* The server will return a token as attribute to the Negotiate key.
- Negotiate YGwGCSqGSIb3EgECAgIAb10wW6ADAgEFoQMCAQ+iTzBNoAMCARCiRgREa6mouM
- BAMFqKVdTGtfpZNXKzyw4Yo1paphJdIA3VOgncaoIlXxZLnkHiIHS2v65pVvrp
- bRIyjF8xve9HxpnNIucCY9c=
+ /* Is this a response from a host/proxy? auth_hdr should always be set. */
+ if (code && auth_hdr) {
+ const char *space = NULL;
+ /* The server will return a token as attribute to the Negotiate key.
+ Negotiate YGwGCSqGSIb3EgECAgIAb10wW6ADAgEFoQMCAQ+iTzBNoAMCARCiRgREa6
+ mouMBAMFqKVdTGtfpZNXKzyw4Yo1paphJdIA3VOgncaoIlXxZLnkHiIHS2v65pVvrp
+ bRIyjF8xve9HxpnNIucCY9c=
- Read this base64 value, decode it and validate it so we're sure the server
- is who we expect it to be. */
- if (auth_hdr)
+ Read this base64 value, decode it and validate it so we're sure the
+ server is who we expect it to be. */
space = strchr(auth_hdr, ' ');
- if (space) {
- token = apr_palloc(pool, apr_base64_decode_len(space + 1));
- token_len = apr_base64_decode(token, space + 1);
+ if (space) {
+ token = apr_palloc(pool, apr_base64_decode_len(space + 1));
+ token_len = apr_base64_decode(token, space + 1);
+ }
+ } else {
+ /* This is a new request, not a retry in response to a 40x of the
+ host/proxy.
+ Only add the Authorization header if we know the server requires
+ per-request authentication (stateless). */
+ if (gss_info->pstate != pstate_stateless)
+ return APR_SUCCESS;
}
- /* We can get a whole batch of 401 responses from the server, but we should
- only start the authentication phase once, so if we started authentication
- already ignore all responses with initial Negotiate authentication header.
+ switch(gss_info->pstate) {
+ case pstate_init:
+ /* Nothing to do here */
+ break;
+ case pstate_undecided: /* Fall through */
+ case pstate_stateful:
+ {
+ /* Switch to stateless mode, from now on handle authentication
+ of each request with a new gss context. This is easiest to
+ manage when sending requests one by one. */
+ serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
+ "Server requires per-request SPNEGO authn, "
+ "switching to stateless mode.\n");
+
+ gss_info->pstate = pstate_stateless;
+ serf_connection_set_max_outstanding_requests(conn, 1);
+ break;
+ }
+ case pstate_stateless:
+ /* Nothing to do here */
+ break;
+ }
- Note: as we set the max. transfer rate to one message at a time until the
- authentication cycle is finished, this check shouldn't be needed. */
- if (!token && gss_info->state != gss_api_auth_not_started)
- return APR_SUCCESS;
+ /* If the server didn't provide us with a token, start with a new initial
+ step in the SPNEGO authentication. */
+ if (!token) {
+ serf__kerb_reset_sec_context(gss_info->gss_ctx);
+ gss_info->state = gss_api_auth_not_started;
+ }
- status = gss_api_get_credentials(token, token_len, conn->host_info.hostname,
- &tmp, &tmp_len,
- gss_info);
+ if (peer == HOST) {
+ status = gss_api_get_credentials(token, token_len,
+ conn->host_info.hostname,
+ &tmp, &tmp_len,
+ gss_info);
+ } else {
+ char *proxy_host;
+ apr_getnameinfo(&proxy_host, conn->ctx->proxy_address, 0);
+ status = gss_api_get_credentials(token, token_len, proxy_host,
+ &tmp, &tmp_len,
+ gss_info);
+ }
if (status)
return status;
- serf__encode_auth_header(&gss_info->value, authn_info->scheme->name,
- tmp,
- tmp_len,
- pool);
- gss_info->header = (code == 401) ? "Authorization" : "Proxy-Authorization";
-
- /* If the handshake is finished tell serf it can send as much requests as it
- likes. */
- if (gss_info->state == gss_api_auth_completed)
- serf_connection_set_max_outstanding_requests(conn, 0);
+ /* On the next request, add an Authorization header. */
+ if (tmp_len) {
+ serf__encode_auth_header(&gss_info->value, authn_info->scheme->name,
+ tmp,
+ tmp_len,
+ pool);
+ gss_info->header = (peer == HOST) ?
+ "Authorization" : "Proxy-Authorization";
+ }
return APR_SUCCESS;
}
@@ -252,6 +359,7 @@ serf__init_kerb_connection(int code,
gss_info = apr_pcalloc(pool, sizeof(*gss_info));
gss_info->pool = conn->pool;
gss_info->state = gss_api_auth_not_started;
+ gss_info->pstate = pstate_init;
status = serf__kerb_create_sec_context(&gss_info->gss_ctx, pool,
gss_info->pool);
@@ -268,10 +376,13 @@ serf__init_kerb_connection(int code,
/* Make serf send the initial requests one by one */
serf_connection_set_max_outstanding_requests(conn, 1);
+ serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
+ "Initialized Kerberos context for this connection.\n");
+
return APR_SUCCESS;
}
-/* A 401 response was received, handle the authentication. */
+/* A 40x response was received, handle the authentication. */
apr_status_t
serf__handle_kerb_auth(int code,
serf_request_t *request,
@@ -285,7 +396,8 @@ serf__handle_kerb_auth(int code,
gss_authn_info_t *gss_info = (code == 401) ? conn->authn_baton :
conn->proxy_authn_baton;
- return do_auth(code,
+ return do_auth(code == 401 ? HOST : PROXY,
+ code,
gss_info,
request->conn,
auth_hdr,
@@ -294,26 +406,80 @@ serf__handle_kerb_auth(int code,
/* Setup the authn headers on this request message. */
apr_status_t
-serf__setup_request_kerb_auth(int code,
+serf__setup_request_kerb_auth(peer_t peer,
+ int code,
serf_connection_t *conn,
+ serf_request_t *request,
const char *method,
const char *uri,
serf_bucket_t *hdrs_bkt)
{
- gss_authn_info_t *gss_info = (code == 401) ? conn->authn_baton :
+ gss_authn_info_t *gss_info = (peer == HOST) ? conn->authn_baton :
conn->proxy_authn_baton;
+ /* If we have an ongoing authentication handshake, the handler of the
+ previous response will have created the authn headers for this request
+ already. */
if (gss_info && gss_info->header && gss_info->value) {
+ serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
+ "Set Negotiate authn header on retried request.\n");
+
serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
gss_info->value);
/* We should send each token only once. */
gss_info->header = NULL;
gss_info->value = NULL;
+
return APR_SUCCESS;
}
- return SERF_ERROR_AUTHN_FAILED;
+ switch (gss_info->pstate) {
+ case pstate_init:
+ /* We shouldn't normally arrive here, do nothing. */
+ break;
+ case pstate_undecided: /* fall through */
+ serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
+ "Assume for now that the server supports persistent "
+ "SPNEGO authentication.\n");
+ /* Nothing to do here. */
+ break;
+ case pstate_stateful:
+ serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
+ "SPNEGO on this connection is persistent, "
+ "don't set authn header on next request.\n");
+ /* Nothing to do here. */
+ break;
+ case pstate_stateless:
+ {
+ apr_status_t status;
+
+ /* Authentication on this connection is known to be stateless.
+ Add an initial Negotiate token for the server, to bypass the
+ 40x response we know we'll otherwise receive.
+ (RFC 4559 section 4.2) */
+ serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
+ "Add initial Negotiate header to request.\n");
+
+ status = do_auth(peer,
+ code,
+ gss_info,
+ conn,
+ 0l, /* no response authn header */
+ conn->pool);
+ if (status)
+ return status;
+
+ serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
+ gss_info->value);
+ /* We should send each token only once. */
+ gss_info->header = NULL;
+ gss_info->value = NULL;
+ break;
+ }
+ }
+
+ return APR_SUCCESS;
}
/* Function is called when 2xx responses are received. Normally we don't
@@ -322,26 +488,62 @@ serf__setup_request_kerb_auth(int code,
* data which should be validated by the client (mutual authentication).
*/
apr_status_t
-serf__validate_response_kerb_auth(int code,
- serf_connection_t *conn,
- serf_request_t *request,
- serf_bucket_t *response,
- apr_pool_t *pool)
+serf__validate_response_kerb_auth(peer_t peer,
+ int code,
+ serf_connection_t *conn,
+ serf_request_t *request,
+ serf_bucket_t *response,
+ apr_pool_t *pool)
{
- gss_authn_info_t *gss_info = (code == 401) ? conn->authn_baton :
- conn->proxy_authn_baton;
- serf_bucket_t *hdrs;
- const char *auth_hdr;
-
- hdrs = serf_bucket_response_get_headers(response);
- auth_hdr = serf_bucket_headers_get(hdrs, "WWW-Authenticate");
+ gss_authn_info_t *gss_info;
+ const char *auth_hdr_name;
+
+ /* TODO: currently this function is only called when a response includes
+ an Authenticate header. This header is optional. If the server does
+ not provide this header on the first 2xx response, we will not promote
+ the connection from undecided to stateful. This won't break anything,
+ but means we stay in non-pipelining mode. */
+ serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
+ "Validate Negotiate response header.\n");
+
+ if (peer == HOST) {
+ gss_info = conn->authn_baton;
+ auth_hdr_name = "WWW-Authenticate";
+ } else {
+ gss_info = conn->proxy_authn_baton;
+ auth_hdr_name = "Proxy-Authenticate";
+ }
if (gss_info->state != gss_api_auth_completed) {
- return do_auth(code,
- gss_info,
- conn,
- auth_hdr,
- pool);
+ serf_bucket_t *hdrs;
+ const char *auth_hdr_val;
+ apr_status_t status;
+
+ hdrs = serf_bucket_response_get_headers(response);
+ auth_hdr_val = serf_bucket_headers_get(hdrs, auth_hdr_name);
+
+ status = do_auth(peer, code, gss_info, conn, auth_hdr_val, pool);
+ if (status)
+ return status;
+ }
+
+ if (gss_info->state == gss_api_auth_completed) {
+ switch(gss_info->pstate) {
+ case pstate_init:
+ /* Authentication of the first request is done. */
+ gss_info->pstate = pstate_undecided;
+ break;
+ case pstate_undecided:
+ /* The server didn't request for authentication even though
+ we didn't add an Authorization header to previous
+ request. That means it supports persistent authentication. */
+ gss_info->pstate = pstate_stateful;
+ serf_connection_set_max_outstanding_requests(conn, 0);
+ break;
+ default:
+ /* Nothing to do here. */
+ break;
+ }
}
return APR_SUCCESS;