Packages

Elixir Keycloak integration, based on plugs and macroses. Mostly compatible with phoenix apps

Current section

Files

Jump to
phx_keycloak lib phx_keycloak.ex
Raw

lib/phx_keycloak.ex

defmodule PhxKeycloak do
@moduledoc """
# Settings
## Main module
First of all, we have to define main module, which will be responsible for auth by Keycloak
Required params:
* realm - realm ID
* site - endpoint Keycloak
* client_id - Keycloak client identity
* client_secret - Keycloak client secret token
* redirect_uri - callback for your app
Optional params:
* expected_group - Role (Group), which have access to application
```elixir
defmodule MyAppWeb.Keycloak do
use PhxKeycloak, realm: "cronos",
site: "http://localhost:8080",
client_id: "cronos_check_app",
client_secret: "7c9fb08e-2d11-4921-bc02-cc21c92b6139",
redirect_uri: "http://localhost:4000/callback",
expected_group: "app_admin"
end
```
## Plugs
After this we have next Plug's:
* MyAppWeb.Keycloak.Plugs.RefreshUserTokenPlug - token update and refresh
* MyAppWeb.Keycloak.Plugs.GetClaimsPlug - get claims about user
* MyAppWeb.Keycloak.Plugs.RedirectUnauthorizedUserToLoginPlug, redirect_route: "/login" - redirect to login page
## Controller
Define controller, for login and logout user:
```elixir
defmodule MyAppWeb.AuthorizeController do
use MyAppWeb, :controller
alias MyAppWeb.Keycloak
def login(conn, _) do
redirect(conn, external: Keycloak.authorize_url())
end
def logout(conn, _) do
refresh_token = get_session(conn, :refresh_token)
Keycloak.logout(refresh_token)
conn
|> clear_session()
|> redirect(to: "/")
end
def callback(conn, %{"code" => code}) do
{_access_token, refresh_token} = Keycloak.get_tokens_by_code(code)
conn
|> put_session(:refresh_token, refresh_token)
|> redirect(to: "/")
end
end
```
## Router
In MyAppWeb.Router add next pipeline's:
```elixir
...
pipeline :keycloak do
plug Keycloak.Plugs.RefreshUserTokenPlug
plug Keycloak.Plugs.GetClaimsPlug
end
pipeline :auth_or_redirect do
plug Keycloak.Plugs.RedirectUnauthorizedUserToLoginPlug, redirect_route: "/login"
end
...
```
Now, our router looks like this:
```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router
alias MyAppWeb.Keycloak
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :keycloak do
plug Keycloak.Plugs.RefreshUserTokenPlug, Keycloak
plug Keycloak.Plugs.GetClaimsPlug, Keycloak
end
pipeline :auth_or_redirect do
plug Keycloak.Plugs.RedirectUnauthorizedUserToLoginPlug, redirect_route: "/login"
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
pipe_through :keycloak
pipe_through :auth_or_redirect
# ...my_web_app_routes
end
scope "/", MyAppWeb do
pipe_through :browser
pipe_through :keycloak
get "/login", AuthorizeController, :login
post "/logout", AuthorizeController, :logout
get "/callback", AuthorizeController, :callback
end
end
```
"""
defmacro __using__(params) do
if is_nil(params[:site]), do: raise("site not set")
if is_nil(params[:realm]), do: raise("realm not set")
if is_nil(params[:client_id]), do: raise("client_id not set")
if is_nil(params[:redirect_uri]), do: raise("redirect_uri not set")
if is_nil(params[:client_secret]), do: raise("client_secret not set")
quote do
def refresh_token(refresh_token) do
PhxKeycloak.UseCases.KeycloakRefreshToken.call(refresh_token, Map.new(unquote(params)))
end
def logout(refresh_token) do
PhxKeycloak.UseCases.KeycloakLogout.call(refresh_token, Map.new(unquote(params)))
end
def get_user_claims(refresh_token) do
PhxKeycloak.UseCases.KeycloakUserClaims.call(refresh_token, Map.new(unquote(params)))
end
def get_tokens_by_code(code) do
PhxKeycloak.UseCases.KeycloakGetTokensByCode.call(code, Map.new(unquote(params)))
end
def authorize_url() do
PhxKeycloak.UseCases.KeycloakAuthorizeUrl.call(Map.new(unquote(params)))
end
def expected_group() do
unquote(params[:expected_group])
end
defmodule Plugs.GetClaimsPlug do
@moduledoc false
import Plug.Conn
import Phoenix.Controller, only: [put_flash: 3]
require Logger
def init(keycloak_module) do
# initialize options
keycloak_module
end
def call(conn, keycloak_module) do
existing_refresh_token = get_session(conn, :refresh_token)
{access_token, refresh_token} = keycloak_module.refresh_token(existing_refresh_token)
claims = keycloak_module.get_user_claims(access_token)
groups = Access.get(claims, "groups")
if is_nil(groups) do
Logger.warn(
"Plugs.GetClaimsPlug: In Client settings enable Mappers -> Add builtin -> groups"
)
end
groups = groups || []
expected_group = keycloak_module.expected_group()
cond do
is_nil(claims) ->
Logger.warn("Plugs.GetClaimsPlug: nil claims, clearing session")
clear_session(conn)
%{} == claims && Map.keys(claims) == [] ->
Logger.warn("Plugs.GetClaimsPlug: empty claims, clearing session")
clear_session(conn)
expected_group != nil and expected_group not in groups ->
Logger.warn(
"Plugs.GetClaimsPlug: no expected_group in claims' groups, clearing session"
)
conn
|> clear_session()
|> put_flash(:error, "PhxKeycloak: User should have role ##{expected_group}")
true ->
conn
|> put_session(:refresh_token, refresh_token)
|> assign(:claims, claims)
end
end
end
defmodule Plugs.RefreshUserTokenPlug do
@moduledoc false
import Plug.Conn
def init(keycloak_module) do
# initialize options
keycloak_module
end
def call(conn, keycloak_module) do
existing_refresh_token = get_session(conn, :refresh_token)
{_access_token, refresh_token} = keycloak_module.refresh_token(existing_refresh_token)
conn
|> put_session(:refresh_token, refresh_token)
end
end
defmodule Plugs.RedirectUnauthorizedUserToLoginPlug do
@moduledoc """
Если пользователь не авторизован, то он будет переадресован на страницу авторизации
"""
def init(redirect_route: redirect_page) do
# initialize options
redirect_page
end
def call(conn, redirect_page) do
case conn.assigns[:claims] do
nil ->
conn
|> Phoenix.Controller.redirect(to: redirect_page)
|> Plug.Conn.halt()
_some_value ->
conn
end
end
end
end
end
end