View Source Setting up Boruta OAuth/OpenID Connect provider
Boruta provides as its core all authorization business rules in order to handle the underlying authorization logic of OAuth and OpenID Connect. Then it provides a generator that helps to create required controllers, views, and templates as we will see.
1. Bootstrap the application
We will start by botsrapping a Phoenix web application with authentication capabilities provided by phx.gen.auth since OAuth and OpenID Connect specifications do not provide any recommendations about how to authenticate users. Instead, it provides all protocols required to authorize them to secure an HTTP service, with relative identity information in destination to the client brought by OpenID Connect core. Here we go, first bootstrapping the application:
~> mix phx.new boruta_example
Then the authentication
~> mix phx.gen.auth Accounts User users
We have now a web application in which we can log in. If you want to know more about those, have a look at phx.new and phx.gen.auth documentations.
In order to run the newly created application, you have to set up dependencies and the database. You'll find the development database configuration in config/dev.exs file, fill it with valid PostgreSQL credentials, you would be able to run
~> mix do deps.get, ecto.setup
Here is the application up and running, starting the webserver with mix phx.server you'll be able to visit http://localhost:4000 with your favorite browser.
2. Bootstrap Boruta
Once the application is up, we can go on to the authorization part. First, you can add the Boruta dependency in mix.exs
# mix.exs
  def deps do
  ...
      {:boruta_ssi, "~> 0.1"}
  ...
  endAfter that, you'll be able to generate controllers in order to expose Oauth and OpenID Connect core specifications endpoints and database migrations to persist required clients, scopes, and tokens needed by your newly created provider.
~> mix do deps.get, boruta.gen.migration, ecto.migrate, boruta.gen.controllers
It will print the remaining steps to have the provider up and running as described in the documentation. From there we will skip the testing part which uses Mox in order to mock Boruta and focus tests on the application layer.
3. Configure Boruta
As described in boruta.gen.controllers mix task output, you need to expose controller actions in router.ex as follow
# lib/boruta_example_web/router.ex
  scope "/oauth", BorutaExampleWeb.Oauth do
    pipe_through :api
    post "/revoke", RevokeController, :revoke
    post "/token", TokenController, :token
    post "/introspect", IntrospectController, :introspect
  end
  scope "/oauth", BorutaExampleWeb.Oauth do
    pipe_through [:browser, :fetch_current_user]
    get "/authorize", AuthorizeController, :authorize
  end
  scope "/openid", BorutaExampleWeb.Openid do
    pipe_through [:browser, :fetch_current_user]
    get "/authorize", AuthorizeController, :authorize
  endAnd give mandatory boruta configuration
# config/config.exs
config :boruta_ssi, Boruta.Oauth,
  repo: BorutaExample.Repo,
  issuer: "https://example.com"Here client credentials flow should be up. For user flows you need further configuration and implement Boruta.Oauth.ResourceOwners context.
4. User flows
In order to have user flows operational, you need to implement Boruta.Oauth.ResourceOwners context as described in Boruta README. Here it would look like
# lib/boruta_example/resource_owners.ex
defmodule BorutaExample.ResourceOwners do
  @behaviour Boruta.Oauth.ResourceOwners
  alias Boruta.Oauth.ResourceOwner
  alias MyApp.Accounts.User
  alias MyApp.Repo
  @impl Boruta.Oauth.ResourceOwners
  def get_by(username: username) do
    with %User{id: id, email: email, last_login_at: last_login_at} <- Repo.get_by(User, email: username) do
      {:ok, %ResourceOwner{sub: to_string(id), username: email, last_login_at: last_login_at}}
    else
      _ -> {:error, "User not found."}
    end
  end
  def get_by(sub: sub) do
    with %User{id: id, email: email, last_login_at: last_login_at} <- Repo.get_by(User, id: sub) do
      {:ok, %ResourceOwner{sub: to_string(id), username: email, last_login_at: last_login_at}}
    else
      _ -> {:error, "User not found."}
    end
  end
  @impl Boruta.Oauth.ResourceOwners
  def check_password(resource_owner, password) do
    user = Repo.get_by(User, id: resource_owner.sub)
    case User.valid_password?(user, password) do
      true -> :ok
      false -> {:error, "Invalid email or password."}
    end
  end
  @impl Boruta.Oauth.ResourceOwners
  def authorized_scopes(%ResourceOwner{}), do: []
  @impl Boruta.Oauth.ResourceOwners
  def claims(_resource_owner, _scope), do: %{}
endand inject it with the main configuration
# config/config.exs
config :boruta_ssi, Boruta.Oauth,
  repo: BorutaExample.Repo,
  contexts: [
    resource_owners: BorutaExample.ResourceOwners
  ]Last, you'll have to setup is the redirection in the OAuth authorize controller
# lib/boruta_example_web/controllers/oauth/authorize_controller.ex
...
  defp redirect_to_login(conn) do
    redirect(conn, to: Routes.user_session_path(conn, :new))
  endHere all OAuth flows should be up and running!
5. OpenID Connect
In order to setup OpenID Connect flows, you need to tweak phx.gen.auth in order to redirect to login after logging out
# lib/boruta_example_web/controllers/user_auth.ex:80
...
  def log_out_user(conn) do
    ...
    conn
    |> renew_session()
    |> delete_resp_cookie(@remember_me_cookie)
    |> redirect(to: Routes.user_session_path(conn, :new))
  endAnd set redirections in the OpenID authorize controller
# lib/boruta_example_web/controllers/openid/authorize_controller.ex
...
  defp redirect_to_login(conn) do
    redirect(conn, to: Routes.user_session_path(conn, :new))
  end
  defp log_out_user(conn) do
    UserAuth.log_out_user(conn)
  endLast, you need to store the last_login_at field of users to keep track of the timestamp when users log in
# priv/repo/migrations/<timestamp>_add_last_login_at_to_users.exs
defmodule BorutaExample.Repo.Migrations.AddLastLoginAtToUsers do
  use Ecto.Migration
  def change do
    alter table(:users) do
      add :last_login_at, :utc_datetime_usec
    end
  end
end
# lib/boruta_example/accounts.ex:223
...
  def update_last_login_at(user) do
    user |> User.login_changeset() |> Repo.update!()
  end
...
  def log_in_user(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    Accounts.update_last_login_at(user)
    ...
# lib/boruta_example/accounts/user.ex
...
  schema "users" do
    ...
    field :last_login_at, :utc_datetime_usec
    timestamps()
  end
...
   def login_changeset(user) do
     change(user, last_login_at: DateTime.utc_now())
   endHere we are! You have a basic OpenID Connect provider. You can now create a client as described here and start using it. Departing from there, you can use any OAuth/OpenID Connect client of your choice.
Hope you enjoyed the way so far. The process can definitely be improved at some points, all contributions of any kind will be very welcome.