Skip to content

Commit

Permalink
Keycloak SSO (#2358)
Browse files Browse the repository at this point in the history
* chore(deps): install ueberauth_oidcc

* feat: replace Cognito with Keycloak

* feat: get group / role membership information from Keycloak

* fix: update fake Ueberauth strategy

* fixup! fix: update fake Ueberauth strategy

* refactor: use verified routes in fake strategy

* refactor: more verified routes

* feat: remove old auth retry logic

* feat: require membership in skate-readonly role

* feat: enable toggling Keycloak / Cognito SSO with test group

* fixup! feat: enable toggling Keycloak / Cognito SSO with test group
  • Loading branch information
lemald authored Feb 8, 2024
1 parent c071338 commit b5b4fef
Show file tree
Hide file tree
Showing 23 changed files with 259 additions and 45 deletions.
3 changes: 2 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ config :phoenix, :json_library, Jason
# Fake Cognito authentication
config :ueberauth, Ueberauth,
providers: [
cognito: nil
cognito: nil,
keycloak: nil
]

# Import environment specific config. This must remain at the bottom
Expand Down
14 changes: 13 additions & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,19 @@ config :ex_aws,

config :ueberauth, Ueberauth,
providers: [
cognito: {Skate.Ueberauth.Strategy.Fake, [groups: ["skate-dispatcher", "skate-admin"]]}
cognito: {Skate.Ueberauth.Strategy.Fake, [groups: ["skate-dispatcher", "skate-admin"]]},
keycloak:
{Skate.Ueberauth.Strategy.Fake,
[groups: ["skate-readonly", "skate-dispatcher", "skate-admin"]]}
]

config :ueberauth_oidcc,
providers: [
keycloak: [
issuer: :keycloak_issuer,
client_id: "dev-client",
client_secret: "fake-secret"
]
]

config :logger, level: :notice
Expand Down
6 changes: 4 additions & 2 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ config :logger, :console,
format: "$time $metadata[$level] node=$node $message\n",
metadata: [:request_id]

# Configure Ueberauth to use Cognito
# Configure Ueberauth to use Cognito / Keycloak
config :ueberauth, Ueberauth,
providers: [
cognito: {Ueberauth.Strategy.Cognito, []}
cognito: {Ueberauth.Strategy.Cognito, []},
keycloak:
{Ueberauth.Strategy.Oidcc, userinfo: true, uid_field: "email", scopes: ~w(openid email)}
]

config :ueberauth, Ueberauth.Strategy.Cognito,
Expand Down
17 changes: 17 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,21 @@ if config_env() == :prod do
sentry_frontend_dsn: System.fetch_env!("SENTRY_FRONTEND_DSN"),
sentry_org_slug: System.fetch_env!("SENTRY_ORG_SLUG")
end

keycloak_opts = [
issuer: :keycloak_issuer,
client_id: System.fetch_env!("KEYCLOAK_CLIENT_ID"),
client_secret: System.fetch_env!("KEYCLOAK_CLIENT_SECRET")
]

config :ueberauth_oidcc,
issuers: [
%{
name: :keycloak_issuer,
issuer: System.fetch_env!("KEYCLOAK_ISSUER")
}
],
providers: [
keycloak: keycloak_opts
]
end
12 changes: 11 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ config :skate, Oban, testing: :inline

config :ueberauth, Ueberauth,
providers: [
cognito: {Skate.Ueberauth.Strategy.Fake, [groups: ["skate-dispatcher", "skate-nav-beta"]]}
cognito: {Skate.Ueberauth.Strategy.Fake, [groups: ["skate-dispatcher", "skate-nav-beta"]]},
keycloak: {Skate.Ueberauth.Strategy.Fake, [groups: ["skate-dispatcher", "skate-nav-beta"]]}
]

config :ueberauth_oidcc,
providers: [
keycloak: [
issuer: :keycloak_issuer,
client_id: "test-client",
client_secret: "fake-secret"
]
]

config :logger, level: :warning
21 changes: 15 additions & 6 deletions lib/skate/ueberauth/strategy/fake.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ defmodule Skate.Ueberauth.Strategy.Fake do

use Ueberauth.Strategy, ignores_csrf_attack: true

use SkateWeb, :verified_routes

@impl Ueberauth.Strategy
def handle_request!(conn) do
conn
|> redirect!("/auth/cognito/callback")
|> redirect!(~p"/auth/keycloak/callback")
|> halt()
end

Expand All @@ -21,16 +23,15 @@ defmodule Skate.Ueberauth.Strategy.Fake do
end

@impl Ueberauth.Strategy
def credentials(conn) do
def credentials(_conn) do
nine_hours_in_seconds = 9 * 60 * 60
expiration_time = System.system_time(:second) + nine_hours_in_seconds

%Ueberauth.Auth.Credentials{
token: "fake_access_token",
refresh_token: "fake_refresh_token",
expires: true,
expires_at: expiration_time,
other: %{groups: Ueberauth.Strategy.Helpers.options(conn)[:groups]}
expires_at: expiration_time
}
end

Expand All @@ -40,8 +41,16 @@ defmodule Skate.Ueberauth.Strategy.Fake do
end

@impl Ueberauth.Strategy
def extra(_conn) do
%Ueberauth.Auth.Extra{raw_info: %{}}
def extra(conn) do
%Ueberauth.Auth.Extra{
raw_info: %UeberauthOidcc.RawInfo{
userinfo: %{
"resource_access" => %{
"dev-client" => %{"roles" => Ueberauth.Strategy.Helpers.options(conn)[:groups]}
}
}
}
}
end

@impl Ueberauth.Strategy
Expand Down
23 changes: 15 additions & 8 deletions lib/skate_web/auth_manager/error_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ defmodule SkateWeb.AuthManager.ErrorHandler do

@impl Guardian.Plug.ErrorHandler
def auth_error(conn, {_type, _reason}, _opts) do
auth_retries = get_session(conn, :auth_retries) || 3
keycloak_enabled? =
"keycloak-sso" in Enum.map(Skate.Settings.TestGroup.get_override_enabled(), & &1.name)

if auth_retries > 0 do
conn
|> put_session(:auth_retries, auth_retries - 1)
|> Phoenix.Controller.redirect(to: ~p"/auth/cognito")
if keycloak_enabled? do
Phoenix.Controller.redirect(conn, to: ~p"/auth/keycloak")
else
conn
|> delete_session(:auth_retries)
|> send_resp(:unauthorized, "unauthorized")
auth_retries = get_session(conn, :auth_retries) || 3

if auth_retries > 0 do
conn
|> put_session(:auth_retries, auth_retries - 1)
|> Phoenix.Controller.redirect(to: ~p"/auth/cognito")
else
conn
|> delete_session(:auth_retries)
|> send_resp(:unauthorized, "unauthorized")
end
end
end
end
38 changes: 36 additions & 2 deletions lib/skate_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,37 @@ defmodule SkateWeb.AuthController do
alias Skate.Settings.User
alias SkateWeb.AuthManager

def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
def callback(%{assigns: %{ueberauth_auth: %{provider: :keycloak} = auth}} = conn, _params) do
username = auth.uid
email = auth.info.email
credentials = auth.credentials
expiration = credentials.expires_at

keycloak_client_id =
get_in(Application.get_env(:ueberauth_oidcc, :providers), [:keycloak, :client_id])

groups =
get_in(auth.extra.raw_info.userinfo, ["resource_access", keycloak_client_id, "roles"]) || []

if "skate-readonly" in groups do
current_time = System.system_time(:second)

%{id: user_id} = User.upsert(username, email)

conn
|> Guardian.Plug.sign_in(
AuthManager,
%{id: user_id},
%{groups: groups},
ttl: {expiration - current_time, :seconds}
)
|> redirect(to: ~p"/")
else
send_resp(conn, :forbidden, "forbidden")
end
end

def callback(%{assigns: %{ueberauth_auth: %{provider: :cognito} = auth}} = conn, _params) do
username = auth.uid
email = auth.info.email
credentials = auth.credentials
Expand All @@ -27,7 +57,7 @@ defmodule SkateWeb.AuthController do
|> redirect(to: ~p"/")
end

def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
def callback(%{assigns: %{ueberauth_failure: %{provider: :cognito}}} = conn, _params) do
# Users are sometimes seeing unexpected Ueberauth failures of unknown provenance.
# Instead of sending a 403 unauthenticated response immediately, we are signing them out and
# sending them to the home page to start the auth path over again. -- MSS 2019-07-03
Expand All @@ -37,4 +67,8 @@ defmodule SkateWeb.AuthController do
|> Guardian.Plug.sign_out(AuthManager, [])
|> redirect(to: ~p"/")
end

def callback(%{assigns: %{ueberauth_failure: %{provider: :keycloak}}} = conn, _params) do
send_resp(conn, :unauthorized, "unauthenticated")
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ defmodule Skate.MixProject do
{:stream_data, "~> 0.6.0", only: :test},
{:timex, "~> 3.7.5"},
{:ueberauth_cognito, "~> 0.4.0"},
{:ueberauth_oidcc, "~> 0.3.1"},
{:ueberauth, "~> 0.10.5"}
]
end
Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"oban": {:hex, :oban, "2.17.1", "42d6221a1c17b63d81c19e3bad9ea82b59e39c47c1f9b7670ee33628569a449b", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c02686ada7979b00e259c0efbafeae2749f8209747b3460001fe695c5bdbeee6"},
"oidcc": {:hex, :oidcc, "3.1.1", "4016f35f08131053bddaae3bf644a6b3ce33cf8b297e6f46d638005b3e68d8fd", [:mix, :rebar3], [{:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "4401959a3674071345c7a8041f3962dfce68b588a9a270f927602dde10a801c4"},
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
Expand All @@ -71,10 +72,12 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
"ueberauth_cognito": {:hex, :ueberauth_cognito, "0.4.0", "62daa3f675298c2b03002d2e1b7e5a30cbc513400e5732a264864a26847e71ac", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.0", [hex: :jose, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "62378f4f34c8569cd95cc4e7463c56e9981c8afc83fdc516922065f0e1302a35"},
"ueberauth_oidcc": {:hex, :ueberauth_oidcc, "0.3.1", "599fe01f97dc97e6620de8c76b7c3b3a9ab4ebf797b4e995432d990b49311958", [:mix], [{:oidcc, "~> 3.1.0", [hex: :oidcc, repo: "hexpm", optional: false]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "f6fab7174cc79825cbbbb018a58bc9d18d3dc9237fa60cb9d985254454ca3943"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
Expand Down
19 changes: 14 additions & 5 deletions test/skate/ueberauth/strategy/fake_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,29 @@ defmodule Skate.Ueberauth.Strategy.FakeTest do

@tag :authenticated
test "credentials returns a credentials struct with groups specified in config", %{conn: conn} do
assert conn |> get("/auth/cognito") |> Fake.credentials() == %Credentials{
assert conn |> get(~p"/auth/keycloak") |> Fake.credentials() == %Credentials{
token: "fake_access_token",
refresh_token: "fake_refresh_token",
expires: true,
expires_at: System.system_time(:second) + 9 * 60 * 60,
other: %{groups: ["skate-dispatcher", "skate-nav-beta"]}
expires_at: System.system_time(:second) + 9 * 60 * 60
}
end

test "info returns an empty Info struct" do
assert Fake.info(%{}) == %Info{email: "[email protected]"}
end

test "extra returns an Extra struct with empty raw_info" do
assert Fake.extra(%{}) == %Extra{raw_info: %{}}
test "extra returns an Extra struct with group membership information", %{conn: conn} do
assert conn |> get(~p"/auth/keycloak") |> Fake.extra() == %Extra{
raw_info: %UeberauthOidcc.RawInfo{
userinfo: %{
"resource_access" => %{
"dev-client" => %{
"roles" => ["skate-dispatcher", "skate-nav-beta"]
}
}
}
}
}
end
end
13 changes: 13 additions & 0 deletions test/skate_web/auth_manager/error_handler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ defmodule SkateWeb.AuthManager.ErrorHandlerTest do
use SkateWeb.ConnCase

describe "auth_error/3" do
test "redirects to Keycloak login", %{conn: conn} do
{:ok, test_group} = Skate.Settings.TestGroup.create("keycloak-sso")

Skate.Settings.TestGroup.update(%{test_group | override: :enabled})

conn =
conn
|> init_test_session(%{username: "[email protected]"})
|> SkateWeb.AuthManager.ErrorHandler.auth_error({:some_type, :reason}, [])

assert response(conn, :found) =~ ~p"/auth/keycloak"
end

test "redirects to Cognito login with two remaining retries", %{conn: conn} do
conn =
conn
Expand Down
Loading

0 comments on commit b5b4fef

Please sign in to comment.