1253893Speter/* Copyright 2009 Justin Erenkrantz and Greg Stein 2253893Speter * 3253893Speter * Licensed under the Apache License, Version 2.0 (the "License"); 4253893Speter * you may not use this file except in compliance with the License. 5253893Speter * You may obtain a copy of the License at 6253893Speter * 7253893Speter * http://www.apache.org/licenses/LICENSE-2.0 8253893Speter * 9253893Speter * Unless required by applicable law or agreed to in writing, software 10253893Speter * distributed under the License is distributed on an "AS IS" BASIS, 11253893Speter * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12253893Speter * See the License for the specific language governing permissions and 13253893Speter * limitations under the License. 14253893Speter */ 15253893Speter 16253893Speter#include "auth_spnego.h" 17253893Speter 18253893Speter#ifdef SERF_HAVE_SPNEGO 19253893Speter 20253893Speter/** These functions implement SPNEGO-based Kerberos and NTLM authentication, 21253893Speter * using either GSS-API (RFC 2743) or SSPI on Windows. 22253893Speter * The HTTP message exchange is documented in RFC 4559. 23253893Speter **/ 24253893Speter 25253893Speter#include <serf.h> 26253893Speter#include <serf_private.h> 27253893Speter#include <auth/auth.h> 28253893Speter 29253893Speter#include <apr.h> 30253893Speter#include <apr_base64.h> 31253893Speter#include <apr_strings.h> 32253893Speter 33253893Speter/** TODO: 34253893Speter ** - send session key directly on new connections where we already know 35253893Speter ** the server requires Kerberos authn. 36253893Speter ** - Add a way for serf to give detailed error information back to the 37253893Speter ** application. 38253893Speter **/ 39253893Speter 40253893Speter/* Authentication over HTTP using Kerberos 41253893Speter * 42253893Speter * Kerberos involves three servers: 43253893Speter * - Authentication Server (AS): verifies users during login 44253893Speter * - Ticket-Granting Server (TGS): issues proof of identity tickets 45253893Speter * - HTTP server (S) 46253893Speter * 47253893Speter * Steps: 48253893Speter * 0. User logs in to the AS and receives a TGS ticket. On workstations 49253893Speter * where the login program doesn't support Kerberos, the user can use 50253893Speter * 'kinit'. 51253893Speter * 52253893Speter * 1. C --> S: GET 53253893Speter * 54253893Speter * C <-- S: 401 Authentication Required 55253893Speter * WWW-Authenticate: Negotiate 56253893Speter * 57253893Speter * -> app contacts the TGS to request a session key for the HTTP service 58253893Speter * @ target host. The returned session key is encrypted with the HTTP 59253893Speter * service's secret key, so we can safely send it to the server. 60253893Speter * 61253893Speter * 2. C --> S: GET 62253893Speter * Authorization: Negotiate <Base64 encoded session key> 63253893Speter * gss_api_ctx->state = gss_api_auth_in_progress; 64253893Speter * 65253893Speter * C <-- S: 200 OK 66253893Speter * WWW-Authenticate: Negotiate <Base64 encoded server 67253893Speter * authentication data> 68253893Speter * 69253893Speter * -> The server returned an (optional) key to proof itself to us. We check this 70253893Speter * key with the TGS again. If it checks out, we can return the response 71253893Speter * body to the application. 72253893Speter * 73253893Speter * Note: It's possible that the server returns 401 again in step 2, if the 74253893Speter * Kerberos context isn't complete yet. This means there is 3rd step 75253893Speter * where we'll send a request with an Authorization header to the 76253893Speter * server. Some (simple) tests with mod_auth_kerb and MIT Kerberos 5 show 77253893Speter * this never happens. 78253893Speter * 79253893Speter * Depending on the type of HTTP server, this handshake is required for either 80253893Speter * every new connection, or for every new request! For more info see the next 81253893Speter * comment on authn_persistence_state_t. 82253893Speter * 83253893Speter * Note: Step 1 of the handshake will only happen on the first connection, once 84253893Speter * we know the server requires Kerberos authentication, the initial requests 85253893Speter * on the other connections will include a session key, so we start at 86253893Speter * step 2 in the handshake. 87253893Speter * ### TODO: Not implemented yet! 88253893Speter */ 89253893Speter 90253893Speter/* Current state of the authentication of the current request. */ 91253893Spetertypedef enum { 92253893Speter gss_api_auth_not_started, 93253893Speter gss_api_auth_in_progress, 94253893Speter gss_api_auth_completed, 95253893Speter} gss_api_auth_state; 96253893Speter 97253893Speter/** 98253893Speter authn_persistence_state_t: state that indicates if we are talking with a 99253893Speter server that requires authentication only of the first request (stateful), 100253893Speter or of each request (stateless). 101253893Speter 102253893Speter INIT: Begin state. Authenticating the first request on this connection. 103253893Speter UNDECIDED: we haven't identified the server yet, assume STATEFUL for now. 104253893Speter Pipeline mode disabled, requests are sent only after the response off the 105253893Speter previous request arrived. 106253893Speter STATELESS: we know the server requires authentication for each request. 107253893Speter On all new requests add the Authorization header with an initial SPNEGO 108253893Speter token (created per request). 109253893Speter To keep things simple, keep the connection in one by one mode. 110253893Speter (otherwise we'd have to keep a queue of gssapi context objects to match 111253893Speter the Negotiate header of the response with the session initiated by the 112253893Speter mathing request). 113253893Speter This state is an final state. 114253893Speter STATEFUL: alright, we have authenticated the connection and for the server 115253893Speter that is enough. Don't add an Authorization header to new requests. 116253893Speter Serf will switch to pipelined mode. 117253893Speter This state is not a final state, although in practical scenario's it will 118253893Speter be. When we receive a 40x response from the server switch to STATELESS 119253893Speter mode. 120253893Speter 121253893Speter We start in state init for the first request until it is authenticated. 122253893Speter 123253893Speter The rest of the state machine starts with the arrival of the response to the 124253893Speter second request, and then goes on with each response: 125253893Speter 126253893Speter -------- 127253893Speter | INIT | C --> S: GET request in response to 40x of the server 128253893Speter -------- add [Proxy]-Authorization header 129253893Speter | 130253893Speter | 131253893Speter ------------ 132253893Speter | UNDECIDED| C --> S: GET request, assume stateful, 133253893Speter ------------ no [Proxy]-Authorization header 134253893Speter | 135253893Speter | 136253893Speter |------------------------------------------------ 137253893Speter | | 138253893Speter | C <-- S: 40x Authentication | C <-- S: 200 OK 139253893Speter | Required | 140253893Speter | | 141253893Speter v v 142253893Speter ------------- ------------ 143253893Speter ->| STATELESS |<------------------------------| STATEFUL |<-- 144253893Speter | ------------- C <-- S: 40x ------------ | 145253893Speter * | | Authentication | | 200 OK 146253893Speter | / Required | | 147253893Speter ----- -----/ 148253893Speter 149253893Speter **/ 150253893Spetertypedef enum { 151253893Speter pstate_init, 152253893Speter pstate_undecided, 153253893Speter pstate_stateless, 154253893Speter pstate_stateful, 155253893Speter} authn_persistence_state_t; 156253893Speter 157253893Speter 158253893Speter/* HTTP Service name, used to get the session key. */ 159253893Speter#define KRB_HTTP_SERVICE "HTTP" 160253893Speter 161253893Speter/* Stores the context information related to Kerberos authentication. */ 162253893Spetertypedef struct 163253893Speter{ 164253893Speter apr_pool_t *pool; 165253893Speter 166253893Speter /* GSSAPI context */ 167253893Speter serf__spnego_context_t *gss_ctx; 168253893Speter 169253893Speter /* Current state of the authentication cycle. */ 170253893Speter gss_api_auth_state state; 171253893Speter 172253893Speter /* Current persistence state. */ 173253893Speter authn_persistence_state_t pstate; 174253893Speter 175253893Speter const char *header; 176253893Speter const char *value; 177253893Speter} gss_authn_info_t; 178253893Speter 179253893Speter/* On the initial 401 response of the server, request a session key from 180253893Speter the Kerberos KDC to pass to the server, proving that we are who we 181253893Speter claim to be. The session key can only be used with the HTTP service 182253893Speter on the target host. */ 183253893Speterstatic apr_status_t 184253893Spetergss_api_get_credentials(char *token, apr_size_t token_len, 185253893Speter const char *hostname, 186253893Speter const char **buf, apr_size_t *buf_len, 187253893Speter gss_authn_info_t *gss_info) 188253893Speter{ 189253893Speter serf__spnego_buffer_t input_buf; 190253893Speter serf__spnego_buffer_t output_buf; 191253893Speter apr_status_t status = APR_SUCCESS; 192253893Speter 193253893Speter /* If the server sent us a token, pass it to gss_init_sec_token for 194253893Speter validation. */ 195253893Speter if (token) { 196253893Speter input_buf.value = token; 197253893Speter input_buf.length = token_len; 198253893Speter } else { 199253893Speter input_buf.value = 0; 200253893Speter input_buf.length = 0; 201253893Speter } 202253893Speter 203253893Speter /* Establish a security context to the server. */ 204253893Speter status = serf__spnego_init_sec_context( 205253893Speter gss_info->gss_ctx, 206253893Speter KRB_HTTP_SERVICE, hostname, 207253893Speter &input_buf, 208253893Speter &output_buf, 209253893Speter gss_info->pool, 210253893Speter gss_info->pool 211253893Speter ); 212253893Speter 213253893Speter switch(status) { 214253893Speter case APR_SUCCESS: 215253893Speter gss_info->state = gss_api_auth_completed; 216253893Speter break; 217253893Speter case APR_EAGAIN: 218253893Speter gss_info->state = gss_api_auth_in_progress; 219253893Speter status = APR_SUCCESS; 220253893Speter break; 221253893Speter default: 222253893Speter return status; 223253893Speter } 224253893Speter 225253893Speter /* Return the session key to our caller. */ 226253893Speter *buf = output_buf.value; 227253893Speter *buf_len = output_buf.length; 228253893Speter 229253893Speter return status; 230253893Speter} 231253893Speter 232253893Speter/* do_auth is invoked in two situations: 233253893Speter - when a response from a server is received that contains an authn header 234253893Speter (either from a 40x or 2xx response) 235253893Speter - when a request is prepared on a connection with stateless authentication. 236253893Speter 237253893Speter Read the header sent by the server (if any), invoke the gssapi authn 238253893Speter code and use the resulting Server Ticket on the next request to the 239253893Speter server. */ 240253893Speterstatic apr_status_t 241253893Speterdo_auth(peer_t peer, 242253893Speter int code, 243253893Speter gss_authn_info_t *gss_info, 244253893Speter serf_connection_t *conn, 245253893Speter const char *auth_hdr, 246253893Speter apr_pool_t *pool) 247253893Speter{ 248253893Speter serf_context_t *ctx = conn->ctx; 249253893Speter serf__authn_info_t *authn_info; 250253893Speter const char *tmp = NULL; 251253893Speter char *token = NULL; 252253893Speter apr_size_t tmp_len = 0, token_len = 0; 253253893Speter apr_status_t status; 254253893Speter 255253893Speter if (peer == HOST) { 256253893Speter authn_info = serf__get_authn_info_for_server(conn); 257253893Speter } else { 258253893Speter authn_info = &ctx->proxy_authn_info; 259253893Speter } 260253893Speter 261253893Speter /* Is this a response from a host/proxy? auth_hdr should always be set. */ 262253893Speter if (code && auth_hdr) { 263253893Speter const char *space = NULL; 264253893Speter /* The server will return a token as attribute to the Negotiate key. 265253893Speter Negotiate YGwGCSqGSIb3EgECAgIAb10wW6ADAgEFoQMCAQ+iTzBNoAMCARCiRgREa6 266253893Speter mouMBAMFqKVdTGtfpZNXKzyw4Yo1paphJdIA3VOgncaoIlXxZLnkHiIHS2v65pVvrp 267253893Speter bRIyjF8xve9HxpnNIucCY9c= 268253893Speter 269253893Speter Read this base64 value, decode it and validate it so we're sure the 270253893Speter server is who we expect it to be. */ 271253893Speter space = strchr(auth_hdr, ' '); 272253893Speter 273253893Speter if (space) { 274253893Speter token = apr_palloc(pool, apr_base64_decode_len(space + 1)); 275253893Speter token_len = apr_base64_decode(token, space + 1); 276253893Speter } 277253893Speter } else { 278253893Speter /* This is a new request, not a retry in response to a 40x of the 279253893Speter host/proxy. 280253893Speter Only add the Authorization header if we know the server requires 281253893Speter per-request authentication (stateless). */ 282253893Speter if (gss_info->pstate != pstate_stateless) 283253893Speter return APR_SUCCESS; 284253893Speter } 285253893Speter 286253893Speter switch(gss_info->pstate) { 287253893Speter case pstate_init: 288253893Speter /* Nothing to do here */ 289253893Speter break; 290253893Speter case pstate_undecided: /* Fall through */ 291253893Speter case pstate_stateful: 292253893Speter { 293253893Speter /* Switch to stateless mode, from now on handle authentication 294253893Speter of each request with a new gss context. This is easiest to 295253893Speter manage when sending requests one by one. */ 296253893Speter serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 297253893Speter "Server requires per-request SPNEGO authn, " 298253893Speter "switching to stateless mode.\n"); 299253893Speter 300253893Speter gss_info->pstate = pstate_stateless; 301253893Speter serf_connection_set_max_outstanding_requests(conn, 1); 302253893Speter break; 303253893Speter } 304253893Speter case pstate_stateless: 305253893Speter /* Nothing to do here */ 306253893Speter break; 307253893Speter } 308253893Speter 309253893Speter /* If the server didn't provide us with a token, start with a new initial 310253893Speter step in the SPNEGO authentication. */ 311253893Speter if (!token) { 312253893Speter serf__spnego_reset_sec_context(gss_info->gss_ctx); 313253893Speter gss_info->state = gss_api_auth_not_started; 314253893Speter } 315253893Speter 316253893Speter if (peer == HOST) { 317253893Speter status = gss_api_get_credentials(token, token_len, 318253893Speter conn->host_info.hostname, 319253893Speter &tmp, &tmp_len, 320253893Speter gss_info); 321253893Speter } else { 322253893Speter char *proxy_host; 323253893Speter apr_getnameinfo(&proxy_host, conn->ctx->proxy_address, 0); 324253893Speter status = gss_api_get_credentials(token, token_len, proxy_host, 325253893Speter &tmp, &tmp_len, 326253893Speter gss_info); 327253893Speter } 328253893Speter if (status) 329253893Speter return status; 330253893Speter 331253893Speter /* On the next request, add an Authorization header. */ 332253893Speter if (tmp_len) { 333253893Speter serf__encode_auth_header(&gss_info->value, authn_info->scheme->name, 334253893Speter tmp, 335253893Speter tmp_len, 336253893Speter pool); 337253893Speter gss_info->header = (peer == HOST) ? 338253893Speter "Authorization" : "Proxy-Authorization"; 339253893Speter } 340253893Speter 341253893Speter return APR_SUCCESS; 342253893Speter} 343253893Speter 344253893Speterapr_status_t 345253893Speterserf__init_spnego(int code, 346253893Speter serf_context_t *ctx, 347253893Speter apr_pool_t *pool) 348253893Speter{ 349253893Speter return APR_SUCCESS; 350253893Speter} 351253893Speter 352253893Speter/* A new connection is created to a server that's known to use 353253893Speter Kerberos. */ 354253893Speterapr_status_t 355253893Speterserf__init_spnego_connection(const serf__authn_scheme_t *scheme, 356253893Speter int code, 357253893Speter serf_connection_t *conn, 358253893Speter apr_pool_t *pool) 359253893Speter{ 360253893Speter gss_authn_info_t *gss_info; 361253893Speter apr_status_t status; 362253893Speter 363253893Speter gss_info = apr_pcalloc(conn->pool, sizeof(*gss_info)); 364253893Speter gss_info->pool = conn->pool; 365253893Speter gss_info->state = gss_api_auth_not_started; 366253893Speter gss_info->pstate = pstate_init; 367253893Speter status = serf__spnego_create_sec_context(&gss_info->gss_ctx, scheme, 368253893Speter gss_info->pool, pool); 369253893Speter 370253893Speter if (status) { 371253893Speter return status; 372253893Speter } 373253893Speter 374253893Speter if (code == 401) { 375253893Speter conn->authn_baton = gss_info; 376253893Speter } else { 377253893Speter conn->proxy_authn_baton = gss_info; 378253893Speter } 379253893Speter 380253893Speter /* Make serf send the initial requests one by one */ 381253893Speter serf_connection_set_max_outstanding_requests(conn, 1); 382253893Speter 383253893Speter serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 384253893Speter "Initialized Kerberos context for this connection.\n"); 385253893Speter 386253893Speter return APR_SUCCESS; 387253893Speter} 388253893Speter 389253893Speter/* A 40x response was received, handle the authentication. */ 390253893Speterapr_status_t 391253893Speterserf__handle_spnego_auth(int code, 392253893Speter serf_request_t *request, 393253893Speter serf_bucket_t *response, 394253893Speter const char *auth_hdr, 395253893Speter const char *auth_attr, 396253893Speter void *baton, 397253893Speter apr_pool_t *pool) 398253893Speter{ 399253893Speter serf_connection_t *conn = request->conn; 400253893Speter gss_authn_info_t *gss_info = (code == 401) ? conn->authn_baton : 401253893Speter conn->proxy_authn_baton; 402253893Speter 403253893Speter return do_auth(code == 401 ? HOST : PROXY, 404253893Speter code, 405253893Speter gss_info, 406253893Speter request->conn, 407253893Speter auth_hdr, 408253893Speter pool); 409253893Speter} 410253893Speter 411253893Speter/* Setup the authn headers on this request message. */ 412253893Speterapr_status_t 413253893Speterserf__setup_request_spnego_auth(peer_t peer, 414253893Speter int code, 415253893Speter serf_connection_t *conn, 416253893Speter serf_request_t *request, 417253893Speter const char *method, 418253893Speter const char *uri, 419253893Speter serf_bucket_t *hdrs_bkt) 420253893Speter{ 421253893Speter gss_authn_info_t *gss_info = (peer == HOST) ? conn->authn_baton : 422253893Speter conn->proxy_authn_baton; 423253893Speter 424253893Speter /* If we have an ongoing authentication handshake, the handler of the 425253893Speter previous response will have created the authn headers for this request 426253893Speter already. */ 427253893Speter if (gss_info && gss_info->header && gss_info->value) { 428253893Speter serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 429253893Speter "Set Negotiate authn header on retried request.\n"); 430253893Speter 431253893Speter serf_bucket_headers_setn(hdrs_bkt, gss_info->header, 432253893Speter gss_info->value); 433253893Speter 434253893Speter /* We should send each token only once. */ 435253893Speter gss_info->header = NULL; 436253893Speter gss_info->value = NULL; 437253893Speter 438253893Speter return APR_SUCCESS; 439253893Speter } 440253893Speter 441253893Speter switch (gss_info->pstate) { 442253893Speter case pstate_init: 443253893Speter /* We shouldn't normally arrive here, do nothing. */ 444253893Speter break; 445253893Speter case pstate_undecided: /* fall through */ 446253893Speter serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 447253893Speter "Assume for now that the server supports persistent " 448253893Speter "SPNEGO authentication.\n"); 449253893Speter /* Nothing to do here. */ 450253893Speter break; 451253893Speter case pstate_stateful: 452253893Speter serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 453253893Speter "SPNEGO on this connection is persistent, " 454253893Speter "don't set authn header on next request.\n"); 455253893Speter /* Nothing to do here. */ 456253893Speter break; 457253893Speter case pstate_stateless: 458253893Speter { 459253893Speter apr_status_t status; 460253893Speter 461253893Speter /* Authentication on this connection is known to be stateless. 462253893Speter Add an initial Negotiate token for the server, to bypass the 463253893Speter 40x response we know we'll otherwise receive. 464253893Speter (RFC 4559 section 4.2) */ 465253893Speter serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 466253893Speter "Add initial Negotiate header to request.\n"); 467253893Speter 468253893Speter status = do_auth(peer, 469253893Speter code, 470253893Speter gss_info, 471253893Speter conn, 472253893Speter 0l, /* no response authn header */ 473253893Speter conn->pool); 474253893Speter if (status) 475253893Speter return status; 476253893Speter 477253893Speter serf_bucket_headers_setn(hdrs_bkt, gss_info->header, 478253893Speter gss_info->value); 479253893Speter /* We should send each token only once. */ 480253893Speter gss_info->header = NULL; 481253893Speter gss_info->value = NULL; 482253893Speter break; 483253893Speter } 484253893Speter } 485253893Speter 486253893Speter return APR_SUCCESS; 487253893Speter} 488253893Speter 489253893Speter/* Function is called when 2xx responses are received. Normally we don't 490253893Speter * have to do anything, except for the first response after the 491253893Speter * authentication handshake. This specific response includes authentication 492253893Speter * data which should be validated by the client (mutual authentication). 493253893Speter */ 494253893Speterapr_status_t 495253893Speterserf__validate_response_spnego_auth(peer_t peer, 496253893Speter int code, 497253893Speter serf_connection_t *conn, 498253893Speter serf_request_t *request, 499253893Speter serf_bucket_t *response, 500253893Speter apr_pool_t *pool) 501253893Speter{ 502253893Speter gss_authn_info_t *gss_info; 503253893Speter const char *auth_hdr_name; 504253893Speter 505253893Speter /* TODO: currently this function is only called when a response includes 506253893Speter an Authenticate header. This header is optional. If the server does 507253893Speter not provide this header on the first 2xx response, we will not promote 508253893Speter the connection from undecided to stateful. This won't break anything, 509253893Speter but means we stay in non-pipelining mode. */ 510253893Speter serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt, 511253893Speter "Validate Negotiate response header.\n"); 512253893Speter 513253893Speter if (peer == HOST) { 514253893Speter gss_info = conn->authn_baton; 515253893Speter auth_hdr_name = "WWW-Authenticate"; 516253893Speter } else { 517253893Speter gss_info = conn->proxy_authn_baton; 518253893Speter auth_hdr_name = "Proxy-Authenticate"; 519253893Speter } 520253893Speter 521253893Speter if (gss_info->state != gss_api_auth_completed) { 522253893Speter serf_bucket_t *hdrs; 523253893Speter const char *auth_hdr_val; 524253893Speter apr_status_t status; 525253893Speter 526253893Speter hdrs = serf_bucket_response_get_headers(response); 527253893Speter auth_hdr_val = serf_bucket_headers_get(hdrs, auth_hdr_name); 528253893Speter 529253893Speter status = do_auth(peer, code, gss_info, conn, auth_hdr_val, pool); 530253893Speter if (status) 531253893Speter return status; 532253893Speter } 533253893Speter 534253893Speter if (gss_info->state == gss_api_auth_completed) { 535253893Speter switch(gss_info->pstate) { 536253893Speter case pstate_init: 537253893Speter /* Authentication of the first request is done. */ 538253893Speter gss_info->pstate = pstate_undecided; 539253893Speter break; 540253893Speter case pstate_undecided: 541253893Speter /* The server didn't request for authentication even though 542253893Speter we didn't add an Authorization header to previous 543253893Speter request. That means it supports persistent authentication. */ 544253893Speter gss_info->pstate = pstate_stateful; 545253893Speter serf_connection_set_max_outstanding_requests(conn, 0); 546253893Speter break; 547253893Speter default: 548253893Speter /* Nothing to do here. */ 549253893Speter break; 550253893Speter } 551253893Speter } 552253893Speter 553253893Speter return APR_SUCCESS; 554253893Speter} 555253893Speter 556253893Speter#endif /* SERF_HAVE_SPNEGO */ 557