/* * Copyright (c) 2009, Stefan Walter * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * 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. * * The names of contributors to this software may not be * used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS 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 * COPYRIGHT OWNER OR 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. * * * CONTRIBUTORS * Stef Walter * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Apache defines these */ #undef PACKAGE_BUGREPORT #undef PACKAGE_NAME #undef PACKAGE_STRING #undef PACKAGE_TARNAME #undef PACKAGE_VERSION #include "config.h" #include "mod_auth_singleid.h" #include #include #include #include extern module AP_MODULE_DECLARE_DATA auth_singleid_module; #define VALID_NAME "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-." enum { NONE = 0, SUFFIX = 1, REGEX = 2 }; /* * Per directory configuration. */ typedef struct sid_context { const char *trust_root; const char *identifier; const char *cookie_name; int user_match; ap_regex_t *converter; sid_storage_t *store; } sid_context_t; #define SID_AUTHTYPE "SingleID" /* ------------------------------------------------------------------------------- * SHARED MEMORY and LOCKING */ static apr_global_mutex_t *shared_lock = NULL; static const char *shared_lock_name = NULL; static int shared_initialize (apr_pool_t *p) { apr_file_t *file = NULL; char *lock_name = NULL; const char *tmpdir; int rc; /* This may be called more than once */ if (shared_lock) return OK; rc = apr_temp_dir_get (&tmpdir, p); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't get temporary directory"); if (rc == APR_SUCCESS) { lock_name = apr_pstrcat (p, tmpdir, "/", "mod-auth-singleid.lock.XXXXXX", NULL); rc = apr_file_mktemp (&file, lock_name, 0, p); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't create temporary file: %s", lock_name); } if (file != NULL) apr_file_close (file); if (rc == APR_SUCCESS) { rc = apr_global_mutex_create (&shared_lock, lock_name, APR_LOCK_DEFAULT, p); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't create shared memory lock: %s", lock_name); } #ifdef AP_NEED_SET_MUTEX_PERMS if (rc == APR_SUCCESS) { rc = unixd_set_global_mutex_perms (shared_lock); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: Could not set permissions on lock. " "check User and Group directives"); } #endif if (rc == APR_SUCCESS) shared_lock_name = lock_name; return OK; } static void shared_child (apr_pool_t *p, server_rec *s) { apr_status_t rc; if (!shared_lock || !shared_lock_name) return; rc = apr_global_mutex_child_init (&shared_lock, shared_lock_name, p); if (rc != APR_SUCCESS) { ap_log_error (APLOG_MARK, APLOG_ERR, rc, s, "httpauth: couldn't create lock for shared memory in child: %s", shared_lock_name); shared_lock = NULL; } } static void* shared_create (apr_pool_t* p, size_t size) { const char *tmpdir; char *filename; apr_file_t *file; apr_mmap_t *map; apr_off_t offset; void *addr; int rc; if (!shared_lock) shared_initialize (p); if (!shared_lock) return NULL; /* Get the temp directory */ rc = apr_temp_dir_get (&tmpdir, p); if (rc != APR_SUCCESS) { ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't get temporary directory"); } /* Create the shared file */ if (rc == APR_SUCCESS) { filename = apr_pstrcat (p, tmpdir, "/", "mod-auth-singleid.shared.XXXXXX", NULL); rc = apr_file_mktemp (&file, filename, 0, p); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't create temporary file: %s", filename); } /* Extend file to required size */ if (rc == APR_SUCCESS) { offset = size - 1; rc = apr_file_seek (file, APR_SET, &offset); if (rc == APR_SUCCESS) rc = apr_file_write_full (file, "\0", 1, NULL); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't write to temporary file: %s", filename); } /* Map the shared file into memory */ if (rc == APR_SUCCESS) { rc = apr_mmap_create (&map, file, 0, size, APR_MMAP_READ | APR_MMAP_WRITE, p); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't map temporary file: %s", filename); } /* Get the actual address of the mapping */ if (rc == APR_SUCCESS) { rc = apr_mmap_offset (&addr, map, 0); if (rc != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_ERR, rc, NULL, "auth-singleid: couldn't get shared memory"); } if (rc == APR_SUCCESS) return addr; return NULL; } void sid_shared_lock (void) { apr_global_mutex_lock (shared_lock); } void sid_shared_unlock (void) { apr_global_mutex_unlock (shared_lock); } /* ------------------------------------------------------------------------------- * CONFIGURATION */ static void* dir_config_creator (apr_pool_t* p, char* dir) { sid_context_t* ctx = apr_pcalloc (p, sizeof (*ctx)); memset (ctx, 0, sizeof (*ctx)); ctx->cookie_name = "mod-auth-singleid"; return ctx; } static const char* set_identifier (cmd_parms* cmd, void* config, const char* val) { sid_context_t *ctx = config; ctx->identifier = apr_pstrdup (cmd->pool, val); return NULL; } static const char* set_trust_root (cmd_parms* cmd, void* config, const char* val) { sid_context_t *ctx = config; if (!ap_is_url (val)) return "Not a valid URL in SingleTrustRoot"; ctx->trust_root = apr_pstrdup (cmd->pool, val); return NULL; } static const char* set_user_match (cmd_parms *cmd, void *config, const char *val) { sid_context_t *ctx = config; /* Remove extraneous spaces */ while (isspace (*val)) ++val; if (strcasecmp (val, "suffix") == 0 && !isalpha (val[6])) { ctx->user_match = SUFFIX; return NULL; } /* Try to compile as a regular expression */ ctx->converter = ap_pregcomp (cmd->pool, val, AP_REG_EXTENDED | AP_REG_ICASE | AP_REG_NEWLINE); if (!ctx->converter) return "Invalid regular expression in SingleUserMatch"; ctx->user_match = REGEX; return NULL; } static const char* set_cache_size (cmd_parms *cmd, void *config, const char *val) { sid_context_t *ctx = config; char *end; int size = 0; void *shared; size_t page; if (strcasecmp (val, "on") == 0) { size = 64 * 1024; } else if (strcasecmp (val, "off") == 0) { size = 0; } else { size = strtol (val, &end, 10); if (*end != '\0') return "Invalid number specified for SingleCache"; } if (size == 0) return NULL; /* Align to a page size */ page = getpagesize (); size = ((size + (page - 1)) / page) * page; shared = shared_create (cmd->pool, size); ctx->store = sid_storage_initialize (shared, size); return NULL; } static const char* set_cookie_name (cmd_parms *cmd, void *config, const char *val) { sid_context_t *ctx = config; const char *end; end = val + strspn (val, VALID_NAME); if (*val == '\0' || *end != '\0') return "Not a valid cookie name in SingleCookieName"; ctx->cookie_name = apr_pstrdup (cmd->pool, val); return NULL; } static const command_rec command_table[] = { AP_INIT_TAKE1 ("SingleIdentifier", set_identifier, NULL, OR_AUTHCFG, "The OpenID identifier we should perform ID selection on when authenticating" ), AP_INIT_TAKE1 ("SingleTrustRoot", set_trust_root, NULL, OR_AUTHCFG, "The OpenID trust root of this site."), AP_INIT_TAKE1 ("SingleCache", set_cache_size, NULL, OR_AUTHCFG, "Enable and optionally set the size of the OpenID association cache"), AP_INIT_TAKE1 ("SingleCookieName", set_cookie_name, NULL, OR_AUTHCFG, "Set the cookie name used once user has logged in via OpenID"), AP_INIT_RAW_ARGS ("SingleUserMatch", set_user_match, NULL, OR_AUTHCFG, "How to convert an OpenID identifier into a user name" ), { NULL } }; /* ------------------------------------------------------------------------------------------- * COOKIE SESSIONS */ typedef struct sid_session { char *identifier; time_t expiry; } sid_session_t; /* Secret used to sign session cookies */ static unsigned char session_secret[40]; static apr_status_t session_initialize (apr_pool_t *p, server_rec *s) { apr_status_t status; #if APR_HAS_RANDOM status = apr_generate_random_bytes (session_secret, sizeof (session_secret)); #else #error APR random number support is missing; you probably need to install the truerand library. #endif if (status != APR_SUCCESS) ap_log_error (APLOG_MARK, APLOG_CRIT, status, s, "auth-singleid: couldn't generate random secret"); return status; } static const char* session_cookie_value (request_rec *r, const char *name) { const char *cookies; const char *value; char *pair; cookies = apr_table_get (r->headers_in, "Cookie"); if (cookies == NULL) return NULL; while (*cookies) { pair = ap_get_token (r->pool, &cookies, 1); if (!pair) break; if (pair[0] == '$') continue; value = ap_stripprefix (pair, name); if (value == pair) continue; while (isspace (*value)) ++value; if (*value != '=') continue; ++value; while (isspace (*value)) ++value; return value; } return NULL; } static char* session_create_sig (apr_pool_t *p, const char *value) { unsigned char digest[APR_SHA1_DIGESTSIZE]; char *sig; apr_sha1_ctx_t ctx; apr_sha1_init (&ctx); apr_sha1_update (&ctx, (const char*)session_secret, sizeof (session_secret)); apr_sha1_update (&ctx, "\0", 1); apr_sha1_update (&ctx, value, strlen (value)); apr_sha1_final (digest, &ctx); sig = apr_pcalloc (p, apr_base64_encode_len (sizeof (digest))); apr_base64_encode (sig, (const char*)digest, sizeof (digest)); return sig; } static int session_validate_sig (apr_pool_t *p, const char *sig, const char *value) { char *valid = session_create_sig (p, value); return strcmp (sig, valid) == 0; } static sid_session_t* session_load_info (sid_context_t *ctx, request_rec *r) { sid_session_t *sess; const char *value; char *token, *sig, *end; char *identifier; long expiry; size_t len; value = session_cookie_value (r, ctx->cookie_name); if (!value) return NULL; sig = ap_get_token (r->pool, &value, 0); if (!session_validate_sig (r->pool, sig, value)) return NULL; /* The version of the session info, only 1 supported for now */ token = ap_get_token (r->pool, &value, 0); if (strcmp (token, "1") != 0) return NULL; token = ap_get_token (r->pool, &value, 0); expiry = strtol (token, &end, 10); if (*end != '\0') return NULL; /* Don't let expired sessions be valid */ if (expiry < time (NULL)) return NULL; /* The identifier */ identifier = ap_get_token (r->pool, &value, 0); len = strlen (identifier); if (identifier[0] == '"' && identifier[len - 1] == '"') { identifier[len - 1] = 0; ++identifier; } if (!ap_is_url (identifier)) return NULL; sess = apr_pcalloc (r->pool, sizeof (sid_session_t)); sess->expiry = expiry; sess->identifier = identifier; return sess; } static void session_send_info (sid_context_t *ctx, request_rec *r, sid_session_t *sess) { char *cookie, *sig, *value; /* Create the cookie value and sign it */ value = apr_psprintf (r->pool, "1 %ld \"%s\"", sess->expiry, ap_escape_quotes (r->pool, sess->identifier)); sig = session_create_sig (r->pool, value); /* Build up the full cookie spec */ cookie = apr_psprintf (r->pool, "%s=%s %s; httponly; max-age=86400", ctx->cookie_name, sig, value); apr_table_addn (r->err_headers_out, "Set-Cookie", cookie); } static sid_session_t* session_copy_info (apr_pool_t *p, sid_session_t *sess) { sid_session_t *copy = apr_pcalloc (p, sizeof (*sess)); copy->expiry = sess->expiry; copy->identifier = apr_pstrdup (p, sess->identifier); return copy; } /* --------------------------------------------------------------------------------------- * REQUEST ABSTRACTIONS */ struct sid_request { int result; request_rec *rec; }; void sid_request_log_error (sid_request_t *req, const char *message, const char *detail) { ap_log_rerror (APLOG_MARK, APLOG_ERR, 0, req->rec, "%s%s%s", message, detail ? ": " : "", detail ? detail : ""); } const char* sid_request_qs (sid_request_t *req) { return req->rec->args; } const char* sid_request_url (sid_request_t *req, int with_path) { /* function to determine if a connection is using https */ static APR_OPTIONAL_FN_TYPE(ssl_is_https) *using_https = NULL; static int using_https_inited = 0; const char *scheme; const char *host; const char *uri; apr_port_t port; int is_ssl; if (!using_https_inited) { using_https = APR_RETRIEVE_OPTIONAL_FN (ssl_is_https); using_https_inited = 1; } is_ssl = using_https && using_https (req->rec->connection); host = req->rec->hostname ? req->rec->hostname : ap_get_server_name (req->rec); scheme = is_ssl ? "https" : "http"; port = ap_get_server_port (req->rec); uri = with_path && req->rec->uri ? req->rec->uri : ""; /* Default ports? */ if ((port == 80 && !is_ssl) || (port == 443 && is_ssl)) return apr_psprintf (req->rec->pool, "%s://%s%s", scheme, host, uri); else return apr_psprintf (req->rec->pool, "%s://%s:%d%s", scheme, host, port, uri); } void sid_request_respond (sid_request_t *req, int code, const char *reason, const char *header, ...) { const char *value; va_list va; if (reason) req->rec->status_line = apr_pstrdup (req->rec->pool, reason); va_start(va, header); while (header) { value = va_arg (va, const char*); apr_table_set (req->rec->err_headers_out, header, value); header = va_arg (va, const char*); } va_end(va); req->result = code; } static void set_request_authenticated (sid_context_t *ctx, request_rec *r, sid_session_t *sess) { ap_regmatch_t matches[AP_MAX_REG_MATCH]; char *user = NULL; /* Try and calculate a user name */ switch (ctx->user_match) { case SUFFIX: if (ctx->identifier) { user = (char*)ap_stripprefix (sess->identifier, ctx->identifier); if (user != sess->identifier) { /* Some delimiters that we strip from between value and identifier */ while (strchr ("?/#", *user)) ++user; } } break; case REGEX: assert (ctx->converter); if (ap_regexec (ctx->converter, sess->identifier, AP_MAX_REG_MATCH, matches, 0) == 0) user = ap_pregsub (r->pool, "$1", sess->identifier, AP_MAX_REG_MATCH, matches); break; } if (!user) user = sess->identifier; r->user = user; r->ap_auth_type = SID_AUTHTYPE; apr_table_set (r->subprocess_env, "OPENID_IDENTIFIER", sess->identifier); ap_set_module_config (r->request_config, &auth_singleid_module, sess); } void sid_request_authenticated (sid_request_t *req, const char *identifier) { sid_context_t *ctx; sid_session_t *sess; sess = apr_pcalloc (req->rec->pool, sizeof (sid_session_t)); sess->identifier = apr_pstrdup (req->rec->pool, identifier); sess->expiry = time (NULL) + 86400; ctx = ap_get_module_config (req->rec->per_dir_config, &auth_singleid_module); assert (ctx); set_request_authenticated (ctx, req->rec, sess); session_send_info (ctx, req->rec, sess); } /* --------------------------------------------------------------------------------------- * MAIN HOOKS */ static int hook_initialize (apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { int rc; rc = shared_initialize (p); if (rc != OK) return rc; return session_initialize (p, s); } static void hook_child (apr_pool_t *p, server_rec *s) { shared_child (p, s); } static int hook_authenticate (request_rec* r) { sid_session_t *sess; sid_context_t *ctx; sid_request_t req; const char* authtype; request_rec* mainreq; /* Make sure it's for us */ if (!(authtype = ap_auth_type (r)) || strcasecmp (SID_AUTHTYPE, authtype) != 0) return DECLINED; ctx = (sid_context_t*)ap_get_module_config (r->per_dir_config, &auth_singleid_module); if (ctx->identifier == NULL) return DECLINED; mainreq = r; while (mainreq->main != NULL) mainreq = mainreq->main; while (mainreq->prev != NULL) mainreq = mainreq->prev; /* Check if we've already authenticated this request */ sess = ap_get_module_config (mainreq->request_config, &auth_singleid_module); if (sess != NULL) { if (mainreq != r) { sess = session_copy_info (r->pool, sess); set_request_authenticated (ctx, r, sess); } return OK; } /* Load the session info from the request and see if we've authenticated */ sess = session_load_info (ctx, r); if (sess != NULL) { set_request_authenticated (ctx, r, sess); return OK; } req.result = OK; req.rec = r; /* Do the OpenID magic */ sid_consumer_authenticate (&req, ctx->store, ctx->trust_root, ctx->identifier); return req.result; } static int hook_access(request_rec *r) { sid_context_t* ctx; const char* authtype; char *user = r->user; int m = r->method_number; int method_restricted = 0; register int x; const char *t, *w; const apr_array_header_t *reqs_arr; require_line *reqs; /* Make sure it's for us */ if (!(authtype = ap_auth_type (r)) || strcasecmp (SID_AUTHTYPE, authtype) != 0) return DECLINED; ctx = (sid_context_t*)ap_get_module_config (r->per_dir_config, &auth_singleid_module); reqs_arr = ap_requires (r); if (!reqs_arr) return OK; reqs = (require_line *)reqs_arr->elts; for (x = 0; x < reqs_arr->nelts; x++) { if (!(reqs[x].method_mask & (AP_METHOD_BIT << m))) continue; method_restricted = 1; t = reqs[x].requirement; w = ap_getword_white (r->pool, &t); if (!strcmp (w, "valid-user")) { return OK; } else if (!strcmp (w, "user")) { while (t[0]) { w = ap_getword_conf (r->pool, &t); if (!strcmp (user, w)) { return OK; } } } else { ap_log_rerror (APLOG_MARK, APLOG_ERR, 0, r, "access to %s failed, reason: unknown require " "directive:\"%s\"", r->uri, reqs[x].requirement); } } if (!method_restricted) return OK; ap_log_rerror (APLOG_MARK, APLOG_ERR, 0, r, "access to %s failed, reason: user %s not allowed access", r->uri, user); return HTTP_UNAUTHORIZED; } static void register_hooks(apr_pool_t *p) { ap_hook_post_config (hook_initialize, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_child_init (hook_child, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_check_user_id (hook_authenticate, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_auth_checker (hook_access, NULL, NULL, APR_HOOK_MIDDLE); } module AP_MODULE_DECLARE_DATA auth_singleid_module = { STANDARD20_MODULE_STUFF, dir_config_creator, /* dir config creater */ NULL, /* dir merger --- default is to override */ NULL, /* server config */ NULL, /* merge server config */ command_table, /* command table */ register_hooks /* register hooks */ };