diff --git a/CHANGELOG.md b/CHANGELOG.md index d72e93143..f83f97948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Version 2.1.0-alpha.6 (2026-10-30) + +- Fix enterprise SSO buttons on the Site Admin Console and Pod sign-in pages. + Ankole sent these buttons an empty sign-in link, so the browser opened + `/null` and showed a "no route found" error instead of the provider's sign-in + page. + ## Version 2.1.0-alpha.5 (2026-10-29) - Internal changes only. diff --git a/app/control_plane/lib/ankole_web/browser_auth.ex b/app/control_plane/lib/ankole_web/browser_auth.ex index e96476d72..cafd34a7f 100644 --- a/app/control_plane/lib/ankole_web/browser_auth.ex +++ b/app/control_plane/lib/ankole_web/browser_auth.ex @@ -782,11 +782,18 @@ defmodule AnkoleWeb.BrowserAuth do end end - defp providers_for_target(_conn, %{purpose: :console}), - do: LoginPolicy.login_providers() + # Console login stays on the site host, so its providers link to the current origin. + defp providers_for_target(_conn, %{purpose: :console}) do + with {:ok, providers} <- LoginPolicy.login_providers(), + do: {:ok, Enum.map(providers, &provider_login_path("", &1))} + end - defp providers_for_target(conn, %{purpose: :pod}) do + defp providers_for_target(conn, %{purpose: :pod, tenant_id: tenant_id}) do with {:ok, providers} <- LoginPolicy.login_providers() do + {:ok, tenant} = Ankole.Tenants.get(tenant_id) + origin = PodURL.origin(conn, tenant) + providers = Enum.map(providers, &provider_login_path(origin, &1)) + if conn.assigns[:pod_email_otp_disabled] == true, do: {:ok, Enum.reject(providers, &(&1["provider_id"] == "email_otp"))}, else: {:ok, providers} @@ -797,9 +804,10 @@ defmodule AnkoleWeb.BrowserAuth do with {:ok, client} <- Ankole.OIDC.get_active_client(request.request["client_id"]), {:ok, providers} <- LoginPolicy.providers(client, request.request) do {:ok, tenant} = Ankole.Tenants.get(request.tenant_id) + origin = PodURL.origin(conn, tenant) {:ok, - Enum.map(providers, &provider_login_path(conn, tenant, &1)) + Enum.map(providers, &provider_login_path(origin, &1)) |> Enum.reject( &(&1["provider_id"] == "email_otp" and conn.assigns[:pod_email_otp_disabled] == true) )} @@ -815,7 +823,8 @@ defmodule AnkoleWeb.BrowserAuth do Ankole.IdentityProviders.Login.list_login_providers() end) - oidc_providers = Enum.map(oidc, &provider_login_path(conn, tenant, &1)) + origin = PodURL.origin(conn, tenant) + oidc_providers = Enum.map(oidc, &provider_login_path(origin, &1)) {:ok, oidc_providers ++ Ankole.IdentityProviders.SiteSources.providers()} end @@ -823,15 +832,15 @@ defmodule AnkoleWeb.BrowserAuth do defp providers_for_target(_conn, %{purpose: :site}), do: {:ok, Ankole.IdentityProviders.SiteSources.providers()} - defp provider_login_path(conn, tenant, %{"kind" => "oidc"} = provider) do + defp provider_login_path(origin, %{"kind" => "oidc"} = provider) do Map.put( provider, "login_path", - PodURL.oidc_authorization_url(conn, tenant, provider["provider_id"]) + origin <> PodURL.oidc_authorization_path(provider["provider_id"]) ) end - defp provider_login_path(_conn, _tenant, provider), do: provider + defp provider_login_path(_origin, provider), do: provider defp perform_request_email_otp(conn, params) do with {:ok, true} <- SetupConfig.completed?(), diff --git a/app/control_plane/lib/ankole_web/pod_url.ex b/app/control_plane/lib/ankole_web/pod_url.ex index ae790a0a2..e327a5861 100644 --- a/app/control_plane/lib/ankole_web/pod_url.ex +++ b/app/control_plane/lib/ankole_web/pod_url.ex @@ -36,10 +36,11 @@ defmodule AnkoleWeb.PodURL do else: {:ok, login_url(conn, tenant, params)} end - def oidc_authorization_url(conn, tenant, provider_id) do - origin(conn, tenant) <> - "/sessions/oidc/" <> URI.encode(provider_id) <> "/authorization" - end + def oidc_authorization_url(conn, tenant, provider_id), + do: origin(conn, tenant) <> oidc_authorization_path(provider_id) + + def oidc_authorization_path(provider_id), + do: "/sessions/oidc/" <> URI.encode(provider_id) <> "/authorization" def oauth_return_to(conn, request) do if @host_policy do diff --git a/app/control_plane/test/ankole_web/controllers/auth_controller_test.exs b/app/control_plane/test/ankole_web/controllers/auth_controller_test.exs index 6ad4ef43b..c3c5ae277 100644 --- a/app/control_plane/test/ankole_web/controllers/auth_controller_test.exs +++ b/app/control_plane/test/ankole_web/controllers/auth_controller_test.exs @@ -45,11 +45,25 @@ defmodule AnkoleWeb.AuthControllerTest do true ) + providers_conn = + conn + |> forwarded_https_conn() + |> init_test_session(%{}) + |> api_get("/api/v2/browser/identity-providers", %{"purpose" => "console"}) + + assert %{"loginPath" => login_path} = + Enum.find( + api_response(providers_conn, 200)["providers"], + &(&1["providerID"] == "lark-main") + ) + + assert login_path == "/sessions/oidc/lark-main/authorization" + conn = conn |> forwarded_https_conn() |> init_test_session(%{}) - |> get("/sessions/oidc/lark-main/authorization", %{ + |> get(login_path, %{ "purpose" => "console", "return_to" => "/site-admin-console/agents" }) diff --git a/app/control_plane/test/ankole_web/pod_membership_authentication_test.exs b/app/control_plane/test/ankole_web/pod_membership_authentication_test.exs index 6a75eef44..7e24f0556 100644 --- a/app/control_plane/test/ankole_web/pod_membership_authentication_test.exs +++ b/app/control_plane/test/ankole_web/pod_membership_authentication_test.exs @@ -257,7 +257,12 @@ defmodule AnkoleWeb.PodMembershipAuthenticationTest do oidc_providers = Enum.filter(api_response(providers_conn, 200)["providers"], &(&1["kind"] == "oidc")) - assert [%{"providerID" => "company-sso", "kind" => "oidc"}] = oidc_providers + assert [%{"providerID" => "company-sso", "kind" => "oidc", "loginPath" => login_path}] = + oidc_providers + + login_uri = URI.parse(login_path) + assert login_uri.host == URI.parse(AnkoleWeb.PodURL.origin(conn, target)).host + assert login_uri.path == "/sessions/oidc/company-sso/authorization" Repo.with_tenant(target.id, fn -> assert {:ok, %{membership: :guest, used_count: 0}} = diff --git a/docs/design-docs/BrowserSessions.md b/docs/design-docs/BrowserSessions.md index 6af2b9810..ad34afa06 100644 --- a/docs/design-docs/BrowserSessions.md +++ b/docs/design-docs/BrowserSessions.md @@ -73,7 +73,10 @@ Login ceremonies target either a specific tenant or the site: while keeping tenant OIDC initiation and callbacks active. - Outbound enterprise SSO creates an `OidcState` row when the browser visits `/sessions/oidc/:provider_id/authorization`, and its callback completes - against that recorded tenant and provider. On shared site sign-in hosts, + against that recorded tenant and provider. `loginProviders` returns this + endpoint as each OIDC provider's `loginPath`: a `console` login keeps it on + the current site host, while `pod`, `oauth`, and tenant-targeted `site` + logins point it at the target Pod host. On shared site sign-in hosts, `POST /api/v2/browser/pod-logins` resolves a workspace slug into its `tenantID`, `displayName`, and active OIDC `providers` (including each provider's `authorizationURL` on the workspace host) without exposing disabled tenants.