summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Newson <rnewson@apache.org>2020-03-20 12:32:16 +0000
committerRobert Newson <rnewson@apache.org>2020-03-22 23:16:29 +0000
commitc1e7c5ac2c754a342fb5fd7dc6473c1630ce422c (patch)
treeac56457ffafeebcfcd43d8cd3dfe5c799b607922
parent623ae9acbed5f60244cde30fc969e0ffb2792abf (diff)
downloadcouchdb-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.erl15
-rw-r--r--src/jwtf/src/jwtf.app.src2
-rw-r--r--src/jwtf/src/jwtf_app.erl28
-rw-r--r--src/jwtf/src/jwtf_keystore.erl118
-rw-r--r--src/jwtf/src/jwtf_sup.erl38
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)]} }.