summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Newson <rnewson@apache.org>2020-03-19 19:06:23 +0000
committerRobert Newson <rnewson@apache.org>2020-03-22 23:16:29 +0000
commitdb21eda6f423e34944344ead346d63a4350918d4 (patch)
tree340fe13f77f00cb10ba55d3c0d3e7a4e95f3e1e2
parent5c77ef0b9cf3be98db3da692527e4c8726b2fc78 (diff)
downloadcouchdb-db21eda6f423e34944344ead346d63a4350918d4.tar.gz
support RSA for JWT auth
-rw-r--r--rel/overlay/etc/default.ini16
-rw-r--r--src/couch/src/couch_httpd_auth.erl21
-rw-r--r--test/elixir/test/jwtauth_test.exs44
3 files changed, 73 insertions, 8 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 82a56590f..25daa4813 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -141,12 +141,24 @@ max_db_number_for_dbs_info_req = 100
; admin_only_all_dbs = true
;[jwt_auth]
-; Symmetric secret to be used when checking JWT token signatures
-; secret =
; List of claims to validate
; required_claims = exp
; List of algorithms to accept during checks
; allowed_algorithms = HS256
+;
+; [jwt_keys]
+; Configure at least one key here if using the JWT auth handler.
+; If your JWT tokens do not include a "kid" attribute, use "_default"
+; as the config key, otherwise use the kid as the config key.
+; Examples
+; _default = aGVsbG8=
+; foo = aGVsbG8=
+; The config values can represent symmetric and asymmetrics keys.
+; For symmetrics keys, the value is base64 encoded;
+; _default = aGVsbG8= # base64-encoded form of "hello"
+; For asymmetric keys, the value is the PEM encoding of the public
+; key with newlines replaced with the escape sequence \n.
+; foo = -----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEDsr0lz/Dg3luarb+Kua0Wcj9WrfR23os\nwHzakglb8GhWRDn+oZT0Bt/26sX8uB4/ij9PEOLHPo+IHBtX4ELFFVr5GTzlqcJe\nyctaTDd1OOAPXYuc67EWtGZ3pDAzztRs\n-----END PUBLIC KEY-----\n\n
[couch_peruser]
; If enabled, couch_peruser ensures that a private per-user database
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 6b85a02cc..62fc694e1 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -189,11 +189,11 @@ proxy_auth_user(Req) ->
end.
jwt_authentication_handler(Req) ->
- case {config:get("jwt_auth", "secret"), header_value(Req, "Authorization")} of
- {Secret, "Bearer " ++ Jwt} when Secret /= undefined ->
+ case header_value(Req, "Authorization") of
+ "Bearer " ++ Jwt ->
RequiredClaims = get_configured_claims(),
AllowedAlgorithms = get_configured_algorithms(),
- case jwtf:decode(?l2b(Jwt), [{alg, AllowedAlgorithms} | RequiredClaims], fun(_,_) -> Secret end) of
+ case jwtf:decode(?l2b(Jwt), [{alg, AllowedAlgorithms} | RequiredClaims], fun jwt_keystore/2) of
{ok, {Claims}} ->
case lists:keyfind(<<"sub">>, 1, Claims) of
false -> throw({unauthorized, <<"Token missing sub claim.">>});
@@ -204,7 +204,7 @@ jwt_authentication_handler(Req) ->
{error, Reason} ->
throw({unauthorized, Reason})
end;
- {_, _} -> Req
+ _ -> Req
end.
get_configured_algorithms() ->
@@ -213,6 +213,19 @@ 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/test/elixir/test/jwtauth_test.exs b/test/elixir/test/jwtauth_test.exs
index aee14b3c5..6b3da9a71 100644
--- a/test/elixir/test/jwtauth_test.exs
+++ b/test/elixir/test/jwtauth_test.exs
@@ -9,8 +9,8 @@ defmodule JwtAuthTest do
server_config = [
%{
- :section => "jwt_auth",
- :key => "secret",
+ :section => "jwt_keys",
+ :key => "_default",
:value => secret
},
%{
@@ -25,8 +25,48 @@ defmodule JwtAuthTest do
run_on_modified_server(server_config, fn -> test_fun("HS512", secret) end)
end
+ defmodule RSA do
+ require Record
+ Record.defrecord :public, :RSAPublicKey,
+ Record.extract(:RSAPublicKey, from_lib: "public_key/include/public_key.hrl")
+ Record.defrecord :private, :RSAPrivateKey,
+ Record.extract(:RSAPrivateKey, from_lib: "public_key/include/public_key.hrl")
+ end
+
+ test "jwt auth with RSA secret", _context do
+ require JwtAuthTest.RSA
+
+ private_key = :public_key.generate_key({:rsa, 2048, 17})
+ public_key = RSA.public(
+ modulus: RSA.private(private_key, :modulus),
+ publicExponent: RSA.private(private_key, :publicExponent))
+
+ public_pem = :public_key.pem_encode(
+ [:public_key.pem_entry_encode(
+ :SubjectPublicKeyInfo, public_key)])
+ public_pem = String.replace(public_pem, "\n", "\\n")
+
+ server_config = [
+ %{
+ :section => "jwt_keys",
+ :key => "_default",
+ :value => public_pem
+ },
+ %{
+ :section => "jwt_auth",
+ :key => "allowed_algorithms",
+ :value => "RS256, RS384, RS512"
+ }
+ ]
+
+ run_on_modified_server(server_config, fn -> test_fun("RS256", private_key) end)
+ run_on_modified_server(server_config, fn -> test_fun("RS384", private_key) end)
+ run_on_modified_server(server_config, fn -> test_fun("RS512", private_key) end)
+ end
+
def test_fun(alg, key) do
{:ok, token} = :jwtf.encode({[{"alg", alg}, {"typ", "JWT"}]}, {[{"sub", "couch@apache.org"}]}, key)
+
resp = Couch.get("/_session",
headers: [authorization: "Bearer #{token}"]
)