You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2020/04/01 14:31:11 UTC

[couchdb] 04/08: Feature - Add JWT support (#2648)

This is an automated email from the ASF dual-hosted git repository.

rnewson pushed a commit to branch backport-jwt-3.x
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 55f71c2c097701ba66057a6a84d755174a99d018
Author: Alexander Trauzzi <ac...@trauzzi.me>
AuthorDate: Thu Mar 19 05:43:47 2020 -0500

    Feature - Add JWT support (#2648)
    
    Add JWT Authentication Handler
    
    Co-authored-by: Robert Newson <rn...@apache.org>
    Co-authored-by: Joan Touzet <wo...@users.noreply.github.com>
---
 rel/overlay/etc/default.ini             | 10 +++++++++
 src/chttpd/src/chttpd_auth.erl          |  4 ++++
 src/couch/src/couch_httpd_auth.erl      | 26 ++++++++++++++++++++++
 test/elixir/test/config/test-config.ini |  2 +-
 test/elixir/test/jwtauth_test.exs       | 39 +++++++++++++++++++++++++++++++++
 5 files changed, 80 insertions(+), 1 deletion(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index d64b88f..79924e9 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -134,10 +134,20 @@ 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}
+; uncomment the next line to enable JWT authentication
+; authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
 
 ; prevent non-admins from accessing /_all_dbs
 ; 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
+
 [couch_peruser]
 ; If enabled, couch_peruser ensures that a private per-user database
 ; exists for each document in _users. These databases are writable only
diff --git a/src/chttpd/src/chttpd_auth.erl b/src/chttpd/src/chttpd_auth.erl
index 607f09a..1b6d16e 100644
--- a/src/chttpd/src/chttpd_auth.erl
+++ b/src/chttpd/src/chttpd_auth.erl
@@ -18,6 +18,7 @@
 -export([default_authentication_handler/1]).
 -export([cookie_authentication_handler/1]).
 -export([proxy_authentication_handler/1]).
+-export([jwt_authentication_handler/1]).
 -export([party_mode_handler/1]).
 
 -export([handle_session_req/1]).
@@ -51,6 +52,9 @@ cookie_authentication_handler(Req) ->
 proxy_authentication_handler(Req) ->
     couch_httpd_auth:proxy_authentication_handler(Req).
 
+jwt_authentication_handler(Req) ->
+    couch_httpd_auth:jwt_authentication_handler(Req).
+
 party_mode_handler(#httpd{method='POST', path_parts=[<<"_session">>]} = Req) ->
     % See #1947 - users should always be able to attempt a login
     Req#httpd{user_ctx=#user_ctx{}};
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 5e44503..58d90b1 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -31,6 +31,8 @@
 -export([cookie_auth_cookie/4, cookie_scheme/1]).
 -export([maybe_value/3]).
 
+-export([jwt_authentication_handler/1]).
+
 -import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
 
 -compile({no_auto_import,[integer_to_binary/1, integer_to_binary/2]}).
@@ -186,6 +188,30 @@ proxy_auth_user(Req) ->
             end
     end.
 
+jwt_authentication_handler(Req) ->
+    case {config:get("jwt_auth", "secret"), header_value(Req, "Authorization")} of
+        {Secret, "Bearer " ++ Jwt} when Secret /= undefined ->
+            RequiredClaims = get_configured_claims(),
+            AllowedAlgorithms = get_configured_algorithms(),
+            case jwtf:decode(?l2b(Jwt), [{alg, AllowedAlgorithms} | RequiredClaims], fun(_,_) -> Secret end) of
+                {ok, {Claims}} ->
+                    case lists:keyfind(<<"sub">>, 1, Claims) of
+                        false -> throw({unauthorized, <<"Token missing sub claim.">>});
+                        {_, User} -> Req#httpd{user_ctx=#user_ctx{
+                            name=User
+                        }}
+                    end;
+                {error, Reason} ->
+                    throw({unauthorized, Reason})
+            end;
+        {_, _} -> Req
+    end.
+
+get_configured_algorithms() ->
+    re:split(config:get("jwt_auth", "allowed_algorithms", "HS256"), "\s*,\s*", [{return, binary}]).
+
+get_configured_claims() ->
+    lists:usort(re:split(config:get("jwt_auth", "required_claims", ""), "\s*,\s*", [{return, binary}])).
 
 cookie_authentication_handler(Req) ->
     cookie_authentication_handler(Req, couch_auth_cache).
diff --git a/test/elixir/test/config/test-config.ini b/test/elixir/test/config/test-config.ini
index 72a13a7..1980139 100644
--- a/test/elixir/test/config/test-config.ini
+++ b/test/elixir/test/config/test-config.ini
@@ -1,2 +1,2 @@
 [chttpd]
-authentication_handlers = {chttpd_auth, proxy_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
+authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, proxy_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
diff --git a/test/elixir/test/jwtauth_test.exs b/test/elixir/test/jwtauth_test.exs
new file mode 100644
index 0000000..2e78ee9
--- /dev/null
+++ b/test/elixir/test/jwtauth_test.exs
@@ -0,0 +1,39 @@
+defmodule JwtAuthTest do
+  use CouchTestCase
+
+  @moduletag :authentication
+
+  test "jwt auth with secret", _context do
+
+    secret = "zxczxc12zxczxc12"
+
+    server_config = [
+      %{
+        :section => "jwt_auth",
+        :key => "secret",
+        :value => secret
+      }
+    ]
+
+    run_on_modified_server(server_config, fn ->
+      test_fun()
+    end)
+  end
+
+  def test_fun() do
+    resp = Couch.get("/_session",
+      headers: [authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjb3VjaEBhcGFjaGUub3JnIn0.KYHmGXWj0HNHzZCjfOfsIfZWdguEBSn31jUdDUA9118"]
+    )
+
+    assert resp.body["userCtx"]["name"] == "couch@apache.org"
+    assert resp.body["info"]["authenticated"] == "jwt"
+  end
+
+  test "jwt auth without secret", _context do
+
+    resp = Couch.get("/_session")
+
+    assert resp.body["userCtx"]["name"] == "adm"
+    assert resp.body["info"]["authenticated"] == "default"
+  end
+end