You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ro...@apache.org on 2023/02/20 15:10:10 UTC

[couchdb] branch main updated: Allow definition of JWT roles claim as comma-seperated list (#4431)

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

ronny pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/main by this push:
     new b38885114 Allow definition of JWT roles claim as comma-seperated list (#4431)
b38885114 is described below

commit b38885114414518c327bc411b46bdbf45c15a6d9
Author: Ronny Berndt <ro...@apache.org>
AuthorDate: Mon Feb 20 16:10:04 2023 +0100

    Allow definition of JWT roles claim as comma-seperated list (#4431)
    
    Now it is possible to define a JWT roles claim as a comma-seperated
    list or as a JSON array of strings (the only allowed old behavior).
---
 src/couch/src/couch_httpd_auth.erl        |  9 ++++++-
 src/docs/src/api/server/authn.rst         | 25 ++++++++++++++++++-
 src/docs/src/config/auth.rst              |  2 +-
 test/elixir/test/jwt_roles_claim_test.exs | 40 ++++++++++++++++++++++++++++++-
 4 files changed, 72 insertions(+), 4 deletions(-)

diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 4a7b217d1..89203bf4f 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -261,7 +261,7 @@ get_roles_claim(Claims) ->
     RolesClaimPath = config:get(
         "jwt_auth", "roles_claim_path"
     ),
-    Result =
+    Roles =
         case RolesClaimPath of
             undefined ->
                 couch_util:get_value(
@@ -284,6 +284,13 @@ get_roles_claim(Claims) ->
                 TokenizedJsonPath = tokenize_json_path(RolesClaimPath, MatchPositions),
                 couch_util:get_nested_json_value({Claims}, TokenizedJsonPath)
         end,
+    Result =
+        case is_list(Roles) of
+            true ->
+                Roles;
+            false ->
+                re:split(Roles, "\\s*,\\s*", [trim, {return, binary}])
+        end,
     case lists:all(fun erlang:is_binary/1, Result) of
         true ->
             Result;
diff --git a/src/docs/src/api/server/authn.rst b/src/docs/src/api/server/authn.rst
index bffe0bf27..982c931fd 100644
--- a/src/docs/src/api/server/authn.rst
+++ b/src/docs/src/api/server/authn.rst
@@ -397,9 +397,32 @@ is valid.
 You can set the user roles claim name through the config setting
 :config:option:`roles_claim_name <jwt_auth/roles_claim_name>`. If you don't set
 an explicit value, then ``_couchdb.roles`` will be set as the default claim name.
-If presented, as a JSON array of strings, it is used as the CouchDB user's roles
+If presented, it is used as the CouchDB user's roles
 list as long as the JWT token is valid.
 
+.. note::
+
+    Before CouchDB v3.3.2 it was only possible to define roles as a JSON
+    array of strings. Now you can also use a comma-seperated list to define
+    the user roles in your JWT token. The following declarations
+    are equal:
+
+    JSON array of strings:
+
+    .. code-block:: json
+
+        {
+            "_couchdb.roles": ["accounting-role", "view-role"]
+        }
+
+    JSON comma-seperated strings:
+
+    .. code-block:: json
+
+        {
+            "_couchdb.roles": "accounting-role, view-role"
+        }
+
 .. warning::
 
     ``roles_claim_name`` is deprecated in CouchDB 3.3, and will be removed later.
diff --git a/src/docs/src/config/auth.rst b/src/docs/src/config/auth.rst
index d43810054..87f181afe 100644
--- a/src/docs/src/config/auth.rst
+++ b/src/docs/src/config/auth.rst
@@ -398,7 +398,7 @@ Authentication Configuration
             ``roles_claim_name`` is deprecated in CouchDB 3.3, and will be removed later.
             Please migrate to ``roles_claim_path``.
 
-        If presented, as a JSON array of strings, it is used as the CouchDB user's roles
+        If presented, it is used as the CouchDB user's roles
         list as long as the JWT token is valid. The default value for ``roles_claim_name``
         is ``_couchdb.roles``.
 
diff --git a/test/elixir/test/jwt_roles_claim_test.exs b/test/elixir/test/jwt_roles_claim_test.exs
index cd23a3c25..28b280e9c 100644
--- a/test/elixir/test/jwt_roles_claim_test.exs
+++ b/test/elixir/test/jwt_roles_claim_test.exs
@@ -17,7 +17,15 @@ defmodule JwtRolesClaimTest do
       :value => ~w(
         NTNv7j0TuYARvmNMmWXo6fKvM4o6nv/aUi9ryX38ZH+L1bkrnD1ObOQ8JAUmHCBq7
         Iy7otZcyAagBLHVKvvYaIpmMuxmARQ97jUVG16Jkpkp1wXOPsrF9zwew6TpczyH
-        kHgX5EuLg2MeBuiT/qJACs1J0apruOOJCg/gOtkjB4c=) |> Enum.join()
+        kHgX5EuLg2MeBuiT/qJACs1J0apruOOJCg/gOtkjB4c=
+        ) |> Enum.join()
+    },
+    %{
+      :section => "jwt_keys",
+      :key => "hmac:myjwttestkey2",
+      :value => ~w(
+        VW5kb3VidGVkbHktRW5nYWdpbmctUm9hZHdheS0wMjk=
+       ) |> Enum.join()
     }
   ]
 
@@ -26,6 +34,7 @@ defmodule JwtRolesClaimTest do
 
     run_on_modified_server(server_config, fn ->
       test_roles(["_couchdb.roles_1", "_couchdb.roles_2"])
+      test_roles_as_string(["_couchdb_string.roles_1", "_couchdb_string.roles_2"])
     end)
   end
 
@@ -41,6 +50,7 @@ defmodule JwtRolesClaimTest do
 
     run_on_modified_server(server_config, fn ->
       test_roles(["my._couchdb.roles_1", "my._couchdb.roles_2"])
+      test_roles_as_string(["my._couchdb_string.roles_1", "my._couchdb_string.roles_2"])
     end)
   end
 
@@ -56,6 +66,7 @@ defmodule JwtRolesClaimTest do
 
     run_on_modified_server(server_config, fn ->
       test_roles(["my_nested_role_1", "my_nested_role_2"])
+      test_roles_as_string(["my_nested_string_role_1", "my_nested_string_role_2"])
     end)
   end
 
@@ -76,6 +87,7 @@ defmodule JwtRolesClaimTest do
 
     run_on_modified_server(server_config, fn ->
       test_roles(["my_nested_role_1", "my_nested_role_2"])
+      test_roles_as_string(["my_nested_string_role_1", "my_nested_string_role_2"])
     end)
   end
 
@@ -143,6 +155,32 @@ defmodule JwtRolesClaimTest do
     assert resp.body["info"]["authenticated"] == "jwt"
   end
 
+  def test_roles_as_string(roles) do
+      # Different token
+      token = ~w(
+        eyJ0eXAiOiJKV1QiLCJraWQiOiJteWp3dHRlc3RrZXkyIiwiYWxnIjoiSFMyNTYifQ.
+        eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWU
+        sImlhdCI6MTY1NTI5NTgxMCwiZXhwIjoxNzU1Mjk5NDEwLCJteSI6eyJuZXN0ZWQiOn
+        siX2NvdWNoZGIucm9sZXMiOiJteV9uZXN0ZWRfY291Y2hkYl9zdHJpbmcucm9sZXNfM
+        SwgbXlfbmVzdGVkX2NvdWNoZGJfc3RyaW5nLnJvbGVzXzEifX0sIl9jb3VjaGRiLnJv
+        bGVzIjoiX2NvdWNoZGJfc3RyaW5nLnJvbGVzXzEsX2NvdWNoZGJfc3RyaW5nLnJvbGV
+        zXzIiLCJteS5fY291Y2hkYi5yb2xlcyI6Im15Ll9jb3VjaGRiX3N0cmluZy5yb2xlc1
+        8xLCBteS5fY291Y2hkYl9zdHJpbmcucm9sZXNfMiIsImZvbyI6eyJiYXIuem9uayI6e
+        yJiYXouYnV1Ijp7ImJhYSI6eyJiYWEuYmVlIjp7InJvbGVzIjoibXlfbmVzdGVkX3N0
+        cmluZ19yb2xlXzEsIG15X25lc3RlZF9zdHJpbmdfcm9sZV8yIn19fX19fQ.rzaLmcA2
+        0R291XuGYNNTM9ypGL3UD_GlVp3DmBtWrZI
+        ) |> Enum.join()
+
+      resp =
+        Couch.get("/_session",
+          headers: [authorization: "Bearer #{token}"]
+        )
+
+      assert resp.body["userCtx"]["name"] == "1234567890"
+      assert resp.body["userCtx"]["roles"] == roles
+      assert resp.body["info"]["authenticated"] == "jwt"
+    end
+
   def test_roles_with_bad_input() do
     token = ~w(
       eyJ0eXAiOiJKV1QiLCJraWQiOiJteWp3dHRlc3RrZXkiLCJhbGciOiJIUzI1NiJ9.