pipeline status coverage report

Boruta OAuth/OpenID Connect provider core

Boruta is the core of an OAuth/OpenID Connect provider giving authentication and authorization business logic. a generator is provided to create phoenix controllers, views and templates.

It is intended to follow RFCs:

And specification from OpenID Connect:

This package is meant to help to provide OAuth 2.0/OpenID Connect to your applications implementing part or all of authorization code, implicit, hybrid, client credentials, or resource owner password credentials grants. It also helps introspecting and revoking tokens.

Documentation

Documentation can be found here

Live example

A live example can be found here

Setup

  1. Schemas migration

If you plan to use Boruta builtin clients and tokens contexts, you'll need a migration for its Ecto schemas. This can be done by running:

mix boruta.gen.migration

Note: You may need to run the task above in case of package upgrade to have up to date database schema.

  1. Implement ResourceOwners context (optional)

In order to have user flows operational, You need to implement Boruta.Oauth.ResourceOwners behaviour.

Here is an example implementation:

defmodule MyApp.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} <- Repo.get_by(User, email: username) do
      {:ok, %ResourceOwner{sub: id, username: email}}
    else
      _ -> {:error, "User not found."}
    end
  end
  def get_by(sub: sub) do
    with %User{id: id, email: email} = user <- Repo.get_by(User, id: sub) do
      {:ok, %ResourceOwner{sub: id, username: email}}
    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)
    User.check_password(user, password)
  end

  @impl Boruta.Oauth.ResourceOwners
  def authorized_scopes(%ResourceOwner{}), do: []
end
  1. Configuration

Boruta provides several configuration options that you can customize in config.exs. Those have following default values:

config :boruta, Boruta.Oauth,
  repo: MyApp.Repo, # mandatory
  cache_backend: Boruta.Cache,
  contexts: [
    access_tokens: Boruta.Ecto.AccessTokens,
    clients: Boruta.Ecto.Clients,
    codes: Boruta.Ecto.Codes,
    resource_owners: MyApp.ResourceOwners, # mandatory for user flows
    scopes: Boruta.Ecto.Scopes
  ],
  max_ttl: [
    authorization_code: 60,
    access_token: 60 * 60 * 24,
    id_token: 60 * 60 * 24,
    refresh_token: 60 * 60 * 24 * 30
  ],
  token_generator: Boruta.TokenGenerator

Integration

This implementation follows an inverted hexagonal architecture, dependencies are inverted from Application layer.

In order to expose endpoints of an OAuth/OpenID Connect server with Boruta, you need implement either the behaviour Boruta.Oauth.Application or the behaviours Boruta.Oauth.AuthorizeApplication, Boruta.Oauth.TokenApplication, Boruta.Oauth.IntrospectApplication and Boruta.Oauth.RevokeApplication to integrate these endpoints separatly. Those behaviours will help you creating callback functions which will be triggered by invoking token/2, authorize/2, introspect/2 and revoke/2 functions from Boruta.Oauth module.

A generator is provided to create phoenix controllers, views and templates needed to implement a basic OAuth/OpenID Connect server.

mix boruta.gen.controllers

This task will create needed files and give you a guide to finish your setup.

Migration from 1.X

Version 2 brings OpenID Connect, several changes were made in order to stick to the specification:

  • Boruta.Oauth.AuthorizeResponse and Boruta.Oauth.TokenResponse do not provide token value in value field but prefer giving value by token type code, access_token or id_token.
    %AuthorizeResponse{
     type: "code",
     value: value,
     expires_in: 60
    }
    becomes
    %AuthorizeResponse{
     type: :code,
     code: value,
     expires_in: 60
    }
  • boruta.gen.migration task has been updated. Running the task will upgrade database schemas according to the new associated Ecto.Schema

Straightforward testing

You can also create a client and test it

alias Boruta.Ecto
alias Boruta.Oauth.Authorization
alias Boruta.Oauth.{ClientCredentialsRequest, Token}

# create a client
{:ok, %Ecto.Client{id: client_id, secret: client_secret}} = Ecto.Admin.create_client(%{})
# obtain a token
{:ok, %Token{value: value}} = Authorization.token(%ClientCredentialsRequest{client_id: client_id, client_secret: client_secret})
# check token
{:ok, _token} = Authorization.AccessToken.authorize(value: value)

Guides

Here are some code samples helping the integration:

Feedback

It is a work in progress, all feedbacks / feature requests / improvements are welcome