/* * mod_authenticache.c -- Apache auth caching module. * * If we cache the auth info, we make less trips to the database, slapd, * pdc, etc... It also allows us to do crazy things like auth against an * fwtk authsrv or something. * * ==================================================================== * The Apache Software License, Version 1.1 * * Copyright (c) 2002 Anthony D. Urso. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "Apache" and "Apache Software Foundation" must * not be used to endorse or promote products derived from this * software without prior written permission. For written * permission, please contact apache@apache.org. * * 5. Products derived from this software may not be called "Apache", * nor may "Apache" appear in their name, without prior written * permission of the Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * * Portions of this software are based upon public domain software * originally written at the National Center for Supercomputing Applications, * University of Illinois, Urbana-Champaign. * * $Id: mod_authenticache.c,v 1.3 2004/03/06 01:46:59 anthonyu Exp $ * */ #include /* for time_t */ #include "httpd.h" #include "http_config.h" #include "http_request.h" #include "http_log.h" #include "apr.h" #include "apr_base64.h" #include "apr_optional.h" #include "apr_strings.h" #ifdef AUTHENTICACHE_USE_SHM # include "apr_shm.h" #endif #include "util_md5.h" #include "defines.h" module AP_MODULE_DECLARE_DATA authenticache_module; #ifdef AUTHENTICACHE_USE_SHM static apr_shm_t *authenticache_keys = NULL; #else static unsigned char *authenticache_keys; #endif static const command_rec authenticache_configs[] = { AP_INIT_FLAG("Authenticache", ap_set_flag_slot, (void *)APR_OFFSETOF(authenticache_cfg, authenticache_on), OR_AUTHCFG, "When \"on\", cache successful authentication attempts. " "Default: off."), AP_INIT_FLAG("AuthenticacheAuthoritative", ap_set_flag_slot, (void *)APR_OFFSETOF(authenticache_cfg, authenticache_authoritative), OR_AUTHCFG, "This should always be off. Default: off."), AP_INIT_TAKE1("AuthenticacheTTL", ap_set_int_slot, (void *)APR_OFFSETOF(authenticache_cfg, authenticache_ttl), OR_AUTHCFG, "Time, in seconds, before expiration of cached auth " "info. Default: 600."), AP_INIT_TAKE1("AuthenticacheMaxTTL", ap_set_int_slot, (void *)APR_OFFSETOF(authenticache_cfg, authenticache_maxttl), OR_AUTHCFG, "Maximum time, in seconds, before expiration of cached auth " "info. Default: 3600."), AP_INIT_TAKE1("AuthenticacheTicketName", ap_set_string_slot, (void *)APR_OFFSETOF(authenticache_cfg, authenticache_tname), OR_AUTHCFG, "Name of cookie to sent to browser. Should be unique amongst " "cookies across the virtual host. " "Default: AuthentiCache"), AP_INIT_TAKE1("AuthenticacheTicketPath", ap_set_string_slot, (void *)APR_OFFSETOF(authenticache_cfg, authenticache_tpath), OR_AUTHCFG, "Path of cookie to sent to browser. Default: /"), AP_INIT_FLAG("AuthenticacheVerifyIP", ap_set_flag_slot, (void *)APR_OFFSETOF(authenticache_cfg, authenticache_verify_ip), OR_AUTHCFG, "When \"on\", verify ticket is returned from the IP we sent " "it to. Default: off"), {NULL} }; /* * Here we shall create the two keys that we'll use for the lifetime of the * server process... This is cheaper than makeing the two keys for each * connection. */ static int authenticache_init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *srec) { unsigned char *key1, *key2; #ifdef DEV_RANDOM apr_size_t size; apr_file_t *fp; #endif int i; ap_add_version_component(p, "AuthentiCache/" VERSION); #ifdef AUTHENTICACHE_USE_SHM if (apr_shm_create(&authenticache_keys, SIZE * 2, NULL, p)) { ap_log_error(APLOG_MARK, APLOG_EMERG, 0, srec, "mod_authenticache: unable to create shm segment"); return DECLINED; } key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys); #else authenticache_keys = (unsigned char *)apr_palloc(p, SIZE * 2); key1 = authenticache_keys; #endif /* AUTHENTICACHE_USE_SHM */ key2 = key1 + SIZE; #if APR_HAS_RANDOM if (apr_generate_random_bytes(key1, SIZE)) { ap_log_error(APLOG_MARK, APLOG_EMERG, 0, srec, "mod_authenticache: unable to generate random keys"); return DECLINED; } #elif defined(DEV_RANDOM) # warning "/dev/random code is untested" size = SIZE; apr_file_open(&fp, DEV_RANDOM, APR_READ, 0600, p); apr_file_read(fp, key1, &size); apr_file_close(fp); #else for (i = 0; i < SIZE; i++) key1[i] = randbyte(); #endif /* Magic bytes from RFC 2107... */ for (i = 0; i < SIZE; i++) { key2[i] = key1[i] ^ 0x5c; key1[i] ^= 0x36; } return OK; } /* * Here we create the default config for those entries not specified in an * .htaccess file. */ static void *authenticache_defaults(apr_pool_t *p, char *dir) { authenticache_cfg *c; c = (authenticache_cfg *)apr_palloc(p, sizeof(authenticache_cfg)); c->authenticache_on = 0; c->authenticache_authoritative = 0; c->authenticache_ttl = 600; c->authenticache_maxttl = 3600; c->authenticache_tname = "AuthentiCache"; c->authenticache_tpath = "/"; c->authenticache_verify_ip = 0; return (void *)c; } static apr_status_t authenticache_auth(request_rec *r) { authenticache_cfg *c; apr_table_t *ttab; c = (authenticache_cfg *)ap_get_module_config(r->per_dir_config, &authenticache_module); if (!c->authenticache_on) return DECLINED; ttab = apr_table_make(r->pool, 5); /* Parse out the cookie for me... */ if (!authenticache_parse_cookie(r, c, ttab)) return DECLINED; /* Test the cookie... If it is old or invalid, delete it and return * an error condition. */ if (!authenticache_valid_ticket(r, c, ttab)) { return (c->authenticache_authoritative) ? HTTP_UNAUTHORIZED : DECLINED; } /* This ticket passed all checks, set the connection user to the * ticket's username and log that fact. */ r->user = (char *)apr_table_get(ttab, "user"); ap_log_rerror(APLOG_MARK, APLOG_INFO|APLOG_NOERRNO, 0, r, "mod_authenticache: valid ticket from %s for %s", r->user, r->uri); /* Renew the ticket. */ authenticache_set_cookie(r, c->authenticache_tname, c->authenticache_tpath, c->authenticache_ttl, (char *)apr_table_get(ttab, "first")); return OK; } /* * We are in the fixup stage of the transaction. We only use this * opportunity to set a cookie since we know the user has already been * authenticated by another module. */ static int authenticache_cache(request_rec *r) { authenticache_cfg *c; const char *hdr; c = (authenticache_cfg *)ap_get_module_config(r->per_dir_config, &authenticache_module); if (!c->authenticache_on) return DECLINED; if (!ap_is_initial_req(r)) return DECLINED; if ((!r->user) || !strlen(r->user)) return DECLINED; if ((hdr = apr_table_get(r->headers_out, "Set-Cookie")) != NULL) if (strstr(hdr, c->authenticache_tname)) return DECLINED; /* The fact that r->connection->user is defined means that some * other module authenticated this user. Let's give browser * a cookie for future connections. */ authenticache_set_cookie(r, c->authenticache_tname, c->authenticache_tpath, c->authenticache_ttl, NULL); return DECLINED; } /* * This funtion requests that a cookie be parsed, then validates the * contents. */ static int authenticache_valid_ticket(request_rec *r, authenticache_cfg *c, apr_table_t *ttab) { unsigned char *key1, *key2; char *ticket, *hmac; time_t timelst, time1st, timenow; /* then put it back the way it was. */ ticket = apr_pstrcat(r->pool, apr_table_get(ttab, "user"), "|", apr_table_get(ttab, "last"), "|", apr_table_get(ttab, "first"), "|", apr_table_get(ttab, "address"), NULL); #ifdef AUTHENTICACHE_USE_SHM key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys); #else key1 = authenticache_keys; #endif /* AUTHENTICACHE_USE_SHM */ key2 = key1 + SIZE; /* Do an HMAC of the sent values... */ hmac = ap_hmac_md5(r->pool, ticket, key1, key2); /* and make sure it matches the sent signature. */ if (strcmp(hmac, apr_table_get(ttab, "hmac"))) return 0; /* Get the sent time, configured TTL and now. */ timelst = (time_t)strtol(apr_table_get(ttab, "last"), (char **)NULL, 10); time1st = (time_t)strtol(apr_table_get(ttab, "first"), (char **)NULL, 10); timenow = time(NULL); /* Drop any stale cookies, or ones that were sent in the future. */ if ((timenow > (time1st + c->authenticache_maxttl)) || (timenow > (timelst + c->authenticache_ttl)) || (timenow < time1st) || (timenow < timelst)) return 0; /* If we're asked to check the IP, let's do so. If this cookie * were sent over a clear channel, it could be stolen. Then again, * it would be easier to sniff the Basic auth info and * authenticate using that... */ if ((c->authenticache_verify_ip) && (strcmp(apr_table_get(ttab, "address"), r->connection->remote_ip))) return 0; return 1; } static char *authenticache_hmac(apr_pool_t *p, char *text) { unsigned char *key1, *key2; #ifdef AUTHENTICACHE_USE_SHM key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys); #else key1 = authenticache_keys; #endif /* AUTHENTICACHE_USE_SHM */ key2 = key1 + SIZE; return ap_hmac_md5(p, text, key1, key2); } static char *ap_hmac_md5(apr_pool_t *p, char *text, unsigned char *k0, unsigned char *k1) { apr_md5_ctx_t ctx; unsigned char *digest; if (!k0) return k0; if (!k1) { unsigned int i; if ((k1 = apr_palloc(p, SIZE)) == NULL) return k1; memcpy(k1, k0, SIZE); /* Magic bytes from RFC 2104... */ for (i = 0; i < SIZE; i++) { k0[i] ^= 0x36; k1[i] ^= 0x5c; } } if ((digest = apr_palloc(p, MD5_DIGESTSIZE)) == NULL) return digest; /* perform inner MD5 */ apr_md5_init(&ctx); apr_md5_update(&ctx, k0, SIZE); apr_md5_update(&ctx, text, strlen(text)); apr_md5_final(digest, &ctx); /* perform outer MD5 */ apr_md5_init(&ctx); apr_md5_update(&ctx, k1, SIZE); apr_md5_update(&ctx, digest, MD5_DIGESTSIZE); return ap_md5contextTo64(p, &ctx); } /* * This function retrieves the authenticache cookie, parses out the fields, * and returns the info in a table provided by the caller. */ static int authenticache_parse_cookie(request_rec *r, authenticache_cfg *c, apr_table_t *ttab) { const char *cookie; char *name, *info, *buf, *end; size_t infolen; char reject[] = { ' ', '\t', '\r', '\n', '\0' }; if ((cookie = apr_table_get(r->headers_in, "Cookie")) == NULL) return 0; /* Is our cookie even in there? Point to it. */ if ((name = strstr(cookie, c->authenticache_tname)) == NULL) return 0; /* Find the start of the cookie data. */ if ((info = strchr(name, '=')) == NULL) return 0; /* Get the length of the coookie data. */ if ((infolen = strcspn(++info, reject)) < 4) /* min size of base64 */ return 0; /* Terminate the string. */ info[infolen] = 0; if ((buf = apr_pcalloc(r->pool, apr_base64_decode_len(info))) == NULL) return 0; /* Base64 decode the ticket data. */ if (apr_base64_decode(buf, info) < 26) /* smallest ticket */ return 0; /* These stanzas populate the table with ticket bits. */ if ((end = strchr(buf, '|')) != NULL) { *end = 0; apr_table_setn(ttab, "user", buf); buf = ++end; } else return 0; if ((end = strchr(buf, '|')) != NULL) { *end = 0; apr_table_setn(ttab, "last", buf); buf = ++end; } else return 0; if ((end = strchr(buf, '|')) != NULL) { *end = 0; apr_table_setn(ttab, "first", buf); buf = ++end; } else return 0; if ((end = strchr(buf, '|')) != NULL) { *end = 0; apr_table_setn(ttab, "address", buf); buf = ++end; } else return 0; if (strlen(buf)) apr_table_setn(ttab, "hmac", buf); else return 0; return 1; } /* * Given that this user already authenticated, lets give them a cookie to * show for it. */ static int authenticache_set_cookie(request_rec *r, char *name, char *path, unsigned int ttl, char *first) { char *timenow, *ticket, *hmac, *infobuf, *info, *date, *cookie; unsigned char *key1, *key2; /* Get the current time as a string. */ if ((timenow = apr_psprintf(r->pool, "%ld", (long)time(NULL))) == NULL) return 0; if (first == NULL) first = timenow; /* Make the ticket part. Looks like "username|now|first|ipaddr." */ if ((ticket = apr_pstrcat(r->pool, r->user, "|", timenow, "|", first, "|", r->connection->remote_ip, NULL)) == NULL) return 0; #ifdef AUTHENTICACHE_USE_SHM key1 = (unsigned char *)apr_shm_baseaddr_get(authenticache_keys); #else key1 = authenticache_keys; #endif /* AUTHENTICACHE_USE_SHM */ key2 = key1 + SIZE; /* Make the signature. */ if ((hmac = ap_hmac_md5(r->pool, ticket, key1, key2)) == NULL) return 0; /* Append the signature to the ticket and Base64 encode it. */ if ((infobuf = apr_pstrcat(r->pool, ticket, "|", hmac, NULL)) == NULL) return 0; if ((info = apr_pcalloc(r->pool, apr_base64_encode_len(strlen(infobuf)) + 1)) == NULL) return 0; apr_base64_encode(info, infobuf, strlen(infobuf)); date = (char *)apr_palloc(r->pool, APR_RFC822_DATE_LEN); apr_rfc822_date(date, apr_time_now() + (ttl * APR_USEC_PER_SEC)); /* This is awful, but it works... */ date[7] = '-'; date[11] = '-'; /* Make it look like a cookie. */ if ((cookie = apr_pstrcat(r->pool, name, "=", info, "; path=", path, "; expires=", date, NULL)) == NULL) return 0; /* And set the Cookie: header. */ apr_table_setn(r->headers_out, "Set-Cookie", cookie); return 1; } /* Register hooks. :) */ static void authenticache_register_hooks(apr_pool_t *p) { APR_REGISTER_OPTIONAL_FN(authenticache_hmac); APR_REGISTER_OPTIONAL_FN(authenticache_set_cookie); ap_hook_post_config(authenticache_init, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_check_user_id(authenticache_auth, NULL, NULL, APR_HOOK_REALLY_FIRST); ap_hook_fixups(authenticache_cache, NULL, NULL, APR_HOOK_MIDDLE); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA authenticache_module = { STANDARD20_MODULE_STUFF, authenticache_defaults, NULL, NULL, NULL, authenticache_configs, authenticache_register_hooks };