diff options
Diffstat (limited to 'auth/auth_kerb.c')
-rw-r--r-- | auth/auth_kerb.c | 358 |
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; |