/* * mod_auth_ldap_cookie.c * $Id: mod_auth_ldap_cookie.c,v 1.3 2005/11/27 10:35:57 huber Exp huber $ * * Copyright (C) 2004-2005 by Marc Huber . * 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. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. * */ /******************************************************************************* Download site: http://www.pro-bono-publico.de/misc/sso/ Initial authentication is done via Basic Authentication. On success, the module sets a cookie which permits access on all suitably configured https servers in the cookie domain. Not quite single-sign-on, but pretty close. Use at your own risk. Sample configuration: ---8<--- CUT HERE ---8<--- AuthType Basic AuthName CookieAuth AuthLDAPCookieEnable on AuthLDAPCookieName myauth AuthLDAPCookieServerURL ldap://ldap01.example.com ldap://ldap02.example.com AuthLDAPCookieServerFirstIsMaster on AuthLDAPCookieDN cn=%s,ou=cookie,dc=example.com AuthLDAPCookieCacheTime 60 AuthLDAPCookieUser cn=webserver,dc=example,dc=com AuthLDAPCookiePass h1dd3n AuthLDAPCookieRenewAge 300 AuthLDAPCookieRenewBy 3600 AuthLDAPCookieSearchTimeout 5 AuthLDAPCookieConnectTimeout 2 AuthLDAPCookieDomain .example.com AuthLDAPCookieUserBase ou=people,dc=example,dc=com AuthLDAPCookieUserScope one AuthLDAPCookieUserFilter ((objectClass=shadowAccount)(uid=%s)) ---8<--- CUT HERE ---8<--- The module will create LDAP objects similar to: ---8<--- CUT HERE ---8<--- dn: o=0123456789abcdef0123456789abcdef,ou=cookie,dc=example,dc=com objectClass: httpCookie cn: 0123456789abcdef0123456789abcdef httpCookieName: CookieAuth httpCookieExpires: 1112949946 httpCookieRemoteDN: cn=marc,ou=people,dc=example,dc=com httpCookieRemoteIP: 192.168.1.1 ---8<--- CUT HERE ---8<--- Take care that only the user(s) specified via AuthLDAPCookieUser have access to authentication cookies, e.g. in OpenLDAP's slapd.conf: ---8<--- CUT HERE ---8<--- access to dn.sub="ou=cookie,dc=example,dc=com" by dn="cn=webserver,dc=example,dc=com" write stop by * =0 stop ---8<--- CUT HERE ---8<--- The required LDAP schema (experimental OIDs, sorry!). This probably should be in /etc/ldap/schema/, with a suitable "include" directive in slapd.conf: ---8<--- CUT HERE ---8<--- # Experimental LDAP schema for http cookies # (C)2005 Marc Huber # Uses experimental OID space: # 1.3.6.1.3.1.1: experimental space # 1.3.6.1.3.1.1.2: experimental vendor 2 space # 1.3.6.1.3.1.1.2.1: subclass 1 space # 1.3.6.1.3.1.1.2.1.1: objectClass space # 1.3.6.1.3.1.1.2.1.2: attributetype space attributetype ( 1.3.6.1.3.1.1.2.1.2.1 NAME 'httpCookieRemoteDN' DESC 'valid DN' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 ) attributetype ( 1.3.6.1.3.1.1.2.1.2.2 NAME 'httpCookieRemoteIP' DESC 'remote address' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{20} ) attributetype ( 1.3.6.1.3.1.1.2.1.2.3 NAME 'httpCookieExpires' DESC 'last change' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{20} ) attributetype ( 1.3.6.1.3.1.1.2.1.2.4 NAME 'httpCookieName' DESC 'name of cookie' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{20} ) objectclass ( 1.3.6.1.3.1.1.2.1.1.1 NAME 'httpCookie' SUP top STRUCTURAL DESC 'HTTP cookie object' MUST ( cn $ httpCookieName $ httpCookieExpires $ httpCookieRemoteIP $ httpCookieRemoteDN ) ) ---8<--- CUT HERE ---8<--- Finally, it's a good idea to periodically clean up outdated objects: ---8<--- CUT HERE ---8<--- #!/usr/bin/perl -w # remove_expired_cookies.pl # # (C)2005 Marc Huber use strict; use Net::LDAP; my $host = 'ldap01'; my $admindn = 'cn=srv01,o=cookieauth,dc=example,dc=com'; my $pass = 'whatever'; my $base = 'ou=cookie,o=cookieauth,dc=example,dc=com'; my ($ldap, $mesg); $ldap = Net::LDAP->new($host); die unless $ldap; $mesg = $ldap->start_tls; die $mesg->error if $mesg->code; $mesg = $ldap->bind($admindn, password => $pass); die $mesg->error if $mesg->code; $mesg = $ldap->search(base=>$base, filter=>'(objectClass=httpCookie)', scope=>'one'); die $mesg->error if $mesg->code; foreach my $entry ($mesg->entries) { my $expiry = $entry->get_value('httpCookieExpires'); $ldap->delete($entry->dn) if $expiry < time; } $ldap->unbind; exit 0; ---8<--- CUT HERE ---8<--- Compilation and installation: - Apache-1.x: apxs -i -c -lldap mod_auth_ldap_cookie.c - Apache-2.x: apxs2 -DAPACHE2 -i -c -lldap mod_auth_ldap_cookie.c *******************************************************************************/ #define MODNAME "AuthLDAPCookie" #ifdef APACHE2 # include "ap_config.h" # include "httpd.h" # include "http_config.h" # include "http_core.h" # include "http_request.h" # include "http_log.h" # include "http_protocol.h" # include "apr.h" # include "apr_errno.h" # include "apr_hooks.h" # include "apr_lib.h" # include "apr_strings.h" # include "apr_time.h" # include "apr_md5.h" # include "apr_thread_mutex.h" # include "apr_thread_rwlock.h" # define Log(format, ...) ap_log_error(APLOG_MARK, \ APLOG_NOERRNO | APLOG_NOTICE, 0, \ r->server, "%s:%4d: " format, __FILE__, __LINE__, \ __VA_ARGS__) # define REMOTEUSER(A) A->user #else /* APACHE1 */ # include "httpd.h" # include "http_config.h" # include "http_protocol.h" # include "http_log.h" # include "util_md5.h" # define apr_array_header_t array_header # define apr_table_get ap_table_get # define apr_pstrdup ap_pstrdup # define apr_table_do ap_table_do # define apr_table_t table # define apr_pool_t pool # define apr_pool_create pool_create # define apr_pcalloc ap_pcalloc # define apr_array_make ap_make_array # define apr_array_push ap_push_array # define apr_table_make ap_make_table # define apr_table_unset ap_table_unset # define apr_table_addn ap_table_addn # define apr_table_set ap_table_set # define apr_table_unset ap_table_unset # define apr_psprintf ap_psprintf # define AP_INIT_FLAG(A,B,C,D,E) {A,B,C,D,FLAG,E} # define AP_INIT_TAKE1(A,B,C,D,E) {A,B,C,D,TAKE1,E} # define AP_INIT_ITERATE(A,B,C,D,E) {A,B,C,D,ITERATE,E} # define APLOGARGS APLOG_MARK, APLOG_NOERRNO | APLOG_NOTICE, r->server # define Log(format, ...) ap_log_error(APLOG_MARK, \ APLOG_NOERRNO | APLOG_NOTICE, \ r->server, "%s:%4d: " format, __FILE__, __LINE__, \ __VA_ARGS__) # define APR_OFFSETOF XtOffsetOf # define REMOTEUSER(A) A->connection->user # define apr_thread_rwlock_create(A,B) while(0); # define apr_thread_rwlock_unlock(A) while(0); # define apr_thread_rwlock_wrlock(A) while(0); # ifndef APR_MD5_DIGESTSIZE # define APR_MD5_DIGESTSIZE 16 # endif #endif #include #include #include #include #define ACT(A) ((auth_cfg_t *)(A)) /* Debugging stuff, unfortunately not thread-safe, but who cares ... */ static int indent = 0; static char *whitespace = " " " " " "; #define Debug(format, ...) if(rec->debug)Log("%.*s" format,*format=='<'?indent--:(*format=='>'?++indent:indent),whitespace, __VA_ARGS__) typedef struct { char *name; char *user; char *pass; char *domain; char *cookie_dn; char *user_base; char *user_filter; int first_is_master; int ldap_idx; int is_enabled; int debug; int user_scope; int search_timeout; int connect_timeout; time_t renew_age; time_t renew_by; time_t last_purge; time_t expires; apr_array_header_t *server; LDAP *ldap; LDAP *ldap_master; #ifdef APACHE2 apr_thread_rwlock_t *ldap_lock; #endif } auth_cfg_t; /* FIXME -- should use shared memory for tables */ static apr_table_t *cookie_userdn = NULL; static apr_table_t *cookie_ip = NULL; static apr_table_t *cookie_purge = NULL; static apr_table_t *cookie_expires = NULL; #ifdef APACHE2 static apr_thread_rwlock_t *global_lock = NULL; #endif typedef struct { char *name; int port; int last_error_code; time_t last_error_time; } ldap_server_t; static void *auth_cfg(apr_pool_t * p, char *d) { auth_cfg_t *rec = apr_pcalloc(p, sizeof(auth_cfg_t)); if (rec) { rec->server = apr_array_make(p, 4, sizeof(ldap_server_t)); rec->last_purge = time(NULL); rec->expires = 10; rec->renew_age = 10; rec->renew_by = 10; rec->ldap_idx = -1; rec->search_timeout = 5; rec->connect_timeout = 2; apr_thread_rwlock_create(&rec->ldap_lock, p); } return (void *) rec; } #ifdef APACHE2 static int auth_init(apr_pool_t * p, apr_pool_t * plog, apr_pool_t * ptemp, server_rec * server) { apr_thread_rwlock_create(&global_lock, p); cookie_ip = apr_table_make(p, 100); cookie_purge = apr_table_make(p, 100); cookie_userdn = apr_table_make(p, 100); cookie_expires = apr_table_make(p, 100); return OK; } #else static void auth_init(server_rec * server, apr_pool_t * p) { cookie_ip = apr_table_make(p, 100); cookie_purge = apr_table_make(p, 100); cookie_userdn = apr_table_make(p, 100); cookie_expires = apr_table_make(p, 100); } #endif static const char *set_RenewAge(cmd_parms * cmd, void *d, const char *name) { time_t t = (time_t) atoi(name); if (t < 0) return MODNAME "RenewAge directive requires " "a non-negative integer as argument"; ACT(d)->renew_age = t; return NULL; } static const char *set_RenewBy(cmd_parms * cmd, void *d, const char *name) { time_t t = (time_t) atoi(name); if (t < 0) return MODNAME "RenewBy directive requires " "a non-negative integer as argument"; ACT(d)->renew_by = t; return NULL; } static const char *set_SearchTimeout(cmd_parms * cmd, void *d, const char *name) { int t = atoi(name); if (t < 0) return MODNAME "SearchTimeout directive requires " "a non-negative integer as argument"; ACT(d)->search_timeout = t; return NULL; } static const char *set_ConnectTimeout(cmd_parms * cmd, void *d, const char *name) { int t = atoi(name); if (t < 0) return MODNAME "ConnectTimeout directive requires " "a non-negative integer as argument"; ACT(d)->connect_timeout = t; return NULL; } static const char *set_CacheTime(cmd_parms * cmd, void *d, const char *name) { time_t t = (time_t) atoi(name); if (t < 0) return MODNAME "CacheTime directive requires " "a non-negative integer as argument"; ACT(d)->expires = t; return NULL; } static const char *set_UserScope(cmd_parms * cmd, void *d, const char *name) { if (!strcasecmp(name, "base")) ACT(d)->user_scope = LDAP_SCOPE_BASE; else if (!strcasecmp(name, "one")) ACT(d)->user_scope = LDAP_SCOPE_ONELEVEL; else if (!strcasecmp(name, "sub")) ACT(d)->user_scope = LDAP_SCOPE_SUBTREE; else return MODNAME "UserScope directive requires " "'base', 'one' or 'scope' as argument"; return NULL; } static const char *set_ServerUrl(cmd_parms * cmd, void *d, const char *name) { ldap_server_t *s; int port = LDAP_PORT; if (strlen(name) > 7 && !strncmp(name, "ldap://", 7)) { char *t; name += 7; t = strchr(name, ':'); if (t) { *t++ = 0; port = atoi(t); } s = (ldap_server_t *) apr_array_push(ACT(d)->server); s->name = apr_pstrdup(cmd->pool, name); s->port = port; s->last_error_code = LDAP_SUCCESS; s->last_error_time = 0; return NULL; } return MODNAME "ServerURL argument format is ldap://server[:port]"; } static const char *set_Domain(cmd_parms * cmd, void *d, const char *name) { if (name[0] != '.' || !strchr(name + 1, '.')) return MODNAME "Domain must start with a dot and contain " "at least another dot."; ACT(d)->domain = apr_pstrdup(cmd->pool, name); return NULL; } static const command_rec auth_cmds[] = { AP_INIT_TAKE1(MODNAME "Name", ap_set_string_slot, (void *) APR_OFFSETOF(auth_cfg_t, name), OR_AUTHCFG, "Name of cookie"), AP_INIT_ITERATE(MODNAME "ServerUrl", set_ServerUrl, NULL, OR_AUTHCFG, "LDAP server urls (in ldap://server[:host] format"), AP_INIT_TAKE1(MODNAME "Domain", set_Domain, NULL, OR_AUTHCFG, "Cookie domain (in .doma.in format)"), AP_INIT_TAKE1(MODNAME "DN", ap_set_string_slot, (void *) APR_OFFSETOF(auth_cfg_t, cookie_dn), OR_AUTHCFG, "Format string for creating cookie DN (e.g. " "'cn=%%s,o=example,o=com')"), AP_INIT_TAKE1(MODNAME "UserBase", ap_set_string_slot, (void *) APR_OFFSETOF(auth_cfg_t, user_base), OR_AUTHCFG, "Base DN for searching users."), AP_INIT_TAKE1(MODNAME "UserFilter", ap_set_string_slot, (void *) APR_OFFSETOF(auth_cfg_t, user_filter), OR_AUTHCFG, "Search filter for users (e.g. " "'(&(uid=%%s)(objectClass=shadowAccount))')"), AP_INIT_TAKE1(MODNAME "UserScope", set_UserScope, NULL, OR_AUTHCFG, "Search scope for users"), AP_INIT_TAKE1(MODNAME "User", ap_set_string_slot, (void *) APR_OFFSETOF(auth_cfg_t, user), OR_AUTHCFG, "User name for LDAP server bind"), AP_INIT_TAKE1(MODNAME "Pass", ap_set_string_slot, (void *) APR_OFFSETOF(auth_cfg_t, pass), OR_AUTHCFG, "Password for LDAP server bind"), AP_INIT_TAKE1(MODNAME "SearchTimeout", set_SearchTimeout, NULL, OR_AUTHCFG, "Seconds to wait for LDAP search results."), AP_INIT_TAKE1(MODNAME "ConnectTimeout", set_ConnectTimeout, NULL, OR_AUTHCFG, "Seconds to wait for LDAP connect."), AP_INIT_TAKE1(MODNAME "CacheTime", set_CacheTime, NULL, OR_AUTHCFG, "Seconds to keep cookie information cached in memory."), AP_INIT_TAKE1(MODNAME "RenewAge", set_RenewAge, NULL, OR_AUTHCFG, "Seconds until cookie should be renewed on server."), AP_INIT_TAKE1(MODNAME "RenewBy", set_RenewBy, NULL, OR_AUTHCFG, "Number of seconds cookie should be renewed."), AP_INIT_FLAG(MODNAME "Enable", ap_set_flag_slot, (void *) APR_OFFSETOF(auth_cfg_t, is_enabled), OR_AUTHCFG, "Enable/disable module"), AP_INIT_FLAG(MODNAME "Debug", ap_set_flag_slot, (void *) APR_OFFSETOF(auth_cfg_t, debug), OR_AUTHCFG, "Enable/disable debugging output"), AP_INIT_FLAG(MODNAME "ServerFirstIsMaster", ap_set_flag_slot, (void *) APR_OFFSETOF(auth_cfg_t, first_is_master), OR_AUTHCFG, "Use first LDAP server for writing, as " "others are replicas."), {NULL} }; static int garbage_collection(void *rec, const char *cookie, const char *value) { const char *t; t = apr_table_get(cookie_purge, cookie); if (t && ((time_t) strtol(t, NULL, 10) < time(NULL))) { apr_table_unset(cookie_userdn, cookie); apr_table_unset(cookie_ip, cookie); apr_table_unset(cookie_purge, cookie); apr_table_unset(cookie_expires, cookie); } return 0; } static LDAP *ldap_replica(request_rec *, auth_cfg_t *); static LDAP *ldap_master(request_rec *, auth_cfg_t *); static void ldap_disconnect(request_rec *, auth_cfg_t *, LDAP *); static LDAP *ldap_connect_one(request_rec * r, auth_cfg_t * rec, ldap_server_t * s) { int err = -1, ldapv3 = LDAP_VERSION3; LDAP *ldap; Debug(">%s(%s, %d)", __func__, s->name, s->port); ldap = ldap_init(s->name, s->port); if (!ldap) { Log("ldap_init(%s, %d): failure", s->name, s->port); Debug("<%s = NULL", __func__); return NULL; } ldap_set_option(ldap, LDAP_OPT_TIMELIMIT, &rec->connect_timeout); if (LDAP_SUCCESS != (err = ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapv3))) { Log("ldap_set_options: %s", ldap_err2string(err)); ldap_unbind(ldap); Debug("<%s = NULL", __func__); return NULL; } if (LDAP_SUCCESS != (err = ldap_start_tls_s(ldap, NULL, NULL))) { /* * Unless there are serious configuration errors, this is the only * place where the LDAP routines may and will complain excessively. */ if (s->last_error_code != err || s->last_error_time < time(NULL) - 10) { s->last_error_code = err; s->last_error_time = time(NULL); Log("ldap_start_tls_s: %s (%s:%d)", ldap_err2string(err), s->name, s->port); } ldap_unbind(ldap); Debug("<%s = NULL", __func__); return NULL; } if (LDAP_SUCCESS != (err = ldap_simple_bind_s(ldap, rec->user, rec->pass))) { Log("ldap_simple_bind_s: %s", ldap_err2string(err)); ldap_unbind(ldap); Debug("<%s = NULL", __func__); return NULL; } Debug("<%s = %s", __func__, ldap ? "OK" : "FAILURE"); return ldap; } static LDAP *ldap_connect(request_rec * r, auth_cfg_t * rec, int master) { int i, first, last; ldap_server_t *servers; LDAP *ldap = NULL; Debug(">%s(..., %d)", __func__, master); first = master ? 0 : 1; last = (master && rec->server->nelts > 0) ? 1 : rec->server->nelts; servers = (ldap_server_t *) rec->server->elts; for (i = first; !ldap && i < last; i++) ldap = ldap_connect_one(r, rec, &servers[i]); if (!master) rec->ldap_idx = i; Debug("<%s = %s", __func__, ldap ? "OK" : "FAILURE"); return ldap; } static char *ldap_find_and_bind(request_rec * r, auth_cfg_t * rec, char *user, const char *pass) { char *attrs[] = { NULL }; int err, len = strlen(rec->user_filter) + strlen(user); char *userdn = NULL, *res = NULL; char *filter = alloca(len); struct timeval timeout; LDAPMessage *result = NULL, *entry = NULL, *nextentry = NULL; LDAP *ldap; int try = 0; Debug(">%s(%s)", __func__, user); timeout.tv_sec = rec->search_timeout; timeout.tv_usec = 0; snprintf(filter, len, rec->user_filter, user); do { if (!(ldap = ldap_replica(r, rec))) { Debug("<%s", __func__); return NULL; } err = ldap_search_st(ldap, rec->user_base, rec->user_scope, filter, attrs, 1, &timeout, &result); switch (err) { case LDAP_SUCCESS: try = 10; break; default: try = 10; case LDAP_SERVER_DOWN: if (!try) Log("ldap_search_st: %s", ldap_err2string(err)); if (err == LDAP_SERVER_DOWN) ldap_disconnect(r, rec, ldap); break; } } while (++try < 3); if (err != LDAP_SUCCESS) { Debug("<%s", __func__); return NULL; } entry = ldap_first_entry(ldap, result); do { if (userdn) ldap_memfree(userdn); userdn = ldap_get_dn(ldap, entry); err = ldap_simple_bind_s(ldap, userdn, pass); nextentry = ldap_next_entry(ldap, entry); } while ((err == LDAP_INVALID_CREDENTIALS) && nextentry && (entry = nextentry)); if (err == LDAP_SUCCESS && userdn) res = apr_pstrdup(r->pool, userdn); if (userdn) ldap_memfree(userdn); if (result) ldap_memfree(result); if (LDAP_SUCCESS != ldap_simple_bind_s(ldap, rec->user, rec->pass)) { ldap_disconnect(r, rec, ldap); } Debug("<%s = %s", __func__, res ? res : ""); return res; } static void ldap_disconnect(request_rec * r, auth_cfg_t * rec, LDAP * ldap) { Debug(">%s", __func__); if (ldap == rec->ldap) { if (rec->ldap == rec->ldap_master) rec->ldap_master = NULL; ldap_unbind(rec->ldap); rec->ldap = NULL; rec->ldap_idx = 0; } else { ldap_unbind(rec->ldap_master); rec->ldap_master = NULL; } Debug("<%s", __func__); } static LDAP *ldap_master(request_rec * r, auth_cfg_t * rec) { Debug(">%s", __func__); if (!rec->ldap_master && rec->first_is_master && rec->ldap_idx == 0) rec->ldap_master = ldap_replica(r, rec); if (!rec->ldap_master && !rec->first_is_master) rec->ldap_master = ldap_replica(r, rec); if (!rec->ldap_master) rec->ldap_master = ldap_connect(r, rec, 1); Debug("<%s = %s", __func__, rec->ldap_master ? "OK" : "FAILURE"); return rec->ldap_master; } static LDAP *ldap_replica(request_rec * r, auth_cfg_t * rec) { Debug(">%s", __func__); if (!rec->ldap) { if (rec->first_is_master && rec->server->nelts == 1) { if (!rec->ldap_master) rec->ldap_master = ldap_connect(r, rec, 1); if (rec->ldap_master) { rec->ldap = rec->ldap_master; rec->ldap_idx = 0; } } if (!rec->ldap && rec->ldap_master) { rec->ldap = rec->ldap_master; rec->ldap_idx = 0; } if (!rec->ldap) rec->ldap = ldap_connect(r, rec, 0); } Debug("<%s = %s", __func__, rec->ldap ? "OK" : "FAILURE"); return rec->ldap; } static void expire_cookie(request_rec * r, auth_cfg_t * rec, const char *cookie) { Debug(" %s(%s)", __func__, cookie); apr_table_addn(r->headers_out, "Set-Cookie", rec->domain ? apr_psprintf(r->pool, "%s=%s; path=/; domain=%s; secure; expires=" "Mon, 1 Jan 1980 00:00:00 GMT", rec->name, cookie, ((auth_cfg_t *) rec)->domain) : apr_psprintf(r->pool, "%s=%s; path=/; secure; expires=" "Mon, 1 Jan 1980 00:00:00 GMT", rec->name, cookie)); } static char *ldap_lookup(request_rec * r, auth_cfg_t * rec, const char *cookie) { #define FILTERFORMAT "(&(httpCookieName=%s)(httpCookieRemoteIP=%s))" int err; size_t len = strlen(rec->name) + strlen(r->connection->remote_ip) + sizeof(FILTERFORMAT); char *cookie_dn, *filter = (char *) alloca(len); struct timeval timeout; char *attrs[] = { "httpCookieExpires", "httpCookieRemoteDN", NULL }; LDAP *ldap; char *remoteuser = NULL; Debug(">%s(%s)", __func__, cookie); snprintf(filter, len, FILTERFORMAT, rec->name, r->connection->remote_ip); len = strlen(rec->cookie_dn) + strlen(cookie); cookie_dn = (char *) alloca(len); snprintf(cookie_dn, len, rec->cookie_dn, cookie); LDAPMessage *result = NULL, *entry = NULL; timeout.tv_sec = rec->search_timeout; timeout.tv_usec = 0; if (!(ldap = ldap_replica(r, rec))) { Debug("<%s = NULL", __func__); return NULL; } Debug(" %s: filter is %s", __func__, filter); err = ldap_search_st(ldap, cookie_dn, LDAP_SCOPE_BASE, filter, attrs, 0, &timeout, &result); if (err == LDAP_SERVER_DOWN) ldap_disconnect(r, rec, ldap); else if (err == LDAP_SUCCESS && 0 < ldap_count_entries(ldap, result)) { time_t expiresAt = 0; char **vals; Debug(" %s: ldap_search succeeded", __func__); entry = ldap_first_entry(ldap, result); if ((vals = ldap_get_values(ldap, entry, "httpCookieExpires"))) { if (*vals) expiresAt = strtol(*vals, NULL, 10); ldap_value_free(vals); } Debug(" %s: expiration in %ld seconds", __func__, (long) (expiresAt - time(NULL))); if (expiresAt >= time(NULL)) { char expires[20]; char *v[2], *dn; vals = ldap_get_values(ldap, entry, "httpCookieRemoteDN"); if (*vals && strlen(*vals) >= strlen(rec->user_base) && !strcasecmp(rec->user_base, *vals + strlen(*vals) - strlen(rec->user_base))) { remoteuser = apr_pstrdup(r->pool, *vals); Debug(" %s: remote user is %s", __func__, remoteuser); ldap_value_free(vals); if (expiresAt - rec->renew_by + rec->renew_age < time(NULL)) { Debug(" %s: extending object expiry", __func__); expiresAt = time(NULL) + rec->renew_by; snprintf(expires, sizeof(expires), "%ld", expiresAt); LDAPMod *mod[2]; mod[0] = (LDAPMod *) apr_pcalloc(r->pool, sizeof(LDAPMod)); mod[0]->mod_op = LDAP_MOD_REPLACE; mod[0]->mod_type = "httpCookieExpires"; v[0] = expires; v[1] = NULL; mod[0]->mod_vals.modv_strvals = v; mod[1] = NULL; dn = ldap_get_dn(ldap, entry); if ((ldap = ldap_master(r, rec)) && (LDAP_SERVER_DOWN == ldap_modify_s(ldap, dn, mod))) ldap_disconnect(r, rec, ldap); ldap_memfree(dn); } snprintf(expires, sizeof(expires), "%ld", time(NULL) + rec->expires); apr_table_set(cookie_purge, cookie, expires); snprintf(expires, sizeof(expires), "%ld", time(NULL) + rec->renew_by); apr_table_set(cookie_expires, cookie, expires); } } } ldap_memfree(result); Debug("<%s = %s (%s)", __func__, remoteuser, ldap_err2string(err)); return remoteuser; } static void cookie_cache_set(request_rec * r, auth_cfg_t * rec, const char *cookie) { char str[20]; apr_thread_rwlock_wrlock(global_lock); Debug(" %s: %s", __func__, REMOTEUSER(r)); apr_table_set(cookie_userdn, cookie, REMOTEUSER(r)); apr_table_set(cookie_ip, cookie, r->connection->remote_ip); snprintf(str, sizeof(str), "%ld", time(NULL) + rec->expires); apr_table_set(cookie_purge, cookie, str); snprintf(str, sizeof(str), "%ld", time(NULL) + rec->renew_by); apr_table_set(cookie_expires, cookie, str); apr_thread_rwlock_unlock(global_lock); } module mod_auth_ldap_cookie_module; static int auth_check(request_rec * r) { const char *sent_pw; const char *cookie = NULL, *cookies = NULL; char cookiestore[APR_MD5_DIGESTSIZE + 2 + 1]; auth_cfg_t *rec = ACT(ap_get_module_config (r->per_dir_config, &mod_auth_ldap_cookie_module)); Debug(">%s", __func__); if (!rec->is_enabled) { Debug("<%s = DECLINED (module is disabled)", __func__); return DECLINED; } if (!rec->name || !rec->cookie_dn || !rec->user_base || !rec->user_filter || !rec->server->nelts) { ap_note_basic_auth_failure(r); Debug("<%s = HTTP_UNAUTHORIZED (configuration incomplete)", __func__); return HTTP_UNAUTHORIZED; } if (rec->last_purge + 60 < time(NULL)) { apr_thread_rwlock_wrlock(global_lock); apr_table_do(garbage_collection, rec, cookie_purge, NULL); rec->last_purge = time(NULL); apr_thread_rwlock_unlock(global_lock); } cookies = apr_table_get(r->headers_in, "Cookie"); while (cookies) { if (!strncmp(cookies, rec->name, strlen(rec->name)) && cookies[strlen(rec->name)] == '=' && strspn(cookies + strlen(rec->name) + 1, "0123456789abcdef") == APR_MD5_DIGESTSIZE * 2) { if (cookie) expire_cookie(r, rec, cookie); strncpy(cookiestore, cookies + strlen(rec->name) + 1, APR_MD5_DIGESTSIZE * 2); cookiestore[APR_MD5_DIGESTSIZE * 2] = 0; cookie = cookiestore; } cookies = strchr(cookies, ';'); if (cookies) { cookies++; while (isspace(*cookies)) cookies++; } } if (cookie) { char *remotedn = NULL; Debug(" %s: cookie is present", __func__); garbage_collection(rec, cookie, NULL); remotedn = (char *) apr_table_get(cookie_userdn, cookie); if (remotedn) { const char *expires; const char *ip = apr_table_get(cookie_ip, cookie); time_t e; if (strcmp(r->connection->remote_ip, ip)) { expire_cookie(r, rec, cookie); ap_note_basic_auth_failure(r); Debug("<%s = HTTP_UNAUTHORIZED (IP mismatch)", __func__); return HTTP_UNAUTHORIZED; } expires = apr_table_get(cookie_purge, cookie); e = strtol(expires, NULL, 10); Debug(" %s: cookie expires from cache in %ld seconds", __func__, (long) (e - time(NULL))); if (e >= time(NULL)) { REMOTEUSER(r) = (char *) remotedn; Debug("<%s = DECLINED (OK, cached)", __func__); return DECLINED; } apr_thread_rwlock_wrlock(global_lock); apr_table_unset(cookie_ip, cookie); apr_table_unset(cookie_purge, cookie); apr_table_unset(cookie_userdn, cookie); apr_table_unset(cookie_expires, cookie); apr_thread_rwlock_unlock(global_lock); } else Debug(" %s: userDN not found in table", __func__); apr_thread_rwlock_wrlock(rec->ldap_lock); remotedn = ldap_lookup(r, ACT(rec), cookie); apr_thread_rwlock_unlock(rec->ldap_lock); if (remotedn) { REMOTEUSER(r) = remotedn; cookie_cache_set(r, ACT(rec), cookie); Debug("<%s: DECLINED", __func__); return DECLINED; } } Debug(" %s", __func__); if (!(ap_get_basic_auth_pw(r, &sent_pw))) { char *userdn; apr_thread_rwlock_wrlock(rec->ldap_lock); userdn = ldap_find_and_bind(r, ACT(rec), REMOTEUSER(r), sent_pw); apr_thread_rwlock_unlock(rec->ldap_lock); if (userdn) { int new_cookie = 0; int len = strlen(rec->cookie_dn) + APR_MD5_DIGESTSIZE * 2; char *cookie_dn = (char *) alloca(len); Debug(" %s: userDN is known", __func__); REMOTEUSER(r) = userdn; if (!cookie || strlen(cookie) != APR_MD5_DIGESTSIZE * 2 || strspn(cookie, "0123456789abcdef") != APR_MD5_DIGESTSIZE * 2) { char t[250]; #ifdef APACHE2 unsigned char digest[APR_MD5_DIGESTSIZE]; int j; char *hex = "0123456789abcdef"; #endif if (cookie) expire_cookie(r, rec, cookie); new_cookie++; snprintf(t, sizeof t, "%s\n%s\n%s\n%ld", REMOTEUSER(r), sent_pw, r->connection->remote_ip, time(NULL)); #ifdef APACHE2 apr_md5(digest, t, strlen(t)); for (j = 0; j < APR_MD5_DIGESTSIZE; j++) { cookiestore[2 * j] = hex[(digest[j] & 0xf0) >> 4]; cookiestore[2 * j + 1] = hex[digest[j] & 0xf]; } cookiestore[APR_MD5_DIGESTSIZE * 2] = 0; cookie = cookiestore; #else cookie = ap_md5(r->pool, (u_char *) t); #endif } snprintf(cookie_dn, len, rec->cookie_dn, cookie); if (ldap_master(r, rec)) { char *v[7][2]; char str[20]; LDAPMod *modp[7], mod[6]; LDAP *ldap; int err, i; Debug(" %s", __func__); snprintf(str, sizeof(str), "%ld", time(NULL) + ACT(rec)->renew_by); #define L(A,B,C) mod[A].mod_type=B;v[A][0]=C; L(0, "objectClass", "httpCookie"); L(1, "cn", (char *) cookie); L(2, "httpCookieExpires", str); L(3, "httpCookieRemoteDN", REMOTEUSER(r)); L(4, "httpCookieRemoteIP", r->connection->remote_ip); L(5, "httpCookieName", rec->name); #undef L for (i = 0; i < 6; i++) { modp[i] = &mod[i]; mod[i].mod_op = 0; mod[i].mod_vals.modv_strvals = v[i]; v[i][1] = NULL; } modp[6] = NULL; apr_thread_rwlock_wrlock(rec->ldap_lock); if ((ldap = ldap_master(r, rec))) { Debug(" %s: master is available", __func__); err = ldap_add_s(ldap, cookie_dn, modp); Debug(" %s: ldap_add: %s", __func__, ldap_err2string(err)); if (err == LDAP_SERVER_DOWN) ldap_disconnect(r, rec, ldap); if (ldap && err != LDAP_SUCCESS) { for (i = 0; i < 6; i++) mod[i].mod_op = LDAP_MOD_REPLACE; err = ldap_modify_s(ldap, cookie_dn, modp); Debug(" %s: ldap_modify: %s", __func__, ldap_err2string(err)); if (err == LDAP_SERVER_DOWN) ldap_disconnect(r, rec, ldap); } if (err != LDAP_SUCCESS) Log("ldap_add/ldap_modify: %s", ldap_err2string(err)); } apr_thread_rwlock_unlock(rec->ldap_lock); } Debug(" %s", __func__); if (new_cookie) apr_table_addn(r->headers_out, "Set-Cookie", ACT(rec)->domain ? apr_psprintf(r->pool, "%s=%s; path=/; domain=%s; secure", ACT(rec)->name, cookie, ACT(rec)->domain) : apr_psprintf(r->pool, "%s=%s; path=/; secure", ACT(rec)->name, cookie)); cookie_cache_set(r, ACT(rec), cookie); Debug("<%s = DECLINED (OK)", __func__); return DECLINED; } } ap_note_basic_auth_failure(r); Debug("<%s = HTTP_UNAUTHORIZED", __func__); return HTTP_UNAUTHORIZED; } #ifdef APACHE2 static void register_hooks(apr_pool_t * p) { ap_hook_post_config(auth_init, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_access_checker(auth_check, NULL, NULL, APR_HOOK_MIDDLE); } module AP_MODULE_DECLARE_DATA mod_auth_ldap_cookie_module = { STANDARD20_MODULE_STUFF, auth_cfg, /* dir config creater */ NULL, /* dir merger --- default is to override */ NULL, /* server config */ NULL, /* merge server config */ auth_cmds, /* command table */ register_hooks /* register hooks */ }; #else module mod_auth_ldap_cookie_module = { STANDARD_MODULE_STUFF, auth_init, /* initializer */ auth_cfg, /* dir config creater */ NULL, /* dir merger --- default is to override */ NULL, /* server config */ NULL, /* merge server config */ auth_cmds, /* command table */ NULL, /* handlers */ NULL, /* filename translation */ NULL, /* auth_check */ NULL, /* check auth */ NULL, /* check access */ auth_check, /* type_checker */ NULL, /* fixups */ NULL, /* logger */ NULL, /* header parser */ NULL, /* child_init */ NULL, /* child_exit */ NULL /* post read-request */ }; #endif