| /* ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| |
| #include "auth_spnego.h" |
| |
| #ifdef SERF_HAVE_SPNEGO |
| |
| /** These functions implement SPNEGO-based Kerberos and NTLM authentication, |
| * using either GSS-API (RFC 2743) or SSPI on Windows. |
| * The HTTP message exchange is documented in RFC 4559. |
| **/ |
| |
| #include <serf.h> |
| #include <serf_private.h> |
| #include <auth/auth.h> |
| |
| #include <apr.h> |
| #include <apr_base64.h> |
| #include <apr_strings.h> |
| |
| /** TODO: |
| ** - send session key directly on new connections where we already know |
| ** the server requires Kerberos authn. |
| ** - Add a way for serf to give detailed error information back to the |
| ** application. |
| ** - This file is both GSSAPI and Kerberos/NTLM independent, so update names |
| ** and comments. |
| **/ |
| |
| /* Authentication over HTTP using Kerberos |
| * |
| * Kerberos involves three servers: |
| * - Authentication Server (AS): verifies users during login |
| * - Ticket-Granting Server (TGS): issues proof of identity tickets |
| * - HTTP server (S) |
| * |
| * Steps: |
| * 0. User logs in to the AS and receives a TGS ticket. On workstations |
| * where the login program doesn't support Kerberos, the user can use |
| * 'kinit'. |
| * |
| * 1. C --> S: GET |
| * |
| * C <-- S: 401 Authentication Required |
| * WWW-Authenticate: Negotiate |
| * |
| * -> app contacts the TGS to request a session key for the HTTP service |
| * @ target host. The returned session key is encrypted with the HTTP |
| * service's secret key, so we can safely send it to the server. |
| * |
| * 2. C --> S: GET |
| * Authorization: Negotiate <Base64 encoded session key> |
| * gss_api_ctx->state = gss_api_auth_in_progress; |
| * |
| * C <-- S: 200 OK |
| * WWW-Authenticate: Negotiate <Base64 encoded server |
| * authentication data> |
| * |
| * -> The server returned an (optional) key to proof itself to us. We check this |
| * key with the TGS again. If it checks out, we can return the response |
| * body to the application. |
| * |
| * Note: It's possible that the server returns 401 again in step 2, if the |
| * Kerberos context isn't complete yet. This means there is 3rd step |
| * where we'll send a request with an Authorization header to the |
| * server. Some (simple) tests with mod_auth_kerb and MIT Kerberos 5 show |
| * this never happens. |
| * |
| * Depending on the type of HTTP server, this handshake is required for either |
| * every new connection, or for every new request! For more info see the next |
| * comment on authn_persistence_state_t. |
| * |
| * Note: Step 1 of the handshake will only happen on the first connection, once |
| * we know the server requires Kerberos authentication, the initial requests |
| * on the other connections will include a session key, so we start at |
| * step 2 in the handshake. |
| * ### TODO: Not implemented yet! |
| */ |
| |
| /* Current state of the authentication of the current request. */ |
| typedef enum gss_api_auth_state { |
| gss_api_auth_not_started, |
| gss_api_auth_in_progress, |
| gss_api_auth_completed, |
| } gss_api_auth_state; |
| |
| /** |
| authn_persistence_state_t: state that indicates if we are talking with a |
| server that requires authentication only of the first request (stateful), |
| or of each request (stateless). |
| |
| INIT: Begin state. Authenticating the first request on this connection. |
| UNDECIDED: we haven't identified the server yet, assume STATEFUL for now. |
| Pipeline mode disabled, requests are sent only after the response off the |
| previous request arrived. |
| STATELESS: we know the server requires authentication for each request. |
| On all new requests add the Authorization header with an initial SPNEGO |
| token (created per request). |
| To keep things simple, keep the connection in one by one mode. |
| (otherwise we'd have to keep a queue of gssapi context objects to match |
| the Negotiate header of the response with the session initiated by the |
| mathing request). |
| This state is an final state. |
| STATEFUL: alright, we have authenticated the connection and for the server |
| that is enough. Don't add an Authorization header to new requests. |
| Serf will switch to pipelined mode. |
| This state is not a final state, although in practical scenario's it will |
| be. When we receive a 40x response from the server switch to STATELESS |
| mode. |
| |
| We start in state init for the first request until it is authenticated. |
| |
| The rest of the state machine starts with the arrival of the response to the |
| second request, and then goes on with each response: |
| |
| -------- |
| | INIT | C --> S: GET request in response to 40x of the server |
| -------- add [Proxy]-Authorization header |
| | |
| | |
| ------------ |
| | UNDECIDED| C --> S: GET request, assume stateful, |
| ------------ no [Proxy]-Authorization header |
| | |
| | |
| |------------------------------------------------ |
| | | |
| | C <-- S: 40x Authentication | C <-- S: 200 OK |
| | Required | |
| | | |
| v v |
| ------------- ------------ |
| ->| STATELESS |<------------------------------| STATEFUL |<-- |
| | ------------- C <-- S: 40x ------------ | |
| * | | Authentication | | 200 OK |
| | / Required | | |
| ----- -----/ |
| |
| **/ |
| typedef enum authn_persistence_state_t { |
| pstate_init, |
| pstate_undecided, |
| pstate_stateless, |
| pstate_stateful, |
| } authn_persistence_state_t; |
| |
| |
| /* HTTP Service name, used to get the session key. */ |
| #define KRB_HTTP_SERVICE "HTTP" |
| |
| /* Stores the context information related to Kerberos authentication. */ |
| typedef struct |
| { |
| apr_pool_t *pool; |
| |
| /* GSSAPI context */ |
| serf__spnego_context_t *gss_ctx; |
| |
| /* Current state of the authentication cycle. */ |
| gss_api_auth_state state; |
| |
| /* Current persistence state. */ |
| authn_persistence_state_t pstate; |
| |
| const char *header; |
| const char *value; |
| } gss_authn_info_t; |
| |
| /* On the initial 401 response of the server, request a session key from |
| the Kerberos KDC to pass to the server, proving that we are who we |
| claim to be. The session key can only be used with the HTTP service |
| on the target host. */ |
| 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) |
| { |
| serf__spnego_buffer_t input_buf; |
| serf__spnego_buffer_t output_buf; |
| apr_status_t status = APR_SUCCESS; |
| |
| /* If the server sent us a token, pass it to gss_init_sec_token for |
| validation. */ |
| if (token) { |
| input_buf.value = token; |
| input_buf.length = token_len; |
| } else { |
| input_buf.value = 0; |
| input_buf.length = 0; |
| } |
| |
| /* Establish a security context to the server. */ |
| status = serf__spnego_init_sec_context( |
| conn, |
| gss_info->gss_ctx, |
| KRB_HTTP_SERVICE, hostname, |
| &input_buf, |
| &output_buf, |
| gss_info->pool, |
| gss_info->pool |
| ); |
| |
| switch(status) { |
| case APR_SUCCESS: |
| if (output_buf.length == 0) { |
| gss_info->state = gss_api_auth_completed; |
| } else { |
| gss_info->state = gss_api_auth_in_progress; |
| } |
| break; |
| case APR_EAGAIN: |
| gss_info->state = gss_api_auth_in_progress; |
| status = APR_SUCCESS; |
| break; |
| default: |
| return status; |
| } |
| |
| /* Return the session key to our caller. */ |
| *buf = output_buf.value; |
| *buf_len = output_buf.length; |
| |
| return status; |
| } |
| |
| /* do_auth is invoked in two situations: |
| - when a response from a server is received that contains an authn header |
| (either from a 40x or 2xx response) |
| - when a request is prepared on a connection with stateless authentication. |
| |
| Read the header sent by the server (if any), invoke the gssapi authn |
| code and use the resulting Server Ticket on the next request to the |
| server. */ |
| static apr_status_t |
| do_auth(const serf__authn_scheme_t *scheme, |
| 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) |
| { |
| serf_context_t *ctx = conn->ctx; |
| serf__authn_info_t *authn_info; |
| const char *tmp = NULL; |
| char *token = NULL; |
| apr_size_t tmp_len = 0, token_len = 0; |
| apr_status_t status; |
| |
| if (peer == HOST) { |
| authn_info = serf__get_authn_info_for_server(conn); |
| } else { |
| authn_info = &ctx->proxy_authn_info; |
| } |
| |
| /* Is this a response from a host/proxy? auth_hdr should always be set. */ |
| if (code && auth_hdr) { |
| const char *space = NULL; |
| /* The server will return a token as attribute to the Negotiate key. |
| Negotiate YGwGCSqGSIb3EgECAgIAb10wW6ADAgEFoQMCAQ+iTzBNoAMCARCiRgREa6 |
| mouMBAMFqKVdTGtfpZNXKzyw4Yo1paphJdIA3VOgncaoIlXxZLnkHiIHS2v65pVvrp |
| bRIyjF8xve9HxpnNIucCY9c= |
| |
| Read this base64 value, decode it and validate it so we're sure the |
| server is who we expect it to be. */ |
| space = strchr(auth_hdr, ' '); |
| |
| if (space) { |
| token = apr_palloc(pool, apr_base64_decode_len(space + 1)); |
| token_len = apr_base64_decode(token, space + 1); |
| } |
| } else { |
| /* This is a new request, not a retry in response to a 40x of the |
| host/proxy. |
| Only add the Authorization header if we know the server requires |
| per-request authentication (stateless). */ |
| if (gss_info->pstate != pstate_stateless) |
| return APR_SUCCESS; |
| } |
| |
| switch(gss_info->pstate) { |
| case pstate_init: |
| /* Nothing to do here */ |
| break; |
| case pstate_undecided: /* Fall through */ |
| case pstate_stateful: |
| { |
| /* Switch to stateless mode, from now on handle authentication |
| of each request with a new gss context. This is easiest to |
| manage when sending requests one by one. */ |
| serf__log(LOGLVL_INFO, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "Server requires per-request SPNEGO authn, " |
| "switching to (slower) stateless mode.\n"); |
| |
| gss_info->pstate = pstate_stateless; |
| serf__connection_set_pipelining(conn, 0); |
| break; |
| } |
| case pstate_stateless: |
| /* Nothing to do here */ |
| break; |
| } |
| |
| if (request->auth_baton && !token) { |
| /* We provided token with this request, but server responded with empty |
| authentication header. This means server rejected our credentials. |
| */ |
| return SERF_ERROR_AUTHN_CREDENTIALS_REJECTED; |
| } |
| |
| /* If the server didn't provide us with a token, start with a new initial |
| step in the SPNEGO authentication. */ |
| if (!token) { |
| serf__spnego_reset_sec_context(gss_info->gss_ctx); |
| gss_info->state = gss_api_auth_not_started; |
| } |
| |
| if (peer == HOST) { |
| status = gss_api_get_credentials(conn, |
| token, token_len, |
| conn->host_info.hostname, |
| &tmp, &tmp_len, |
| gss_info); |
| } else { |
| char *proxy_host = conn->ctx->proxy_address->hostname; |
| status = gss_api_get_credentials(conn, |
| token, token_len, proxy_host, |
| &tmp, &tmp_len, |
| gss_info); |
| } |
| if (status) |
| return status; |
| |
| /* On the next request, add an Authorization header. */ |
| if (tmp_len) { |
| serf__encode_auth_header(&gss_info->value, scheme->name, |
| tmp, |
| tmp_len, |
| pool); |
| gss_info->header = (peer == HOST) ? |
| "Authorization" : "Proxy-Authorization"; |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| /* A new connection is created to a server that's known to use |
| Kerberos. Implements serf__init_conn_func_t callback. */ |
| static apr_status_t |
| serf__init_spnego_connection(const serf__authn_scheme_t *scheme, |
| int code, |
| serf_connection_t *conn, |
| apr_pool_t *pool) |
| { |
| serf_context_t *ctx = conn->ctx; |
| serf__authn_info_t *authn_info; |
| gss_authn_info_t *gss_info = NULL; |
| |
| /* For proxy authentication, reuse the gss context for all connections. |
| For server authentication, create a new gss context per connection. */ |
| if (code == 401) { |
| authn_info = &conn->authn_info; |
| } else { |
| authn_info = &ctx->proxy_authn_info; |
| } |
| gss_info = authn_info->baton; |
| |
| if (!gss_info) { |
| apr_status_t status; |
| |
| gss_info = apr_pcalloc(conn->pool, sizeof(*gss_info)); |
| gss_info->pool = conn->pool; |
| gss_info->state = gss_api_auth_not_started; |
| gss_info->pstate = pstate_init; |
| status = serf__spnego_create_sec_context(&gss_info->gss_ctx, scheme, |
| gss_info->pool, pool); |
| if (status) { |
| return status; |
| } |
| authn_info->baton = gss_info; |
| } |
| |
| /* Make serf send the initial requests one by one */ |
| serf__connection_set_pipelining(conn, 0); |
| |
| serf__log(LOGLVL_DEBUG, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "Initialized Kerberos context for this connection.\n"); |
| |
| return APR_SUCCESS; |
| } |
| |
| /* Implements serf__auth_handler_func_t callback. |
| A 40x response was received, handle the authentication. */ |
| static apr_status_t |
| serf__handle_spnego_auth(const serf__authn_scheme_t *scheme, |
| int code, |
| serf_request_t *request, |
| serf_bucket_t *response, |
| const char *auth_hdr, |
| const char *auth_attr, |
| apr_pool_t *pool) |
| { |
| serf_connection_t *conn = request->conn; |
| serf_context_t *ctx = conn->ctx; |
| gss_authn_info_t *gss_info = (code == 401) ? conn->authn_info.baton : |
| ctx->proxy_authn_info.baton; |
| |
| return do_auth(scheme, |
| code == 401 ? HOST : PROXY, |
| code, |
| gss_info, |
| request->conn, |
| request, |
| auth_hdr, |
| pool); |
| } |
| |
| /* Callback function (implements serf__setup_request_func_t). Setup the authn |
| headers on this request message. */ |
| static apr_status_t |
| serf__setup_request_spnego_auth(const serf__authn_scheme_t *scheme, |
| peer_t peer, |
| int code, |
| serf_connection_t *conn, |
| serf_request_t *request, |
| const char *method, |
| const char *uri, |
| serf_bucket_t *hdrs_bkt) |
| { |
| serf_context_t *ctx = conn->ctx; |
| gss_authn_info_t *gss_info = (peer == HOST) ? conn->authn_info.baton : |
| ctx->proxy_authn_info.baton; |
| |
| /* If we have an ongoing authentication handshake, the handler of the |
| previous response will have created the authn headers for this request |
| already. */ |
| if (gss_info && gss_info->header && gss_info->value) { |
| serf__log(LOGLVL_DEBUG, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "Set Negotiate authn header on retried request.\n"); |
| |
| serf_bucket_headers_setn(hdrs_bkt, gss_info->header, |
| gss_info->value); |
| |
| /* Remember that we're using this request for authentication |
| handshake. */ |
| request->auth_baton = (void*) TRUE; |
| |
| /* We should send each token only once. */ |
| gss_info->header = NULL; |
| gss_info->value = NULL; |
| |
| return APR_SUCCESS; |
| } |
| |
| switch (gss_info->pstate) { |
| case pstate_init: |
| /* We shouldn't normally arrive here, do nothing. */ |
| break; |
| case pstate_undecided: /* fall through */ |
| serf__log(LOGLVL_DEBUG, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "Assume for now that the server supports persistent " |
| "SPNEGO authentication.\n"); |
| /* Nothing to do here. */ |
| break; |
| case pstate_stateful: |
| serf__log(LOGLVL_DEBUG, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "SPNEGO on this connection is persistent, " |
| "don't set authn header on next request.\n"); |
| /* Nothing to do here. */ |
| break; |
| case pstate_stateless: |
| { |
| apr_status_t status; |
| |
| /* Authentication on this connection is known to be stateless. |
| Add an initial Negotiate token for the server, to bypass the |
| 40x response we know we'll otherwise receive. |
| (RFC 4559 section 4.2) */ |
| serf__log(LOGLVL_DEBUG, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "Add initial Negotiate header to request.\n"); |
| |
| status = do_auth(scheme, |
| peer, |
| code, |
| gss_info, |
| conn, |
| request, |
| 0l, /* no response authn header */ |
| conn->pool); |
| if (status) |
| return status; |
| |
| serf_bucket_headers_setn(hdrs_bkt, gss_info->header, |
| gss_info->value); |
| |
| /* Remember that we're using this request for authentication |
| handshake. */ |
| request->auth_baton = (void*) TRUE; |
| |
| /* We should send each token only once. */ |
| gss_info->header = NULL; |
| gss_info->value = NULL; |
| break; |
| } |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| /** |
| * Baton passed to the get_auth_header callback function. |
| */ |
| typedef struct get_auth_header_baton_t { |
| const char *hdr_name; |
| const char *auth_name; |
| const char *hdr_value; |
| apr_pool_t *pool; |
| } get_auth_header_baton_t; |
| |
| static int |
| get_auth_header_cb(void *baton, |
| const char *key, |
| const char *header) |
| { |
| get_auth_header_baton_t *b = baton; |
| |
| /* We're only interested in xxxx-Authenticate headers. */ |
| if (strcasecmp(key, b->hdr_name) != 0) |
| return 0; |
| |
| /* Check if header value starts with interesting auth name. */ |
| if (strncmp(header, b->auth_name, strlen(b->auth_name)) == 0) { |
| /* Save interesting header value and stop iteration. */ |
| b->hdr_value = apr_pstrdup(b->pool, header); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static const char * |
| get_auth_header(serf_bucket_t *hdrs, |
| const char *hdr_name, |
| const char *auth_name, |
| apr_pool_t *pool) |
| { |
| get_auth_header_baton_t b; |
| |
| b.auth_name = hdr_name; |
| b.hdr_name = auth_name; |
| b.hdr_value = NULL; |
| b.pool = pool; |
| |
| serf_bucket_headers_do(hdrs, get_auth_header_cb, &b); |
| |
| return b.hdr_value; |
| } |
| |
| /* Callback function (implements serf__validate_response_func_t). |
| * Function is called when 2xx responses are received. Normally we don't |
| * have to do anything, except for the first response after the |
| * authentication handshake. This specific response includes authentication |
| * data which should be validated by the client (mutual authentication). |
| */ |
| 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) |
| { |
| serf_context_t *ctx = conn->ctx; |
| gss_authn_info_t *gss_info; |
| const char *auth_hdr_name; |
| |
| if (peer == HOST) { |
| gss_info = conn->authn_info.baton; |
| auth_hdr_name = "WWW-Authenticate"; |
| } else { |
| gss_info = ctx->proxy_authn_info.baton; |
| auth_hdr_name = "Proxy-Authenticate"; |
| } |
| |
| if (gss_info->state != gss_api_auth_completed) { |
| serf_bucket_t *hdrs; |
| const char *auth_hdr_val; |
| apr_status_t status; |
| |
| serf__log(LOGLVL_DEBUG, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "Validate SPNEGO response header.\n"); |
| |
| hdrs = serf_bucket_response_get_headers(response); |
| auth_hdr_val = get_auth_header(hdrs, auth_hdr_name, scheme->name, |
| pool); |
| |
| if (auth_hdr_val) { |
| status = do_auth(scheme, peer, code, gss_info, conn, request, |
| auth_hdr_val, pool); |
| if (status) { |
| return status; |
| } |
| } else { |
| /* No Authenticate headers, nothing to validate: authentication |
| completed.*/ |
| gss_info->state = gss_api_auth_completed; |
| |
| serf__log(LOGLVL_DEBUG, LOGCOMP_AUTHN, __FILE__, conn->config, |
| "SPNEGO handshake completed.\n"); |
| } |
| } |
| |
| if (gss_info->state == gss_api_auth_completed) { |
| switch(gss_info->pstate) { |
| case pstate_init: |
| /* Authentication of the first request is done. */ |
| gss_info->pstate = pstate_undecided; |
| break; |
| case pstate_undecided: |
| /* The server didn't request for authentication even though |
| we didn't add an Authorization header to previous |
| request. That means it supports persistent authentication. */ |
| gss_info->pstate = pstate_stateful; |
| serf__connection_set_pipelining(conn, 1); |
| break; |
| default: |
| /* Nothing to do here. */ |
| break; |
| } |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| const serf__authn_scheme_t serf__spnego_authn_scheme = { |
| "Negotiate", |
| "negotiate", |
| SERF_AUTHN_NEGOTIATE, |
| serf__init_spnego_connection, |
| serf__handle_spnego_auth, |
| serf__setup_request_spnego_auth, |
| serf__validate_response_spnego_auth, |
| }; |
| |
| #ifdef WIN32 |
| const serf__authn_scheme_t serf__ntlm_authn_scheme = { |
| "NTLM", |
| "ntlm", |
| SERF_AUTHN_NTLM, |
| serf__init_spnego_connection, |
| serf__handle_spnego_auth, |
| serf__setup_request_spnego_auth, |
| serf__validate_response_spnego_auth, |
| }; |
| #endif /* #ifdef WIN32 */ |
| |
| #endif /* SERF_HAVE_SPNEGO */ |