1 /* Copyright 2009 Justin Erenkrantz and Greg Stein
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
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 #include "auth_spnego.h"
18 #ifdef SERF_HAVE_SPNEGO
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.
26 #include <serf_private.h>
27 #include <auth/auth.h>
30 #include <apr_base64.h>
31 #include <apr_strings.h>
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
40 /* Authentication over HTTP using Kerberos
42 * Kerberos involves three servers:
43 * - Authentication Server (AS): verifies users during login
44 * - Ticket-Granting Server (TGS): issues proof of identity tickets
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
54 * C <-- S: 401 Authentication Required
55 * WWW-Authenticate: Negotiate
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.
62 * Authorization: Negotiate <Base64 encoded session key>
63 * gss_api_ctx->state = gss_api_auth_in_progress;
66 * WWW-Authenticate: Negotiate <Base64 encoded server
67 * authentication data>
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.
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
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.
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!
90 /* Current state of the authentication of the current request. */
92 gss_api_auth_not_started,
93 gss_api_auth_in_progress,
94 gss_api_auth_completed,
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).
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
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
121 We start in state init for the first request until it is authenticated.
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:
127 | INIT | C --> S: GET request in response to 40x of the server
128 -------- add [Proxy]-Authorization header
132 | UNDECIDED| C --> S: GET request, assume stateful,
133 ------------ no [Proxy]-Authorization header
136 |------------------------------------------------
138 | C <-- S: 40x Authentication | C <-- S: 200 OK
142 ------------- ------------
143 ->| STATELESS |<------------------------------| STATEFUL |<--
144 | ------------- C <-- S: 40x ------------ |
145 * | | Authentication | | 200 OK
155 } authn_persistence_state_t;
158 /* HTTP Service name, used to get the session key. */
159 #define KRB_HTTP_SERVICE "HTTP"
161 /* Stores the context information related to Kerberos authentication. */
167 serf__spnego_context_t *gss_ctx;
169 /* Current state of the authentication cycle. */
170 gss_api_auth_state state;
172 /* Current persistence state. */
173 authn_persistence_state_t pstate;
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. */
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)
189 serf__spnego_buffer_t input_buf;
190 serf__spnego_buffer_t output_buf;
191 apr_status_t status = APR_SUCCESS;
193 /* If the server sent us a token, pass it to gss_init_sec_token for
196 input_buf.value = token;
197 input_buf.length = token_len;
200 input_buf.length = 0;
203 /* Establish a security context to the server. */
204 status = serf__spnego_init_sec_context(
206 KRB_HTTP_SERVICE, hostname,
215 gss_info->state = gss_api_auth_completed;
218 gss_info->state = gss_api_auth_in_progress;
219 status = APR_SUCCESS;
225 /* Return the session key to our caller. */
226 *buf = output_buf.value;
227 *buf_len = output_buf.length;
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.
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
243 gss_authn_info_t *gss_info,
244 serf_connection_t *conn,
245 const char *auth_hdr,
248 serf_context_t *ctx = conn->ctx;
249 serf__authn_info_t *authn_info;
250 const char *tmp = NULL;
252 apr_size_t tmp_len = 0, token_len = 0;
256 authn_info = serf__get_authn_info_for_server(conn);
258 authn_info = &ctx->proxy_authn_info;
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=
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, ' ');
274 token = apr_palloc(pool, apr_base64_decode_len(space + 1));
275 token_len = apr_base64_decode(token, space + 1);
278 /* This is a new request, not a retry in response to a 40x of the
280 Only add the Authorization header if we know the server requires
281 per-request authentication (stateless). */
282 if (gss_info->pstate != pstate_stateless)
286 switch(gss_info->pstate) {
288 /* Nothing to do here */
290 case pstate_undecided: /* Fall through */
291 case pstate_stateful:
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");
300 gss_info->pstate = pstate_stateless;
301 serf_connection_set_max_outstanding_requests(conn, 1);
304 case pstate_stateless:
305 /* Nothing to do here */
309 /* If the server didn't provide us with a token, start with a new initial
310 step in the SPNEGO authentication. */
312 serf__spnego_reset_sec_context(gss_info->gss_ctx);
313 gss_info->state = gss_api_auth_not_started;
317 status = gss_api_get_credentials(token, token_len,
318 conn->host_info.hostname,
323 apr_getnameinfo(&proxy_host, conn->ctx->proxy_address, 0);
324 status = gss_api_get_credentials(token, token_len, proxy_host,
331 /* On the next request, add an Authorization header. */
333 serf__encode_auth_header(&gss_info->value, authn_info->scheme->name,
337 gss_info->header = (peer == HOST) ?
338 "Authorization" : "Proxy-Authorization";
345 serf__init_spnego(int code,
352 /* A new connection is created to a server that's known to use
355 serf__init_spnego_connection(const serf__authn_scheme_t *scheme,
357 serf_connection_t *conn,
360 gss_authn_info_t *gss_info;
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);
375 conn->authn_baton = gss_info;
377 conn->proxy_authn_baton = gss_info;
380 /* Make serf send the initial requests one by one */
381 serf_connection_set_max_outstanding_requests(conn, 1);
383 serf__log_skt(AUTH_VERBOSE, __FILE__, conn->skt,
384 "Initialized Kerberos context for this connection.\n");
389 /* A 40x response was received, handle the authentication. */
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,
399 serf_connection_t *conn = request->conn;
400 gss_authn_info_t *gss_info = (code == 401) ? conn->authn_baton :
401 conn->proxy_authn_baton;
403 return do_auth(code == 401 ? HOST : PROXY,
411 /* Setup the authn headers on this request message. */
413 serf__setup_request_spnego_auth(peer_t peer,
415 serf_connection_t *conn,
416 serf_request_t *request,
419 serf_bucket_t *hdrs_bkt)
421 gss_authn_info_t *gss_info = (peer == HOST) ? conn->authn_baton :
422 conn->proxy_authn_baton;
424 /* If we have an ongoing authentication handshake, the handler of the
425 previous response will have created the authn headers for this request
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");
431 serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
434 /* We should send each token only once. */
435 gss_info->header = NULL;
436 gss_info->value = NULL;
441 switch (gss_info->pstate) {
443 /* We shouldn't normally arrive here, do nothing. */
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. */
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. */
457 case pstate_stateless:
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");
468 status = do_auth(peer,
472 0l, /* no response authn header */
477 serf_bucket_headers_setn(hdrs_bkt, gss_info->header,
479 /* We should send each token only once. */
480 gss_info->header = NULL;
481 gss_info->value = NULL;
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).
495 serf__validate_response_spnego_auth(peer_t peer,
497 serf_connection_t *conn,
498 serf_request_t *request,
499 serf_bucket_t *response,
502 gss_authn_info_t *gss_info;
503 const char *auth_hdr_name;
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");
514 gss_info = conn->authn_baton;
515 auth_hdr_name = "WWW-Authenticate";
517 gss_info = conn->proxy_authn_baton;
518 auth_hdr_name = "Proxy-Authenticate";
521 if (gss_info->state != gss_api_auth_completed) {
523 const char *auth_hdr_val;
526 hdrs = serf_bucket_response_get_headers(response);
527 auth_hdr_val = serf_bucket_headers_get(hdrs, auth_hdr_name);
529 status = do_auth(peer, code, gss_info, conn, auth_hdr_val, pool);
534 if (gss_info->state == gss_api_auth_completed) {
535 switch(gss_info->pstate) {
537 /* Authentication of the first request is done. */
538 gss_info->pstate = pstate_undecided;
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);
548 /* Nothing to do here. */
556 #endif /* SERF_HAVE_SPNEGO */