diff options
author | Robert Newson <rnewson@apache.org> | 2020-03-20 12:32:16 +0000 |
---|---|---|
committer | Robert Newson <rnewson@apache.org> | 2020-03-22 23:16:29 +0000 |
commit | c1e7c5ac2c754a342fb5fd7dc6473c1630ce422c (patch) | |
tree | ac56457ffafeebcfcd43d8cd3dfe5c799b607922 | |
parent | 623ae9acbed5f60244cde30fc969e0ffb2792abf (diff) | |
download | couchdb-c1e7c5ac2c754a342fb5fd7dc6473c1630ce422c.tar.gz |
Create in-memory cache of JWT keys
Decoding RSA and EC keys is a little expensive and we don't want to do
it for every single request. Add a cache that is invalidated on config change.
-rw-r--r-- | src/couch/src/couch_httpd_auth.erl | 15 | ||||
-rw-r--r-- | src/jwtf/src/jwtf.app.src | 2 | ||||
-rw-r--r-- | src/jwtf/src/jwtf_app.erl | 28 | ||||
-rw-r--r-- | src/jwtf/src/jwtf_keystore.erl | 118 | ||||
-rw-r--r-- | src/jwtf/src/jwtf_sup.erl | 38 |
5 files changed, 187 insertions, 14 deletions
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl index 62fc694e1..86d583c56 100644 --- a/src/couch/src/couch_httpd_auth.erl +++ b/src/couch/src/couch_httpd_auth.erl @@ -193,7 +193,7 @@ jwt_authentication_handler(Req) -> "Bearer " ++ Jwt -> RequiredClaims = get_configured_claims(), AllowedAlgorithms = get_configured_algorithms(), - case jwtf:decode(?l2b(Jwt), [{alg, AllowedAlgorithms} | RequiredClaims], fun jwt_keystore/2) of + case jwtf:decode(?l2b(Jwt), [{alg, AllowedAlgorithms} | RequiredClaims], fun jwtf_keystore:get/2) of {ok, {Claims}} -> case lists:keyfind(<<"sub">>, 1, Claims) of false -> throw({unauthorized, <<"Token missing sub claim.">>}); @@ -213,19 +213,6 @@ get_configured_algorithms() -> get_configured_claims() -> re:split(config:get("jwt_auth", "required_claims", ""), "\s*,\s*", [{return, binary}]). -jwt_keystore(Alg, undefined) -> - jwt_keystore(Alg, "_default"); -jwt_keystore(Alg, KID) -> - Key = config:get("jwt_keys", KID), - case jwtf:verification_algorithm(Alg) of - {hmac, _} -> - Key; - {public_key, _} -> - BinKey = ?l2b(string:replace(Key, "\\n", "\n", all)), - [PEMEntry] = public_key:pem_decode(BinKey), - public_key:pem_entry_decode(PEMEntry) - end. - cookie_authentication_handler(Req) -> cookie_authentication_handler(Req, couch_auth_cache). diff --git a/src/jwtf/src/jwtf.app.src b/src/jwtf/src/jwtf.app.src index 304bb9e0a..24081bf6f 100644 --- a/src/jwtf/src/jwtf.app.src +++ b/src/jwtf/src/jwtf.app.src @@ -18,10 +18,12 @@ kernel, stdlib, b64url, + config, crypto, jiffy, public_key ]}, + {mod, {jwtf_app, []}}, {env,[]}, {modules, []}, {maintainers, []}, diff --git a/src/jwtf/src/jwtf_app.erl b/src/jwtf/src/jwtf_app.erl new file mode 100644 index 000000000..bd708e2a3 --- /dev/null +++ b/src/jwtf/src/jwtf_app.erl @@ -0,0 +1,28 @@ +% 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(jwtf_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, stop/1]). + +%% =================================================================== +%% Application callbacks +%% =================================================================== + +start(_StartType, _StartArgs) -> + jwtf_sup:start_link(). + +stop(_State) -> + ok. diff --git a/src/jwtf/src/jwtf_keystore.erl b/src/jwtf/src/jwtf_keystore.erl new file mode 100644 index 000000000..82df54e5b --- /dev/null +++ b/src/jwtf/src/jwtf_keystore.erl @@ -0,0 +1,118 @@ +% 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(jwtf_keystore). +-behaviour(gen_server). +-behaviour(config_listener). + +% public api. +-export([ + get/2, + start_link/0 +]). + +% gen_server api. +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +% config_listener api +-export([handle_config_change/5, handle_config_terminate/3]). + +% public functions + +get(Alg, undefined) -> + get(Alg, "_default"); + +get(Alg, KID) when is_binary(KID) -> + get(Alg, binary_to_list(KID)); + +get(Alg, KID) -> + case ets:lookup(?MODULE, KID) of + [] -> + Key = get_from_config(Alg, KID), + ok = gen_server:call(?MODULE, {set, KID, Key}), + Key; + [{KID, Key}] -> + Key + end. + + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +% gen_server functions + +init(_) -> + ok = config:listen_for_changes(?MODULE, nil), + ets:new(?MODULE, [public, named_table]), + {ok, nil}. + + +handle_call({set, KID, Key}, _From, State) -> + true = ets:insert(?MODULE, {KID, Key}), + {reply, ok, State}. + + +handle_cast({delete, KID}, State) -> + true = ets:delete(?MODULE, KID), + {noreply, State}; + +handle_cast(_Msg, State) -> + {noreply, State}. + + +handle_info(restart_config_listener, State) -> + ok = config:listen_for_changes(?MODULE, nil), + {noreply, State}; + +handle_info(_Msg, State) -> + {noreply, State}. + + +terminate(_Reason, _State) -> + ok. + + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +% config listener callback + +handle_config_change("jwt_keys", KID, _Value, _, _) -> + {ok, gen_server:cast(?MODULE, {delete, KID})}; + +handle_config_change(_, _, _, _, _) -> + {ok, nil}. + +handle_config_terminate(_Server, stop, _State) -> + ok; + +handle_config_terminate(_Server, _Reason, _State) -> + erlang:send_after(100, whereis(?MODULE), restart_config_listener). + +% private functions + +get_from_config(Alg, KID) -> + case config:get("jwt_keys", KID) of + undefined -> + throw({bad_request, <<"Unknown kid">>}); + Key -> + case jwtf:verification_algorithm(Alg) of + {hmac, _} -> + list_to_binary(Key); + {public_key, _} -> + BinKey = iolist_to_binary(string:replace(Key, "\\n", "\n", all)), + [PEMEntry] = public_key:pem_decode(BinKey), + public_key:pem_entry_decode(PEMEntry) + end + end. diff --git a/src/jwtf/src/jwtf_sup.erl b/src/jwtf/src/jwtf_sup.erl new file mode 100644 index 000000000..6f44808de --- /dev/null +++ b/src/jwtf/src/jwtf_sup.erl @@ -0,0 +1,38 @@ +% 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(jwtf_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +%% Helper macro for declaring children of supervisor +-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). + +%% =================================================================== +%% API functions +%% =================================================================== + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +init([]) -> + {ok, { {one_for_one, 5, 10}, [?CHILD(jwtf_keystore, worker)]} }. |