/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* $Rev$ $Date$ */ /** * HTTPD module for OAuth 1.0 authentication. */ #include #include extern "C" { #include } #define WANT_HTTPD_LOG 1 #include "string.hpp" #include "stream.hpp" #include "list.hpp" #include "tree.hpp" #include "value.hpp" #include "monad.hpp" #include "parallel.hpp" #include "../json/json.hpp" #include "../http/httpd.hpp" #include "../http/http.hpp" #include "../http/openauth.hpp" #include "../../components/cache/memcache.hpp" extern "C" { extern module AP_MODULE_DECLARE_DATA mod_tuscany_oauth1; } namespace tuscany { namespace oauth1 { /** * Server configuration. */ class ServerConf { public: ServerConf(apr_pool_t* p, server_rec* s) : p(p), server(s) { } const gc_pool p; server_rec* server; string ca; string cert; string key; list > appkeys; list mcaddrs; memcache::MemCached mc; perthread_ptr cs; }; /** * Directory configuration. */ class DirConf { public: DirConf(apr_pool_t* p, char* d) : p(p), dir(d), enabled(false), login("") { } const gc_pool p; const char* dir; bool enabled; string login; list > scopeattrs; }; /** * Return the user info for a session. */ const failable userInfo(const value& sid, const memcache::MemCached& mc) { return memcache::get(mklist("tuscanyOpenAuth", sid), mc); } /** * Handle an authenticated request. */ const failable authenticated(const list >& attrs, const list >& info, request_rec* r) { debug(info, "modoauth1::authenticated::info"); if (isNil(attrs)) { // Store user id in an environment variable const list id = assoc("id", info); if (isNil(id) || isNil(cdr(id))) return mkfailure("Couldn't retrieve user id"); apr_table_set(r->subprocess_env, "OAUTH1_ID", apr_pstrdup(r->pool, c_str(cadr(id)))); // If the request user field has not been mapped to another attribute, map the // OAuth id attribute to it if (r->user == NULL || r->user[0] == '\0') r->user = apr_pstrdup(r->pool, c_str(cadr(id))); return OK; } // Store each configure OAuth scope attribute in an environment variable const list a = car(attrs); const list v = assoc(cadr(a), info); if (!isNil(v) && !isNil(cdr(v))) { // Map the REMOTE_USER attribute to the request user field if (string(car(a)) == "REMOTE_USER") r->user = apr_pstrdup(r->pool, c_str(cadr(v))); else apr_table_set(r->subprocess_env, apr_pstrdup(r->pool, c_str(car(a))), apr_pstrdup(r->pool, c_str(cadr(v)))); } return authenticated(cdr(attrs), info, r); } /** * Convert a query string containing oauth args to an authorization header. */ const string header(const string& qs, const string& redir, const string& verif) { const list > args = httpd::queryArgs(qs); ostringstream hdr; hdr << "Authorization: OAuth " << "oauth_nonce=\"" << string(cadr(assoc("oauth_nonce", args))) << "\", "; if (length(redir) != 0) hdr << "oauth_callback=\"" << httpd::escape(redir) << "\", "; hdr << "oauth_signature_method=\"" << string(cadr(assoc("oauth_signature_method", args))) << "\", " << "oauth_timestamp=\"" << string(cadr(assoc("oauth_timestamp", args))) << "\", " << "oauth_consumer_key=\"" << string(cadr(assoc("oauth_consumer_key", args))) << "\", "; const list atok = assoc("oauth_token", args); if (!isNil(atok) && !isNil(cdr(atok))) hdr << "oauth_token=\"" << string(cadr(atok)) << "\", "; if (length(verif) != 0) hdr << "oauth_verifier=\"" << verif << "\", "; hdr << "oauth_signature=\"" << string(cadr(assoc("oauth_signature", args))) << "\", " << "oauth_version=\"" << string(cadr(assoc("oauth_version", args))) << "\""; debug(str(hdr), "modoauth1::authheader"); return str(hdr); } /** * Sign a request. */ const list sign(const string& verb, const string& uri, const list appkey, const string& tok, const string& sec) { char* qs = NULL; char* suri = oauth_sign_url2(c_str(uri), &qs, OA_HMAC, c_str(verb), c_str(car(appkey)), c_str(cadr(appkey)), length(tok) != 0? c_str(tok) : NULL, length(sec) != 0? c_str(sec) : NULL); const list res = mklist(suri, qs); free(suri); free(qs); return res; } /** * Handle an authorize request. */ const failable authorize(const list >& args, request_rec* r, const ServerConf& sc) { // Extract authorize, access_token, client ID and info URIs const list req = assoc("mod_oauth1_request_token", args); if (isNil(req) || isNil(cdr(req))) return mkfailure("Missing mod_oauth1_request_token parameter"); const list auth = assoc("mod_oauth1_authorize", args); if (isNil(auth) || isNil(cdr(auth))) return mkfailure("Missing mod_oauth1_authorize parameter"); const list tok = assoc("mod_oauth1_access_token", args); if (isNil(tok) || isNil(cdr(tok))) return mkfailure("Missing mod_oauth1_access_token parameter"); const list cid = assoc("mod_oauth1_client_id", args); if (isNil(cid) || isNil(cdr(cid))) return mkfailure("Missing mod_oauth1_client_id parameter"); const list info = assoc("mod_oauth1_info", args); if (isNil(info) || isNil(cdr(info))) return mkfailure("Missing mod_oauth1_info parameter"); // Build the redirect URI const list > redirargs = mklist >(mklist("mod_oauth1_step", "access_token"), tok, cid, info); const string redir = httpd::url(r->uri, r) + string("?") + http::queryString(redirargs); debug(redir, "modoauth1::authorize::redir"); // Lookup client app configuration const list app = assoc(cadr(cid), sc.appkeys); if (isNil(app) || isNil(cdr(app))) return mkfailure(string("client id not found: ") + cadr(cid)); list appkey = cadr(app); // Build and sign the request token URI const string requri = httpd::unescape(cadr(req)) + string("&") + http::queryString(mklist >(mklist("oauth_callback", httpd::escape(redir)))); const list srequri = sign("POST", requri, appkey, "", ""); debug(srequri, "modoauth1::authorize::srequri"); // Put the args into an oauth header const string reqhdr = header(cadr(srequri), redir, ""); // Send the request token request char* pres = oauth_http_post2(c_str(car(srequri)), "", c_str(reqhdr)); if (pres == NULL) return mkfailure("Couldn't send request token request"); const string res(pres); free(pres); debug(res, "modoauth1::authorize::res"); const list > resargs = httpd::queryArgs(res); // Retrieve the request token const list conf = assoc("oauth_callback_confirmed", resargs); if (isNil(conf) || isNil(cdr(conf)) || cadr(conf) != "true") return mkfailure("Couldn't confirm oauth_callback"); const list tv = assoc("oauth_token", resargs); if (isNil(tv) || isNil(cdr(tv))) return mkfailure("Couldn't retrieve oauth_token"); const list sv = assoc("oauth_token_secret", resargs); if (isNil(sv) || isNil(cdr(sv))) return mkfailure("Couldn't retrieve oauth_token_secret"); // Store the request token in memcached const failable prc = memcache::put(mklist("tuscanyOAuth1Token", cadr(tv)), cadr(sv), sc.mc); if (!hasContent(prc)) return mkfailure(reason(prc)); // Redirect to the authorize URI const string authuri = httpd::unescape(cadr(auth)) + string("?") + http::queryString(mklist >(tv)); debug(authuri, "modoauth1::authorize::authuri"); return httpd::externalRedirect(authuri, r); } /** * Extract user info from a profile/info response. * TODO This currently only works for Twitter, Foursquare and LinkedIn. * User profile parsing needs to be made configurable. */ const failable > profileUserInfo(const value& cid, const string& info) { string b = substr(info, 0, 1); if (b == "[") { // Twitter JSON profile js::JSContext cx; const list infov(json::jsonValues(content(json::readJSON(mklist(info), cx)))); if (isNil(infov)) return mkfailure >("Couldn't retrieve user info"); debug(infov, "modoauth1::access_token::info"); const list uv = assoc("user", car(infov)); debug(uv, "modoauth1::access_token::userInfo"); if (isNil(uv) || isNil(cdr(uv))) return mkfailure >("Couldn't retrieve user info"); const list iv = cdr(uv); return cons(mklist("realm", cid), iv); } if (b == "{") { // Foursquare JSON profile js::JSContext cx; const list infov(json::jsonValues(content(json::readJSON(mklist(info), cx)))); if (isNil(infov)) return mkfailure >("Couldn't retrieve user info"); debug(infov, "modoauth1::access_token::info"); const list uv = assoc("user", infov); debug(uv, "modoauth1::access_token::userInfo"); if (isNil(uv) || isNil(cdr(uv))) return mkfailure >("Couldn't retrieve user info"); const list iv = cdr(uv); return cons(mklist("realm", cid), iv); } if (b == "<") { // XML profile const list infov = elementsToValues(readXML(mklist(info))); if (isNil(infov)) return mkfailure >("Couldn't retrieve user info"); debug(infov, "modoauth1::access_token::info"); const list pv = car(infov); debug(pv, "modoauth1::access_token::userInfo"); if (isNil(pv) || isNil(cdr(pv))) return mkfailure >("Couldn't retrieve user info"); const list iv = cdr(pv); return cons(mklist("realm", cid), iv); } return mkfailure >("Couldn't retrieve user info"); } /** * Handle an access_token request. */ const failable access_token(const list >& args, request_rec* r, const ServerConf& sc) { // Extract access_token URI, client ID and verification code const list tok = assoc("mod_oauth1_access_token", args); if (isNil(tok) || isNil(cdr(tok))) return mkfailure("Missing mod_oauth1_access_token parameter"); const list cid = assoc("mod_oauth1_client_id", args); if (isNil(cid) || isNil(cdr(cid))) return mkfailure("Missing mod_oauth1_client_id parameter"); const list info = assoc("mod_oauth1_info", args); if (isNil(info) || isNil(cdr(info))) return mkfailure("Missing mod_oauth1_info parameter"); const list tv = assoc("oauth_token", args); if (isNil(tv) || isNil(cdr(tv))) return mkfailure("Missing oauth_token parameter"); const list vv = assoc("oauth_verifier", args); if (isNil(vv) || isNil(cdr(vv))) return mkfailure("Missing oauth_verifier parameter"); // Lookup client app configuration const list app = assoc(cadr(cid), sc.appkeys); if (isNil(app) || isNil(cdr(app))) return mkfailure(string("client id not found: ") + cadr(cid)); list appkey = cadr(app); // Retrieve the request token from memcached const failable sv = memcache::get(mklist("tuscanyOAuth1Token", cadr(tv)), sc.mc); if (!hasContent(sv)) return mkfailure(reason(sv)); // Build and sign access token request URI const string tokuri = httpd::unescape(cadr(tok)) + string("?") + http::queryString(mklist >(vv)); const list stokuri = sign("POST", tokuri, appkey, cadr(tv), content(sv)); debug(stokuri, "modoauth1::access_token::stokuri"); // Put the args into an oauth header string tokhdr = header(cadr(stokuri), "", cadr(vv)); // Send the access token request char* ptokres = oauth_http_post2(c_str(car(stokuri)), "", c_str(tokhdr)); if (ptokres == NULL) return mkfailure("Couldn't post access_token request"); const string tokres(ptokres); free(ptokres); debug(tokres, "modoauth1::access_token::res"); const list > tokresargs = httpd::queryArgs(tokres); // Retrieve the access token const list atv = assoc("oauth_token", tokresargs); if (isNil(atv) || isNil(cdr(atv))) return mkfailure("Couldn't retrieve oauth_token"); const list asv = assoc("oauth_token_secret", tokresargs); if (isNil(asv) || isNil(cdr(asv))) return mkfailure("Couldn't retrieve oauth_token_secret"); debug(atv, "modoauth1::access_token::token"); // Build and sign user profile request URI const string profuri = httpd::unescape(cadr(info)); const list sprofuri = sign("GET", profuri, appkey, cadr(atv), cadr(asv)); debug(sprofuri, "modoauth1::access_token::sprofuri"); // Put the args into an oauth header string profhdr = header(cadr(sprofuri), "", ""); // Send the user profile request char* pprofres = oauth_http_get2(c_str(car(sprofuri)), NULL, c_str(profhdr)); if (pprofres == NULL) return mkfailure("Couldn't get user info"); const string profres(pprofres); free(pprofres); debug(profres, "modoauth1::access_token::profres"); // Retrieve the user info from the profile const failable > iv = profileUserInfo(cadr(cid), profres); if (!hasContent(iv)) return mkfailure(reason(iv)); // Store user info in memcached keyed by session ID const value sid = string("OAuth1_") + mkrand(); const failable prc = memcache::put(mklist("tuscanyOpenAuth", sid), content(iv), sc.mc); if (!hasContent(prc)) return mkfailure(reason(prc)); // Send session ID to the client in a cookie debug(c_str(openauth::cookie(sid, httpd::hostName(r))), "modoauth1::access_token::setcookie"); apr_table_set(r->err_headers_out, "Set-Cookie", c_str(openauth::cookie(sid, httpd::hostName(r)))); return httpd::externalRedirect(httpd::url(r->uri, r), r); } /** * Check user authentication. */ static int checkAuthn(request_rec *r) { // Decline if we're not enabled or AuthType is not set to Open const DirConf& dc = httpd::dirConf(r, &mod_tuscany_oauth1); if (!dc.enabled) return DECLINED; const char* atype = ap_auth_type(r); if (atype == NULL || strcasecmp(atype, "Open")) return DECLINED; // Create a scoped memory pool gc_scoped_pool pool(r->pool); // Get the server configuration httpdDebugRequest(r, "modoauth1::checkAuthn::input"); const ServerConf& sc = httpd::serverConf(r, &mod_tuscany_oauth1); // Get session id from the request const maybe sid = openauth::sessionID(r); if (hasContent(sid)) { // Decline if the session id was not created by this module if (substr(content(sid), 0, 7) != "OAuth1_") return DECLINED; // If we're authenticated store the user info in the request const failable info = userInfo(content(sid), sc.mc); if (hasContent(info)) { r->ap_auth_type = const_cast(atype); return httpd::reportStatus(authenticated(dc.scopeattrs, content(info), r)); } } // Get the request args const list > args = httpd::queryArgs(r); // Decline if the request is for another authentication provider if (!isNil(assoc("openid_identifier", args))) return DECLINED; if (!isNil(assoc("mod_oauth2_step", args))) return DECLINED; // Determine the OAuth protocol flow step, conveniently passed // around in a request arg const list sl = assoc("mod_oauth1_step", args); const value step = !isNil(sl) && !isNil(cdr(sl))? cadr(sl) : ""; // Handle OAuth authorize request step if (step == "authorize") { r->ap_auth_type = const_cast(atype); return httpd::reportStatus(authorize(args, r, sc)); } // Handle OAuth access_token request step if (step == "access_token") { r->ap_auth_type = const_cast(atype); return httpd::reportStatus(access_token(args, r, sc)); } // Redirect to the login page r->ap_auth_type = const_cast(atype); return httpd::reportStatus(openauth::login(dc.login, r)); } /** * Process the module configuration. */ int postConfigMerge(ServerConf& mainsc, server_rec* s) { if (s == NULL) return OK; ServerConf& sc = httpd::serverConf(s, &mod_tuscany_oauth1); debug(httpd::serverName(s), "modoauth1::postConfigMerge::serverName"); // Merge configuration from main server if (isNil(sc.appkeys)) sc.appkeys = mainsc.appkeys; if (isNil(sc.mcaddrs)) sc.mcaddrs = mainsc.mcaddrs; sc.mc = mainsc.mc; sc.cs = mainsc.cs; return postConfigMerge(mainsc, s->next); } int postConfig(apr_pool_t* p, unused apr_pool_t* plog, unused apr_pool_t* ptemp, server_rec* s) { gc_scoped_pool pool(p); ServerConf& sc = httpd::serverConf(s, &mod_tuscany_oauth1); debug(httpd::serverName(s), "modoauth1::postConfig::serverName"); // Merge server configurations return postConfigMerge(sc, s); } /** * Lambda function that creates a new CURL session. */ class newsession { public: newsession(const string& ca, const string& cert, const string& key) : ca(ca), cert(cert), key(key) { } const gc_ptr operator()() const { return new (gc_new()) http::CURLSession(ca, cert, key, ""); } private: const string ca; const string cert; const string key; }; /** * Child process initialization. */ void childInit(apr_pool_t* p, server_rec* s) { gc_scoped_pool pool(p); ServerConf* psc = (ServerConf*)ap_get_module_config(s->module_config, &mod_tuscany_oauth1); if(psc == NULL) { cfailure << "[Tuscany] Due to one or more errors mod_tuscany_oauth1 loading failed. Causing apache to stop loading." << endl; exit(APEXIT_CHILDFATAL); } ServerConf& sc = *psc; // Connect to Memcached if (isNil(sc.mcaddrs)) sc.mc = *(new (gc_new()) memcache::MemCached("localhost", 11211)); else sc.mc = *(new (gc_new()) memcache::MemCached(sc.mcaddrs)); // Setup a CURL session sc.cs = perthread_ptr(lambda()>(newsession(sc.ca, sc.cert, sc.key))); // Merge the updated configuration into the virtual hosts postConfigMerge(sc, s->next); } /** * Configuration commands. */ const char* confAppKey(cmd_parms *cmd, unused void *c, const char *arg1, const char* arg2, const char* arg3) { gc_scoped_pool pool(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth1); sc.appkeys = cons >(mklist(arg1, mklist(arg2, arg3)), sc.appkeys); return NULL; } const char* confMemcached(cmd_parms *cmd, unused void *c, const char *arg) { gc_scoped_pool pool(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth1); sc.mcaddrs = cons(arg, sc.mcaddrs); return NULL; } const char* confEnabled(cmd_parms *cmd, void *c, const int arg) { gc_scoped_pool pool(cmd->pool); DirConf& dc = httpd::dirConf(c); dc.enabled = (bool)arg; return NULL; } const char* confLogin(cmd_parms *cmd, void *c, const char* arg) { gc_scoped_pool pool(cmd->pool); DirConf& dc = httpd::dirConf(c); dc.login = arg; return NULL; } const char* confCAFile(cmd_parms *cmd, unused void *c, const char *arg) { gc_scoped_pool pool(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth1); sc.ca = arg; return NULL; } const char* confCertFile(cmd_parms *cmd, unused void *c, const char *arg) { gc_scoped_pool pool(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth1); sc.cert = arg; return NULL; } const char* confCertKeyFile(cmd_parms *cmd, unused void *c, const char *arg) { gc_scoped_pool pool(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth1); sc.key = arg; return NULL; } const char* confScopeAttr(cmd_parms *cmd, void* c, const char* arg1, const char* arg2) { gc_scoped_pool pool(cmd->pool); DirConf& dc = httpd::dirConf(c); dc.scopeattrs = cons >(mklist(arg1, arg2), dc.scopeattrs); return NULL; } /** * HTTP server module declaration. */ const command_rec commands[] = { AP_INIT_TAKE3("AddAuthOAuth1AppKey", (const char*(*)())confAppKey, NULL, RSRC_CONF, "OAuth 1.0 name app-id app-key"), AP_INIT_ITERATE("AddAuthOAuthMemcached", (const char*(*)())confMemcached, NULL, RSRC_CONF, "Memcached server host:port"), AP_INIT_FLAG("AuthOAuth", (const char*(*)())confEnabled, NULL, OR_AUTHCFG, "OAuth 1.0 authentication On | Off"), AP_INIT_TAKE1("AuthOAuthLoginPage", (const char*(*)())confLogin, NULL, OR_AUTHCFG, "OAuth 1.0 login page"), AP_INIT_TAKE1("AuthOAuthSSLCACertificateFile", (const char*(*)())confCAFile, NULL, RSRC_CONF, "OAUth 1.0 SSL CA certificate file"), AP_INIT_TAKE1("AuthOAuthSSLCertificateFile", (const char*(*)())confCertFile, NULL, RSRC_CONF, "OAuth 1.0 SSL certificate file"), AP_INIT_TAKE1("AuthOAuthSSLCertificateKeyFile", (const char*(*)())confCertKeyFile, NULL, RSRC_CONF, "OAuth 1.0 SSL certificate key file"), AP_INIT_TAKE2("AddAuthOAuth1ScopeAttr", (const char*(*)())confScopeAttr, NULL, OR_AUTHCFG, "OAuth 1.0 scope attribute"), {NULL, NULL, NULL, 0, NO_ARGS, NULL} }; void registerHooks(unused apr_pool_t *p) { ap_hook_post_config(postConfig, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_child_init(childInit, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_check_authn(checkAuthn, NULL, NULL, APR_HOOK_MIDDLE, AP_AUTH_INTERNAL_PER_CONF); } } } extern "C" { module AP_MODULE_DECLARE_DATA mod_tuscany_oauth1 = { STANDARD20_MODULE_STUFF, // dir config and merger tuscany::httpd::makeDirConf, NULL, // server config and merger tuscany::httpd::makeServerConf, NULL, // commands and hooks tuscany::oauth1::commands, tuscany::oauth1::registerHooks }; }