Skip to content

Commit

Permalink
Expose abilities as jwt claims (#3240)
Browse files Browse the repository at this point in the history
* Expose abilities as jwt claims

* Improve claims destructuring

* Remove abilities from conn private attributes

* Fix activity logging related test

* Use abilities as claim only for access tokens

* Add necessary claims to access token during refresh

* Add missing assertion on access token creation

* Assert abilities are loaded for a user
  • Loading branch information
nelsonkopliku authored Jan 21, 2025
1 parent 2ab7045 commit a6dd300
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 17 deletions.
41 changes: 33 additions & 8 deletions lib/trento_web/plugs/app_jwt_auth_plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ defmodule TrentoWeb.Plugs.AppJWTAuthPlug do
"""
def fetch(conn, _config) do
with {:ok, jwt_token} <- read_token(conn),
{:ok, claims} <- validate_access_token(jwt_token) do
{:ok, %{"sub" => sub, "abilities" => abilities}} <- validate_access_token(jwt_token) do
conn =
conn
|> Conn.put_private(:api_access_token, jwt_token)
|> Conn.put_private(:user_id, claims["sub"])

{conn, %{"access_token" => jwt_token, "user_id" => claims["sub"]}}
|> Conn.put_private(:user_id, sub)

{conn,
%{
"access_token" => jwt_token,
"user_id" => sub,
"abilities" => abilities
}}
else
_ -> {conn, nil}
end
Expand All @@ -42,9 +47,12 @@ defmodule TrentoWeb.Plugs.AppJWTAuthPlug do
The generated credentials will be stored in private section of the Plug.Conn struct
"""
def create(conn, user, _config) do
claims = %{"sub" => user.id}
access_token = AccessToken.generate_access_token!(claims)
refresh_token = RefreshToken.generate_refresh_token!(claims)
{:ok, user} = Users.get_user(user.id)

{default_claims, access_token_claims} = token_claims(user)

access_token = AccessToken.generate_access_token!(access_token_claims)
refresh_token = RefreshToken.generate_refresh_token!(default_claims)

conn =
conn
Expand Down Expand Up @@ -102,7 +110,9 @@ defmodule TrentoWeb.Plugs.AppJWTAuthPlug do

defp attach_refresh_token_to_conn(conn, user) do
if user_allowed_to_renew?(user) do
new_access_token = AccessToken.generate_access_token!(%{"sub" => user.id})
{_, access_token_claims} = token_claims(user)

new_access_token = AccessToken.generate_access_token!(access_token_claims)

conn =
conn
Expand All @@ -120,4 +130,19 @@ defmodule TrentoWeb.Plugs.AppJWTAuthPlug do
do: false

defp user_allowed_to_renew?(%User{}), do: true

defp token_claims(%{id: id, abilities: abilities}) do
default_claims = %{
"sub" => id
}

access_token_claims =
Map.put(
default_claims,
"abilities",
Enum.map(abilities, &%{name: &1.name, resource: &1.resource})
)

{default_claims, access_token_claims}
end
end
11 changes: 11 additions & 0 deletions test/support/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,17 @@ defmodule Trento.Factory do
}
end

def user_with_abilities_factory do
user = build(:user)
ability = build(:ability)
build(:users_abilities, user_id: user.id, ability_id: ability.id)

%User{
user
| abilities: [ability]
}
end

def user_identity_factory do
%UserIdentity{
user_id: 1,
Expand Down
2 changes: 1 addition & 1 deletion test/trento/activity_logging/activity_logger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule Trento.ActivityLog.ActivityLoggerTest do
end

defp with_token(conn, user_id) do
jwt = AccessToken.generate_access_token!(%{"sub" => user_id})
jwt = AccessToken.generate_access_token!(%{"sub" => user_id, "abilities" => []})

Plug.Conn.put_req_header(conn, "authorization", "Bearer " <> jwt)
end
Expand Down
3 changes: 2 additions & 1 deletion test/trento_web/controllers/session_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ defmodule TrentoWeb.SessionControllerTest do
end
)

good_jwt = TrentoWeb.Auth.AccessToken.generate_access_token!(%{"sub" => user.id})
good_jwt =
TrentoWeb.Auth.AccessToken.generate_access_token!(%{"sub" => user.id, "abilities" => []})

resp =
conn
Expand Down
38 changes: 31 additions & 7 deletions test/trento_web/plugs/app_jwt_auth_plug_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule TrentoWeb.Plugs.AppJWTAuthPlugTest do
alias TrentoWeb.Plugs.AppJWTAuthPlug

import Mox
import Trento.Factory

@pow_config [otp_app: :trento]

Expand Down Expand Up @@ -132,33 +133,56 @@ defmodule TrentoWeb.Plugs.AppJWTAuthPlugTest do

describe "create/3" do
test "should add to the conn the access/refresh token pair and the expiration", %{conn: conn} do
user = %{id: 1}

assert {res_conn, ^user} = AppJWTAuthPlug.create(conn, user, @pow_config)
%{id: user_id, abilities: [%{name: name, resource: resource} | _]} =
user = insert(:user_with_abilities)

assert {
res_conn,
%{
id: ^user_id,
abilities: [%{name: ^name, resource: ^resource}]
}
} = AppJWTAuthPlug.create(conn, user, @pow_config)

assert %{
private: %{
api_access_token: _jwt,
api_access_token: jwt,
access_token_expiration: 180,
api_refresh_token: _refresh
}
} = res_conn

assert {:ok,
%{
"sub" => ^user_id,
"abilities" => [%{"name" => ^name, "resource" => ^resource}]
}} = AccessToken.verify_and_validate(jwt)
end
end

describe "fetch/2" do
test "should fetch a user when the jwt is valid", %{conn: conn} do
jwt = AccessToken.generate_access_token!(%{"sub" => 1})
jwt =
AccessToken.generate_access_token!(%{
"sub" => 1,
"abilities" => [%{name: "foo", resource: "bar"}]
})

conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer " <> jwt)

assert {res_conn,
%{
"access_token" => ^jwt,
"user_id" => 1
"user_id" => 1,
"abilities" => [%{"name" => "foo", "resource" => "bar"}]
}} = AppJWTAuthPlug.fetch(conn, @pow_config)

assert %{private: %{api_access_token: ^jwt, user_id: 1}} = res_conn
assert %{
private: %{
api_access_token: ^jwt,
user_id: 1
}
} = res_conn
end

test "should not fetch a user when the jwt signature is invalid", %{conn: conn} do
Expand Down

0 comments on commit a6dd300

Please sign in to comment.