auth_spnego.c revision 269847
1/* Copyright 2009 Justin Erenkrantz and Greg Stein
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16#include "auth_spnego.h"
17
18#ifdef SERF_HAVE_SPNEGO
19
20/** These functions implement SPNEGO-based Kerberos and NTLM authentication,
21 *  using either GSS-API (RFC 2743) or SSPI on Windows.
22 *  The HTTP message exchange is documented in RFC 4559.
23 **/
24
25#include <serf.h>
26#include <serf_private.h>
27#include <auth/auth.h>
28
29#include <apr.h>
30#include <apr_base64.h>
31#include <apr_strings.h>
32
33/** TODO:
34 ** - send session key directly on new connections where we already know
35 **   the server requires Kerberos authn.
36 ** - Add a way for serf to give detailed error information back to the
37 **   application.
38 **/
39
40/* Authentication over HTTP using Kerberos
41 *
42 * Kerberos involves three servers:
43 * - Authentication Server (AS): verifies users during login
44 * - Ticket-Granting Server (TGS): issues proof of identity tickets
45 * - HTTP server (S)
46 *
47 * Steps:
48 * 0. User logs in to the AS and receives a TGS ticket. On workstations
49 * where the login program doesn't support Kerberos, the user can use
50 * 'kinit'.
51 *
52 * 1. C  --> S:    GET
53 *
54 *    C <--  S:    401 Authentication Required
55 *                 WWW-Authenticate: Negotiate
56 *
57 * -> app contacts the TGS to request a session key for the HTTP service
58 *    @ target host. The returned session key is encrypted with the HTTP
59 *    service's secret key, so we can safely send it to the server.
60 *
61 * 2. C  --> S:    GET
62 *                 Authorization: Negotiate <Base64 encoded session key>
63 *                 gss_api_ctx->state = gss_api_auth_in_progress;
64 *
65 *    C <--  S:    200 OK
66 *                 WWW-Authenticate: Negotiate <Base64 encoded server
67 *                                              authentication data>
68 *
69 * -> The server returned an (optional) key to proof itself to us. We check this
70 *    key with the TGS again. If it checks out, we can return the response
71 *    body to the application.
72 *
73 * Note: It's possible that the server returns 401 again in step 2, if the
74 *       Kerberos context isn't complete yet. This means there is 3rd step
75 *       where we'll send a request with an Authorization header to the
76 *       server. Some (simple) tests with mod_auth_kerb and MIT Kerberos 5 show
77 *       this never happens.
78 *
79 * Depending on the type of HTTP server, this handshake is required for either
80 * every new connection, or for every new request! For more info see the next
81 * comment on authn_persistence_state_t.
82 *
83 * Note: Step 1 of the handshake will only happen on the first connection, once
84 * we know the server requires Kerberos authentication, the initial requests
85 * on the other connections will include a session key, so we start at
86 * step 2 in the handshake.
87 * ### TODO: Not implemented yet!
88 */
89
90/* Current state of the authentication of the current request. */
91typedef enum {
92    gss_api_auth_not_started,
93    gss_api_auth_in_progress,
94    gss_api_auth_completed,
95} gss_api_auth_state;
96
97/**
98   authn_persistence_state_t: state that indicates if we are talking with a
99   server that requires authentication only of the first request (stateful),
100   or of each request (stateless).
101
102   INIT: Begin state. Authenticating the first request on this connection.
103   UNDECIDED: we haven't identified the server yet, assume STATEFUL for now.
104     Pipeline mode disabled, requests are sent only after the response off the
105     previous request arrived.
106   STATELESS: we know the server requires authentication for each request.
107     On all new requests add the Authorization header with an initial SPNEGO
108     token (created per request).
109     To keep things simple, keep the connection in one by one mode.
110     (otherwise we'd have to keep a queue of gssapi context objects to match
111      the Negotiate header of the response with the session initiated by the
112      mathing request).
113     This state is an final state.
114   STATEFUL: alright, we have authenticated the connection and for the server
115     that is enough. Don't add an Authorization header to new requests.
116     Serf will switch to pipelined mode.
117     This state is not a final state, although in practical scenario's it will
118     be. When we receive a 40x response from the server switch to STATELESS
119     mode.
120
121   We start in state init for the first request until it is authenticated.
122
123   The rest of the state machine starts with the arrival of the response to the
124   second request, and then goes on with each response:
125
126      --------
127      | INIT |     C --> S:    GET request in response to 40x of the server
128      --------                 add [Proxy]-Authorization header
129          |
130          |
131    ------------
132    | UNDECIDED|   C --> S:    GET request, assume stateful,
133    ------------               no [Proxy]-Authorization header
134          |
135          |
136          |------------------------------------------------
137          |                                               |
138          | C <-- S: 40x Authentication                   | C <-- S: 200 OK
139          |          Required                             |
140          |                                               |
141          v                                               v
142      -------------                               ------------
143    ->| STATELESS |<------------------------------| STATEFUL |<--
144    | -------------       C <-- S: 40x            ------------  |
145  * |    |                Authentication                  |     | 200 OK
146    |    /                Required                        |     |
147    -----                                                 -----/
148
149 **/
150typedef enum {
151    pstate_init,
152    pstate_undecided,
153    pstate_stateless,
154    pstate_stateful,
155} authn_persistence_state_t;
156
157
158/* HTTP Service name, used to get the session key.  */
159#define KRB_HTTP_SERVICE "HTTP"
160
161/* Stores the context information related to Kerberos authentication. */
162typedef struct
163{
164    apr_pool_t *pool;
165
166    /* GSSAPI context */
167    serf__spnego_context_t *gss_ctx;
168
169    /* Current state of the authentication cycle. */
170    gss_api_auth_state state;
171
172    /* Current persistence state. */
173    authn_persistence_state_t pstate;
174
175    const char *header;
176    const char *value;
177} gss_authn_info_t;
178
179/* On the initial 401 response of the server, request a session key from
180   the Kerberos KDC to pass to the server, proving that we are who we
181   claim to be. The session key can only be used with the HTTP service
182   on the target host. */
183static apr_status_t
184gss_api_get_credentials(serf_connection_t *conn,
185                        char *token, apr_size_t token_len,
186                        const char *hostname,
187                        const char **buf, apr_size_t *buf_len,
188                        gss_authn_info_t *gss_info)
189{
190    serf__spnego_buffer_t input_buf;
191    serf__spnego_buffer_t output_buf;
192    apr_status_t status = APR_SUCCESS;
193
194    /* If the server sent us a token, pass it to gss_init_sec_token for
195       validation. */
196    if (token) {
197        input_buf.value = token;
198        input_buf.length = token_len;
199    } else {
200        input_buf.value = 0;
201        input_buf.length = 0;
202    }
203
204    /* Establish a security context to the server. */
205    status = serf__spnego_init_sec_context(
206         conn,
207         gss_info->gss_ctx,
208         KRB_HTTP_SERVICE, hostname,
209         &input_buf,
210         &output_buf,
211         gss_info->pool,
212         gss_info->pool
213        );
214
215    switch(status) {
216    case APR_SUCCESS:
217        if (output_buf.length == 0) {
218            gss_info->state = gss_api_auth_completed;
219        } else {
220            gss_info->state = gss_api_auth_in_progress;
221        }
222        break;
223    case APR_EAGAIN:
224        gss_info->state = gss_api_auth_in_progress;
225        status = APR_SUCCESS;
226        break;
227    default:
228        return status;
229    }
230
231    /* Return the session key to our caller. */
232    *buf = output_buf.value;
233    *buf_len = output_buf.length;
234
235    return status;
236}
237
238/* do_auth is invoked in two situations:
239   - when a response from a server is received that contains an authn header
240     (either from a 40x or 2xx response)
241   - when a request is prepared on a connection with stateless authentication.
242
243   Read the header sent by the server (if any), invoke the gssapi authn
244   code and use the resulting Server Ticket on the next request to the
245   server. */
246static apr_status_t
247do_auth(peer_t peer,
248        int code,
249        gss_authn_info_t *gss_info,
250        serf_connection_t *conn,
251        serf_request_t *request,
252        const char *auth_hdr,
253        apr_pool_t *pool)
254{
255    serf_context_t *ctx = conn->ctx;
256    serf__authn_info_t *authn_info;
257    const char *tmp = NULL;
258    char *token = NULL;
259    apr_size_t tmp_len = 0, token_len = 0;
260    apr_status_t status;
261
262    if (peer == HOST) {
263        authn_info = serf__get_authn_info_for_server(conn);
264    } else {
265        authn_info = &ctx->proxy_authn_info;
266    }
267
268    /* Is this a response from a host/proxy? auth_hdr should always be set. */
269    if (code && auth_hdr) {
270        const char *space = NULL;
271        /* The server will return a token as attribute to the Negotiate key.
272           Negotiate YGwGCSqGSIb3EgECAgIAb10wW6ADAgEFoQMCAQ+iTzBNoAMCARCiRgREa6
273           mouMBAMFqKVdTGtfpZNXKzyw4Yo1paphJdIA3VOgncaoIlXxZLnkHiIHS2v65pVvrp
274           bRIyjF8xve9HxpnNIucCY9c=
275
276           Read this base64 value, decode it and validate it so we're sure the
277           server is who we expect it to be. */
278        space = strchr(auth_hdr, ' ');
279
280        if (space) {
281            token = apr_palloc(pool, apr_base64_decode_len(space + 1));
282            token_len = apr_base64_decode(token, space + 1);
283        }
284    } else {
285        /* This is a new request, not a retry in response to a 40x of the
286           host/proxy.
287           Only add the Authorization header if we know the server requires
288           per-request authentication (stateless). */
289        if (gss_info->pstate != pstate_stateless)
290            return APR_SUCCESS;
291    }
292
293    switch(gss_info->pstate) {
294        case pstate_init:
295            /* Nothing to do here */
296            break;
297        case pstate_undecided: /* Fall through */
298        case pstate_stateful:
299            {
300                /* Switch to stateless mode, from now on handle authentication
301                   of each request with a new gss context. This is easiest to
302                   manage when sending requests one by one. */
303                serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
304                              "Server requires per-request SPNEGO authn, "
305                              "switching to stateless mode.\n");
306
307                gss_info->pstate = pstate_stateless;
308                serf_connection_set_max_outstanding_requests(conn, 1);
309                break;
310            }
311        case pstate_stateless:
312            /* Nothing to do here */
313            break;
314    }
315
316    if (request->auth_baton && !token) {
317        /* We provided token with this request, but server responded with empty
318           authentication header. This means server rejected our credentials.
319           XXX: Probably we need separate error code for this case like
320           SERF_ERROR_AUTHN_CREDS_REJECTED? */
321        return SERF_ERROR_AUTHN_FAILED;
322    }
323
324    /* If the server didn't provide us with a token, start with a new initial
325       step in the SPNEGO authentication. */
326    if (!token) {
327        serf__spnego_reset_sec_context(gss_info->gss_ctx);
328        gss_info->state = gss_api_auth_not_started;
329    }
330
331    if (peer == HOST) {
332        status = gss_api_get_credentials(conn,
333                                         token, token_len,
334                                         conn->host_info.hostname,
335                                         &tmp, &tmp_len,
336                                         gss_info);
337    } else {
338        char *proxy_host = conn->ctx->proxy_address->hostname;
339        status = gss_api_get_credentials(conn,
340                                         token, token_len, proxy_host,
341                                         &tmp, &tmp_len,
342                                         gss_info);
343    }
344    if (status)
345        return status;
346
347    /* On the next request, add an Authorization header. */
348    if (tmp_len) {
349        serf__encode_auth_header(&gss_info->value, authn_info->scheme->name,
350                                 tmp,
351                                 tmp_len,
352                                 pool);
353        gss_info->header = (peer == HOST) ?
354            "Authorization" : "Proxy-Authorization";
355    }
356
357    return APR_SUCCESS;
358}
359
360apr_status_t
361serf__init_spnego(int code,
362                  serf_context_t *ctx,
363                  apr_pool_t *pool)
364{
365    return APR_SUCCESS;
366}
367
368/* A new connection is created to a server that's known to use
369   Kerberos. */
370apr_status_t
371serf__init_spnego_connection(const serf__authn_scheme_t *scheme,
372                             int code,
373                             serf_connection_t *conn,
374                             apr_pool_t *pool)
375{
376    serf_context_t *ctx = conn->ctx;
377    serf__authn_info_t *authn_info;
378    gss_authn_info_t *gss_info = NULL;
379
380    /* For proxy authentication, reuse the gss context for all connections.
381       For server authentication, create a new gss context per connection. */
382    if (code == 401) {
383        authn_info = &conn->authn_info;
384    } else {
385        authn_info = &ctx->proxy_authn_info;
386    }
387    gss_info = authn_info->baton;
388
389    if (!gss_info) {
390        apr_status_t status;
391
392        gss_info = apr_pcalloc(conn->pool, sizeof(*gss_info));
393        gss_info->pool = conn->pool;
394        gss_info->state = gss_api_auth_not_started;
395        gss_info->pstate = pstate_init;
396        status = serf__spnego_create_sec_context(&gss_info->gss_ctx, scheme,
397                                                 gss_info->pool, pool);
398        if (status) {
399            return status;
400        }
401        authn_info->baton = gss_info;
402    }
403
404    /* Make serf send the initial requests one by one */
405    serf_connection_set_max_outstanding_requests(conn, 1);
406
407    serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
408                  "Initialized Kerberos context for this connection.\n");
409
410    return APR_SUCCESS;
411}
412
413/* A 40x response was received, handle the authentication. */
414apr_status_t
415serf__handle_spnego_auth(int code,
416                         serf_request_t *request,
417                         serf_bucket_t *response,
418                         const char *auth_hdr,
419                         const char *auth_attr,
420                         void *baton,
421                         apr_pool_t *pool)
422{
423    serf_connection_t *conn = request->conn;
424    serf_context_t *ctx = conn->ctx;
425    gss_authn_info_t *gss_info = (code == 401) ? conn->authn_info.baton :
426                                                 ctx->proxy_authn_info.baton;
427
428    return do_auth(code == 401 ? HOST : PROXY,
429                   code,
430                   gss_info,
431                   request->conn,
432                   request,
433                   auth_hdr,
434                   pool);
435}
436
437/* Setup the authn headers on this request message. */
438apr_status_t
439serf__setup_request_spnego_auth(peer_t peer,
440                                int code,
441                                serf_connection_t *conn,
442                                serf_request_t *request,
443                                const char *method,
444                                const char *uri,
445                                serf_bucket_t *hdrs_bkt)
446{
447    serf_context_t *ctx = conn->ctx;
448    gss_authn_info_t *gss_info = (peer == HOST) ? conn->authn_info.baton :
449                                                  ctx->proxy_authn_info.baton;
450
451    /* If we have an ongoing authentication handshake, the handler of the
452       previous response will have created the authn headers for this request
453       already. */
454    if (gss_info && gss_info->header && gss_info->value) {
455        serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
456                      "Set Negotiate authn header on retried request.\n");
457
458        serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
459                                 gss_info->value);
460
461        /* Remember that we're using this request for authentication
462           handshake. */
463        request->auth_baton = (void*) TRUE;
464
465        /* We should send each token only once. */
466        gss_info->header = NULL;
467        gss_info->value = NULL;
468
469        return APR_SUCCESS;
470    }
471
472    switch (gss_info->pstate) {
473        case pstate_init:
474            /* We shouldn't normally arrive here, do nothing. */
475            break;
476        case pstate_undecided: /* fall through */
477            serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
478                          "Assume for now that the server supports persistent "
479                          "SPNEGO authentication.\n");
480            /* Nothing to do here. */
481            break;
482        case pstate_stateful:
483            serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
484                          "SPNEGO on this connection is persistent, "
485                          "don't set authn header on next request.\n");
486            /* Nothing to do here. */
487            break;
488        case pstate_stateless:
489            {
490                apr_status_t status;
491
492                /* Authentication on this connection is known to be stateless.
493                   Add an initial Negotiate token for the server, to bypass the
494                   40x response we know we'll otherwise receive.
495                  (RFC 4559 section 4.2) */
496                serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
497                              "Add initial Negotiate header to request.\n");
498
499                status = do_auth(peer,
500                                 code,
501                                 gss_info,
502                                 conn,
503                                 request,
504                                 0l,    /* no response authn header */
505                                 conn->pool);
506                if (status)
507                    return status;
508
509                serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
510                                         gss_info->value);
511
512                /* Remember that we're using this request for authentication
513                   handshake. */
514                request->auth_baton = (void*) TRUE;
515
516                /* We should send each token only once. */
517                gss_info->header = NULL;
518                gss_info->value = NULL;
519                break;
520            }
521    }
522
523    return APR_SUCCESS;
524}
525
526/**
527 * Baton passed to the get_auth_header callback function.
528 */
529typedef struct {
530    const char *hdr_name;
531    const char *auth_name;
532    const char *hdr_value;
533    apr_pool_t *pool;
534} get_auth_header_baton_t;
535
536static int
537get_auth_header_cb(void *baton,
538                   const char *key,
539                   const char *header)
540{
541    get_auth_header_baton_t *b = baton;
542
543    /* We're only interested in xxxx-Authenticate headers. */
544    if (strcasecmp(key, b->hdr_name) != 0)
545        return 0;
546
547    /* Check if header value starts with interesting auth name. */
548    if (strncmp(header, b->auth_name, strlen(b->auth_name)) == 0) {
549        /* Save interesting header value and stop iteration. */
550        b->hdr_value = apr_pstrdup(b->pool,  header);
551        return 1;
552    }
553
554    return 0;
555}
556
557static const char *
558get_auth_header(serf_bucket_t *hdrs,
559                const char *hdr_name,
560                const char *auth_name,
561                apr_pool_t *pool)
562{
563    get_auth_header_baton_t b;
564
565    b.auth_name = hdr_name;
566    b.hdr_name = auth_name;
567    b.hdr_value = NULL;
568    b.pool = pool;
569
570    serf_bucket_headers_do(hdrs, get_auth_header_cb, &b);
571
572    return b.hdr_value;
573}
574
575/* Function is called when 2xx responses are received. Normally we don't
576 * have to do anything, except for the first response after the
577 * authentication handshake. This specific response includes authentication
578 * data which should be validated by the client (mutual authentication).
579 */
580apr_status_t
581serf__validate_response_spnego_auth(const serf__authn_scheme_t *scheme,
582                                    peer_t peer,
583                                    int code,
584                                    serf_connection_t *conn,
585                                    serf_request_t *request,
586                                    serf_bucket_t *response,
587                                    apr_pool_t *pool)
588{
589    serf_context_t *ctx = conn->ctx;
590    gss_authn_info_t *gss_info;
591    const char *auth_hdr_name;
592
593    /* TODO: currently this function is only called when a response includes
594       an Authenticate header. This header is optional. If the server does
595       not provide this header on the first 2xx response, we will not promote
596       the connection from undecided to stateful. This won't break anything,
597       but means we stay in non-pipelining mode. */
598    serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
599                  "Validate Negotiate response header.\n");
600
601    if (peer == HOST) {
602        gss_info = conn->authn_info.baton;
603        auth_hdr_name = "WWW-Authenticate";
604    } else {
605        gss_info = ctx->proxy_authn_info.baton;
606        auth_hdr_name = "Proxy-Authenticate";
607    }
608
609    if (gss_info->state != gss_api_auth_completed) {
610        serf_bucket_t *hdrs;
611        const char *auth_hdr_val;
612        apr_status_t status;
613
614        hdrs = serf_bucket_response_get_headers(response);
615        auth_hdr_val = get_auth_header(hdrs, auth_hdr_name, scheme->name,
616                                       pool);
617
618        if (auth_hdr_val) {
619            status = do_auth(peer, code, gss_info, conn, request, auth_hdr_val,
620                             pool);
621            if (status) {
622                return status;
623            }
624        } else {
625            /* No Authenticate headers, nothing to validate: authentication
626               completed.*/
627            gss_info->state = gss_api_auth_completed;
628
629            serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
630                          "SPNEGO handshake completed.\n");
631        }
632    }
633
634    if (gss_info->state == gss_api_auth_completed) {
635        switch(gss_info->pstate) {
636            case pstate_init:
637                /* Authentication of the first request is done. */
638                gss_info->pstate = pstate_undecided;
639                break;
640            case pstate_undecided:
641                /* The server didn't request for authentication even though
642                   we didn't add an Authorization header to previous
643                   request. That means it supports persistent authentication. */
644                gss_info->pstate = pstate_stateful;
645                serf_connection_set_max_outstanding_requests(conn, 0);
646                break;
647            default:
648                /* Nothing to do here. */
649                break;
650        }
651    }
652
653    return APR_SUCCESS;
654}
655
656#endif /* SERF_HAVE_SPNEGO */
657