Contents

Phoenix with GitHub OAuth Authentication

Phoenix with GitHub OAuth Authentication webp image

JOHN K THORNE @ Flickr

With the new year already here and all the new year’s resolutions already forgotten, we finally have some time to do something for our development as Programmers :) . The best way to expand horizons, imho, especially in the Functional Programming world is to look into other languages and new frameworks to see how things are solved in there and maybe get inspired or incorporate new tools into your toolbox.

With the rising popularity of Phoenix and Elixir (at least from what I saw in Stack Overflow Developers Survey for 2022) I have decided to give it a go.

Elixir is functional but at the same time dynamically typed language which feels really strange for a person programming in Scala for many years, nevertheless, it's fun - at least it was, for a couple of projects I have already created with it. If you were ever programming anything with Ruby On Rails (like me a few decades ago ;) ) you will very quickly realize that it's almost the same thing but done with a functional programming language under the hood and it just feels very good.

The Phoenix framework can be a valuable tool, definitely for simple microservices with CRUD functionality like we used to create with Play framework but possibly for many many more up to a full-blown production application, especially with its LiveView functionality - which will not be discussed in this tutorial.

The first issue you will probably encounter while digging into the framework and trying out new things is that the Phoenix is changing quite a lot and a lot of things are not working as expected when you follow some tutorials or courses online if it's a bit older than a year. I had this problem when trying to use the OAuth library with some 3rd party provider and this is why I have decided to write yet another blog post on OAuth with Phoenix. You can download the full source code for this project, replace a couple of tokens needed for GitHub integration and you are ready to roll. All the steps to integrate OAuth into your Phoenix application can be found below. Enjoy!

First Steps

1 Create a new project
mix phx.new oauth_tutorial

When asked to download dependencies, select ‘Y’

2 Modify config/dev.exs to set up credentials to your db server

# Configure your database
config :oauth_tutorial, OauthTutorial.Repo,
  username: "postgres",
  password: "your-password-goes-here",
  hostname: "localhost",
  database: "oauth_tutorial_dev",
  stacktrace: true,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

3 Add logic to store and manage users

mix phx.gen.schema Accounts.User users email:string:unique provider:string token:string

This command creates for us

  • a new context called accounts under lib/oauth_tutorial/accounts
  • User schema called user.ex under lib/oauth_tutorial/accounts/user.ex
  • Migration file with a timestamp of creation in priv/repo/migrations folder

Schema

defmodule OauthTutorial.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :provider, :string
    field :token, :string

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :provider, :token])
    |> validate_required([:email, :provider, :token])
    |> unique_constraint(:email)
  end
end

Our user schema contains all the field definitions we have passed as an argument to the Phoenix schema generator. Additionally, the framework adds a couple of fields automatically (created_at, updated_at) with the method timestamps() at the end of the schema definition, we can remove it of course, if we want, but it does no harm to have it, at least in this kind of a project.

Migration

defmodule OauthTutorial.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string
      add :provider, :string
      add :token, :string

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end

Phoenix framework uses the Ecto library to handle all database interactions, together with a migration mechanism which we can use to populate the database schema, modify existing tables, add indexes, etc.

Run the migration to populate the database and the single users’ table defined in the migration file we have created with the previous command.

mix ecto.create - create a database on our db server

mix ecto.migrate - runs the migrations specified in the migrations folder

Now you can log in to your database and see the tables populated for you, the users’ table contains additional fields generated from the timestamp() call in our schema.

users table

Auth Controller

Define a controller which will handle our authorization logic, allow us to log in and out of the application as well as receive any data from our third-party OAuth provider (in this case GitHub) as part of the standard OAuth authentication flow.

Before we start adding the actual controller code, we need to add a library to our project which will help us with OAuth authentication. The library is called Ueberauth and provides multiple integrations for many different providers. More details about the library itself can be found on the project’s GitHub pages here.

Add two dependencies inside deps function in mix.exs file:

{:ueberauth, "~> 0.7.0"},
{:ueberauth_github, "~> 0.8.1"}

Modify config file again and add two new entries at the bottom of the file:

config :ueberauth, Ueberauth,
      providers: [
        github: {Ueberauth.Strategy.Github, [default_scope: "user:email"]}
      ]
config :ueberauth, Ueberauth.Strategy.Github.OAuth,
 client_id: "your-client-id",
 client_secret: "your-client-secret"

The get the values for clientId and clientSecret you need to log in to your GitHub account, navigate to Settings -> Developer Settings (at the very bottom) -> OAuth Apps -> New OAuth App

Register a new OAuth app

oauth tutorial

Once the library was added and the configuration set up, we can execute another call with our friend mix to get and install the dependencies for us:

mix deps.get

Now it's time to create our controller and execute some basic tests to check if our integration is working as expected.

Create a new file called auth_controller.ex inside lib/oauth_tutorial_web/controllers directory.

The only thing we will add to this controller, for now, is the callback function to handle incoming data from GitHub about our authentication result. To use our Ueberauth library with our controller we need to plug Ueberauth and we are almost ready to roll.

defmodule OauthTutorialWeb.AuthController do
 use OauthTutorialWeb, :controller
 plug Ueberauth

 def callback(conn, params) do
   IO.inspect(conn)
   IO.inspect(params)
 end

end

For our controller to work though, we need to configure routing in a project router.ex file. We will add new scope for our authentication logic, and for now, will handle only 2 requests:

1 handled automatically by the Ueberauth library to redirect us to our provider
2 one to handle the callbacks we receive from the provider.

scope "/auth", OauthTutorialWeb do
 pipe_through :browser

 get "/:provider", AuthController, :request
 get "/:provider/callback", AuthController, :callback
end

You could already notice that all we did in our callback function was to inspect incoming data, this is not how Phoenix handles the requests in its controller methods usually so expect a big exception when this method returns, but you will be able to see in a console all the data received from GitHub after successful authentication.

Start up your application with mix phx.server and navigate to http://localhost:4000/auth/github, you should get automatically redirected to a GitHub authorization page where you can authorise a new app to use data from your account, once you authorise, Github will redirect the call to our callback method printing the values received as well as big fat exception about not handling the conn object appropriately - don’t worry about that for now, we will handle it soon.

Authorize oauth tutorial

Now it's time to preserve some data we receive from GitHub and create a new user inside our database.

Storing the data

storing the data

We will follow the steps described in the diagram above. First, we extract the information we need to have to create a new user in the database. For this purpose we will pattern match on the argument list to our callback function:

def callback(%{assigns: %{ueberauth_auth: auth}} = conn, params) do
  user_data = %{token: auth.credentials.token, email: auth.info.email, provider: "github"}
    ……
end

Having the data, we can try to either find the existing user by the email provided or create a new one if the user doesn’t exist. For this, we need to create a schema changeset and execute a couple of repo methods. Let’s create a method that will find or create an existing user given our map of parameters received with the callback:

defp findOrCreateUser(user_data) do
 changeset = User.changeset(%User{}, user_data)
 case Repo.get_by(User, email: changeset.changes.email) do
   nil ->
     IO.puts("User not found, creating new one")
     Repo.insert(changeset)
   user -> {:ok, user}
 end
end

Then we can plug this method and execute the rest of the logic inside the callback function, like so:

def callback(%{assigns: %{ueberauth_auth: auth}} = conn, params) do
 user_data = %{token: auth.credentials.token, email: auth.info.email, provider: "github"}
 case findOrCreateUser(user_data) do
   {:ok, user} ->
     conn
     |> put_flash(:info, "Welcome to OAuth Tutorial!")
     |> put_session(:user_id, user.id)
     |> redirect(to: "/")

   {:error, changeset} ->
     conn
     |> put_flash(:error, "Something went wrong")
     |> redirect(to: "/")
 end
end

Run your application once again with mix phx.server and you should see a flash message displayed on the main page.
phoenix framework

Log Out functionality

Being able to log in is one thing but once we are logged in we need the possibility to log out and clear our session. For this, we need to add a new controller method and link it to one of the endpoints of our choice in router.ex file.

def signout(conn, _params) do
 conn
 |> configure_session(drop: true)
 |> redirect(to: Routes.page_path(conn, :index))
end
get "/signout", AuthController, :signout

Please remember to add that route before the /:provider route, otherwise we will first hit the provider one and will try to interpret the word signout as a type of OAuth provider.

Widget

At this point, we have login and logout functionality and it will be nice to have it somewhere in a sharable part of the template where we can see the status of our session and execute the login/logout actions respectively.

We will display either Log In or Log Out links together with other links in the default template, in the top right corner of the header.

Although we haveuser_id in our session object it would be much better to have a user object which we can use to read its properties, set the avatar if needed based on the user’s email address, etc. For this purpose, we will use a nifty Phoenix utility called a plug. We will create a small plug that will be used as part of our request pipeline, the plug will check if the user_id exists in the session and will set the proper user object fetched from the database and assign it to our connection object so it will be available globally whenever needed.

defmodule OauthTutorial.Plugs.SetUser do
 import Plug.Conn
 import Phoenix.Controller

 alias OauthTutorial.Repo
 alias OauthTutorial.Accounts.User

 def init(opts), do: opts

 def call(conn, _opts) do
   user_id = get_session(conn, :user_id)
   cond do
     user = user_id && Repo.get(User, user_id)->
       assign(conn, :user, user)
     true ->
       assign(conn, :user, nil)
   end
 end
end

To use the plug, simply add it to the request pipeline in our router.ex file:

pipeline :browser do
 plug :accepts, ["html"]
 plug :fetch_session
 plug :fetch_live_flash
 plug :put_root_layout, {OauthTutorialWeb.LayoutView, :root}
 plug :protect_from_forgery
 plug :put_secure_browser_headers
 plug OauthTutorial.Plugs.SetUser
end

Finally, add additional list items to our list in the header. Header definition can be found inside oauth_tutorial_web/templates/layout/root.html.heex

<ul>
 <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
 <%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
   <li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
 <% end %>
 <%= if @conn.assigns[:user] do %>
 <li><%= link "Logout", to: Routes.auth_path(@conn, :signout) %></li>
 <% else %>
 <li>
   <%= link "Sign in with Github", to: Routes.auth_path(@conn, :request, "github") %>
 </li>
 <% end %>
</ul>

phoenix framework 2

Epic, at this point you should be able to log in and out with GitHub as your OAuth provider. In the last part of this tutorial, we are going to secure some resources from unauthorised access, in other words, the web resource you will be able to see only after you have logged in.

Protecting Resources

Let’s generate a to-do list functionality in our Phoenix application. With Phoenix, this is of course a couple of commands and we are all set:

mix phx.gen.html Todos Todo todos content:text user_id:references:users
Now we will modify a bit our schemas so we will let Ecto know that one user can have many to-do items and a single to-do item belongs to a specific user. To do that, modify the schemas accordingly:

User schema:

schema "users" do
 field :email, :string
 field :provider, :string
 field :token, :string

 has_many :todos, OauthTutorial.Todos.Todo

 timestamps()
end

Todo schema:

schema "todos" do
 field :content, :string

 belongs_to :user, OauthTutorial.Accounts.User

 timestamps()
end

Now, it's time to modify our router.ex and enable todos handling:

scope "/", OauthTutorialWeb do
 pipe_through :browser

 get "/", PageController, :index
 resources "/todos", TodoController
end

As you can see above, we are not specifying each method like GET/POST/PUT/DELETE separately but we use Phoenix’s built-in resources macro which will generate ‘Restful’ routes to a given resource for us.

Now you can open up our application, navigate to /todos and play around with Todo functionality.

listing todos

It’s time to secure our todos from unauthorised access. For this purpose, we will create another plug that will check if you are logged in, and if not it will redirect you to a home page with some appropriate flash message saying that you were not able to access the requested resource.

defmodule OauthTutorial.Plugs.RequireAuth do
 import Plug.Conn
 import Phoenix.Controller

 alias OauthTutorialWeb.Router.Helpers, as: Routes

 def init(_params) do

 end

 def call(conn, _params) do
   if conn.assigns[:user] do
     conn
   else
     conn
     |> put_flash(:error, "You are not authorized to access requested page.")
     |> redirect(to: Routes.page_path(conn, :index))
     |> halt()
   end
 end
end

This plug won’t be used in our router but directly in the controller where we specify explicitly which methods we want to secure - this way you can allow listing todos for all the users but prevent modifying the data to authorised users only etc.

Inside our newly created todo_controller.ex plug the require_auth like so:

plug OauthTutorial.Plugs.RequireAuth when action in [:index, :new, :create, :show, :edit, :update, :delete]

When you try to access our todos page without being logged in you should see the message about not authorised access like below:

phoenix framework access

For convenience, we will add a link to our todos page, together with other links on the right side of the header (replacing default Get Started link)

<li><%= link "Todos", to: Routes.todo_path(@conn, :index) %></li>

Final notes

This tutorial was rather long as I tried to explain it step by step in detail but basically adding OAuth functionality to your Phoenix application is just a couple of imports, another couple of changes in your configuration and a custom plug or two to integrate OAuth into your app.

Hope you liked it! Full source code is available at Softwaremill’s public GitHub repository here.

Blog Comments powered by Disqus.