An authentication framework for use with Elixir applications.
Guardian is based on similar ideas to Warden and Omniauth but is re-imagined for modern systems where Elixir manages the authentication requirements.
Guardian remains a functional system. It integrates with Plug, but can be used outside of it. If you're implementing a TCP/UDP protocol directly, or want to utilize your authentication via channels, Guardian is your friend.
The core currency of authentication in Guardian is JWT. You can use the JWT to authenticate web endpoints, channels, and TCP sockets and it can contain any authenticated assertions that the issuer wants to include.
Guardian relies on Joken. You'll need to install and configure Joken for your application.
Add Guardian to your application
mix.deps
defp deps do
[
# ...
{:guardian, "~> 0.6.2"}
# ...
]
end
config.exs
config :joken, config_module: Guardian.JWT
config :guardian, Guardian,
issuer: "MyApp",
ttl: { 30, :days },
verify_issuer: true,
secret_key: <guardian secret key>,
serializer: MyApp.GuardianSerializer
The serializer knows how to encode and decode your resource into and out of the token. A simple serializer:
defmodule MyApp.GuardianSerializer do
@behaviour Guardian.Serializer
alias MyApp.Repo
alias MyApp.User
def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
def for_token(_), do: { :error, "Unknown resource type" }
def from_token("User:" <> id), do: { :ok, Repo.get(User, id) }
def from_token(_), do: { :error, "Unknown resource type" }
end
Guardian ships with some plugs to help integrate into your application.
Looks for a token in the session. Useful for browser sessions. If one is not found, this does nothing.
Looks for a token in the Authorization header. Useful for apis. If one is not found, this does nothing.
Looks for a previously verified token. If one is found, continues, otherwise it
will call the :on_failure
function.
When you ensure a session, you must declare an error handler. This can be done as part of a pipeline or inside a pheonix controller.
defmodule MyApp.MyController do
use MyApp.Web, :controller
plug Guardian.Plug.EnsureAuthenticated, on_failure: { MyApp.MyController, :unauthenticated }
end
The failure function must receive the connection, and the connection params.
Looks for a previously verified token. If one is found, confirms that all listed permissions are present in the token. If not, the failure function is called.
defmodule MyApp.MyController do
use MyApp.Web, :controller
plug Guardian.Plug.EnsurePermissions, on_failure: { MyApp.MyController, :forbidden }, default: [:read, :write]
end
These plugs can be used to construct pipelines in Phoenix.
pipeline :browser_session do
plug Guardian.Plug.VerifySession
plug Guardian.Plug.LoadResource
end
pipeline :api do
plug :accepts, ["json"]
plug Guardian.Plug.VerifyHeader
plug Guardian.Plug.LoadResource
end
scope "/", MyApp do
pipe_through [:browser, :browser_session] # Use the default browser stack
# ...
end
scope "/api", MyApp.Api do
pipe_through [:api] # Use the default browser stack
end
From here, you can either EnsureAuthenticated in your pipeline, or on a per-controller basis.
defmodule MyApp.MyController do
use MyApp.Web, :controller
plug Guardian.Plug.EnsureAuthenticated, on_failure: { MyApp.MyHandler, :unauthenticated }
end
It's up to you how you verify the claims to encode into the token Guardian uses. As an example, here's the important parts of a SessionController
defmodule MyApp.SessionController do
use MyApp.Web, :controller
alias MyApp.User
alias MyApp.UserQuery
plug :scrub_params, "user" when action in [:create]
def create(conn, params = %{}) do
conn
|> put_flash(:info, "Logged in.")
|> Guardian.Plug.sign_in(verified_user) # verify your logged in resource
|> redirect(to: user_path(conn, :index))
end
def delete(conn, _params) do
Guardian.Plug.sign_out(conn)
|> put_flash(:info, "Logged out successfully.")
|> redirect(to: "/")
end
end
You can sign in with a resource (that the serializer knows about)
Guardian.Plug.sign_in(conn, user) # Sign in with the default storage
Guardian.Plug.sign_in(conn, user, :token, claims) # give some claims to use for the token jwt
Guardian.Plug.sign_in(conn, user, :token, key: :secret) # create a token in the :secret location
To attach permissions to the token, use the :perms
key and pass it a map.
Note. To add permissions, you should configure them in your guardian config.
Guardian.Plug.sign_in(conn, user, :token, perms: %{ default: [:read, :write], admin: [:all] })
Guardian.Plug.sign_in(conn, user, :token, key: :secret, perms: %{ default: [:read, :write], admin: [:all]}) # create a token in the :secret location
Guardian.Plug.sign_out(conn) # Sign out everything (clear session)
Guardian.Plug.sign_out(conn, :secret) # Clear the token and associated user from the 'secret' location
Access to the current resource, token and claims is useful. Note, you'll need to have run the VerifySession/Header for token and claim access, and LoadResource to access the resource.
Guardian.Plug.claims(conn) # Access the claims in the default location
Guardian.Plug.claims(conn, :secret) # Access the claims in the secret location
Guardian.Plug.current_token(conn) # access the token in the default location
Guardian.Plug.current_token(conn, :secret) # access the token in the secret location
For the resource
Guardian.Plug.current_resource(conn) # Access the loaded resource in the default location
Guardian.Plug.current_resource(conn, :secret) # Access the loaded resource in the secret location
There are many instances where Plug might not be in use. Channels, and raw sockets for e.g. If you need to do things your own way.
{ :ok, jwt, encoded_claims } = Guardian.encode_and_sign(resource, <token_type>, claims_map)
This will give you a new JWT to use with the claims ready to go. The token type is encoded into the JWT as the 'aud' field and is intended to be used as the type of token.
{ :ok, jwt, full_claims } = Guardian.encode_and_sign(resource, :token)
Add some permissions
{ :ok, jwt, full_claims } = Guardian.encode_and_sign(resource, :token, perms: %{ default: [:read, :write], admin: Guardian.Permissions.max})
Currently suggested token types are:
"token"
- Use for API or CORS access. These are basic tokens.
You can also customize the claims you're asserting.
claims = Guardian.Claims.app_claims
|> Dict.put(:some_claim, some_value)
|> Guardian.Claims.ttl({3, :days})
{ :ok, jwt, full_claims } = Guardian.encode_and_sign(resource, :token, claims)
To verify the token:
case Guardian.decode_and_verify(jwt) do
{ :ok, claims } -> do_things_with_claims(claims)
{ :error, reason } -> do_things_with_an_error(reason)
end
Accessing the resource from a set of claims:
case Guardian.serializer.from_token(claims.sub) do
{ :ok, resource } -> do_things_with_resource(resource)
{ :error, reason } -> do_things_without_a_resource(reason)
end
Guardian includes support for including permissions. Declare your permissions in your configuration. All known permissions must be included.
config :guardian, Guardian,
permissions: %{
default: [:read, :write],
admin: [:dashboard, :reconcile]
}
JWTs need to be kept reasonably small so that they can fit into an authorization
header. For this reason, permissions are encded as bits (an integer) in the
token. You can have up to 64 permissions per set, and as many sets as you like.
In the example above, we have the :default
set, and the :admin
set.
The bit value of the permissions within a set is determined by it's position in the config.
# Fetch permissions from the claims map
Guardian.Permissions.from_claims(claims, :default)
Guardian.Permissions.from_claims(claims, :admin)
# Check the permissions for all present
Guardian.Permissions.from_claims(claims, :default) |> Guardian.Permissions.all?([:read, :write], :default)
Guardian.Permissions.from_claims(claims, :admin) |> Guardian.Permissions.all?([:reconcile], :admin)
# Check for any permissions
Guardian.Permissions.from_claims(claims, :default) |> Guardian.Permissions.any?([:read, :write], :default)
Guardian.Permissions.from_claims(claims, :admin) |> Guardian.Permissions.any?([:reconcile, :dashboard], :admin)
You can use a plug to ensure permissions are present. See Guardian.Plug.EnsurePermissions
When you generate (or sign in) a token, you can inject permissions into it.
Guardian.encode_and_sign(resource, :token, perms: %{ admin: [:dashaboard], default: Guardian.Permissions.max}})
By setting a permission using Guardian.Permission.max you're setting all the bits, so even if new permissions are added, they will be set.
You can similarly pass a :perms
key to the sign_in method to have the
permissions encoded into the token.
Often you'll need to take action on some event within the lifecycle of authentication. Recording logins etc. Guardian provides hooks to allow you to do this. Use the Guardian.Hooks module to setup. Default implementations are available for all callbacks.
defmodule MyApp.GuardianHooks do
use Guardian.Hooks
def after_sign_in(conn, location) do
user = Guardian.Plug.current_resource(conn, location)
IO.puts("SIGNED INTO LOCATION WITH: #{user.email}")
conn
end
end
By default, JWTs are not tracked. This means that after 'logout' the token can still be used if it is stored outside the system. This is because Guardian does not track tokens and only interprates them live. When using Guardian in this way, be sure you consider the expiry time as this is one of the few options you have to make your tokens invalid.
If you want more control over this you should implement a hook that tracks the
tokens in some storage. When calling Guardian.revoke!
(called automatically
with sign_out).
To keep track of all tokens and ensure they're revoked on sign out you can use GuardianDb. This is a simple Guardian.Hooks module that implements database integration.
config :guardian, Guardian,
hooks: GuardianDb
config :guardian_db, GuardianDb, repo: MyRepo
Configure Guardian to know which module to use.
config :guardian, Guardian,
hooks: MyApp.GuardianHooks,
#…
Guardian uses JWTs to make the integration of authentication management as seamless as possible. Channel integration is part of that.
defmodule MyApp.UsersChannel do
use Phoenix.Channel
use Guardian.Channel
def join(_room, %{ claims: claims, resource: resource }, socket) do
{ :ok, %{ message: "Joined" }, socket }
end
def join(room, _, socket) do
{ :error, :authentication_required }
end
def handle_in("ping", _payload, socket) do
user = Guardian.Channel.current_resource(socket)
broadcast socket, "pong", %{ message: "pong", from: user.email }
{ :noreply, socket }
end
end
Guardian picks up on joins that have been made and automatically verifies the token and makes available the claims and resource making the request.
let socket = new Socket("/ws");
socket.connect();
let guardianToken = jQuery('meta[name="guardian_token"]').attr('content');
let chan = socket.chan("pings", { guardian_token: guardianToken });
How to get the tokens onto the page?
<%= if Guardian.Plug.current_token(@conn) do %>
<meta name='guardian_token' content="<%= Guardian.Plug.current_token(@conn) %>">
<% end %>
Many thanks to Sonny Scroggin (@scrogson) for the name Guardian and great feedback to get up and running.
- Flexible serialization
- Integration with Plug
- Basic integrations like raw TCP
- Sevice2Service credentials. That is, pass the authentication results through many downstream requests.
- [-] Create a "csrf" token type that ensures that CSRF protection is included
- Integration with Phoenix channels
- Integrated permission sets
- Hooks into the authentication cycle
- Revoke tokens
- Refresh tokens
- Two-factor authentication
- Single sign-in
- Device specific signing (can be implemented via custom claims atm)