diff options
author | Robert Newson <rnewson@apache.org> | 2018-09-19 23:02:08 +0100 |
---|---|---|
committer | Robert Newson <rnewson@apache.org> | 2018-09-20 18:05:15 +0100 |
commit | 9dd28e944fba8c6495c85c51a9027495e700b904 (patch) | |
tree | 8335f733c4f1cacb83e0fd630e92d3e7af0ef3c1 | |
parent | b4bfc529efdba4b971cd8351dc1fa6b552089744 (diff) | |
download | couchdb-enhance-session-cookies.tar.gz |
-rw-r--r-- | rel/overlay/etc/default.ini | 4 | ||||
-rw-r--r-- | src/chttpd/src/chttpd_auth_request.erl | 2 | ||||
-rw-r--r-- | src/chttpd/src/chttpd_httpd_handlers.erl | 1 | ||||
-rw-r--r-- | src/chttpd/src/chttpd_session_cookie.erl | 191 |
4 files changed, 197 insertions, 1 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index dc2e51cc0..55952e4b5 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -98,6 +98,7 @@ max_db_number_for_dbs_info_req = 100 ; authentication_handlers = {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler} ; uncomment the next line to enable proxy authentication ; authentication_handlers = {chttpd_auth, proxy_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler} +authentication_handlers = {chttpd_session_cookie, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler} ; prevent non-admins from accessing /_all_dbs ;admin_only_all_dbs = false @@ -165,7 +166,8 @@ max_http_request_size = 4294967296 ; 4GB [ssl] port = 6984 -; [chttpd_auth] +[chttpd_auth] +cookie_key = nwWCCrz0Cd4MouJNtY4dnwO++gw+/DHwkdRI4yzIuxg= ; authentication_db = _users ; [chttpd_auth_cache] diff --git a/src/chttpd/src/chttpd_auth_request.erl b/src/chttpd/src/chttpd_auth_request.erl index 9110ed6bc..6bd6693b7 100644 --- a/src/chttpd/src/chttpd_auth_request.erl +++ b/src/chttpd/src/chttpd_auth_request.erl @@ -80,6 +80,8 @@ server_authorization_check(#httpd{path_parts=[<<"_uuids">>]}=Req) -> Req; server_authorization_check(#httpd{path_parts=[<<"_session">>]}=Req) -> Req; +server_authorization_check(#httpd{path_parts=[<<"_new_session">>]}=Req) -> + Req; server_authorization_check(#httpd{path_parts=[<<"_replicate">>]}=Req) -> Req; server_authorization_check(#httpd{path_parts=[<<"_stats">>]}=Req) -> diff --git a/src/chttpd/src/chttpd_httpd_handlers.erl b/src/chttpd/src/chttpd_httpd_handlers.erl index cb52e2c40..14c1a5ef3 100644 --- a/src/chttpd/src/chttpd_httpd_handlers.erl +++ b/src/chttpd/src/chttpd_httpd_handlers.erl @@ -26,6 +26,7 @@ url_handler(<<"_reload_query_servers">>) -> fun chttpd_misc:handle_reload_query_ url_handler(<<"_replicate">>) -> fun chttpd_misc:handle_replicate_req/1; url_handler(<<"_uuids">>) -> fun chttpd_misc:handle_uuids_req/1; url_handler(<<"_session">>) -> fun chttpd_auth:handle_session_req/1; +url_handler(<<"_new_session">>) -> fun chttpd_session_cookie:handle_session_req/1; url_handler(<<"_up">>) -> fun chttpd_misc:handle_up_req/1; url_handler(_) -> no_match. diff --git a/src/chttpd/src/chttpd_session_cookie.erl b/src/chttpd/src/chttpd_session_cookie.erl new file mode 100644 index 000000000..bc62621eb --- /dev/null +++ b/src/chttpd/src/chttpd_session_cookie.erl @@ -0,0 +1,191 @@ +% Licensed 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. + +-module(chttpd_session_cookie). +-on_load(bump_server_epoch/0). +-include_lib("couch/include/couch_db.hrl"). +-export([ + cookie_authentication_handler/1, + handle_session_req/1 +]). + +-import(couch_httpd, [send_json/3, send_json/4]). + +-define(COOKIE_NAME, "AuthSession"). + +-define(UNAUTHORIZED, {[ + {error, <<"unauthorized">>}, + {reason, <<"Name or password is incorrect.">>} +]}). + +-define(COOKIE_INVALID, + {unauthorized, <<"session cookie is invalid.">>}). + + +cookie_authentication_handler(#httpd{mochi_req=MochiReq} = Req) -> + CookieValue = MochiReq:get_cookie_value(?COOKIE_NAME), + Key = get_cookie_key(), + AAD = aad(Req), + case decode_cookie(Key, AAD, CookieValue) of + false -> + Req; + Data ->couch_log:notice("decoded ~p", [Data]), + UserName = proplists:get_value(u, Data), + Expiration = proplists:get_value(x, Data), + Expired = Expiration < system_time(), + {ok, UserProps} = get_user_props(Req, UserName), + ExpectedUserEpoch = list_to_integer(proplists:get_value( + <<"session_epoch">>, UserProps, "0")), + ActualUserEpoch = proplists:get_value(e, Data, 0), + UserEpochMatch = ActualUserEpoch == ExpectedUserEpoch, + if + Expired -> + throw(?COOKIE_INVALID); + not UserEpochMatch -> + throw(?COOKIE_INVALID); + true -> + Req#httpd{user_ctx = #user_ctx{ + name = UserName, + roles = proplists:get_value(<<"roles">>, UserProps) + }} + end + end. + + +handle_session_req(#httpd{method = 'POST'} = Req) -> + MochiReq = Req#httpd.mochi_req, + ReqBody = MochiReq:recv_body(), + Form = case MochiReq:get_primary_header_value("content-type") of + "application/x-www-form-urlencoded" ++ _ -> + mochiweb_util:parse_qs(ReqBody); + "application/json" ++ _ -> + {Props} = jiffy:decode(ReqBody), + Props + end, + UserName = ?l2b(proplists:get_value("username", Form)), + Password = ?l2b(proplists:get_value("password", Form)), + {ok, UserProps} = get_user_props(Req, UserName), + case couch_httpd_auth:authenticate(Password, UserProps) of + true -> + couch_httpd_auth:verify_totp(UserProps, Form), + AAD = aad(Req), + CookieHeader = new_cookie_from_props(UserName, AAD, UserProps), + Roles = proplists:get_value(<<"roles">>, UserProps, []), + Body = {[{ok, true}, {name, UserName}, {roles, Roles}]}, + send_json(Req, 200, [CookieHeader], Body); + false -> + send_json(Req, 401, ?UNAUTHORIZED) + end; + +handle_session_req(#httpd{method = 'GET'} = Req) -> + send_json(Req, 200, {[ + {ok, true}, + {<<"userCtx">>, {[ + {name, Req#httpd.user_ctx#user_ctx.name}, + {roles, Req#httpd.user_ctx#user_ctx.roles} + ]}} + ]}). + + +new_cookie_from_props(UserName, AAD, UserProps) -> + UserEpoch = list_to_integer( + proplists:get_value(<<"session_epoch">>, UserProps, "0")), + Key = get_cookie_key(), + TimeStamp = system_time(), + MaxAge = max_age(), + Data = [ + {u, UserName}, + {e, UserEpoch}, + {x, TimeStamp + MaxAge}], + Options = [{path, "/"}, {max_age, MaxAge}], + new_cookie(Key, AAD, Data, Options). + + +new_cookie(Key, AAD, Data, Options) -> + PlainText = term_to_binary(Data), + IV = new_iv(), + CipherText = encrypt(Key, IV, AAD, PlainText), + EncodedText = couch_util:encodeBase64Url(CipherText), + mochiweb_cookies:cookie(?COOKIE_NAME, [$+, EncodedText], Options). + + +%% New-style cookies start with + as it does not appear in base64url +%% alphabet. +decode_cookie(Key, AAD, [$+ | EncodedText]) -> + CipherText = couch_util:decodeBase64Url(EncodedText), + PlainText = decrypt(Key, AAD, CipherText), + binary_to_term(PlainText, [safe]); + +% undefined or empty or old-style session cookie. +decode_cookie(_Key, _AAD, _CookieValue) -> + false. + + +new_iv() -> + HostPos = host_pos(), + Epoch = config:get_integer("chttpd_auth", "cookie_epoch", 0), + true = Epoch < 16#10000000000000000, + Counter = erlang:unique_integer([positive]), + true = Counter < 16#10000000000000000, + <<HostPos:8, Epoch:22, Counter:66>>. + + +host_pos() -> + host_pos(node(), mem3:nodes(), 1). + + +host_pos(N, [N | _Rest], Pos) -> + Pos; +host_pos(N, [_ | Rest], Pos) -> + host_pos(N, Rest, Pos + 1). + + +% use AAD to tie the cookie's validity to the host header. +aad(#httpd{} = Req) -> + ?l2b((Req#httpd.mochi_req):get_header_value("Host")). + + +encrypt(Key, IV, AAD, PlainText) + when is_binary(Key), bit_size(IV) == 96, is_binary(AAD) -> + {CipherText, CipherTag} = crypto:block_encrypt( + aes_gcm, Key, IV, {AAD, PlainText, 16}), + <<CipherTag/binary, IV/binary, CipherText/binary>>. + + +decrypt(Key, AAD, <<CipherTag:16/binary, IV:12/binary, CipherText/binary>>) + when is_binary(Key), is_binary(AAD), is_binary(IV) -> + crypto:block_decrypt( + aes_gcm, Key, IV, {AAD, CipherText, CipherTag}). + + +get_cookie_key() -> + CookieKey = config:get("chttpd_auth", "cookie_key"), + base64:decode(CookieKey). + + +system_time() -> + os:system_time(second). + + +max_age() -> + config:get_integer("chttpd_auth", "cookie_max_age", 600). + + +get_user_props(Req, UserName) -> + {ok, UserProps, _AuthCtx} = chttpd_auth_cache:get_user_creds(Req, UserName), + {ok, UserProps}. + + +bump_server_epoch() -> + %% Bump the IV epoch to ensure uniqueness across reboots. + Epoch = config:get_integer("chttpd_auth", "cookie_epoch", 0), + config:set_integer("chttpd_auth", "cookie_epoch", Epoch + 1). |