Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Keycloak Migration #1414

Closed
wants to merge 15 commits into from
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[
inputs: ["mix.exs", "lib/**/*.{ex,exs}", "test/**/*.{ex,exs}"],
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"],
rename_deprecated_at: "1.6.0"
]
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
elixir 1.14.0-otp-25
erlang 25.0.4
elixir 1.14.5-otp-26
erlang 26.0.2
nodejs 18.12.1
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# first, get the elixir dependencies within an elixir container
FROM hexpm/elixir:1.14.0-erlang-25.0.2-debian-buster-20210902-slim as elixir-builder
FROM hexpm/elixir:1.14.5-erlang-26.0.2-debian-buster-20240513-slim as elixir-builder
ENV LANG="C.UTF-8" MIX_ENV="prod"

WORKDIR /root
Expand Down Expand Up @@ -36,7 +36,7 @@ RUN mix do compile --force, phx.digest, release
FROM debian:buster

RUN apt-get update --allow-releaseinfo-change && \
apt-get install -y --no-install-recommends libssl1.1 libsctp1 curl && \
apt-get install -y --no-install-recommends libssl1.1 libsctp1 curl ca-certificates && \
rm -rf /var/lib/apt/lists/*

WORKDIR /root
Expand Down
12 changes: 10 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ config :signs_ui, SignsUiWeb.Endpoint,
# Internal configuration
config :signs_ui,
config_store: SignsUi.Config.S3,
refresh_token_store: SignsUi.RefreshTokenStore,
alert_producer: ServerSentEventStage,
alert_consumer_opts: [
name: SignsUi.Alerts.State,
Expand All @@ -32,7 +31,16 @@ config :logger,

config :ueberauth, Ueberauth,
providers: [
cognito: {SignsUi.Ueberauth.Strategy.Fake, []}
keycloak: {SignsUi.Ueberauth.Strategy.Fake, []}
]

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

config :phoenix, :json_library, Jason
Expand Down
10 changes: 2 additions & 8 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,10 @@ config :logger, level: :info

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

config :ueberauth, Ueberauth.Strategy.Cognito,
auth_domain: {System, :get_env, ["COGNITO_DOMAIN"]},
client_id: {System, :get_env, ["COGNITO_CLIENT_ID"]},
client_secret: {System, :get_env, ["COGNITO_CLIENT_SECRET"]},
user_pool_id: {System, :get_env, ["COGNITO_USER_POOL_ID"]},
aws_region: {System, :get_env, ["COGNITO_AWS_REGION"]}

# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
Expand Down
24 changes: 20 additions & 4 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@ import Config
if config_env() == :prod do
config :signs_ui, SignsUiWeb.Endpoint,
secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE")
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

screenplay_base_url =
if System.get_env("ENVIRONMENT_NAME") == "prod",
do: "https://screenplay.mbta.com/",
else: "localhost:4000/ https://screenplay-dev.mbtace.com/ https://screenplay-dev-green.mbtace.com/"
else:
"localhost:4000/ https://screenplay-dev.mbtace.com/ https://screenplay-dev-green.mbtace.com/"

config :signs_ui, SignsUiWeb.Endpoint,
screenplay_base_url: screenplay_base_url
config :signs_ui, SignsUiWeb.Endpoint, screenplay_base_url: screenplay_base_url
16 changes: 13 additions & 3 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@ config :signs_ui, SignsUiWeb.AuthManager,
issuer: "signs_ui",
secret_key: "test key"

config :ueberauth, Ueberauth.Strategy.Cognito,
auth_domain: "test_auth_domain",
client_id: "test_client_secret"
config :ueberauth, Ueberauth,
providers: [
keycloak: {SignsUi.Ueberauth.Strategy.Fake, [roles: []]}
]

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

# Print only warnings, errors, and info during test
config :logger, level: :info
1 change: 0 additions & 1 deletion lib/signs_ui/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ defmodule SignsUi.Application do
SignsUi.Config.Writer,
{SignsUi.Signs.State, [name: SignsUi.Signs.State]},
SignsUi.Config.Expiration,
SignsUi.RefreshTokenStore,
{Application.get_env(:signs_ui, :alert_producer),
name: AlertProducer,
url:
Expand Down
34 changes: 0 additions & 34 deletions lib/signs_ui/refresh_token_store.ex

This file was deleted.

19 changes: 13 additions & 6 deletions lib/signs_ui/ueberauth/strategy/fake.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
defmodule SignsUi.Ueberauth.Strategy.Fake do
@moduledoc """
Fake Ueberauth strategy that allows the app to run locally without using
Cognito.
Keycloak.
"""

use Ueberauth.Strategy
use Ueberauth.Strategy, ignores_csrf_attack: true

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

Expand All @@ -29,8 +29,7 @@ defmodule SignsUi.Ueberauth.Strategy.Fake do
token: "fake_access_token",
refresh_token: "fake_refresh_token",
expires: true,
expires_at: System.system_time(:second) + 60 * 60,
other: %{groups: ["signs-ui-admin"]}
expires_at: System.system_time(:second) + 60 * 60
}
end

Expand All @@ -41,7 +40,15 @@ defmodule SignsUi.Ueberauth.Strategy.Fake do

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

@impl Ueberauth.Strategy
Expand Down
7 changes: 3 additions & 4 deletions lib/signs_ui_web/auth_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ defmodule SignsUiWeb.AuthManager do
def resource_from_claims(_), do: {:error, :invalid_claims}

@spec claims_access_level(Guardian.Token.claims()) :: access_level()
def claims_access_level(%{"groups" => groups}) do
def claims_access_level(%{"roles" => roles}) when not is_nil(roles) do
cond do
is_nil(groups) -> :none
@signs_ui_read_only_group in groups -> :read_only
@signs_ui_admin_group in groups -> :admin
@signs_ui_read_only_group in roles -> :read_only
@signs_ui_admin_group in roles -> :admin
true -> :none
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/signs_ui_web/auth_manager/error_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule SignsUiWeb.AuthManager.ErrorHandler do
def auth_error(conn, {_type, _reason}, _opts) do
Phoenix.Controller.redirect(
conn,
to: SignsUiWeb.Router.Helpers.auth_path(conn, :request, "cognito")
to: SignsUiWeb.Router.Helpers.auth_path(conn, :request, "keycloak")
)
end
end
83 changes: 20 additions & 63 deletions lib/signs_ui_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,29 @@ defmodule SignsUiWeb.AuthController do
plug(Ueberauth)
require Logger

@spec callback(Plug.Conn.t(), map()) :: Plug.Conn.t()
alias Plug.Conn

@spec callback(Conn.t(), map()) :: Conn.t()
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
username = auth.uid
expiration = auth.credentials.expires_at
credentials = conn.assigns.ueberauth_auth.credentials

current_time = System.system_time(:second)

if credentials.refresh_token do
refresh_token_store = Application.get_env(:signs_ui, :refresh_token_store)
refresh_token_store.put_refresh_token(username, credentials.refresh_token)
end
keycloak_client_id =
get_in(Application.get_env(:ueberauth_oidcc, :providers), [:keycloak, :client_id])

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

conn
|> Guardian.Plug.sign_in(
SignsUiWeb.AuthManager,
username,
%{groups: credentials.other[:groups]},
%{roles: roles},
ttl: {expiration - current_time, :seconds}
)
|> Plug.Conn.put_session(:signs_ui_username, username)
|> Conn.put_session(:signs_ui_username, username)
|> redirect(to: SignsUiWeb.Router.Helpers.messages_path(conn, :index))
end

Expand All @@ -33,76 +35,31 @@ defmodule SignsUiWeb.AuthController do
) do
Logger.error("ueberauth_failure #{inspect(errors)}")

cond do
error?(errors, "refresh_token_failure") ->
refresh_token_store = Application.get_env(:signs_ui, :refresh_token_store)

conn
|> Plug.Conn.fetch_session()
|> Plug.Conn.get_session(:signs_ui_username)
|> refresh_token_store.clear_refresh_token()

reauthenticate(conn)

error?(errors, "bad_state") ->
reauthenticate(conn)

true ->
send_resp(conn, 403, "unauthenticated")
if error?(errors, "bad_state") do
reauthenticate(conn)
else
send_resp(conn, 403, "unauthenticated")
end
end

@spec logout(Plug.Conn.t(), map()) :: Plug.Conn.t()
def logout(conn, params) do
refresh_token_store = Application.get_env(:signs_ui, :refresh_token_store)

conn
|> Plug.Conn.fetch_session()
|> Plug.Conn.get_session(:signs_ui_username)
|> refresh_token_store.clear_refresh_token()

redirect_url = logout_redirect_url_for_provider(conn, params["provider"])

@spec logout(Conn.t(), map()) :: Conn.t()
def logout(conn, _params) do
conn
|> Guardian.Plug.sign_out(SignsUiWeb.AuthManager)
|> redirect(external: redirect_url)
|> Conn.clear_session()
|> redirect(to: SignsUiWeb.Router.Helpers.page_path(conn, :index))
end

@spec error?([Ueberauth.Failure.t(), ...], String.t()) :: boolean
defp error?(errors, key) do
Enum.any?(errors, fn e -> e.message_key == key end)
end

@spec reauthenticate(Plug.Conn.t()) :: Plug.Conn.t()
@spec reauthenticate(Conn.t()) :: Conn.t()
defp reauthenticate(conn) do
Phoenix.Controller.redirect(
conn,
to: SignsUiWeb.Router.Helpers.auth_path(conn, :request, "cognito")
to: SignsUiWeb.Router.Helpers.auth_path(conn, :request, "keycloak")
)
end

@spec logout_redirect_url_for_provider(Plug.Conn.t(), String.t()) :: String.t()
defp logout_redirect_url_for_provider(conn, "cognito") do
auth_domain =
:ueberauth
|> Application.get_env(Ueberauth.Strategy.Cognito)
|> Keyword.get(:auth_domain)
|> config_value

redirect_params =
URI.encode_query(%{
"client_id" =>
:ueberauth
|> Application.get_env(Ueberauth.Strategy.Cognito)
|> Keyword.get(:client_id)
|> config_value,
"logout_uri" => SignsUiWeb.Router.Helpers.page_url(conn, :index)
})

"https://#{auth_domain}/logout?" <> redirect_params
end

@spec config_value(binary() | {module(), atom(), [any()]}) :: any()
defp config_value(value) when is_binary(value), do: value
defp config_value({m, f, a}), do: apply(m, f, a)
end
2 changes: 1 addition & 1 deletion lib/signs_ui_web/controllers/messages_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ defmodule SignsUiWeb.MessagesController do

chelsea_bridge_announcements = Map.get(config, :chelsea_bridge_announcements, "off")
sign_groups = config |> Map.fetch!(:sign_groups) |> SignGroups.by_route()
sign_out_path = SignsUiWeb.Router.Helpers.auth_path(conn, :logout, "cognito")
sign_out_path = SignsUiWeb.Router.Helpers.auth_path(conn, :logout, "keycloak")

render(conn, "index.html",
alerts: alerts,
Expand Down
2 changes: 1 addition & 1 deletion lib/signs_ui_web/controllers/unauthorized_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule SignsUiWeb.UnauthorizedController do

@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
def index(conn, _params) do
sign_out_path = SignsUiWeb.Router.Helpers.auth_path(conn, :logout, "cognito")
sign_out_path = SignsUiWeb.Router.Helpers.auth_path(conn, :logout, "keycloak")

conn
|> put_status(403)
Expand Down
2 changes: 1 addition & 1 deletion lib/signs_ui_web/ensure_signs_ui_group.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule SignsUiWeb.EnsureSignsUiGroup do
@moduledoc """
Ensure the Cognito user is in the correct group for authorization.
Ensure the Keycloak user is in the correct group for authorization.
"""

import Plug.Conn
Expand Down
Loading
Loading