/* * 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* const p, server_rec* const s) : p(p), server(s) { } const gc_pool p; server_rec* const server; gc_mutable_ref ca; gc_mutable_ref cert; gc_mutable_ref key; gc_mutable_ref > > appkeys; gc_mutable_ref > mcaddrs; gc_mutable_ref mc; gc_mutable_ref > cs; }; /** * Authentication provider configuration. */ class AuthnProviderConf { public: AuthnProviderConf() : name(), provider(NULL) { } AuthnProviderConf(const string name, const authn_provider* provider) : name(name), provider(provider) { } const string name; const authn_provider* provider; }; /** * Directory configuration. */ class DirConf { public: DirConf(apr_pool_t* const p, const char* const d) : p(p), dir(d), enabled(false), login(emptyString) { } const gc_pool p; const char* const dir; bool enabled; gc_mutable_ref login; gc_mutable_ref > > scopeattrs; gc_mutable_ref > apcs; }; /** * Run the authnz hooks to authenticate a request. */ const failable checkAuthnzProviders(const string& user, request_rec* const r, const list& apcs) { if (isNil(apcs)) return mkfailure("Authentication failure for: " + user, HTTP_UNAUTHORIZED); const AuthnProviderConf apc = car(apcs); if (apc.provider == NULL || !apc.provider->check_password) return checkAuthnzProviders(user, r, cdr(apcs)); apr_table_setn(r->notes, AUTHN_PROVIDER_NAME_NOTE, c_str(apc.name)); const authn_status auth_result = apc.provider->check_password(r, c_str(string("/oauth2/") + user), "password"); apr_table_unset(r->notes, AUTHN_PROVIDER_NAME_NOTE); if (auth_result != AUTH_GRANTED) return checkAuthnzProviders(user, r, cdr(apcs)); return OK; } const failable checkAuthnz(const string& user, request_rec* const r, const list& apcs) { if (substr(user, 0, 1) == "/") return mkfailure(string("Encountered FakeBasicAuth spoof: ") + user, HTTP_UNAUTHORIZED); if (isNil(apcs)) { const authn_provider* provider = (const authn_provider*)ap_lookup_provider(AUTHN_PROVIDER_GROUP, AUTHN_DEFAULT_PROVIDER, AUTHN_PROVIDER_VERSION); return checkAuthnzProviders(user, r, mklist(AuthnProviderConf(AUTHN_DEFAULT_PROVIDER, provider))); } return checkAuthnzProviders(user, r, apcs); } /** * 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 >& userinfo, const bool check, request_rec* const r, const list >& scopeattrs, const list& apcs) { debug(userinfo, "modoauth2::authenticated::userinfo"); if (isNil(scopeattrs)) { // Store user id in an environment variable const list id = assoc("id", userinfo); if (isNil(id) || isNil(cdr(id))) return mkfailure("Couldn't retrieve user id", HTTP_UNAUTHORIZED); 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))); // Run the authnz hooks to check the authenticated user if (check) return checkAuthnz(r->user == NULL? emptyString : r->user, r, apcs); return OK; } // Store each configured OAuth scope attribute in an environment variable const list a = car(scopeattrs); const list v = assoc(cadr(a), userinfo); 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(userinfo, check, r, cdr(scopeattrs), apcs); } /** * Handle an authorize request. */ const failable authorize(const list >& args, request_rec* const 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: ") + (string)cadr(cid)); list appkey = cadr(app); // Redirect to the authorize URI const list adisplay = (isNil(display) || isNil(cdr(display)))? nilListValue : 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 http::CURLSession& cs, const list >& scopeattrs, const list& apcs, 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: ") + (string)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 > userinfo = profileUserInfo(cadr(cid), content(profres)); if (!hasContent(userinfo)) return mkfailure(userinfo); // Validate the authenticated user const failable authrc = authenticated(content(userinfo), true, r, scopeattrs, apcs); if (!hasContent(authrc)) return authrc; // Store user info in memcached keyed by a session ID const value sid = string("OAuth2_") + (string)mkrand(); const failable prc = memcache::put(mklist("tuscanyOAuth2", sid), content(userinfo), mc); if (!hasContent(prc)) return mkfailure(prc); // Send the 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) { const gc_scoped_pool sp(r->pool); // 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); if (atype == NULL || strcasecmp(atype, "Open")) return DECLINED; debug_httpdRequest(r, "modoauth2::checkAuthn::input"); debug(atype, "modoauth2::checkAuthn::auth_type"); // Get the server configuration 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; // Extract the user info from the auth session const failable userinfo = userInfo(content(sid), sc.mc); if (!hasContent(userinfo)) return openauth::reportStatus(mkfailure(reason(userinfo), HTTP_UNAUTHORIZED), dc.login, nilValue, r); r->ap_auth_type = const_cast(atype); return openauth::reportStatus(authenticated(content(userinfo), false, r, dc.scopeattrs, dc.apcs), dc.login, nilValue, r); } // Get the request args const list > args = httpd::queryArgs(r); // Handle OAuth authorize request step if (string(r->uri) == "/oauth2/authorize/") { r->ap_auth_type = const_cast(atype); return openauth::reportStatus(authorize(args, r, sc.appkeys), dc.login, 1, r); } // Handle OAuth access_token request step if (string(r->uri) == "/oauth2/access_token/") { r->ap_auth_type = const_cast(atype); const failable authrc = accessToken(args, r, sc.appkeys, *(*(perthread_ptr*)sc.cs), dc.scopeattrs, dc.apcs, sc.mc); return openauth::reportStatus(authrc, dc.login, 1, r); } // 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", args))) return DECLINED; r->ap_auth_type = const_cast(atype); return httpd::reportStatus(openauth::login(dc.login, nilValue, nilValue, r)); } /** * Process the module configuration. */ int postConfigMerge(const ServerConf& mainsc, server_rec* const 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((list >)sc.appkeys)) sc.appkeys = mainsc.appkeys; if (isNil((list)sc.mcaddrs)) sc.mcaddrs = mainsc.mcaddrs; sc.mc = mainsc.mc; sc.cs = mainsc.cs; return postConfigMerge(mainsc, s->next); } int postConfig(apr_pool_t* const p, unused apr_pool_t* const plog, unused apr_pool_t* const ptemp, server_rec* const s) { const gc_scoped_pool sp(p); const ServerConf& sc = httpd::serverConf(s, &mod_tuscany_oauth2); debug(httpd::serverName(s), "modoauth2::postConfig::serverName"); // Merge server configurations return postConfigMerge(sc, s); } /** * Child process initialization. */ void childInit(apr_pool_t* const p, server_rec* const s) { const gc_scoped_pool sp(p); ServerConf* const 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((list)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 const string ca = sc.ca; const string cert = sc.cert; const string key = sc.key; const gc_pool cp = gc_current_pool(); const lambda()> newsession = [ca, cert, key, cp]() -> const gc_ptr { const gc_scoped_pool sp(pool(cp)); return new (gc_new()) http::CURLSession(ca, cert, key, emptyString, 0); }; sc.cs = *(new (gc_new >()) perthread_ptr(newsession)); // Merge the updated configuration into the virtual hosts postConfigMerge(sc, s->next); } /** * Configuration commands. */ char* confAppKey(cmd_parms *cmd, unused void *c, char *arg1, char* arg2, char* arg3) { const gc_scoped_pool sp(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth2); sc.appkeys = cons >(mklist(arg1, mklist(arg2, arg3)), (list >)sc.appkeys); return NULL; } char* confMemcached(cmd_parms *cmd, unused void *c, char *arg) { const gc_scoped_pool sp(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth2); sc.mcaddrs = cons(arg, (list)sc.mcaddrs); return NULL; } char* confEnabled(cmd_parms *cmd, void *c, int arg) { const gc_scoped_pool sp(cmd->pool); DirConf& dc = httpd::dirConf(c); dc.enabled = (bool)arg; return NULL; } char* confLogin(cmd_parms *cmd, void *c, char* arg) { const gc_scoped_pool sp(cmd->pool); DirConf& dc = httpd::dirConf(c); dc.login = arg; return NULL; } char* confCAFile(cmd_parms *cmd, unused void *c, char *arg) { const gc_scoped_pool sp(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth2); sc.ca = arg; return NULL; } char* confCertFile(cmd_parms *cmd, unused void *c, char *arg) { const gc_scoped_pool sp(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth2); sc.cert = arg; return NULL; } char* confCertKeyFile(cmd_parms *cmd, unused void *c, char *arg) { const gc_scoped_pool sp(cmd->pool); ServerConf& sc = httpd::serverConf(cmd, &mod_tuscany_oauth2); sc.key = arg; return NULL; } char* confScopeAttr(cmd_parms *cmd, void* c, char* arg1, char* arg2) { const gc_scoped_pool sp(cmd->pool); DirConf& dc = httpd::dirConf(c); dc.scopeattrs = cons >(mklist(arg1, arg2), (list >)dc.scopeattrs); return NULL; } char* confAuthnProvider(cmd_parms *cmd, void *c, char* arg) { const gc_scoped_pool sp(cmd->pool); DirConf& dc = httpd::dirConf(c); // Lookup and cache the Authn provider const authn_provider* provider = (authn_provider*)ap_lookup_provider(AUTHN_PROVIDER_GROUP, arg, AUTHN_PROVIDER_VERSION); if (provider == NULL) return apr_psprintf(cmd->pool, "Unknown Authn provider: %s", arg); if (!provider->check_password) return apr_psprintf(cmd->pool, "The '%s' Authn provider doesn't support password authentication", arg); dc.apcs = append(dc.apcs, mklist(AuthnProviderConf(arg, provider))); return NULL; } /** * HTTP server module declaration. */ const command_rec commands[] = { AP_INIT_ITERATE("AuthOAuthProvider", (const char*(*)())confAuthnProvider, NULL, OR_AUTHCFG, "Auth providers for a directory or location"), 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 }; }