/*
* 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
};