In the last couple of posts I outlined some of the simple parts of Guardian. They covered browser login and api authentication. I wanted to continue along similar lines by going over how to use Guardian to embed permissions into your tokens.
Before we proceed
While permissions aren't strictly part of the JWT spec, they're also not against it. JWT allows for arbitrary data to be encoded into the token and Guardian uses this to encode permissions into the token under a specific key.
Most of the code I'm going to reference in this post is part of the Phoenix Guardian example app. It may change since the time of writing.
At the time of writing the version of Guardian is 0.9.0.
Declaring permissions
Permissions in Guardian are listed in the configuration. You can encode multiple permission sets, where the :default
set is the default. I usually enumerate the permissions in the config/config.exs file since they're going to be applicable for all your environments.
config :guardian, Guardian,
# …
permissions: %{
default: [
:read_token,
:revoke_token,
],
}
This enumerates a default list of permissions with two permissions included. Ultimately when used in a JWT these permissions will be encoded into an integer as a bit string so they're limited in number. To conform to JSON you have 32 bits only. Don't worry about running out though, Guardian allows you to declare multiple permission sets. Imagine that you also had a "profile" set of permissions. You'd declare those like:
config :guardian, Guardian,
# …
permissions: %{
default: [
:read_token,
:revoke_token,
],
profile: [
:full,
:update,
:read_settings,
:update_settings
]
}
You can add as many sets of permissions as you need to help organize your permissions and make sure you don't run out of them.
One thing to bear in mind, a JWT needs to be able to fit into the header of an HTTP request. That's partly the reason that we encode the permissions as an INT value. If you add too many the keys will start taking up a lot of space.
Now we've got some permissions, lets have a look at how to use them.
Making a token with permissions
When you sign in, either via api_sign_in
or sign_in
and even encode_and_sign
you're able to pass in a :perms
param that contains a Map of
<permission set>: <set of permissions>
- The permission set is the name of the permission set in the config
- The set of permissions can be
- A list of strings that appear in the declared list
- A list of atoms that appear in the declared list
- An integer of encoded permissions
- Guardian.Permissions.max
Guardian.Permissions.max
is a way to set all the bits so that all permissions in that set are granted, even if new ones are added.
So when you generate your token you can declare the permissions
def login(conn, params) do
user = # get your user somehow
conn
|> put_flash(:info, "Signed in as #{user.name}")
|> Guardian.Plug.sign_in(user, :token, perms: %{default: Guardian.Permissions.max})
|> redirect(to: private_page_path(conn, :index))
end
Notice here we've granted the max permissions for the default set. If we wanted to enumerate them we'd do it like:
def login(conn, params) do
user = # get your user somehow
conn
|> put_flash(:info, "Signed in as #{user.name}")
|> Guardian.Plug.sign_in(user, :token, perms: %{default: [:read_token, :revoke_token]})
|> redirect(to: private_page_path(conn, :index))
end
Remember though, if you do it this way any new permissions will not be present in the token.
Protecting endpoints with Plug
A lot of times it's possible to simply use a plug to prevent unauthorized access. To prevent access, in your controller declare a plug that requires the correct permissions.
plug EnsurePermissions, [handler: __MODULE__, default: ~w(read_token)] when action in [:index]
plug EnsurePermissions, [handler: __MODULE__, default: ~w(revoke_token)] when action in [:delete]
We use the Guardian.Plug.EnsurePermissions
plug to enforce permissions before it even gets to our action. I've aliased it in this controller. Like the EnsureAuthenticated
plug from our previous examples, the EnsurePermissions
plug takes a handler module. In this case our own controller. This means that we need to implement an unauthorized
function on our handler module.
def unauthorized(conn, _params) do
conn
|> put_flash(:error, "Unauthorized")
|> redirect(to: "/")
end
One thing to note. When using the plug, all permissions listed must be present.
For the simple case that's pretty much it. But if you need to get closer to the metal you can customize how you handle permissions.
Custom endpoint protection
Sometimes the plug may not be sufficient. For example if you have an OR condition in your permission requirements Guardians plug won't be able to help you. In those cases you can implement your own plug or just put it into your action.
Something like:
default_permissions = Guardian.Permissions.from_claims(claims, :default)
# From here we can check for permissions
Guardian.Permissions.all?(
default_permissions,
[:read_token, :revoke_token],
:default
)
# OR we can check if there are any permissions
Guardian.Permissions.any?(
default_permissions,
[:read_token, :revoke_token],
:default
)
The default you see here in the above refers to the name of the permission set.
You may need, from time to time to list out the encoded permissions, or, from a list of permissions - find the bit string value.
# To return a list of permissions that were encoded
Guardian.Permissions.to_list(default_permissions, :default)
# To get the encoded value of permissions from a list of permissions
Guardian.Permissions.to_value([:revoke_token], :default)