]> CyberLeo.Net >> Repos - FreeBSD/stable/10.git/blob - contrib/serf/auth/auth_spnego.c
MFC r262324: serf 1.3.4 - improve SSL handling with svn-1.8.8 and other
[FreeBSD/stable/10.git] / contrib / serf / auth / auth_spnego.c
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. */
91 typedef 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  **/
150 typedef 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. */
162 typedef 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. */
183 static apr_status_t
184 gss_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. */
246 static apr_status_t
247 do_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;
339         apr_getnameinfo(&proxy_host, conn->ctx->proxy_address, 0);
340         status = gss_api_get_credentials(conn,
341                                          token, token_len, proxy_host,
342                                          &tmp, &tmp_len,
343                                          gss_info);
344     }
345     if (status)
346         return status;
347
348     /* On the next request, add an Authorization header. */
349     if (tmp_len) {
350         serf__encode_auth_header(&gss_info->value, authn_info->scheme->name,
351                                  tmp,
352                                  tmp_len,
353                                  pool);
354         gss_info->header = (peer == HOST) ?
355             "Authorization" : "Proxy-Authorization";
356     }
357
358     return APR_SUCCESS;
359 }
360
361 apr_status_t
362 serf__init_spnego(int code,
363                   serf_context_t *ctx,
364                   apr_pool_t *pool)
365 {
366     return APR_SUCCESS;
367 }
368
369 /* A new connection is created to a server that's known to use
370    Kerberos. */
371 apr_status_t
372 serf__init_spnego_connection(const serf__authn_scheme_t *scheme,
373                              int code,
374                              serf_connection_t *conn,
375                              apr_pool_t *pool)
376 {
377     serf_context_t *ctx = conn->ctx;
378     serf__authn_info_t *authn_info;
379     gss_authn_info_t *gss_info = NULL;
380
381     /* For proxy authentication, reuse the gss context for all connections. 
382        For server authentication, create a new gss context per connection. */
383     if (code == 401) {
384         authn_info = &conn->authn_info;
385     } else {
386         authn_info = &ctx->proxy_authn_info;
387     }
388     gss_info = authn_info->baton;
389
390     if (!gss_info) {
391         apr_status_t status;
392
393         gss_info = apr_pcalloc(conn->pool, sizeof(*gss_info));
394         gss_info->pool = conn->pool;
395         gss_info->state = gss_api_auth_not_started;
396         gss_info->pstate = pstate_init;
397         status = serf__spnego_create_sec_context(&gss_info->gss_ctx, scheme,
398                                                  gss_info->pool, pool);
399         if (status) {
400             return status;
401         }
402         authn_info->baton = gss_info;
403     }
404
405     /* Make serf send the initial requests one by one */
406     serf_connection_set_max_outstanding_requests(conn, 1);
407
408     serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
409                   "Initialized Kerberos context for this connection.\n");
410
411     return APR_SUCCESS;
412 }
413
414 /* A 40x response was received, handle the authentication. */
415 apr_status_t
416 serf__handle_spnego_auth(int code,
417                          serf_request_t *request,
418                          serf_bucket_t *response,
419                          const char *auth_hdr,
420                          const char *auth_attr,
421                          void *baton,
422                          apr_pool_t *pool)
423 {
424     serf_connection_t *conn = request->conn;
425     serf_context_t *ctx = conn->ctx;
426     gss_authn_info_t *gss_info = (code == 401) ? conn->authn_info.baton :
427                                                  ctx->proxy_authn_info.baton;
428
429     return do_auth(code == 401 ? HOST : PROXY,
430                    code,
431                    gss_info,
432                    request->conn,
433                    request,
434                    auth_hdr,
435                    pool);
436 }
437
438 /* Setup the authn headers on this request message. */
439 apr_status_t
440 serf__setup_request_spnego_auth(peer_t peer,
441                                 int code,
442                                 serf_connection_t *conn,
443                                 serf_request_t *request,
444                                 const char *method,
445                                 const char *uri,
446                                 serf_bucket_t *hdrs_bkt)
447 {
448     serf_context_t *ctx = conn->ctx;
449     gss_authn_info_t *gss_info = (peer == HOST) ? conn->authn_info.baton :
450                                                   ctx->proxy_authn_info.baton;
451
452     /* If we have an ongoing authentication handshake, the handler of the
453        previous response will have created the authn headers for this request
454        already. */
455     if (gss_info && gss_info->header && gss_info->value) {
456         serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
457                       "Set Negotiate authn header on retried request.\n");
458
459         serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
460                                  gss_info->value);
461
462         /* Remember that we're using this request for authentication
463            handshake. */
464         request->auth_baton = (void*) TRUE;
465
466         /* We should send each token only once. */
467         gss_info->header = NULL;
468         gss_info->value = NULL;
469
470         return APR_SUCCESS;
471     }
472
473     switch (gss_info->pstate) {
474         case pstate_init:
475             /* We shouldn't normally arrive here, do nothing. */
476             break;
477         case pstate_undecided: /* fall through */
478             serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
479                           "Assume for now that the server supports persistent "
480                           "SPNEGO authentication.\n");
481             /* Nothing to do here. */
482             break;
483         case pstate_stateful:
484             serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
485                           "SPNEGO on this connection is persistent, "
486                           "don't set authn header on next request.\n");
487             /* Nothing to do here. */
488             break;
489         case pstate_stateless:
490             {
491                 apr_status_t status;
492
493                 /* Authentication on this connection is known to be stateless.
494                    Add an initial Negotiate token for the server, to bypass the
495                    40x response we know we'll otherwise receive.
496                   (RFC 4559 section 4.2) */
497                 serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
498                               "Add initial Negotiate header to request.\n");
499
500                 status = do_auth(peer,
501                                  code,
502                                  gss_info,
503                                  conn,
504                                  request,
505                                  0l,    /* no response authn header */
506                                  conn->pool);
507                 if (status)
508                     return status;
509
510                 serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
511                                          gss_info->value);
512
513                 /* Remember that we're using this request for authentication
514                    handshake. */
515                 request->auth_baton = (void*) TRUE;
516
517                 /* We should send each token only once. */
518                 gss_info->header = NULL;
519                 gss_info->value = NULL;
520                 break;
521             }
522     }
523
524     return APR_SUCCESS;
525 }
526
527 /**
528  * Baton passed to the get_auth_header callback function.
529  */
530 typedef struct {
531     const char *hdr_name;
532     const char *auth_name;
533     const char *hdr_value;
534     apr_pool_t *pool;
535 } get_auth_header_baton_t;
536
537 static int
538 get_auth_header_cb(void *baton,
539                    const char *key,
540                    const char *header)
541 {
542     get_auth_header_baton_t *b = baton;
543
544     /* We're only interested in xxxx-Authenticate headers. */
545     if (strcasecmp(key, b->hdr_name) != 0)
546         return 0;
547
548     /* Check if header value starts with interesting auth name. */
549     if (strncmp(header, b->auth_name, strlen(b->auth_name)) == 0) {
550         /* Save interesting header value and stop iteration. */
551         b->hdr_value = apr_pstrdup(b->pool,  header);
552         return 1;
553     }
554
555     return 0;
556 }
557
558 static const char *
559 get_auth_header(serf_bucket_t *hdrs,
560                 const char *hdr_name,
561                 const char *auth_name,
562                 apr_pool_t *pool)
563 {
564     get_auth_header_baton_t b;
565
566     b.auth_name = hdr_name;
567     b.hdr_name = auth_name;
568     b.hdr_value = NULL;
569     b.pool = pool;
570
571     serf_bucket_headers_do(hdrs, get_auth_header_cb, &b);
572
573     return b.hdr_value;
574 }
575
576 /* Function is called when 2xx responses are received. Normally we don't
577  * have to do anything, except for the first response after the
578  * authentication handshake. This specific response includes authentication
579  * data which should be validated by the client (mutual authentication).
580  */
581 apr_status_t
582 serf__validate_response_spnego_auth(const serf__authn_scheme_t *scheme,
583                                     peer_t peer,
584                                     int code,
585                                     serf_connection_t *conn,
586                                     serf_request_t *request,
587                                     serf_bucket_t *response,
588                                     apr_pool_t *pool)
589 {
590     serf_context_t *ctx = conn->ctx;
591     gss_authn_info_t *gss_info;
592     const char *auth_hdr_name;
593
594     /* TODO: currently this function is only called when a response includes
595        an Authenticate header. This header is optional. If the server does
596        not provide this header on the first 2xx response, we will not promote
597        the connection from undecided to stateful. This won't break anything,
598        but means we stay in non-pipelining mode. */
599     serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
600                   "Validate Negotiate response header.\n");
601
602     if (peer == HOST) {
603         gss_info = conn->authn_info.baton;
604         auth_hdr_name = "WWW-Authenticate";
605     } else {
606         gss_info = ctx->proxy_authn_info.baton;
607         auth_hdr_name = "Proxy-Authenticate";
608     }
609
610     if (gss_info->state != gss_api_auth_completed) {
611         serf_bucket_t *hdrs;
612         const char *auth_hdr_val;
613         apr_status_t status;
614
615         hdrs = serf_bucket_response_get_headers(response);
616         auth_hdr_val = get_auth_header(hdrs, auth_hdr_name, scheme->name,
617                                        pool);
618
619         if (auth_hdr_val) {
620             status = do_auth(peer, code, gss_info, conn, request, auth_hdr_val,
621                              pool);
622             if (status) {
623                 return status;
624             }
625         } else {
626             /* No Authenticate headers, nothing to validate: authentication
627                completed.*/
628             gss_info->state = gss_api_auth_completed;
629
630             serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
631                           "SPNEGO handshake completed.\n");
632         }
633     }
634
635     if (gss_info->state == gss_api_auth_completed) {
636         switch(gss_info->pstate) {
637             case pstate_init:
638                 /* Authentication of the first request is done. */
639                 gss_info->pstate = pstate_undecided;
640                 break;
641             case pstate_undecided:
642                 /* The server didn't request for authentication even though
643                    we didn't add an Authorization header to previous
644                    request. That means it supports persistent authentication. */
645                 gss_info->pstate = pstate_stateful;
646                 serf_connection_set_max_outstanding_requests(conn, 0);
647                 break;
648             default:
649                 /* Nothing to do here. */
650                 break;
651         }
652     }
653
654     return APR_SUCCESS;
655 }
656
657 #endif /* SERF_HAVE_SPNEGO */