]> CyberLeo.Net >> Repos - FreeBSD/stable/10.git/blob - contrib/serf/auth/auth_spnego.c
Copy head (r256279) to stable/10 as part of the 10.0-RELEASE cycle.
[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(char *token, apr_size_t token_len,
185                         const char *hostname,
186                         const char **buf, apr_size_t *buf_len,
187                         gss_authn_info_t *gss_info)
188 {
189     serf__spnego_buffer_t input_buf;
190     serf__spnego_buffer_t output_buf;
191     apr_status_t status = APR_SUCCESS;
192
193     /* If the server sent us a token, pass it to gss_init_sec_token for
194        validation. */
195     if (token) {
196         input_buf.value = token;
197         input_buf.length = token_len;
198     } else {
199         input_buf.value = 0;
200         input_buf.length = 0;
201     }
202
203     /* Establish a security context to the server. */
204     status = serf__spnego_init_sec_context(
205          gss_info->gss_ctx,
206          KRB_HTTP_SERVICE, hostname,
207          &input_buf,
208          &output_buf,
209          gss_info->pool,
210          gss_info->pool
211         );
212
213     switch(status) {
214     case APR_SUCCESS:
215         gss_info->state = gss_api_auth_completed;
216         break;
217     case APR_EAGAIN:
218         gss_info->state = gss_api_auth_in_progress;
219         status = APR_SUCCESS;
220         break;
221     default:
222         return status;
223     }
224
225     /* Return the session key to our caller. */
226     *buf = output_buf.value;
227     *buf_len = output_buf.length;
228
229     return status;
230 }
231
232 /* do_auth is invoked in two situations:
233    - when a response from a server is received that contains an authn header
234      (either from a 40x or 2xx response)
235    - when a request is prepared on a connection with stateless authentication.
236
237    Read the header sent by the server (if any), invoke the gssapi authn
238    code and use the resulting Server Ticket on the next request to the
239    server. */
240 static apr_status_t
241 do_auth(peer_t peer,
242         int code,
243         gss_authn_info_t *gss_info,
244         serf_connection_t *conn,
245         const char *auth_hdr,
246         apr_pool_t *pool)
247 {
248     serf_context_t *ctx = conn->ctx;
249     serf__authn_info_t *authn_info;
250     const char *tmp = NULL;
251     char *token = NULL;
252     apr_size_t tmp_len = 0, token_len = 0;
253     apr_status_t status;
254
255     if (peer == HOST) {
256         authn_info = serf__get_authn_info_for_server(conn);
257     } else {
258         authn_info = &ctx->proxy_authn_info;
259     }
260
261     /* Is this a response from a host/proxy? auth_hdr should always be set. */
262     if (code && auth_hdr) {
263         const char *space = NULL;
264         /* The server will return a token as attribute to the Negotiate key.
265            Negotiate YGwGCSqGSIb3EgECAgIAb10wW6ADAgEFoQMCAQ+iTzBNoAMCARCiRgREa6
266            mouMBAMFqKVdTGtfpZNXKzyw4Yo1paphJdIA3VOgncaoIlXxZLnkHiIHS2v65pVvrp
267            bRIyjF8xve9HxpnNIucCY9c=
268
269            Read this base64 value, decode it and validate it so we're sure the
270            server is who we expect it to be. */
271         space = strchr(auth_hdr, ' ');
272
273         if (space) {
274             token = apr_palloc(pool, apr_base64_decode_len(space + 1));
275             token_len = apr_base64_decode(token, space + 1);
276         }
277     } else {
278         /* This is a new request, not a retry in response to a 40x of the
279            host/proxy. 
280            Only add the Authorization header if we know the server requires
281            per-request authentication (stateless). */
282         if (gss_info->pstate != pstate_stateless)
283             return APR_SUCCESS;
284     }
285
286     switch(gss_info->pstate) {
287         case pstate_init:
288             /* Nothing to do here */
289             break;
290         case pstate_undecided: /* Fall through */
291         case pstate_stateful:
292             {
293                 /* Switch to stateless mode, from now on handle authentication
294                    of each request with a new gss context. This is easiest to
295                    manage when sending requests one by one. */
296                 serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
297                               "Server requires per-request SPNEGO authn, "
298                               "switching to stateless mode.\n");
299
300                 gss_info->pstate = pstate_stateless;
301                 serf_connection_set_max_outstanding_requests(conn, 1);
302                 break;
303             }
304         case pstate_stateless:
305             /* Nothing to do here */
306             break;
307     }
308
309     /* If the server didn't provide us with a token, start with a new initial
310        step in the SPNEGO authentication. */
311     if (!token) {
312         serf__spnego_reset_sec_context(gss_info->gss_ctx);
313         gss_info->state = gss_api_auth_not_started;
314     }
315
316     if (peer == HOST) {
317         status = gss_api_get_credentials(token, token_len,
318                                          conn->host_info.hostname,
319                                          &tmp, &tmp_len,
320                                          gss_info);
321     } else {
322         char *proxy_host;
323         apr_getnameinfo(&proxy_host, conn->ctx->proxy_address, 0);
324         status = gss_api_get_credentials(token, token_len, proxy_host,
325                                          &tmp, &tmp_len,
326                                          gss_info);
327     }
328     if (status)
329         return status;
330
331     /* On the next request, add an Authorization header. */
332     if (tmp_len) {
333         serf__encode_auth_header(&gss_info->value, authn_info->scheme->name,
334                                  tmp,
335                                  tmp_len,
336                                  pool);
337         gss_info->header = (peer == HOST) ?
338             "Authorization" : "Proxy-Authorization";
339     }
340
341     return APR_SUCCESS;
342 }
343
344 apr_status_t
345 serf__init_spnego(int code,
346                   serf_context_t *ctx,
347                   apr_pool_t *pool)
348 {
349     return APR_SUCCESS;
350 }
351
352 /* A new connection is created to a server that's known to use
353    Kerberos. */
354 apr_status_t
355 serf__init_spnego_connection(const serf__authn_scheme_t *scheme,
356                              int code,
357                              serf_connection_t *conn,
358                              apr_pool_t *pool)
359 {
360     gss_authn_info_t *gss_info;
361     apr_status_t status;
362
363     gss_info = apr_pcalloc(conn->pool, sizeof(*gss_info));
364     gss_info->pool = conn->pool;
365     gss_info->state = gss_api_auth_not_started;
366     gss_info->pstate = pstate_init;
367     status = serf__spnego_create_sec_context(&gss_info->gss_ctx, scheme,
368                                              gss_info->pool, pool);
369
370     if (status) {
371         return status;
372     }
373
374     if (code == 401) {
375         conn->authn_baton = gss_info;
376     } else {
377         conn->proxy_authn_baton = gss_info;
378     }
379
380     /* Make serf send the initial requests one by one */
381     serf_connection_set_max_outstanding_requests(conn, 1);
382
383     serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
384                   "Initialized Kerberos context for this connection.\n");
385
386     return APR_SUCCESS;
387 }
388
389 /* A 40x response was received, handle the authentication. */
390 apr_status_t
391 serf__handle_spnego_auth(int code,
392                          serf_request_t *request,
393                          serf_bucket_t *response,
394                          const char *auth_hdr,
395                          const char *auth_attr,
396                          void *baton,
397                          apr_pool_t *pool)
398 {
399     serf_connection_t *conn = request->conn;
400     gss_authn_info_t *gss_info = (code == 401) ? conn->authn_baton :
401         conn->proxy_authn_baton;
402
403     return do_auth(code == 401 ? HOST : PROXY,
404                    code,
405                    gss_info,
406                    request->conn,
407                    auth_hdr,
408                    pool);
409 }
410
411 /* Setup the authn headers on this request message. */
412 apr_status_t
413 serf__setup_request_spnego_auth(peer_t peer,
414                                 int code,
415                                 serf_connection_t *conn,
416                                 serf_request_t *request,
417                                 const char *method,
418                                 const char *uri,
419                                 serf_bucket_t *hdrs_bkt)
420 {
421     gss_authn_info_t *gss_info = (peer == HOST) ? conn->authn_baton :
422         conn->proxy_authn_baton;
423
424     /* If we have an ongoing authentication handshake, the handler of the
425        previous response will have created the authn headers for this request
426        already. */
427     if (gss_info && gss_info->header && gss_info->value) {
428         serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
429                       "Set Negotiate authn header on retried request.\n");
430
431         serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
432                                  gss_info->value);
433
434         /* We should send each token only once. */
435         gss_info->header = NULL;
436         gss_info->value = NULL;
437
438         return APR_SUCCESS;
439     }
440
441     switch (gss_info->pstate) {
442         case pstate_init:
443             /* We shouldn't normally arrive here, do nothing. */
444             break;
445         case pstate_undecided: /* fall through */
446             serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
447                           "Assume for now that the server supports persistent "
448                           "SPNEGO authentication.\n");
449             /* Nothing to do here. */
450             break;
451         case pstate_stateful:
452             serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
453                           "SPNEGO on this connection is persistent, "
454                           "don't set authn header on next request.\n");
455             /* Nothing to do here. */
456             break;
457         case pstate_stateless:
458             {
459                 apr_status_t status;
460
461                 /* Authentication on this connection is known to be stateless.
462                    Add an initial Negotiate token for the server, to bypass the
463                    40x response we know we'll otherwise receive.
464                   (RFC 4559 section 4.2) */
465                 serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
466                               "Add initial Negotiate header to request.\n");
467
468                 status = do_auth(peer,
469                                  code,
470                                  gss_info,
471                                  conn,
472                                  0l,    /* no response authn header */
473                                  conn->pool);
474                 if (status)
475                     return status;
476
477                 serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
478                                          gss_info->value);
479                 /* We should send each token only once. */
480                 gss_info->header = NULL;
481                 gss_info->value = NULL;
482                 break;
483             }
484     }
485
486     return APR_SUCCESS;
487 }
488
489 /* Function is called when 2xx responses are received. Normally we don't
490  * have to do anything, except for the first response after the
491  * authentication handshake. This specific response includes authentication
492  * data which should be validated by the client (mutual authentication).
493  */
494 apr_status_t
495 serf__validate_response_spnego_auth(peer_t peer,
496                                     int code,
497                                     serf_connection_t *conn,
498                                     serf_request_t *request,
499                                     serf_bucket_t *response,
500                                     apr_pool_t *pool)
501 {
502     gss_authn_info_t *gss_info;
503     const char *auth_hdr_name;
504
505     /* TODO: currently this function is only called when a response includes
506        an Authenticate header. This header is optional. If the server does
507        not provide this header on the first 2xx response, we will not promote
508        the connection from undecided to stateful. This won't break anything,
509        but means we stay in non-pipelining mode. */
510     serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
511                   "Validate Negotiate response header.\n");
512
513     if (peer == HOST) {
514         gss_info = conn->authn_baton;
515         auth_hdr_name = "WWW-Authenticate";
516     } else {
517         gss_info = conn->proxy_authn_baton;
518         auth_hdr_name = "Proxy-Authenticate";
519     }
520
521     if (gss_info->state != gss_api_auth_completed) {
522         serf_bucket_t *hdrs;
523         const char *auth_hdr_val;
524         apr_status_t status;
525
526         hdrs = serf_bucket_response_get_headers(response);
527         auth_hdr_val = serf_bucket_headers_get(hdrs, auth_hdr_name);
528
529         status = do_auth(peer, code, gss_info, conn, auth_hdr_val, pool);
530         if (status)
531             return status;
532     }
533
534     if (gss_info->state == gss_api_auth_completed) {
535         switch(gss_info->pstate) {
536             case pstate_init:
537                 /* Authentication of the first request is done. */
538                 gss_info->pstate = pstate_undecided;
539                 break;
540             case pstate_undecided:
541                 /* The server didn't request for authentication even though
542                    we didn't add an Authorization header to previous
543                    request. That means it supports persistent authentication. */
544                 gss_info->pstate = pstate_stateful;
545                 serf_connection_set_max_outstanding_requests(conn, 0);
546                 break;
547             default:
548                 /* Nothing to do here. */
549                 break;
550         }
551     }
552
553     return APR_SUCCESS;
554 }
555
556 #endif /* SERF_HAVE_SPNEGO */