Skip to content

Commit

Permalink
Allow definition of JWT roles claim as comma-seperated list (#4431)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
big-r81 committed Feb 20, 2023
1 parent 9f8cf48 commit b388851
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 4 deletions.
9 changes: 8 additions & 1 deletion src/couch/src/couch_httpd_auth.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
Expand Down
25 changes: 24 additions & 1 deletion src/docs/src/api/server/authn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/docs/src/config/auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.

Expand Down
40 changes: 39 additions & 1 deletion test/elixir/test/jwt_roles_claim_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
]

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit b388851

Please sign in to comment.