Simple passwordless authentication for your Phoenix apps.
$ mix phx.new your_app
$ cd your_app
veil
to your list of dependencies in mix.exs
:def deps do
[{:veil, "~> 0.2"}]
end
$ mix deps.get
$ mix veil.add
config.exs
file to add an API key for your preferred email service (for more details on the email options, please refer to the Swoosh Documentation).config :veil, YourAppWeb.Veil.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: "your-api-key"
$ mix ecto.migrate
$ mix phx.server
If you click the sign-in link in the top right and enter your email address, you'll be sent an email with a sign-in button. Click this to re-open the website and you'll see you are now authenticated.
The full configuration section added to your config/config.exs
looks like this:
config :veil,
site_name: "Your Website Name",
email_from_name: "Your Name",
email_from_address: "yourname@example.com",
sign_in_link_expiry: 12 * 3_600, # How long should emailed sign-in links be valid for?
session_expiry: 86_400 * 30, # How long should sessions be valid for?
refresh_expiry_interval: 86_400, # How often should existing sessions be extended to session_expiry
sessions_cache_limit: 250, # How many recent sessions to keep in cache (to reduce database operations)
users_cache_limit: 100 # How many recent users to keep in cache
config :veil, YourApp.Veil.Scheduler,
jobs: [
# Runs every midnight to delete all expired requests and sessions
{"@daily", {YourApp.Veil.Clean, :expired, []}}
]
config :veil, YourAppWeb.Veil.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: "your-api-key"
You should move the third part of this to a file that is not under version control, or save your API key as an environment variable instead.
Most users choose insecure passwords, and for most use cases it's safer and easier to have them click a link in an email to authenticate themselves. An added advantage is that the lack of a password prevents the website leaking user passwords in the event of a data breach/hack. Furthermore, if a website offers a password reset link it is effectively just a more clumsy version of authentication by email.
The username/password paradigm is going to gradually die and the sooner the web can move past this for simple websites the better - Veil is my attempt to help speed this process up.
Virtually all of Veil's code is copied directly into your project when you run mix veil.add
, so you can customise it to your heart's content.
By default Plugs.Veil.UserId
is added to your default Router paths, which assigns the client's user_id to conn.assigns[:veil_user_id]
. Additionally Plugs.Veil.User
is also added, and assigns the full Veil.User
struct to conn.assigns[:veil_user]
. Veil.User
objects are cached so this doesn't require database requests each time. You can easily extend the Veil.User
struct to add additional fields, just make sure you use the Veil.update_user
function to make any updates, so that these are also updated in the cache.
Authentication can either be handled using scopes in the Router, or in your Controllers. On adding Veil to your project, it adds the following block to your Router. Paths in this block will only be accessible to logged in users.
# Add your routes that require authentication in this block.
# Alternatively, you can use the default block and authenticate in the controllers.
# See the Veil README for more.
scope "/", YourAppWeb do
pipe_through([:browser, YourAppWeb.Plugs.Veil.Authenticate])
end
To restrict access in controllers, you can add Plugs.Veil.Authenticate
like so:
defmodule YourAppWeb.Controller
use YourAppWeb, :controller
plug YourAppWeb.Plugs.Veil.Authenticate when action in [:new, :create]
# ...
This would restrict access to the new and create paths to logged in users only.
The sign-in/auth process works as follows:
User requests to sign-in using their email address. If they didn't previously have an account, one is created.
An email is then sent to the user with a sign-in link. The email and website response is identical whether or not they previously had an account - this is to avoid leaking information to attackers.
The link contains a Base32 encoded request id that is stored in a database by the server. When the user clicks the link, the server checks in the database to find out which user made the request, and how long it has been since they first tried to sign-in (default sign_in_link_expiry is 12 hours).
If the link hasn't expired, a new session is created for the user along with a new Base32 encoded session id, and this is saved to a cookie. The browser sends the session id to the server for each web request, and the server checks it hasn't expired. If it is older than the refresh_expiry_interval (default 1 day), then the session is extended until the session_expiry (default 30 days).
Request/Session ids are generated by concatenating together the following, calculating the SHA256 hash and then encoding to Base32. This should give sufficient entropy and length to be unguessable by an attacker.
If you have any suggestions or questions about this process, please let me know.
Veil works for Phoenix APIs as well, although in order for it to work seamlessly you would need to use it as the backend to a mobile app where you can deep link the sign-in link sent via email to cause the app to open on the client's mobile device. That app can then post the request id to the server and receive a session id in return. The session id can then be sent along with future API requests by the mobile app.
Note that by default Sendgrid changes links to redirect via their domain, so you would need to change mail adaptor or set up whitelabelling in order for deep links to work.
If your phoenix app was created with the --no-html
flag, then Veil will install it's own no-html
version.
$ curl --data "email=you@domain.com" http://localhost:4000/veil/users/
{"ok":true}
Email is sent containing the following URL: http://localhost:4000/veil/sessions/new/DK6VNMHPNRVDVEFLVTYAGXKSHBXEIXK5WTY7O2RG6RC3STDQLAZA====
Your mobile app intercepts this URL after the user taps it, strips out the request id and then posts it back to the server to receive a session id in return:
$ curl --data "request_id=DK6VNMHPNRVDVEFLVTYAGXKSHBXEIXK5WTY7O2RG6RC3STDQLAZA====" \
http://localhost:4000/veil/sessions/new/
{"session_id":"BICJZMZHQY3UOL7SVUJNHGBJYUI2ZSE47MV2T6NZZNQUJ3UHIPCQ===="}
The session id will then need to be sent for future requests to protected api routes.
Any thoughts on how this could be easily extended to work better for APIs out-of-the-box are welcome.
Use Pow, Coherence or Guardian.
Veil
is Copyright (c) 2018 Zander Khan
The source is released under the MIT License.
Check LICENSE for more information.