auth_spnego.c revision 262339
1/* Copyright 2009 Justin Erenkrantz and Greg Stein 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16#include "auth_spnego.h" 17 18#ifdef SERF_HAVE_SPNEGO 19 20/** These functions implement SPNEGO-based Kerberos and NTLM authentication, 21 * using either GSS-API (RFC 2743) or SSPI on Windows. 22 * The HTTP message exchange is documented in RFC 4559. 23 **/ 24 25#include <serf.h> 26#include <serf_private.h> 27#include <auth/auth.h> 28 29#include <apr.h> 30#include <apr_base64.h> 31#include <apr_strings.h> 32 33/** TODO: 34 ** - send session key directly on new connections where we already know 35 ** the server requires Kerberos authn. 36 ** - Add a way for serf to give detailed error information back to the 37 ** application. 38 **/ 39 40/* Authentication over HTTP using Kerberos 41 * 42 * Kerberos involves three servers: 43 * - Authentication Server (AS): verifies users during login 44 * - Ticket-Granting Server (TGS): issues proof of identity tickets 45 * - HTTP server (S) 46 * 47 * Steps: 48 * 0. User logs in to the AS and receives a TGS ticket. On workstations 49 * where the login program doesn't support Kerberos, the user can use 50 * 'kinit'. 51 * 52 * 1. C --> S: GET 53 * 54 * C <-- S: 401 Authentication Required 55 * WWW-Authenticate: Negotiate 56 * 57 * -> app contacts the TGS to request a session key for the HTTP service 58 * @ target host. The returned session key is encrypted with the HTTP 59 * service's secret key, so we can safely send it to the server. 60 * 61 * 2. C --> S: GET 62 * Authorization: Negotiate <Base64 encoded session key> 63 * gss_api_ctx->state = gss_api_auth_in_progress; 64 * 65 * C <-- S: 200 OK 66 * WWW-Authenticate: Negotiate <Base64 encoded server 67 * authentication data> 68 * 69 * -> The server returned an (optional) key to proof itself to us. We check this 70 * key with the TGS again. If it checks out, we can return the response 71 * body to the application. 72 * 73 * Note: It's possible that the server returns 401 again in step 2, if the 74 * Kerberos context isn't complete yet. This means there is 3rd step 75 * where we'll send a request with an Authorization header to the 76 * server. Some (simple) tests with mod_auth_kerb and MIT Kerberos 5 show 77 * this never happens. 78 * 79 * Depending on the type of HTTP server, this handshake is required for either 80 * every new connection, or for every new request! For more info see the next 81 * comment on authn_persistence_state_t. 82 * 83 * Note: Step 1 of the handshake will only happen on the first connection, once 84 * we know the server requires Kerberos authentication, the initial requests 85 * on the other connections will include a session key, so we start at 86 * step 2 in the handshake. 87 * ### TODO: Not implemented yet! 88 */ 89 90/* Current state of the authentication of the current request. */ 91typedef enum { 92 gss_api_auth_not_started, 93 gss_api_auth_in_progress, 94 gss_api_auth_completed, 95} gss_api_auth_state; 96 97/** 98 authn_persistence_state_t: state that indicates if we are talking with a 99 server that requires authentication only of the first request (stateful), 100 or of each request (stateless). 101 102 INIT: Begin state. Authenticating the first request on this connection. 103 UNDECIDED: we haven't identified the server yet, assume STATEFUL for now. 104 Pipeline mode disabled, requests are sent only after the response off the 105 previous request arrived. 106 STATELESS: we know the server requires authentication for each request. 107 On all new requests add the Authorization header with an initial SPNEGO 108 token (created per request). 109 To keep things simple, keep the connection in one by one mode. 110 (otherwise we'd have to keep a queue of gssapi context objects to match 111 the Negotiate header of the response with the session initiated by the 112 mathing request). 113 This state is an final state. 114 STATEFUL: alright, we have authenticated the connection and for the server 115 that is enough. Don't add an Authorization header to new requests. 116 Serf will switch to pipelined mode. 117 This state is not a final state, although in practical scenario's it will 118 be. When we receive a 40x response from the server switch to STATELESS 119 mode. 120 121 We start in state init for the first request until it is authenticated. 122 123 The rest of the state machine starts with the arrival of the response to the 124 second request, and then goes on with each response: 125 126 -------- 127 | INIT | C --> S: GET request in response to 40x of the server 128 -------- add [Proxy]-Authorization header 129 | 130 | 131 ------------ 132 | UNDECIDED| C --> S: GET request, assume stateful, 133 ------------ no [Proxy]-Authorization header 134 | 135 | 136 |------------------------------------------------ 137 | | 138 | C <-- S: 40x Authentication | C <-- S: 200 OK 139 | Required | 140 | | 141 v v 142 ------------- ------------ 143 ->| STATELESS |<------------------------------| STATEFUL |<-- 144 | ------------- C <-- S: 40x ------------ | 145 * | | Authentication | | 200 OK 146 | / Required | | 147 ----- -----/ 148 149 **/ 150typedef enum { 151 pstate_init, 152 pstate_undecided, 153 pstate_stateless, 154 pstate_stateful, 155} authn_persistence_state_t; 156 157 158/* HTTP Service name, used to get the session key. */ 159#define KRB_HTTP_SERVICE "HTTP" 160 161/* Stores the context information related to Kerberos authentication. */ 162typedef struct 163{ 164 apr_pool_t *pool; 165 166 /* GSSAPI context */ 167 serf__spnego_context_t *gss_ctx; 168 169 /* Current state of the authentication cycle. */ 170 gss_api_auth_state state; 171 172 /* Current persistence state. */ 173 authn_persistence_state_t pstate; 174 175 const char *header; 176 const char *value; 177} gss_authn_info_t; 178 179/* On the initial 401 response of the server, request a session key from 180 the Kerberos KDC to pass to the server, proving that we are who we 181 claim to be. The session key can only be used with the HTTP service 182 on the target host. */ 183static apr_status_t 184gss_api_get_credentials(serf_connection_t *conn, 185 char *token, apr_size_t token_len, 186 const char *hostname, 187 const char **buf, apr_size_t *buf_len, 188 gss_authn_info_t *gss_info) 189{ 190 serf__spnego_buffer_t input_buf; 191 serf__spnego_buffer_t output_buf; 192 apr_status_t status = APR_SUCCESS; 193 194 /* If the server sent us a token, pass it to gss_init_sec_token for 195 validation. */ 196 if (token) { 197 input_buf.value = token; 198 input_buf.length = token_len; 199 } else { 200 input_buf.value = 0; 201 input_buf.length = 0; 202 } 203 204 /* Establish a security context to the server. */ 205 status = serf__spnego_init_sec_context( 206 conn, 207 gss_info->gss_ctx, 208 KRB_HTTP_SERVICE, hostname, 209 &input_buf, 210 &output_buf, 211 gss_info->pool, 212 gss_info->pool 213 ); 214 215 switch(status) { 216 case APR_SUCCESS: 217 if (output_buf.length == 0) { 218 gss_info->state = gss_api_auth_completed; 219 } else { 220 gss_info->state = gss_api_auth_in_progress; 221 } 222 break; 223 case APR_EAGAIN: 224 gss_info->state = gss_api_auth_in_progress; 225 status = APR_SUCCESS; 226 break; 227 default: 228 return status; 229 } 230 231 /* Return the session key to our caller. */ 232 *buf = output_buf.value; 233 *buf_len = output_buf.length; 234 235 return status; 236} 237 238/* do_auth is invoked in two situations: 239 - when a response from a server is received that contains an authn header 240 (either from a 40x or 2xx response) 241 - when a request is prepared on a connection with stateless authentication. 242 243 Read the header sent by the server (if any), invoke the gssapi authn 244 code and use the resulting Server Ticket on the next request to the 245 server. */ 246static apr_status_t 247do_auth(peer_t peer, 248 int code, 249 gss_authn_info_t *gss_info, 250 serf_connection_t *conn, 251 serf_request_t *request, 252 const char *auth_hdr, 253 apr_pool_t *pool) 254{ 255 serf_context_t *ctx = conn->ctx; 256 serf__authn_info_t *authn_info; 257 const char *tmp = NULL; 258 char *token = NULL; 259 apr_size_t tmp_len = 0, token_len = 0; 260 apr_status_t status; 261 262 if (peer == HOST) { 263 authn_info = serf__get_authn_info_for_server(conn); 264 } else { 265 authn_info = &ctx->proxy_authn_info; 266 } 267 268 /* Is this a response from a host/proxy? auth_hdr should always be set. */ 269 if (code && auth_hdr) { 270 const char *space = NULL; 271 /* The server will return a token as attribute to the Negotiate key. 272 Negotiate YGwGCSqGSIb3EgECAgIAb10wW6ADAgEFoQMCAQ+iTzBNoAMCARCiRgREa6 273 mouMBAMFqKVdTGtfpZNXKzyw4Yo1paphJdIA3VOgncaoIlXxZLnkHiIHS2v65pVvrp 274 bRIyjF8xve9HxpnNIucCY9c= 275 276 Read this base64 value, decode it and validate it so we're sure the 277 server is who we expect it to be. */ 278 space = strchr(auth_hdr, ' '); 279 280 if (space) { 281 token = apr_palloc(pool, apr_base64_decode_len(space + 1)); 282 token_len = apr_base64_decode(token, space + 1); 283 } 284 } else { 285 /* This is a new request, not a retry in response to a 40x of the 286 host/proxy. 287 Only add the Authorization header if we know the server requires 288 per-request authentication (stateless). */ 289 if (gss_info->pstate != pstate_stateless) 290 return APR_SUCCESS; 291 } 292 293 switch(gss_info->pstate) { 294 case pstate_init: 295 /* Nothing to do here */ 296 break; 297 case pstate_undecided: /* Fall through */ 298 case pstate_stateful: 299 { 300 /* Switch to stateless mode, from now on handle authentication 301 of each request with a new gss context. This is easiest to 302 manage when sending requests one by one. */ 303 serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 304 "Server requires per-request SPNEGO authn, " 305 "switching to stateless mode.\n"); 306 307 gss_info->pstate = pstate_stateless; 308 serf_connection_set_max_outstanding_requests(conn, 1); 309 break; 310 } 311 case pstate_stateless: 312 /* Nothing to do here */ 313 break; 314 } 315 316 if (request->auth_baton && !token) { 317 /* We provided token with this request, but server responded with empty 318 authentication header. This means server rejected our credentials. 319 XXX: Probably we need separate error code for this case like 320 SERF_ERROR_AUTHN_CREDS_REJECTED? */ 321 return SERF_ERROR_AUTHN_FAILED; 322 } 323 324 /* If the server didn't provide us with a token, start with a new initial 325 step in the SPNEGO authentication. */ 326 if (!token) { 327 serf__spnego_reset_sec_context(gss_info->gss_ctx); 328 gss_info->state = gss_api_auth_not_started; 329 } 330 331 if (peer == HOST) { 332 status = gss_api_get_credentials(conn, 333 token, token_len, 334 conn->host_info.hostname, 335 &tmp, &tmp_len, 336 gss_info); 337 } else { 338 char *proxy_host; 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 361apr_status_t 362serf__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. */ 371apr_status_t 372serf__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. */ 415apr_status_t 416serf__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. */ 439apr_status_t 440serf__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 */ 530typedef 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 537static int 538get_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 558static const char * 559get_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 */ 581apr_status_t 582serf__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 */ 658