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, "~> 2.0"}
...
end
After 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
end
And give mandatory boruta configuration
# config/config.exs
config :boruta, 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: %{}
end
and inject it with the main configuration
# config/config.exs
config :boruta, 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))
end
Here 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))
end
And 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)
end
Last, 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())
end
Here 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.