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
10 * http://www.apache.org/licenses/LICENSE-2.0
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
18 * ====================================================================
22 #include "auth_spnego.h"
24 #ifdef SERF_HAVE_SPNEGO
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.
32 #include <serf_private.h>
33 #include <auth/auth.h>
36 #include <apr_base64.h>
37 #include <apr_strings.h>
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
46 /* Authentication over HTTP using Kerberos
48 * Kerberos involves three servers:
49 * - Authentication Server (AS): verifies users during login
50 * - Ticket-Granting Server (TGS): issues proof of identity tickets
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
60 * C <-- S: 401 Authentication Required
61 * WWW-Authenticate: Negotiate
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.
68 * Authorization: Negotiate <Base64 encoded session key>
69 * gss_api_ctx->state = gss_api_auth_in_progress;
72 * WWW-Authenticate: Negotiate <Base64 encoded server
73 * authentication data>
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.
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
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.
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!
96 /* Current state of the authentication of the current request. */
98 gss_api_auth_not_started,
99 gss_api_auth_in_progress,
100 gss_api_auth_completed,
101 } gss_api_auth_state;
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).
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
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
127 We start in state init for the first request until it is authenticated.
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:
133 | INIT | C --> S: GET request in response to 40x of the server
134 -------- add [Proxy]-Authorization header
138 | UNDECIDED| C --> S: GET request, assume stateful,
139 ------------ no [Proxy]-Authorization header
142 |------------------------------------------------
144 | C <-- S: 40x Authentication | C <-- S: 200 OK
148 ------------- ------------
149 ->| STATELESS |<------------------------------| STATEFUL |<--
150 | ------------- C <-- S: 40x ------------ |
151 * | | Authentication | | 200 OK
161 } authn_persistence_state_t;
164 /* HTTP Service name, used to get the session key. */
165 #define KRB_HTTP_SERVICE "HTTP"
167 /* Stores the context information related to Kerberos authentication. */
173 serf__spnego_context_t *gss_ctx;
175 /* Current state of the authentication cycle. */
176 gss_api_auth_state state;
178 /* Current persistence state. */
179 authn_persistence_state_t pstate;
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. */
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)
196 serf__spnego_buffer_t input_buf;
197 serf__spnego_buffer_t output_buf;
198 apr_status_t status = APR_SUCCESS;
200 /* If the server sent us a token, pass it to gss_init_sec_token for
203 input_buf.value = token;
204 input_buf.length = token_len;
207 input_buf.length = 0;
210 /* Establish a security context to the server. */
211 status = serf__spnego_init_sec_context(
214 KRB_HTTP_SERVICE, hostname,
223 if (output_buf.length == 0) {
224 gss_info->state = gss_api_auth_completed;
226 gss_info->state = gss_api_auth_in_progress;
230 gss_info->state = gss_api_auth_in_progress;
231 status = APR_SUCCESS;
237 /* Return the session key to our caller. */
238 *buf = output_buf.value;
239 *buf_len = output_buf.length;
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.
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
255 gss_authn_info_t *gss_info,
256 serf_connection_t *conn,
257 serf_request_t *request,
258 const char *auth_hdr,
261 serf_context_t *ctx = conn->ctx;
262 serf__authn_info_t *authn_info;
263 const char *tmp = NULL;
265 apr_size_t tmp_len = 0, token_len = 0;
269 authn_info = serf__get_authn_info_for_server(conn);
271 authn_info = &ctx->proxy_authn_info;
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=
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, ' ');
287 token = apr_palloc(pool, apr_base64_decode_len(space + 1));
288 token_len = apr_base64_decode(token, space + 1);
291 /* This is a new request, not a retry in response to a 40x of the
293 Only add the Authorization header if we know the server requires
294 per-request authentication (stateless). */
295 if (gss_info->pstate != pstate_stateless)
299 switch(gss_info->pstate) {
301 /* Nothing to do here */
303 case pstate_undecided: /* Fall through */
304 case pstate_stateful:
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");
313 gss_info->pstate = pstate_stateless;
314 serf_connection_set_max_outstanding_requests(conn, 1);
317 case pstate_stateless:
318 /* Nothing to do here */
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;
330 /* If the server didn't provide us with a token, start with a new initial
331 step in the SPNEGO authentication. */
333 serf__spnego_reset_sec_context(gss_info->gss_ctx);
334 gss_info->state = gss_api_auth_not_started;
338 status = gss_api_get_credentials(conn,
340 conn->host_info.hostname,
344 char *proxy_host = conn->ctx->proxy_address->hostname;
345 status = gss_api_get_credentials(conn,
346 token, token_len, proxy_host,
353 /* On the next request, add an Authorization header. */
355 serf__encode_auth_header(&gss_info->value, authn_info->scheme->name,
359 gss_info->header = (peer == HOST) ?
360 "Authorization" : "Proxy-Authorization";
367 serf__init_spnego(int code,
374 /* A new connection is created to a server that's known to use
377 serf__init_spnego_connection(const serf__authn_scheme_t *scheme,
379 serf_connection_t *conn,
382 serf_context_t *ctx = conn->ctx;
383 serf__authn_info_t *authn_info;
384 gss_authn_info_t *gss_info = NULL;
386 /* For proxy authentication, reuse the gss context for all connections.
387 For server authentication, create a new gss context per connection. */
389 authn_info = &conn->authn_info;
391 authn_info = &ctx->proxy_authn_info;
393 gss_info = authn_info->baton;
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);
407 authn_info->baton = gss_info;
410 /* Make serf send the initial requests one by one */
411 serf_connection_set_max_outstanding_requests(conn, 1);
413 serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
414 "Initialized Kerberos context for this connection.\n");
419 /* A 40x response was received, handle the authentication. */
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,
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;
434 return do_auth(code == 401 ? HOST : PROXY,
443 /* Setup the authn headers on this request message. */
445 serf__setup_request_spnego_auth(peer_t peer,
447 serf_connection_t *conn,
448 serf_request_t *request,
451 serf_bucket_t *hdrs_bkt)
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;
457 /* If we have an ongoing authentication handshake, the handler of the
458 previous response will have created the authn headers for this request
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");
464 serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
467 /* Remember that we're using this request for authentication
469 request->auth_baton = (void*) TRUE;
471 /* We should send each token only once. */
472 gss_info->header = NULL;
473 gss_info->value = NULL;
478 switch (gss_info->pstate) {
480 /* We shouldn't normally arrive here, do nothing. */
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. */
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. */
494 case pstate_stateless:
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");
505 status = do_auth(peer,
510 0l, /* no response authn header */
515 serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
518 /* Remember that we're using this request for authentication
520 request->auth_baton = (void*) TRUE;
522 /* We should send each token only once. */
523 gss_info->header = NULL;
524 gss_info->value = NULL;
533 * Baton passed to the get_auth_header callback function.
536 const char *hdr_name;
537 const char *auth_name;
538 const char *hdr_value;
540 } get_auth_header_baton_t;
543 get_auth_header_cb(void *baton,
547 get_auth_header_baton_t *b = baton;
549 /* We're only interested in xxxx-Authenticate headers. */
550 if (strcasecmp(key, b->hdr_name) != 0)
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);
564 get_auth_header(serf_bucket_t *hdrs,
565 const char *hdr_name,
566 const char *auth_name,
569 get_auth_header_baton_t b;
571 b.auth_name = hdr_name;
572 b.hdr_name = auth_name;
576 serf_bucket_headers_do(hdrs, get_auth_header_cb, &b);
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).
587 serf__validate_response_spnego_auth(const serf__authn_scheme_t *scheme,
590 serf_connection_t *conn,
591 serf_request_t *request,
592 serf_bucket_t *response,
595 serf_context_t *ctx = conn->ctx;
596 gss_authn_info_t *gss_info;
597 const char *auth_hdr_name;
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");
608 gss_info = conn->authn_info.baton;
609 auth_hdr_name = "WWW-Authenticate";
611 gss_info = ctx->proxy_authn_info.baton;
612 auth_hdr_name = "Proxy-Authenticate";
615 if (gss_info->state != gss_api_auth_completed) {
617 const char *auth_hdr_val;
620 hdrs = serf_bucket_response_get_headers(response);
621 auth_hdr_val = get_auth_header(hdrs, auth_hdr_name, scheme->name,
625 status = do_auth(peer, code, gss_info, conn, request, auth_hdr_val,
631 /* No Authenticate headers, nothing to validate: authentication
633 gss_info->state = gss_api_auth_completed;
635 serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
636 "SPNEGO handshake completed.\n");
640 if (gss_info->state == gss_api_auth_completed) {
641 switch(gss_info->pstate) {
643 /* Authentication of the first request is done. */
644 gss_info->pstate = pstate_undecided;
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);
654 /* Nothing to do here. */
662 #endif /* SERF_HAVE_SPNEGO */