Gamora
View SourceAmco OpenID Connect strategy for Überauth.
Installation
- Setup your application at Amco OIDC Provider. 
- Add - :gamorato your list of dependencies in- mix.exs:- def deps do [ {:gamora, "~> 0.18"} ] end
- Add Gamora to your Überauth configuration: - config :ueberauth, Ueberauth, providers: [ amco: {Gamora, []} ]
- Update your provider configuration: - Example 1: Using environment variables at compile time: - config :ueberauth, Gamora.OAuth, site: System.get_env("AMCO_IDP_URL"), client_id: System.get_env("AMCO_CLIENT_ID"), client_secret: System.get_env("AMCO_CLIENT_SECRET")- Example 2: Using environment variables from a runtime file: - config :ueberauth, Gamora.OAuth, site: {System, :get_env, ["AMCO_IDP_URL"]}, client_id: {System, :get_env, ["AMCO_CLIENT_ID"]}, client_secret: {System, :get_env, ["AMCO_CLIENT_SECRET"]}- Example 3: Using strings in a managed file at runtime: - config :ueberauth, Gamora.OAuth, site: "https://my_idp.example.com", client_id: "my client id", client_secret: "my client secret"
- Include the Überauth plug in your controller: - defmodule MyAppWeb.AuthController do use MyAppWeb, :controller plug Ueberauth ... end
- Create the request and callback routes if you haven't already: - scope "/auth", MyAppWeb do pipe_through :browser get "/logout", AuthController, :logout get "/:provider", AuthController, :request get "/:provider/callback", AuthController, :callback # If your app is a JSON API, you'll want to exchange the # authorization code using a POST request. post "/:provider/callback", AuthController, :callback end
- Your controller needs to implement callbacks to deal with - Ueberauth.Authand- Ueberauth.Failureresponses.
For an example implementation see the Überauth Example application.
Callbacks
Web-based applications
For web-based applications you should add the auth response to the session and redirect the user to the path you want. Your callbacks in the auth controller should look like this:
defmodule MyAppWeb.AuthController do
  use MyAppWeb, :controller
  plug Ueberauth
  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: redirect_path(conn))
  end
  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    conn
    |> put_flash(:info, "Successfully authenticated.")
    |> put_session(:access_token, auth.credentials.token)
    |> put_session(:refresh_token, auth.credentials.refresh_token)
    |> configure_session(renew: true)
    |> redirect(to: redirect_path(conn))
  end
  defp redirect_path(conn) do
    get_session(conn, :original_url) || "/"
  end
endJSON API applications
For JSON API applications you should return the access token, refresh token and id token to the native application that is consuming the API. Your callbacks in the auth controller should look like this:
defmodule MyAppWeb.AuthController do
  use MyAppWeb, :controller
  plug Ueberauth
  def callback(%{assigns: %{ueberauth_failure: fails}} = conn, _params) do
    conn
    |> put_status(:unauthorized)
    |> json(%{
      message: fails.message,
      message_key: fails.message_key
    })
  end
  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    conn
    |> json(%{
      access_token: auth.credentials.token,
      id_token: auth.credentials.other["id_token"],
      refresh_token: auth.credentials.refresh_token
    })
  end
  def logout(conn, _params) do
    conn
    |> put_session(:access_token, nil)
    |> put_session(:refresh_token, nil)
    |> redirect(to: ~p"/auth/amco?max_age=0")
  end
endProtected Routes
Protecting a route means that incoming requests should contain an
access token. That access token will be validated against the
Identity Provider to verify it has not expired and is still valid.
If the access token is valid, you will have the current user in the
conn.assigns[:current_user] based on the claims returned by de IdP.
Otherwise the error handler will be called and the connection must be
halted.
Web-based applications
Use the plug Gamora.Plugs.AuthenticatedUser in
your protected routes. This will get the access token from session
and validate it against the IDP (OIDC Identity Provider).
defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  pipeline :protected do
    plug Gamora.Plugs.AuthenticatedUser,
      error_handler: MyAppWeb.AuthenticationErrorHandler,
      access_token_source: :session
  end
  scope "/", MyAppWeb do
    pipe_through [:browser, :protected]
    # Add your protected routes here
  end
endAnd define your callbacks module in your application. It may look something like the following in a phoenix application:
defmodule MyAppWeb.AuthenticationErrorHandler do
  @behaviour Gamora.ErrorHandler
  import Plug.Conn, only: [halt: 1, put_session: 3]
  import Phoenix.Controller, only: [redirect: 2]
  @impl Gamora.ErrorHandler
  def access_token_error(conn, error) do
    conn
    |> put_session(:original_url, conn.request_path)
    |> redirect(to: "/auth/amco")
    |> halt()
  end
endJSON API applications
If your app requires json response you'll need to add access_token_source: :headers
to the plug options. It will get the access token from the request
header Authorization: Bearer <access_token>.
defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  pipeline :protected do
    plug Gamora.Plugs.AuthenticatedUser,
      error_handler: MyAppWeb.AuthenticationErrorHandler,
      access_token_source: :headers
  end
endAnd then update your ErrorHandler to response with a json. It may
look something like this:
defmodule MyAppWeb.AuthenticationErrorHandler do
  @behaviour Gamora.ErrorHandler
  import Plug.Conn
  import Phoenix.Controller
  @impl Gamora.ErrorHandler
  def access_token_error(conn, error) do
    conn
    |> put_status(:unauthorized)
    |> json(%{error: error})
    |> halt()
  end
endCalling
Depending on the configured url you can initiate the request through:
/auth/amcoOr with options:
/auth/amco?scope=email%20profile&strategy=phone_numberBy default the requested scope is openid profile email. Scope can be configured
either explicitly as a scope query value on the request path or in your
configuration:
config :ueberauth, Ueberauth,
  providers: [
    amco: {Gamora, [default_scope: "openid email phone"]}
  ]By default the strategy to be used is default. Strategy can be configured
either explicitly as a strategy query value on the request path or in your
configuration:
config :ueberauth, Ueberauth,
  providers: [
    amco: {Gamora, [default_strategy: "phone_number"]}
  ]By default the theme to be used is default. Theme can be configured
either explicitly as a theme query value on the request path or in your
configuration:
config :ueberauth, Ueberauth,
  providers: [
    amco: {Gamora, [default_theme: "dark_blue"]}
  ]By default the brand to be used is amco. Branding can be configured
either explicitly as a branding query value on the request path or in your
configuration:
config :ueberauth, Ueberauth,
  providers: [
    amco: {Gamora, [default_branding: "amco"]}
  ]By default IDP will allow users to create new accounts. Account creation
can be configured either explicitly as a allow_create query value on the
request path or in your configuration:
config :ueberauth, Ueberauth,
  providers: [
    amco: {Gamora, [default_allow_create: false]}
  ]By default IDP won't allow users to sign in with Amco badge. Amco badge
strategy can be configured either explicitly as a allow_amco_badge
query value on the request path or in your configuration:
config :ueberauth, Ueberauth,
  providers: [
    amco: {Gamora, [default_allow_amco_badge: true]}
  ]By default prompt is not present in the authorization url. Prompt can be
configured either explicitly as a prompt query value on the request
path or in your configuration:
config :ueberauth, Ueberauth,
  providers: [
    amco: {Gamora, [default_prompt: "login"]}
  ]To guard against client-side request modification, it's important to still
check the domain in info.urls[:website] within the Ueberauth.Auth struct
if you want to limit sign-in to a specific domain.
Caching
In order to avoid performing requests to the IDP on each request in the
application, it is possible to set a caching time for introspection and
userinfo endpoints. Make sure to not have a too long expiration time for
introspect_cache_expires_in but not too short to impact the application
performance, it is a balance.
config :ueberauth, Gamora.Plugs.AuthenticatedUser,
  introspect_cache_expires_in: :timer.seconds(0), # Default value
  userinfo_cache_expires_in: :timer.minutes(1)    # Default valueThen, add Gamora.Cache in lib/my_app/application.ex:
defmodule MyApp.Application do
  use Application
  @impl true
  def start(_type, _args) do
    children =
      [
        Gamora.Cache,
        ...
      ]
    ...Custom Cache Adapter
By default, Gamora uses Gamora.Cache which uses the Nebulex.Adapters.Local.
Custom Nebulex cache module can be used in your application passing the
cache_adapter configuration:
config :ueberauth, Gamora.Plugs.AuthenticatedUser,
  cache_adapter: MyApp.CacheTesting
In test environment you should avoid making requests to authenticate
users in protected routes. In order to do that, you can configure the
TestAdapter for the AuthenticatedUser plug in your config/test.exs:
config :ueberauth, Gamora.Plugs.AuthenticatedUser,
  adapter: Gamora.Plugs.AuthenticatedUser.TestAdapterCopyright and License
Copyright (c) 2022 Amco
Released under the MIT License, which can be found in the repository in LICENSE.