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