summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Trauzzi <acj@trauzzi.me>2020-03-19 05:43:47 -0500
committerGitHub <noreply@github.com>2020-03-19 10:43:47 +0000
commit032934f3764c9e1ae2f8f359cf039349bf56cf86 (patch)
tree12b10362c8ea32307ec208d30f0dc0978838a731
parentf7bdc8c085fcb3cf7c4155adcb97b5675b5e2467 (diff)
downloadcouchdb-032934f3764c9e1ae2f8f359cf039349bf56cf86.tar.gz
Feature - Add JWT support (#2648)
Add JWT Authentication Handler Co-authored-by: Robert Newson <rnewson@apache.org> Co-authored-by: Joan Touzet <wohali@users.noreply.github.com>
-rw-r--r--rel/overlay/etc/default.ini10
-rw-r--r--src/chttpd/src/chttpd_auth.erl4
-rw-r--r--src/couch/src/couch_httpd_auth.erl26
-rw-r--r--test/elixir/test/config/test-config.ini2
-rw-r--r--test/elixir/test/jwtauth_test.exs39
5 files changed, 80 insertions, 1 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 2676ef530..82a56590f 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 607f09a8a..1b6d16eb3 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 43ecda958..7c55f390e 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 72a13a707..1980139d1 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 000000000..2e78ee989
--- /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