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
gss_api_get_credentials(serf_connection_t * conn,char * token,apr_size_t token_len,const char * hostname,const char ** buf,apr_size_t * buf_len,gss_authn_info_t * gss_info)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
do_auth(peer_t peer,int code,gss_authn_info_t * gss_info,serf_connection_t * conn,serf_request_t * request,const char * auth_hdr,apr_pool_t * pool)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
serf__init_spnego(int code,serf_context_t * ctx,apr_pool_t * pool)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
serf__init_spnego_connection(const serf__authn_scheme_t * scheme,int code,serf_connection_t * conn,apr_pool_t * pool)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
serf__handle_spnego_auth(int code,serf_request_t * request,serf_bucket_t * response,const char * auth_hdr,const char * auth_attr,void * baton,apr_pool_t * pool)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
serf__setup_request_spnego_auth(peer_t peer,int code,serf_connection_t * conn,serf_request_t * request,const char * method,const char * uri,serf_bucket_t * hdrs_bkt)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
get_auth_header_cb(void * baton,const char * key,const char * header)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 *
get_auth_header(serf_bucket_t * hdrs,const char * hdr_name,const char * auth_name,apr_pool_t * pool)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
serf__validate_response_spnego_auth(const serf__authn_scheme_t * scheme,peer_t peer,int code,serf_connection_t * conn,serf_request_t * request,serf_bucket_t * response,apr_pool_t * pool)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 */
663