LDAP authentication with Phoenix

20 Sep 2016

Introduction

LDAP is mostly used by medium-to-large organi­zations to have one centralized place to store users and groups and to allow others internal systems to authenticate the users. If you want to build Phoenix applications that will work within an enterprise you will likely have to integrate with an existing LDAP server.

In this article I’m going to show you how you can authenticate and synchronize users to your Phoenix application.

Setup Phoenix

Let’s setup a new Phoenix project and a user model that we can use to demonstrate LDAP authentication / synchronization in Phoenix:

mix phoenix.new ldap_example
...
Fetch and install dependencies? [Yn] Y

cd ldap_example

mix ecto.create (configure your db in config/dev.exs if needed)

mix phoenix.gen.model User users username:string name:string email:string

mix ecto.migrate
Setup Guardian

To manage the authentication process I’m going to use Guardian which is one of the most popular authentication framework for use with Elixir. This article is not about Guardian so I’m not going to explain in detail what the code below does. If you’re new to Guardian and want to know more you should read the documentation for Guardian.

mix.exs:

defp deps do
  [
    {:guardian, "~> 0.12.0"}
  ]
end
mix.deps.get

config/config.exs:

config :guardian, Guardian,
  allowed_algos: ["HS512"], # optional
  verify_module: Guardian.JWT,  # optional
  issuer: "LdapExample",
  ttl: { 30, :days },
  verify_issuer: true, # optional
  secret_key: "NotSoSecretButWorksForADemo",
  serializer: LdapExample.GuardianSerializer

lib/ldap_example/guardian_serializer.ex:

defmodule LdapExample.GuardianSerializer do
  @behaviour Guardian.Serializer
  alias LdapExample.User
  alias LdapExample.Repo

  def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
  def for_token(_), do: { :error, "Unknown resource type" }

  def from_token("User:" <> id), do: { :ok, Repo.get(User, id) }
  def from_token(_), do: { :error, "Unknown resource type" }
end

web/router.ex:

defmodule LdapExample.Router do
  use LdapExample.Web, :router

  pipeline :browser_session do
    plug Guardian.Plug.VerifySession
    plug Guardian.Plug.LoadResource
  end

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  scope "/", LdapExample do
    pipe_through [:browser, :browser_session]

    get "/", PageController, :index
  end
end

web/controllers/page_controller.ex:

defmodule LdapExample.PageController do
  use LdapExample.Web, :controller
  plug Guardian.Plug.EnsureAuthenticated

  def index(conn, _params) do
    render conn, "index.html"
  end
end

This is the basic setup for Guardian that we need to get started. To verify that everything works you can run mix phoenix.server and point your browser at http://localhost:4000 and you should just see the message ‘Unauthenticated’. That’s because the PageController is now protected and we’re not logged in yet.

Setup Exldap

To connect to a LDAP server and authenticate users I’m going to use the Exldap library. Exldap is basically a thin wrapper for the eldap module in Erlang. To make everything a little bit easier we’re also going to use a public LDAP server with demo users from Forumsystems so we don’t have to spend time setting up our own LDAP server for testing.

mix.exs:

def deps do
  [{:exldap, "~> 0.3.4"}]
end

mix.exs:

def application do
  [applications: [:exldap]]
end

config/config.exs (use config.secret.exs in a real applications):

config :exldap, :settings,
  server: "ldap.forumsys.com",
  base: "dc=example,dc=com",
  port: 389,
  ssl: false,
  user_dn: "cn=read-only-admin,dc=example,dc=com",
  password: "password",
  search_timeout: 5_000

The code above will give us access to forumsys public LDAP server which have a few users setup: einstein, newton, galieleo, tesla, riemann, gauss, euler, euclid. All the users have the password password.

Develop our own LDAP module

Now we have finally come to the part where we will implement a module to communicate with the LDAP server:

lib/ldap_example/ldap.ex:

defmodule LdapExample.Ldap do

  def authenticate(uid, password) do
    {:ok, ldap_conn} = Exldap.open
    bind = "uid=#{uid},dc=example,dc=com"
    case Exldap.verify_credentials(ldap_conn, bind, password) do
      :ok -> :ok
      _ -> {:error, "Invalid username / password"}
    end
  end

  def get_by_uid(uid) do
    {:ok, ldap_conn} = Exldap.connect
    {:ok, search_results} = Exldap.search_field(ldap_conn, "uid", uid)
    case search_results do
      [] -> {:error, "Could not find user with uid #{uid}"}
      _ -> search_results |> Enum.fetch(0)
    end
  end

  def to_map(entry) do
    username = Exldap.search_attributes(entry, "uid")
    name = Exldap.search_attributes(entry, "cn")
    email = Exldap.search_attributes(entry, "mail")
    %{username: username, name: name, email: email}
  end
end

The authenticate function takes an uid and a password as arguments so we can authenticate the user. uid stands for user id in LDAP and is used as the computer system login name. The function opens a connection to the LDAP server and verifies the credentials.

The get_by_uid function is used to search for the object with a specified uid in LDAP. We’ll use this function later to synchronize username, name & email to our local PostgreSQL database. Even though we have the information in LDAP we probably want to have a local table with our users so we can have real database relationships with other tables in our application.

The to_map functions is just a helper function which transforms an ldap_entry to a map with more sane keys names that we use in our local database.

Setup the SessionController and templates

To authenticate users in Phoenix we need to create a very basic GUI and a Session controller to handle sign in and sign out scenarios.

web/controllers/session_controller.ex:

defmodule LdapExample.SessionController do
  use LdapExample.Web, :controller
  alias LdapExample.{User, Repo, Ldap}

  def new(conn, _params) do
    render conn, "new.html", changeset: User.login_changeset
  end

  def create(conn, %{"user" => params}) do
    username = params["username"]
    password = params["password"]
    case Ldap.authenticate(username, password) do
      :ok -> handle_sign_in(conn, username)
      _   -> handle_error(conn)
    end
  end

  defp handle_sign_in(conn, username) do
    {:ok, user} = insert_or_update_user(username)
    conn
    |> put_flash(:info, "Logged in.")
    |> Guardian.Plug.sign_in(user)
    |> redirect(to: page_path(conn, :index))
  end

  defp insert_or_update_user(username) do
    {:ok, ldap_entry} = Ldap.get_by_uid(username)
    user_attributes = Ldap.to_map(ldap_entry)
    user = Repo.get_by(User, username: username)
    changeset =
      case user do
        nil -> User.changeset(%User{}, user_attributes)
        _ -> User.changeset(user, user_attributes)
      end
    Repo.insert_or_update changeset
  end

  defp handle_error(conn) do
    conn
    |> put_flash(:error, "Wrong username or password")
    |> redirect(to: session_path(conn, :new))
  end

  def delete(conn, _params) do
    Guardian.Plug.sign_out(conn)
    |> put_flash(:info, "Logged out successfully.")
    |> redirect(to: "/")
  end
end

The interesting parts in SessionController happens in the create, handle_sign_in and insert_or_update functions. In the create function we just authenticate the user with username / password using our own Ldap module. If the user is authenticated in LDAP we continue to the handle_sign_in function and calls the insert_or_update_user function. That function just gets the user attributes from LDAP and creates a map that we can use when we create an Ecto changeset. The changeset deals with all the details and determines if we need to insert the user (first time sign in) or just update it. The user will only be updated if the attributes in LDAP differs from the attributes in our local user table.

The user model need to be updated with a virtual field for the password and a special login_changeset that we use in the sign in form.

web/model/user.ex:

defmodule LdapExample.User do
  use LdapExample.Web, :model

  schema "users" do
    field :username, :string
    field :name, :string
    field :email, :string
    field :password, :string, virtual: true
    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:username, :name, :email])
    |> validate_required([:username, :name, :email])
  end

  def login_changeset do
    %__MODULE__{} |> cast(%{}, ~w(username password), ~w())
  end
end

web/templates/session/new.html.eex:

<h2>Sign in</h2>
<%= form_for @changeset, session_path(@conn, :create), [method: :post], fn f -> %>
  <div class="form-group">
    <label>Username</label>
    <%= text_input f, :username, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Password</label>
    <%= password_input f, :password, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= submit "Sign in", class: "btn btn-primary" %>
  </div>
<% end %>

web/views/session_view.ex:

defmodule LdapExample.SessionView do
  use LdapExample.Web, :view
end

web/router.ex

...
get "/sign_in", SessionController, :new
post "/sign_in", SessionController, :create
get "/sign_out", SessionController, :delete
...

And finally we need to add our SessionController to the router. Now you can try to start your Phoenix application and point your browser at http://localhost:4000/sign_in and try to login with einstein / password. You should now see the default Phoenix page and a message saying that you’re logged in. To sign out again you can just point your browser at http://localhost:4000/sign_out and you should see the Unauthenticated message again.

Conclusion

Implementing LDAP authentication and synchronization with Elixir / Phoenix was much more straight forward than I thought. The library support is already in place thanks to Guardian and Exldap and if you want dig deeper and implement more advanced things you can always fall back to the eldap module in Erlang which seems to have virtually everything you need to work with LDAP.

It also worth mentioning that there are another LDAP library in Elixir which integrates nicely with Ecto called EctoLdap.

Happy LDAPing!


comments powered by

Contact me

GithubTwitterLinkedinE-mail