Sentry
"Sentry provides a set of helpers and conventions that will guide you in leveraging Elixir modules to build a simple, robust authorization system." - Inspired by elabs/pundit
TODOs
- Generators
- JWT
Installation
Add sentry to your list of dependencies in mix.exs:
def deps do
[{:sentry, "~> 0.2"}]
endEnsure sentry is started before your application:
def application do
[applications: [:sentry]]
end
Ensure your User model and users table has the following fields:
:encrypted_passwordfield-
a user identification field. Defaults to
email -
and a virtual password field. Defaults to
password
# web/models/user.ex
defmodule MyApp.User do
use MyApp.Web, :model
schema "users" do
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
...
timestamps
end
end
Configure Ueberauth and Sentry in config/config.exs
# config/config.exs
# Ueberauth
config :ueberauth, Ueberauth,
providers: [
identity: {Ueberauth.Strategy.Identity, [
callback_methods: ["POST"]
]}
]
# Sentry
config :sentry, Sentry,
repo: MyApp.Repo,
model: MyApp.User # you may use a different model as you like
# uid_field: :some_id_field \\ defaults to :email
# password_field: :some_pw_field \\ defaults to :passwordAuthentication
Sentry leverages ueberauth for the authentication layer with an addition of bcrypt encryption when storing and authenticating user.
Sentry provides useful helpers for working with users on your system
Sentry.Authenticator.attempt/1Sentry.Authenticator.logout/1Sentry.Authenticator.encrypt_password/1Sentry.Authenticator.logged_in?/1Sentry.Authenticator.current_user/1
here's an example use case
# web/controllers/auth_controller.ex
defmodule MyApp.AuthController do
use MyApp.Web, :controller
use Sentry, :authenticator
alias MyApp.User
def request(conn, %{"provider" => "identity"} = _params) do
render(conn, callback_url: callback_url(conn),
changeset: User.changeset(%User{}))
end
def request(conn, _params) do
conn
|> put_status(:not_found)
|> render(MyApp.ErrorView, "404.html")
end
def callback(%{ assigns: %{ ueberauth_failure: fails } } = conn, _) do
conn
|> put_flash(:error, "Failed to authenticate.")
|> redirect(to: "/")
end
def callback(conn, %{"provider" => "identity"}) do
# attempts to authorize user
# once authorized you can then access the user info with
# `Sentry.Authenticator.current_user/1`
# or check if logged in with `Sentry.Authenticator.logged_in?/1`
case attempt(conn) do
{:ok, conn} ->
conn
|> put_flash(:info, "You've successfully logged in")
|> redirect(to: "/")
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: request_path(conn))
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "You've been logged out!")
|> logout()
|> redirect(to: "/")
end
end# web/router.ex
defmodule MyApp.Router do
...
pipeline :auth do
plug Ueberauth
end
scope "/auth", MyApp do
pipe_through [:browser, :auth]
get "/logout", AuthController, :delete
get "/:provider", AuthController, :request
get "/:provider/callback", AuthController, :callback
post "/:provider/callback", AuthController, :callback
end
end# web/view/layout_view.ex
defmodule MyApp.LayoutView do
use MyApp.Web, :view
# this adds the logged_in?/1 and current_user/1 helper to the view
use Sentry, :view
end# web/templates/auth/request.html.eex
<%= form_for @changeset, @callback_url, fn f -> %>
<%= if f.errors != [] do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below:</p>
<ul>
<%= for {attr, message} <- f.errors do %>
<li><%= humanize(attr) %> <%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<label>Email</label>
<%= text_input f, :email, class: "form-control" %>
</div>
<div class="form-group">
<label>Password</label>
<%= password_input f, :password, class: "form-control" %>
</div>
<div class="form-group">
<%= submit "Login", class: "btn btn-primary" %>
</div>
<% end %>Authorization
For authorization, we have the following functions for dealing with it
Sentry.Authorizer.authorize/2Sentry.Authorizer.authorize_changeset/3Sentry.Authorizer.authorize!/2Sentry.Authorizer.authorize_changeset!/3
The bang functions follows Elixir's convention of throwing exceptions and returning values instead of returning tuples like {:ok, result} or {:error, reason}
# web/controllers/auth_controller.ex
defmodule MyApp.AuthController do
use MyApp.Web, :controller
use Sentry, :authenticator
alias MyApp.User
def request(conn, %{"provider" => "identity"} = _params) do
# you may pass an optional second parameter as
# a data that is then accessable on the `AuthPolicy.request/2` action
case authorize(conn) do
{:ok, conn} ->
render(conn, callback_url: callback_url(conn),
changeset: User.changeset(%User{}))
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: "/")
end
end
def request(conn, _params) do
conn
|> put_status(:not_found)
|> render(MyApp.ErrorView, "404.html")
end
def callback(%{ assigns: %{ ueberauth_failure: fails } } = conn, _) do
conn
|> put_flash(:error, "Failed to authenticate.")
|> redirect(to: "/")
end
def callback(conn, %{"provider" => "identity"}) do
# attempts to authorize user
# once authorized you can then access the user info with
# `Sentry.Authenticator.current_user/1`
# or check if logged in with `Sentry.Authenticator.logged_in?/1`
case attempt(conn) do
{:ok, conn} ->
conn
|> put_flash(:info, "You've successfully logged in")
|> redirect(to: "/")
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: request_path(conn))
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "You've been logged out!")
|> logout()
|> redirect(to: "/")
end
endor you can use it as a plug function or even cleaner module
defmod MyApp.AuthController do
...
plug :authorize_action when action in [:request]
def request(conn, _params) do
...
end
def authorize_action (conn, _options) do
case authorize(conn) do
{:ok, conn} ->
conn
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: "/")
end
end
end# web/policies/auth_policy.ex
defmodule MyApp.AuthPolicy do
use Sentry, :authenticator
def request(conn, options) do
# `logged_in?/1` is a helper function from
# `Sentry.Authenticator` that checks if a user is logged in
if logged_in?(conn) do
{:ok, conn}
else
{:error, "You're already logged in"}
end
end
endAuthorizing resource/changeset
If you are working on resource/changeset, you might want to use the Sentry.Authorizer.authorize_changeset/2 function as it will use a policy based on the model name
you can also use authorize_changeset/2 with plugs.
# web/controllers/user_controller.ex
defmodule PostController do
use Sentry, :authorizer
alias MyApp.Post
def update(conn, %{"id" => id, "post" => post_params}) do
changeset = Post.changeset(post, post_params)
# you can pass an optional third argument as an
# atom to override the function
# to be executed on the policy for example:
# authorize_changeset(conn, changeset, :belongs_to_current_user)
# this will instead run the
# `PostPolicy.belongs_to_current_user/2` action
authorize_changeset(conn, changeset)
end
end# web/policies/post_policy.ex
defmodule PostPolicy do
use Sentry, :authenticator
def update(conn, changeset) do
...
end
def belongs_to_current_user(conn,
%Ecto.Changeset{params: %{user_id: user_id}}) do
if current_user(conn).id === user_id do
{:ok, conn}
else
{:error, "You're not authorized to edit this post}
end
end
endLicense
Sentry is open-sourced software licensed under the MIT license