diff options
author | Robert Newson <rnewson@apache.org> | 2020-03-19 19:06:23 +0000 |
---|---|---|
committer | Robert Newson <rnewson@apache.org> | 2020-03-22 23:16:29 +0000 |
commit | db21eda6f423e34944344ead346d63a4350918d4 (patch) | |
tree | 340fe13f77f00cb10ba55d3c0d3e7a4e95f3e1e2 | |
parent | 5c77ef0b9cf3be98db3da692527e4c8726b2fc78 (diff) | |
download | couchdb-db21eda6f423e34944344ead346d63a4350918d4.tar.gz |
support RSA for JWT auth
-rw-r--r-- | rel/overlay/etc/default.ini | 16 | ||||
-rw-r--r-- | src/couch/src/couch_httpd_auth.erl | 21 | ||||
-rw-r--r-- | test/elixir/test/jwtauth_test.exs | 44 |
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}"] ) |