Turnkey Auth Plug lets you protect any route in an Elixir/Phoenix App.


Keywords
auth0, authentication, elixir, jwt, phoenix, phoenix-framework
License
CNRI-Python-GPL-Compatible

Documentation

auth_plug

The Elixir Plug that seamlessly handles all your authentication/authorization needs.

GitHub Workflow Status codecov.io Hex.pm HitCount contributions welcome


Why? 🤷

Frustrated by the complexity and incomplete docs/tests in existing auth solutions, we built auth_plug to simplify our lives.

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.

true

What? 🔐

An Elixir Plug (HTTP Middleware) that a complete beginner can use to add auth to a Phoenix App and understand how it works.
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.

auth_plug protects any routes in your app that require authentication.

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:
https://auth-plug-example.herokuapp.com/admin

auth_plug_example

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


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

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:

dwyl-auth-app-api-key-setup

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

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

E.g: /lib/app_web/router.ex#L23-L29

Explanation

There are two parts to this code:

  1. Create a new pipeline called :auth which will execute the AuthPlug.
  2. Create a new scope where we pipe_through both the :browser and :auth pipelines.

This means that the "/admin" route is protected by AuthPlug.

Note: Ensure the route you are protecting works without AuthPlug. If in doubt simply comment out the line pipe_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.

admin-route


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


Try it: http://auth-plug-example.herokuapp.com/optional

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.

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

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
}

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:

github-settings-dependabot-secrets

Thanks to: @danielabar for her excellent/succinct post on this topic: danielabaron.me/blog/dependabot-secrets


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.