/* * 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 2.0 authentication. */ #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 "../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_oauth2; } namespace tuscany { namespace oauth2 { /** * 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("tuscanyOAuth2", sid), mc); } /** * Handle an authenticated request. */ const failable authenticated(const list >& attrs, const list >& info, request_rec* r) { debug(info, "modoauth2::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, "OAUTH2_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); } /** * Handle an authorize request. */ const failable authorize(const list >& args, request_rec* r, const list >& appkeys) { // Extract authorize, access_token, client ID and info URIs const list ref = assoc("openauth_referrer", args); if (isNil(ref) || isNil(cdr(ref))) return mkfailure("Missing openauth_referrer parameter"); const list auth = assoc("oauth2_authorize", args); if (isNil(auth) || isNil(cdr(auth))) return mkfailure("Missing oauth2_authorize parameter"); const list tok = assoc("oauth2_access_token", args); if (isNil(tok) || isNil(cdr(tok))) return mkfailure("Missing oauth2_access_token parameter"); const list cid = assoc("oauth2_client_id", args); if (isNil(cid) || isNil(cdr(cid))) return mkfailure("Missing oauth2_client_id parameter"); const list info = assoc("oauth2_info", args); if (isNil(info) || isNil(cdr(info))) return mkfailure("Missing oauth2_info parameter"); const list scope = assoc("oauth2_scope", args); if (isNil(scope) || isNil(cdr(scope))) return mkfailure("Missing oauth2_scope parameter"); const list display = assoc("oauth2_display", args); // Build the redirect URI const string redir = httpd::url("/oauth2/access_token/", r); debug(redir, "modoauth2::authorize::redir"); // Build the state URI const list > stargs = mklist >(tok, cid, info, ref); const string state = http::queryString(stargs); debug(state, "modoauth2::authorize::state"); // Lookup client app configuration const list app = assoc(cadr(cid), appkeys); if (isNil(app) || isNil(cdr(app))) return mkfailure(string("client id not found: ") + cadr(cid)); list appkey = cadr(app); // Redirect to the authorize URI const list adisplay = (isNil(display) || isNil(cdr(display)))? list() : mklist("display", cadr(display)); const list > aargs = mklist >(mklist("response_type", "code"), mklist("client_id", car(appkey)), mklist("scope", cadr(scope)), adisplay, mklist("redirect_uri", httpd::escape(redir)), mklist("state", httpd::escape(state))); const string uri = httpd::unescape(cadr(auth)) + string("?") + http::queryString(aargs); debug(uri, "modoauth2::authorize::uri"); return httpd::externalRedirect(uri, r); } /** * Extract user info from a profile/info response. * TODO This currently only works for Facebook and Gowalla. * User profile parsing needs to be made configurable. */ const failable > profileUserInfo(const value& cid, const list& info) { return cons(mklist("realm", cid), info); } /** * Handle an access_token request. */ const failable accessToken(const list >& args, request_rec* r, const list >& appkeys, const perthread_ptr& cs, const memcache::MemCached& mc) { // Extract access_token URI, client ID and authorization code parameters const list state = assoc("state", args); if (isNil(state) || isNil(cdr(state))) return mkfailure("Missing state parameter"); const list >& stargs = httpd::queryArgs(httpd::unescape(cadr(state))); const list ref = assoc("openauth_referrer", stargs); if (isNil(ref) || isNil(cdr(ref))) return mkfailure("Missing openauth_referrer parameter"); const list tok = assoc("oauth2_access_token", stargs); if (isNil(tok) || isNil(cdr(tok))) return mkfailure("Missing oauth2_access_token parameter"); const list cid = assoc("oauth2_client_id", stargs); if (isNil(cid) || isNil(cdr(cid))) return mkfailure("Missing oauth2_client_id parameter"); const list info = assoc("oauth2_info", stargs); if (isNil(info) || isNil(cdr(info))) return mkfailure("Missing oauth2_info parameter"); const list code = assoc("code", args); if (isNil(code) || isNil(cdr(code))) return mkfailure("Missing code parameter"); // Lookup client app configuration const list app = assoc(cadr(cid), appkeys); if (isNil(app) || isNil(cdr(app))) return mkfailure(string("client id not found: ") + cadr(cid)); list appkey = cadr(app); // Build the redirect URI const string redir = httpd::url("/oauth2/access_token/", r); debug(redir, "modoauth2::access_token::redir"); // Request access token const list > targs = mklist >(mklist("client_id", car(appkey)), mklist("redirect_uri", httpd::escape(redir)), mklist("client_secret", cadr(appkey)), code, mklist("grant_type", "authorization_code")); const string tqs = http::queryString(targs); debug(tqs, "modoauth2::access_token::tokenqs"); const string turi = httpd::unescape(cadr(tok)); debug(turi, "modoauth2::access_token::tokenuri"); const value tval = mklist(string("application/x-www-form-urlencoded;charset=UTF-8"), mklist(tqs)); const failable ftr = http::post(tval, turi, *(cs)); if (!hasContent(ftr)) return mkfailure(ftr); const value tr = content(ftr); debug(tr, "modoauth2::access_token::response"); if (!isList(tr) || isNil(tr)) return mkfailure("Empty access token"); const list tv = isString(car(tr)) ? assoc("access_token", httpd::queryArgs(join("", convertValues(cadr(tr))))) : assoc("access_token", tr); if (isNil(tv) || isNil(cdr(tv))) return mkfailure("Couldn't retrieve access_token"); debug(tv, "modoauth2::access_token::token"); // Request user info // TODO Make this step configurable const list > iargs = mklist >(tv); const string iuri = httpd::unescape(cadr(info)) + string("?") + http::queryString(iargs); debug(iuri, "modoauth2::access_token::infouri"); const failable profres = http::get(iuri, *(cs)); if (!hasContent(profres)) return mkfailure("Couldn't retrieve user info"); debug(content(profres), "modoauth2::access_token::info"); // Retrieve the user info from the profile const failable > iv = profileUserInfo(cadr(cid), content(profres)); if (!hasContent(iv)) return mkfailure(iv); // Store user info in memcached keyed by session ID const value sid = string("OAuth2_") + mkrand(); const failable prc = memcache::put(mklist("tuscanyOAuth2", sid), content(iv), mc); if (!hasContent(prc)) return mkfailure(prc); // Send session ID to the client in a cookie debug(c_str(openauth::cookie("TuscanyOAuth2", sid, httpd::hostName(r))), "modoauth2::access_token::setcookie"); apr_table_set(r->err_headers_out, "Set-Cookie", c_str(openauth::cookie("TuscanyOAuth2", sid, httpd::hostName(r)))); return httpd::externalRedirect(httpd::url(httpd::unescape(cadr(ref)), 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_oauth2); if (!dc.enabled) return DECLINED; const char* atype = ap_auth_type(r); debug(atype, "modopenauth::checkAuthn::auth_type"); if (atype == NULL || strcasecmp(atype, "Open")) return DECLINED; // Create a scoped memory pool gc_scoped_pool pool(r->pool); // Get the server configuration debug_httpdRequest(r, "modoauth2::checkAuthn::input"); const ServerConf& sc = httpd::serverConf(r, &mod_tuscany_oauth2); // Get session id from the request const maybe sid = openauth::sessionID(r, "TuscanyOAuth2"); if (hasContent(sid)) { // Decline if the session id was not created by this module if (substr(content(sid), 0, 7) != "OAuth2_") 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)); } } // Handle OAuth authorize request step if (string(r->uri) == "/oauth2/authorize/") { r->ap_auth_type = const_cast(atype); return httpd::reportStatus(authorize(httpd::queryArgs(r), r, sc.appkeys)); } // Handle OAuth access_token request step if (string(r->uri) == "/oauth2/access_token/") { r->ap_auth_type = const_cast(atype); return httpd::reportStatus(accessToken(httpd::queryArgs(r), r, sc.appkeys, sc.cs, sc.mc)); } // Redirect to the login page, unless we have a session id or an authorization // header from another module if (apr_table_get(r->headers_in, (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authorization" : "Authorization") != NULL) return DECLINED; if (hasContent(openauth::sessionID(r, "TuscanyOpenIDAuth")) || hasContent(openauth::sessionID(r, "TuscanyOpenAuth")) || hasContent(openauth::sessionID(r, "TuscanyOAuth1"))) return DECLINED; if ((substr(string(r->uri), 0, 8) == "/oauth1/") || !isNil(assoc("openid_identifier", httpd::queryArgs(r)))) return DECLINED; 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_oauth2); debug(httpd::serverName(s), "modoauth2::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_oauth2); debug(httpd::serverName(s), "modoauth2::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_oauth2); if(psc == NULL) { cfailure << "[Tuscany] Due to one or more errors mod_tuscany_oauth2 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_oauth2); 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_oauth2); 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_oauth2); 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_oauth2); 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_oauth2); 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("AddAuthOAuth2AppKey", (const char*(*)())confAppKey, NULL, RSRC_CONF, "OAuth 2.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 2.0 authentication On | Off"), AP_INIT_TAKE1("AuthOAuthLoginPage", (const char*(*)())confLogin, NULL, OR_AUTHCFG, "OAuth 2.0 login page"), AP_INIT_TAKE1("AuthOAuthSSLCACertificateFile", (const char*(*)())confCAFile, NULL, RSRC_CONF, "OAUth 2.0 SSL CA certificate file"), AP_INIT_TAKE1("AuthOAuthSSLCertificateFile", (const char*(*)())confCertFile, NULL, RSRC_CONF, "OAuth 2.0 SSL certificate file"), AP_INIT_TAKE1("AuthOAuthSSLCertificateKeyFile", (const char*(*)())confCertKeyFile, NULL, RSRC_CONF, "OAuth 2.0 SSL certificate key file"), AP_INIT_TAKE2("AddAuthOAuth2ScopeAttr", (const char*(*)())confScopeAttr, NULL, OR_AUTHCFG, "OAuth 2.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_oauth2 = { STANDARD20_MODULE_STUFF, // dir config and merger tuscany::httpd::makeDirConf, NULL, // server config and merger tuscany::httpd::makeServerConf, NULL, // commands and hooks tuscany::oauth2::commands, tuscany::oauth2::registerHooks }; }