Awesome
<div align="center">auth_plug
The Elixir Plug that seamlessly handles all your authentication/authorization needs.
<!-- [![Libraries.io dependency status](https://img.shields.io/librariesio/release/hex/auth_plug?logoColor=brightgreen&style=flat-square)](https://libraries.io/hex/auth_plug) --> </div> <br />Why? 🤷
<!-- You want a way to add authentication to your Elixir/Phoenix App in the fewest steps and least code. We did too. So we built `auth_plug`. -->Frustrated by the complexity
and incomplete docs/tests
in existing auth solutions,
we built auth_plug
to simplify our lives. <br />
We needed a way to minimise
the steps
and code required
to add auth to our app(s).
With auth_plug
we can setup
auth in any Elixir/Phoenix
App in less than 2 minutes
with only 5 lines of config/code
and one environment variable.
What? 🔐
An Elixir Plug (HTTP Middleware)
that a complete beginner can use to add auth to a
Phoenix App
and understand how it works. <br />
No macros/behaviours to use
(confuse).
No complex configuration or "implementation".
Just a basic plug that uses Phoenix Sessions
and standards-based JSON Web Tokens (JWT).
Refreshingly simple. The way auth should be done.
Edit this diagram: docs.google.com/presentation/d/1PUKzbRQOEgHaOmaEheU7T3AHQhRT8mhGuqVKotEJkM0
</div>auth_plug
protects any routes in your app
that require authentication. <br />
auth_plug
is just
57 lines
of (significant)
code;
the rest is comprehensive comments
to help everyone understand how it works.
As with all our code,
it's meant to be as beginner-friendly as possible.
If you get stuck or have any questions,
please ask!
Who? 👥
We built this plug for use in our products/services. It does exactly what we want it to and nothing more. It's tested, documented and open source the way all our code is. It's not yet a general purpose auth solution that anyone can use. If after reading through this you feel that this is something you would like to have in your own Elixir/Phoenix project, tell us!
How? 💡
Before you attempt to use the auth_plug
,
try the Heroku example version so you know what to expect: <br />
https://auth-plug-example.herokuapp.com/admin
Notice how when you first visit the
auth-plug-example.herokuapp.com/admin
page, your browser is redirected to:
https://dwylauth.herokuapp.com/?referer=https://auth-plug-example.herokuapp.com/admin&auth_client_id=etc.
The auth service handles the actual authentication
and then transparently redirects back to
auth-plug-example.herokuapp.com/admin?jwt=etc.
with a JWT session.
For more detail on how the Auth
service works,
please see: https://github.com/dwyl/auth
If you get stuck during setup, clone and run our fully working example: https://github.com/dwyl/auth_plug_example#how
<br />1. Installation 📝
Add auth_plug
to your list of dependencies in mix.exs
:
def deps do
[
{:auth_plug, "~> 1.5"}
]
end
Once you've saved the mix.exs
file,
download the dependency with:
mix deps.get
<br />
2. Get Your AUTH_API_KEY
🔑
Visit:
https://authdemo.fly.dev/
and create a New App.
Once you have an App,
you can export an AUTH_API_KEY
environment variable.
e.g:
2.1 Save it as an Environment Variable
Create a file called .env
in the root directory of your app
and add the following line:
export AUTH_API_KEY=2cfxNaWUwJBq1F4nPndoEHZJ5Y/2cfxNadrhMZk3iaT1L5k6Wt67c9ScbGNP/dwylauth.herokuapp.com
The run the following command in your terminal:
source .env
That will export the environment variable AUTH_API_KEY.
Remember to add .env
to your .gitignore
file.
e.g:
echo ".env" >> .gitignore
<br />
3. Add AuthPlug
to Your router.ex
file to Protect a Route 🔒
Open the lib/app_web/router.ex
file and locate the section:
scope "/", AppWeb do
pipe_through :browser
get "/", PageController, :index
end
Immediately below this add the following lines of code:
pipeline :auth, do: plug(AuthPlug)
scope "/", AppWeb do
pipe_through :browser
pipe_through :auth
get "/admin", PageController, :admin
end
Explanation
There are two parts to this code:
- Create a new pipeline called
:auth
which will execute theAuthPlug
. - Create a new scope where we
pipe_through
both the:browser
and:auth
pipelines.
This means that the "/admin"
route is protected by AuthPlug
.
<br />Note: Ensure the route you are protecting works without
AuthPlug
. If in doubt simply comment out the linepipe_through :auth
to check.
4. Attempt to view the protected route to test the authentication! 👩💻
Now that the /admin
route is protected by auth_plug
,
attempt to view it in your browser e.g: http://localhost:4000/admin
If you are not already authenticated, your browser will be redirected to: https://dwylauth.herokuapp.com/?referer=http://localhost:4000/admin&auth_client_id=etc
Once you have successfully authenticated with your GitHub or Google account,
you will be redirected back to localhost:4000/admin
where the /admin
route will be visible.
That's it!! 🎉
You just setup auth in a Phoenix app using auth_plug
!
If you got stuck or have any questions, please open an issue, we are here to help!
Optional Auth
The use case shown above is protecting an endpoint that you don't want people to see if they haven't authenticated. If you're building an app that has routes where you want to show generic content to people who have not authenticated, but then show more detail/custom actions to people who have authenticated, that's where Optional Auth comes in.
To use optional auth it's even easier than required auth.
Open your lib/app_web/router.ex
file and add the following line
above the routes you want show optional data on:
pipeline :authoptional, do: plug(AuthPlugOptional, %{})
e.g:
/lib/app_web/router.ex#L13
Then add the following line to your main router scope:
pipe_through :authoptional
e.g:
/lib/app_web/router.ex#L17
That's it now you can check for conn.assigns.person
in your templates
and display relevant info/actions to the person if they are logged in.
<%= if Map.has_key?(@conn.assigns, :person) do %> Hello <%=
@conn.assigns.person.givenName %>! <% end %>
e.g:
/lib/app_web/templates/page/optional.html.eex#L2-L3
Using with LiveView
If you are using LiveView
,
having socket assigns with this info
is useful for conditional rendering.
For this, you can use the
assign_jwt_to_socket/3
function
to add the jwt
information to the socket,
e.g:
socket = socket
|> AuthPlug.assign_jwt_to_socket(&Phoenix.Component.assign_new/3, jwt)
This will add a person
object with information
about the authenticated person.
Here is the assigns should look like
after calling this function.
socket #=> #Phoenix.LiveView.Socket<
id: 123,
...
assigns: %{
__changed__: %{loggedin: true, person: true},
loggedin: true,
person: %{
aud: "Joken",
email: "person@dwyl.com",
exp: 1701020233,
iat: 1669483233,
iss: "Joken",
jti: "2slj49u49a3f3896l8000083",
nbf: 1669483233,
session: 1,
username: "username",
givenName: "Test Smith",
username: "dwyl_username"
}
},
transport_pid: nil,
...
>
Using with an API
Although you'll probably use this package
in scopes that are piped through
:browser
pipelines,
it can still be used in APIs -
using a pipeline that only accepts json
requests.
Like so:
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", AppWeb do
pipe_through [:api]
resources "/items", ItemController, only: [:create, :update]
end
You may create a pipeline using auth_plug
,
either be it for normal or optional authentication.
However, this pipeline
has to to use the fetch_session
plug
for auth_plug
to work.
For example,
if you wanted to pipe your API scope
inside your router.ex
file
with :authOptional
...
scope "/api", AppWeb do
pipe_through [:api, :authOptional]
resources "/items", ItemController, only: [:create, :update]
end
it should be defined as such:
pipeline :authOptional do
plug :fetch_session
plug(AuthPlugOptional)
end
Documentation
Function docs are available at: https://hexdocs.pm/auth_plug. <br />
As always, we attempt to comment our code as much as possible, but if anything is unclear, please open an issue: github.com/dwyl/auth_plug/issues
Development
If you want to contribute to this project, that's great!
Please ensure you create an issue to discuss your idea/plan before working on a feature/update to avoid any wasted effort.
Clone
git clone git@github.com:dwyl/auth_plug.git
Create a .env
file:
cp .env_sample .env
source .env
Run the test with coverage:
mix c
<br />
Available information
By default using the
auth
authentication service,
auth_plug
makes the following information available in conn.assigns
:
jwt :: string()
person :: %{
id :: integer() # This stays unique across providers
auth_provider :: string()
email :: string()
givenName :: string()
picture :: string()
# Also includes standard jwt metadata you may find useful:
aud, exp, iat, iss
}
<br />
Testing / CI
If you are using GitHub CI
to test your Auth Controller code
that invokes any of the AuthPlug.Token
functions -
and you should be ... -
then you will need to make the
AUTH_API_KEY
accessible to @dependabot
.
Visit: https://github.com/{org}/{project}/settings/secrets/dependabot
e.g: https://github.com/dwyl/mvp/settings/secrets/dependabot
And add the AUTH_API_KEY
environment variable,
e.g:
Thanks to: @danielabar for her excellent/succinct post on this topic: danielabaron.me/blog/dependabot-secrets
<br />Recommended / Relevant Reading
If you are new to Elixir Plug, we recommend following: github.com/dwyl/elixir-plug-tutorial.
To understand JSON Web Tokens, read: https://github.com/dwyl/learn-json-web-tokens.