From 2989ee8b72ddb3995e5a4686c988385d05493365 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Tue, 7 Jul 2009 20:05:29 +0000 Subject: Implement simple AX attribute exchange. * Does not yet handle setting attributes from the cookie. --- ideas.txt | 2 +- module/consumer.cc | 255 ++++++++++++++++++++++++++++++++++++++------- module/mod_auth_singleid.c | 130 +++++++++++++++++++++-- module/mod_auth_singleid.h | 16 ++- 4 files changed, 358 insertions(+), 45 deletions(-) diff --git a/ideas.txt b/ideas.txt index f10cbb5..2848b71 100644 --- a/ideas.txt +++ b/ideas.txt @@ -3,7 +3,7 @@ AuthSingleIdIdentifier https://id.familymembers.com/ AuthSingleIdCookie openid AuthSingleIdUserPrefix https://id.familymembers.com/ AuthSingleIdUserSuffix -AuthSingleIdAttribute url alias count +AuthSingleIdAttribute alias url [count [required]] AX_TYPE_FNAME=http://example.com/schema/fullname AX_VALUE_FNAME=John Smith diff --git a/module/consumer.cc b/module/consumer.cc index 49620db..32db8cb 100644 --- a/module/consumer.cc +++ b/module/consumer.cc @@ -26,6 +26,12 @@ using opkele::secret_t; using std::string; using std::vector; +typedef std::list string_list; + +/* ----------------------------------------------------------------------------- + * UTILITY + */ + class LockShared { public: @@ -35,6 +41,102 @@ public: { sid_shared_unlock (); } }; + +static int +parse_number (const string& str) +{ + char *end = NULL; + int result = strtoul (str.c_str(), &end, 10); + if (!end || *end != '\0') + result = 0; + return result; +} + +static string +format_number (int num) +{ + char buf[64]; + snprintf (buf, sizeof (buf), "%d", num); + return string(buf); +} + +static void +parse_query_string (const char *qs, params_t ¶ms) +{ + string pair, key, value; + const char *at; + + if (qs == NULL) + return; + + while (*qs != 0) { + at = strchr (qs, '&'); + if (at == NULL) + at = qs + strlen (qs); + pair = string(qs, at); + string::size_type loc = pair.find('=', 0); + if (loc != string::npos) { + key = pair.substr (0, loc); + value = pair.substr (loc + 1); + } else { + key = pair; + value = ""; + } + + params[opkele::util::url_decode(key)] = opkele::util::url_decode(value); + if (*at == 0) + break; + qs = at + 1; + } +} + +static void +filter_prefixed_params (params_t ¶ms, params_t &openid, const string& prefix) +{ + /* + * Expects message to have openid. prefix present, and strips + * openid. prefix from output + */ + + for (params_t::iterator it = params.begin(); it != params.end(); ) { + const string& name = it->first; + if (name.find (prefix) == 0) { + openid[name.substr(prefix.length())] = it->second; + + /* We erase an increment together, must use post-increment operator */ + params.erase (it++); + } else { + /* Did not match, just go to next element */ + ++it; + } + } +} + +static void +filter_listed_params (const params_t ¶ms, params_t &listed, const string& list) +{ + /* Expects message to have openid. prefix stripped from params */ + + if(!params.has_param(list)) + return; + + string slist = params.get_param(list); + string::size_type pos = 0; + for (;;) { + string::size_type at = slist.find(',', pos); + string name = slist.substr(pos, at - pos); + if(params.has_param(name)) + listed[name] = params.get_param(name); + if(at == string::npos) + return; + pos = at + 1; + } +} + +/* ----------------------------------------------------------------------------- + * CONSUMER CLASS + */ + class Consumer : public prequeue_RP { @@ -204,55 +306,132 @@ Consumer::check_nonce(const string& server, const string& nonce) */ static void -filter_openid_params (params_t ¶ms, params_t &openid) +request_ax_attributes (sid_request_t *req, openid_message_t &message, sid_attribute_t *attrs) { - for (params_t::iterator it = params.begin(); it != params.end(); ) { - const string& name = it->first; - if (name.find ("openid.") == 0) { - openid[name.substr(7)] = it->second; + sid_attribute_t *attr; - /* We erase an increment together, must use post-increment operator */ - params.erase (it++); - } else { - /* Did not match, just go to next element */ - ++it; + /* Request attributes for attribute exchange */ + if (!attrs) + return; + + message["ns.ax"] = "http://openid.net/srv/ax/1.0"; + message["ax.mode"] = "fetch_request"; + + string required, available; + for (attr = attrs; attr; attr = attr->next) { + + /* Request the attribute */ + string field("ax.type."); + field.append(attr->alias); + message[field] = attr->url; + + /* Add to our reqired list */ + string& list = attr->required ? required : available; + if(!list.empty()) + list.append(","); + list.append(attr->alias); + + /* How many to request? */ + if (attr->count != 1) { + field.assign("ax.count."); + field.append(attr->alias); + if (attr->count == 0) + message[field] = "unlimited"; + else + message[field] = format_number (attr->count); } } + + if(!required.empty()) + message["ax.required"] = required; + if(!available.empty()) + message["ax.if_available"] = available; } static void -parse_query_string (const char *qs, params_t ¶ms) +process_ax_values (sid_request_t *req, sid_attribute_t *attr, const string_list& values) { - string pair, key, value; - const char *at; + const char** array = new const char*[values.size() + 1]; + if (!array) { + sid_request_log_error (req, "out of memory", NULL); + return; + } - if (qs == NULL) + int i = 0; + for(string_list::const_iterator it = values.begin(); it != values.end(); ++it, ++i) + array[i] = it->c_str(); + array[i] = NULL; + + sid_request_attribute_values (req, attr, array); + delete [] array; +} + +static void +parse_ax_attributes (sid_request_t *req, params_t &message, sid_attribute_t *attrs) +{ + /* Build a list of all signed params */ + params_t checked; + filter_listed_params(message, checked, "signed"); + + /* Look through and find ax namespace */ + string prefix; + for (params_t::iterator it = checked.begin(); it != checked.end(); ++it) { + if(it->first.find("ns.") == 0 && it->second == "http://openid.net/srv/ax/1.0") { + prefix = it->first.substr(3) + "."; + break; + } + } + + if(prefix.empty()) return; - while (*qs != 0) { - at = strchr (qs, '&'); - if (at == NULL) - at = qs + strlen (qs); - pair = string(qs, at); - string::size_type loc = pair.find('=', 0); - if (loc != string::npos) { - key = pair.substr (0, loc); - value = pair.substr (loc + 1); + /* Look through and find all aliases */ + params_t aliases; + string type_prefix = prefix + "type."; + for (params_t::iterator it = checked.begin(); it != checked.end(); ++it) { + if(it->first.find(type_prefix) == 0) + aliases[it->second] = it->first.substr(type_prefix.length()); + } + + if(aliases.empty()) + return; + + /* Now pull out the values for the attributes we want */ + string_list values; + for(sid_attribute_t *attr = attrs; attr; attr = attr->next) { + if(!aliases.has_param(attr->url)) + continue; + + string alias = aliases.get_param(attr->url); + values.clear(); + + /* See if there's a count attribute */ + string field = prefix + "count." + alias; + if(checked.has_param(field)) { + + /* Get each value in turn */ + int count = parse_number (checked.get_param(field)); + for(int i = 0; i < count; ++i) { + field = prefix + "value." + alias + "." + format_number (i + 1); + if (checked.has_param (field)) + values.push_back (checked.get_param (field)); + } + + /* No count, just a single value */ } else { - key = pair; - value = ""; + field = prefix + "value." + alias; + if(checked.has_param(field)) + values.push_back(checked.get_param(field)); } - params[opkele::util::url_decode(key)] = opkele::util::url_decode(value); - if (*at == 0) - break; - qs = at + 1; + /* Send off values */ + process_ax_values(req, attr, values); } } static void -begin_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms, - const string& trust_root, const string &identity) +begin_auth (sid_request_t *req, Consumer &consumer, const string& trust_root, + const string &identity, sid_attribute_t *attributes) { params_t result; string redirect; @@ -261,6 +440,7 @@ begin_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms, openid_message_t cm; consumer.initiate (identity); consumer.checkid_ (cm, opkele::mode_checkid_setup, consumer.get_this_url(), trust_root); + request_ax_attributes (req, cm, attributes); redirect = cm.append_query (consumer.get_endpoint().uri); } catch (failed_xri_resolution &ex) { @@ -295,12 +475,14 @@ begin_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms, } static void -complete_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms) +complete_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms, + sid_attribute_t *attributes) { try { consumer.id_res(params); string identity = consumer.get_claimed_id(); sid_request_authenticated (req, identity.c_str()); + parse_ax_attributes(req, params, attributes); } catch (exception &ex) { sid_request_respond (req, 500, NULL, NULL); sid_request_log_error (req, "error while completing authentication", ex.what()); @@ -315,7 +497,8 @@ cancelled_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms) extern "C" void sid_consumer_authenticate(sid_request_t *req, sid_storage_t *store, - const char *trust_root, const char *identity) + const char *trust_root, const char *identity, + sid_attribute_t *attributes) { params_t params; params_t openid; @@ -324,7 +507,7 @@ sid_consumer_authenticate(sid_request_t *req, sid_storage_t *store, const char *qs = sid_request_qs (req); parse_query_string (qs, params); - filter_openid_params (params, openid); + filter_prefixed_params (params, openid, "openid."); string url = sid_request_url (req, 1); if (!params.empty()) @@ -333,7 +516,7 @@ sid_consumer_authenticate(sid_request_t *req, sid_storage_t *store, /* Returning (hopefully successful) authentication */ if (openid.has_param("assoc_handle")) { - complete_auth (req, consumer, openid); + complete_auth (req, consumer, openid, attributes); /* Returning cancelled authentication */ } else if (openid.has_param("mode") && openid.get_param("mode") == "cancel") { @@ -343,6 +526,6 @@ sid_consumer_authenticate(sid_request_t *req, sid_storage_t *store, } else { if (!trust_root) trust_root = sid_request_url (req, 0); - begin_auth (req, consumer, params, trust_root, identity); + begin_auth (req, consumer, trust_root, identity, attributes); } } diff --git a/module/mod_auth_singleid.c b/module/mod_auth_singleid.c index 2e46586..d2e53e8 100644 --- a/module/mod_auth_singleid.c +++ b/module/mod_auth_singleid.c @@ -73,6 +73,7 @@ extern module AP_MODULE_DECLARE_DATA auth_singleid_module; #define VALID_NAME "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-." +#define VALID_ALIAS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" enum { NONE = 0, @@ -90,10 +91,35 @@ typedef struct sid_context { int user_match; ap_regex_t *converter; sid_storage_t *store; + sid_attribute_t *attributes; } sid_context_t; #define SID_AUTHTYPE "SingleID" +/* ------------------------------------------------------------------------------- + * UTILITY + */ + +static void +strupcase(char *str) +{ + while (*str) { + *str = apr_toupper(*str); + ++str; + } +} + +static char* +safe_get_token (apr_pool_t *pool, const char **line, int accept_white) +{ + /* HACK: ap_get_token() endless loop if string starts with delim */ + const char *orig = *line; + char *result = ap_get_token (pool, line, accept_white); + if (orig == *line && orig[0]) + (*line)++; + return result; +} + /* ------------------------------------------------------------------------------- * SHARED MEMORY and LOCKING */ @@ -344,6 +370,67 @@ set_cookie_name (cmd_parms *cmd, void *config, const char *val) return NULL; } +static const char* +set_attribute (cmd_parms *cmd, void *config, const char *val) +{ + sid_context_t *ctx = config; + sid_attribute_t attr, *at; + const char *flag; + char *end; + + memset (&attr, 0, sizeof (attr)); + attr.count = 1; + + /* First word is the alias */ + attr.alias = ap_getword_conf (cmd->pool, &val); + if (!attr.alias || strspn (attr.alias, VALID_ALIAS) != strlen (attr.alias)) + return "Invalid attribute alias"; + + /* Check if we already have this alias */ + for (at = ctx->attributes; at; at = at->next) { + if (strcasecmp (at->alias, attr.alias) == 0) + return "Duplicate attribute alias"; + } + + /* Next word is the url */ + attr.url = ap_getword_conf (cmd->pool, &val); + if (!attr.url || !ap_is_url (attr.url)) + return "Invalid attribute URL"; + + /* Now come all the various flags */ + for (;;) { + flag = ap_getword_conf (cmd->pool, &val); + if (!flag || !flag[0]) + break; + + if (strcasecmp (flag, "required") == 0) { + attr.required = 1; + + } else if (strcasecmp (flag, "unlimited") == 0) { + attr.count = 0; + + } else { + attr.count = strtol (flag, &end, 10); + if (*end != '\0') { + if (attr.count != 0) + return "Invalid attribute count"; + else + return "Unrecognized value or flag"; + } + if (attr.count <= 0) + return "Attribute count must a number greater than zero or 'unlimited'"; + } + } + + /* Instantiate the copy */ + attr.next = ctx->attributes; + at = apr_pcalloc (cmd->pool, sizeof (attr)); + memcpy (at, &attr, sizeof (attr)); + ctx->attributes = at; + + 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" ), @@ -355,6 +442,8 @@ static const command_rec command_table[] = { "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" ), + AP_INIT_RAW_ARGS ("SingleAttribute", set_attribute, NULL, OR_AUTHCFG, + "Specify an attribute exchange url and alias."), { NULL } }; @@ -399,8 +488,11 @@ session_cookie_value (request_rec *r, const char *name) if (cookies == NULL) return NULL; + while (cookies[0] == ',' || cookies[0] == ';') + ++cookies; + while (*cookies) { - pair = ap_get_token (r->pool, &cookies, 1); + pair = safe_get_token (r->pool, &cookies, 1); if (!pair) break; if (pair[0] == '$') @@ -463,16 +555,16 @@ session_load_info (sid_context_t *ctx, request_rec *r) if (!value) return NULL; - sig = ap_get_token (r->pool, &value, 0); + sig = safe_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); + token = safe_get_token (r->pool, &value, 0); if (strcmp (token, "1") != 0) return NULL; - token = ap_get_token (r->pool, &value, 0); + token = safe_get_token (r->pool, &value, 0); expiry = strtol (token, &end, 10); if (*end != '\0') return NULL; @@ -482,7 +574,7 @@ session_load_info (sid_context_t *ctx, request_rec *r) return NULL; /* The identifier */ - identifier = ap_get_token (r->pool, &value, 0); + identifier = safe_get_token (r->pool, &value, 0); len = strlen (identifier); if (identifier[0] == '"' && identifier[len - 1] == '"') { identifier[len - 1] = 0; @@ -652,6 +744,31 @@ sid_request_authenticated (sid_request_t *req, const char *identifier) session_send_info (ctx, req->rec, sess); } +void +sid_request_attribute_values (sid_request_t *req, sid_attribute_t *attr, const char **values) +{ + const char **val; + char buf[64]; + char *name; + int i = 0; + + /* Setup all the values */ + for (i = 0, val = values; *val; ++val, ++i) { + snprintf (buf, sizeof (buf), "AX_VALUE_%s_%d", attr->alias, i); + name = apr_pstrdup (req->rec->pool, buf); + strupcase (name); + apr_table_setn (req->rec->subprocess_env, name, + apr_pstrdup (req->rec->pool, *val)); + } + + /* Put in the count */ + snprintf (buf, sizeof (buf), "AX_COUNT_%s", attr->alias); + name = apr_pstrdup (req->rec->pool, buf); + strupcase (name); + snprintf (buf, sizeof (buf), "%d", i); + apr_table_set (req->rec->subprocess_env, name, apr_pstrdup (req->rec->pool, buf)); +} + /* --------------------------------------------------------------------------------------- * MAIN HOOKS */ @@ -720,7 +837,8 @@ hook_authenticate (request_rec* r) req.rec = r; /* Do the OpenID magic */ - sid_consumer_authenticate (&req, ctx->store, ctx->trust_root, ctx->identifier); + sid_consumer_authenticate (&req, ctx->store, ctx->trust_root, + ctx->identifier, ctx->attributes); return req.result; } diff --git a/module/mod_auth_singleid.h b/module/mod_auth_singleid.h index 17e1f22..a81652d 100644 --- a/module/mod_auth_singleid.h +++ b/module/mod_auth_singleid.h @@ -8,6 +8,14 @@ extern "C" { #endif +typedef struct sid_attribute { + const char *url; + const char *alias; + int required; + int count; + struct sid_attribute *next; +} sid_attribute_t; + void sid_shared_lock (void); void sid_shared_unlock (void); @@ -32,6 +40,10 @@ void sid_request_respond (sid_request_t *req, void sid_request_authenticated (sid_request_t *req, const char *identifier); +void sid_request_attribute_values (sid_request_t *req, + sid_attribute_t *attr, + const char **values); + /* ----------------------------------------------------------------------------------- * STORAGE: Actually, communications white-board between processes/threads. */ @@ -75,8 +87,8 @@ void sid_storage_invalidate_assoc (sid_storage_t *storage, void sid_consumer_authenticate (sid_request_t *req, sid_storage_t *store, const char *trust_root, - const char *identity); - + const char *identity, + sid_attribute_t *attributes); #ifdef __cplusplus } /* extern "C" */ -- cgit v1.2.3