/* * 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 "mod_auth_singleid.h" #include #include #include #include #include using opkele::assoc_t; using opkele::association; using opkele::bad_input; using opkele::dumb_RP; using opkele::exception; using opkele::exception_curl; using opkele::failed_discovery; using opkele::failed_lookup; using opkele::failed_xri_resolution; using opkele::id_res_bad_nonce; using opkele::id_res_bad_return_to; using opkele::id_res_failed; using opkele::id_res_mismatch; using opkele::no_endpoint; using opkele::openid_endpoint_t; using opkele::openid_message_t; using opkele::params_t; using opkele::prequeue_RP; using opkele::secret_t; using std::string; using std::vector; typedef std::list string_list; /* ----------------------------------------------------------------------------- * UTILITY */ class LockShared { public: LockShared() { sid_shared_lock (); } ~LockShared() { 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 { private: // types typedef vector endpoints; public: // interface Consumer(const string& url, sid_storage_t *store) : _store(store), _url(url), _index(0) { } bool is_dumb() const { return _store ? false : true; } bool has_assoc(const string& handle) const; public: // overrides virtual void begin_queueing() { _endpoints.clear(); _index = 0; } virtual void queue_endpoint(const openid_endpoint_t& oep) { _endpoints.push_back(oep); } virtual const openid_endpoint_t& get_endpoint() const; virtual void next_endpoint() { _index++; } virtual void set_normalized_id(const string& nid) { _normalized = nid; } virtual const string get_normalized_id() const { return _normalized; } virtual const string get_this_url() const { return _url; } virtual assoc_t store_assoc(const string& server, const string& handle, const string& type, const secret_t& secret, int expires_in); virtual assoc_t find_assoc(const string& server); virtual assoc_t retrieve_assoc(const string& server, const string& handle); virtual void invalidate_assoc(const string& server, const string& handle); virtual void check_nonce(const string& server, const string& nonce); private: // data sid_storage_t *_store; endpoints _endpoints; string _normalized; string _url; endpoints::size_type _index; }; const openid_endpoint_t& Consumer::get_endpoint() const { if (_index >= _endpoints.size()) throw no_endpoint("no more endpoints"); return _endpoints[_index]; } assoc_t Consumer::store_assoc(const string& server, const string& handle, const string& type, const secret_t& secret, int expires_in) { sid_assoc_t data; int res; if (!_store) throw dumb_RP("no storage initialized"); data.server = server.c_str(); data.handle = handle.c_str(); data.type = type.c_str(); data.secret = &(secret.front()); data.n_secret = secret.size(); data.expires = expires_in; { LockShared lock; /* scoped lock */ res = sid_storage_store_assoc (_store, &data); } if (!res) throw dumb_RP("association data was too large to fit in shared storage"); return assoc_t(new association(server, handle, type, secret, expires_in, false)); } bool Consumer::has_assoc(const string& handle) const { sid_assoc_t data = { 0, }; int res; if (!_store) return false; { LockShared lock; res = sid_storage_find_assoc (_store, NULL, handle.c_str(), &data); } return res ? true : false; } assoc_t Consumer::find_assoc(const string& server) { sid_assoc_t data = { 0, }; association *assoc = NULL; if (!_store) throw dumb_RP("no storage initialized"); { LockShared lock; if (sid_storage_find_assoc (_store, server.c_str(), NULL, &data)) { secret_t secret; secret.assign(data.secret, data.secret + data.n_secret); assoc = new association(data.server, data.handle, data.type, secret, data.expires, false); } } if (!assoc) throw failed_lookup("could not find association for server: " + server); return assoc_t(assoc); } assoc_t Consumer::retrieve_assoc(const string& server, const string& handle) { sid_assoc_t data = { 0, }; association *assoc = NULL; if (!_store) throw dumb_RP("no storage initialized"); { LockShared lock; if (sid_storage_find_assoc (_store, server.c_str(), handle.c_str(), &data)) { secret_t secret; secret.assign(data.secret, data.secret + data.n_secret); assoc = new association(data.server, data.handle, data.type, secret, data.expires, false); } } if (!assoc) throw failed_lookup("could not retrieve association for server: " + server); return assoc_t(assoc); } void Consumer::invalidate_assoc(const string& server, const string& handle) { if (!_store) throw dumb_RP("no storage initialized"); LockShared lock; sid_storage_invalidate_assoc (_store, server.c_str(), handle.c_str()); } void Consumer::check_nonce(const string& server, const string& nonce) { int res = 0; if (_store) { LockShared lock; res = sid_storage_check_nonce (_store, server.c_str(), nonce.c_str()); } if (res == 1) throw id_res_bad_nonce ("nonce is too old, or has already been used"); } /* ----------------------------------------------------------------------- * AUTHENTICATION */ static void request_ax_attributes (sid_request_t *req, openid_message_t &message, sid_attribute_t *attrs) { sid_attribute_t *attr; /* 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 process_ax_values (sid_request_t *req, sid_attribute_t *attr, const string_list& values) { const char** array = new const char*[values.size() + 1]; if (!array) { sid_request_log_error (req, "out of memory", NULL); return; } 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, values.size()); 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; /* 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 { field = prefix + "value." + alias; if(checked.has_param(field)) values.push_back(checked.get_param(field)); } /* Send off values */ process_ax_values(req, attr, values); } } static void begin_auth (sid_request_t *req, Consumer &consumer, const string& trust_root, const string &identity, sid_attribute_t *attributes) { params_t result; string redirect; /* We cannot authenticate anything but a GET request */ if (strcmp (sid_request_method (req), "GET") != 0) { sid_request_respond_html (req, 401, "Must Login", "

Must Login

You must be logged in before you can complete this action.

", "

Login

", NULL); return; } try { 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) { sid_request_respond_html (req, 503, "Invalid Identifier", "

Invalid identifier

Details: Could not resolve identity provider.

", NULL); sid_request_log_error (req, "failed xri resolution while while discovering identity provider", ex.what ()); return; } catch (failed_discovery &ex) { sid_request_respond_html (req, 503, "Invalid Identifier", "

Invalid identifier

Details: Could not discover identity provider.

", NULL); sid_request_log_error (req, "failed discovery while while discovering identity provider", ex.what ()); return; } catch (bad_input &ex) { sid_request_respond_headers (req, 500, "Internal Server Error", NULL); sid_request_log_error (req, "bad input to libopkele", ex.what()); return; } catch (no_endpoint &ex) { sid_request_respond_html (req, 503, "No Identity Provider", "

No identity provider

Details: Could not contact a valid identity provider to authenticate you.

", NULL); sid_request_log_error (req, "no more endpoints", ex.what()); return; } catch (exception_curl &ex) { sid_request_respond_html (req, 503, "Bad Identity Provider", "

Bad identity provider

Details: Could not communicate with the identity provider to authenticate you.

", NULL); sid_request_log_error (req, "could not contact identity provider", ex.what()); return; } catch (exception &ex) { sid_request_respond_headers (req, 500, NULL, NULL); sid_request_log_error (req, "error while while discovering identity provider", ex.what()); return; } sid_request_respond_headers (req, 302, "Found", "Location", redirect.c_str(), "Cache-Control", "no-cache", NULL); } static void complete_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms, sid_attribute_t *attributes, bool &finished) { try { finished = true; consumer.id_res(params); string identity = consumer.get_claimed_id(); sid_request_authenticated (req, identity.c_str()); parse_ax_attributes(req, params, attributes); } catch (id_res_mismatch &ex) { sid_request_respond_headers (req, 403, "Signature mismatch", NULL); sid_request_log_error (req, "signature did not match data", ex.what()); } catch (bad_input &ex) { sid_request_respond_headers (req, 403, "Bad authentication input", NULL); sid_request_log_error (req, "bad input", ex.what()); } catch (id_res_bad_return_to &ex) { sid_request_respond_headers (req, 403, "Bad authenticated address", NULL); sid_request_log_error (req, "bad return to", ex.what()); } catch (id_res_failed &ex) { /* If we don't have this association, then try again */ if (params.has_param("assoc_handle") && !consumer.has_assoc(params.get_param("assoc_handle"))) { sid_request_log_error (req, "response from invalid association, retrying authentication", NULL); finished = false; } else { sid_request_respond_headers (req, 503, "Service error, try again", NULL); sid_request_log_error (req, "checking response failed", ex.what()); } } catch (exception &ex) { sid_request_respond_headers (req, 500, NULL, NULL); sid_request_log_error (req, "error while completing authentication", ex.what()); } } static void cancelled_auth (sid_request_t *req, Consumer &consumer, params_t ¶ms, const string& return_to) { sid_request_respond_html (req, 401, "Login Cancelled", "

Login Cancelled

" "

This website requires authentication, but the authentication process was cancelled.

", "

Retry login

", NULL); } extern "C" void sid_consumer_authenticate(sid_request_t *req, sid_storage_t *store, const char *trust_root, const char *identity, sid_attribute_t *attributes) { params_t params; params_t openid; assert (req); const char *qs; if (strcmp (sid_request_method (req), "POST") == 0) qs = sid_request_form (req); else qs = sid_request_qs (req); parse_query_string (qs, params); filter_prefixed_params (params, openid, "openid."); string url = sid_request_url (req, 1); if (!params.empty()) url = params.append_query (url, ""); Consumer consumer(url, store); /* Returning (hopefully successful) authentication */ if (openid.has_param("assoc_handle")) { bool finished = true; complete_auth (req, consumer, openid, attributes, finished); if (finished) return; } /* Returning cancelled authentication */ if (openid.has_param("mode") && openid.get_param("mode") == "cancel") { cancelled_auth (req, consumer, openid, url); /* Begin a new authentication */ } else { if (!trust_root) trust_root = sid_request_url (req, 0); begin_auth (req, consumer, trust_root, identity, attributes); } } void sid_consumer_redirect_after (sid_request_t *req) { assert (req); const char *qs = sid_request_qs (req); params_t params; parse_query_string (qs, params); params_t unused; filter_prefixed_params (params, unused, "openid."); string url = sid_request_url (req, 1); if (!params.empty()) url = params.append_query (url, ""); sid_request_respond_headers (req, 302, "Found", "Location", url.c_str(), "Cache-Control", "no-cache", NULL); }