From bed6a60056a943f3481404a22855c4acc90e9d7e Mon Sep 17 00:00:00 2001 From: Brian Manifold Date: Fri, 11 Apr 2025 14:25:07 -0700 Subject: [PATCH] fix(portal): Fetch latest Okta access_token before API call (#8745) Why: * The Okta IdP sync job needs to make sure it is always using the latest access token available. If not, there is the possibility for the job to take too long to complete and the access token that the job started with might time out. This commit updates the Okta API client to always check and make sure it is using the latest access token for each request to the Okta API. --- .../adapter/openid_connect/directory_sync.ex | 4 +- .../domain/auth/adapters/okta/api_client.ex | 114 +++++++- .../auth/adapters/okta/jobs/sync_directory.ex | 13 +- .../auth/adapters/okta/api_client_test.exs | 263 +++++++++--------- .../okta/jobs/sync_directory_test.exs | 22 +- .../apps/domain/test/support/fixtures/auth.ex | 7 +- 6 files changed, 272 insertions(+), 151 deletions(-) diff --git a/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex b/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex index ea9c2cf07..fea3f25fc 100644 --- a/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex +++ b/elixir/apps/domain/lib/domain/auth/adapter/openid_connect/directory_sync.ex @@ -140,12 +140,12 @@ defmodule Domain.Auth.Adapter.OpenIDConnect.DirectorySync do ) finish_time = System.monotonic_time(:millisecond) - Logger.info("Finished syncing in #{time_taken(start_time, finish_time)}") + Logger.info("Finished syncing #{adapter} providers in #{time_taken(start_time, finish_time)}") end defp sync_provider(module, provider) do start_time = System.monotonic_time(:millisecond) - Logger.debug("Syncing provider") + Logger.info("Syncing provider: #{provider.id}") if Domain.Accounts.idp_sync_enabled?(provider.account) do {:ok, pid} = Task.Supervisor.start_link() diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex index 1c5f899e8..5664cbce8 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/api_client.ex @@ -1,6 +1,7 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do use Supervisor require Logger + alias Domain.Auth.Provider @pool_name __MODULE__.Finch @@ -29,7 +30,9 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do [conn_opts: [transport_opts: transport_opts]] end - def list_users(endpoint, api_token) do + def list_users(%Provider{} = provider) do + endpoint = provider.adapter_config["api_base_url"] + uri = URI.parse("#{endpoint}/api/v1/users") |> URI.append_query( @@ -42,7 +45,7 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do {"Content-Type", "application/json; okta-response=omitCredentials,omitCredentialsLinks"} ] - with {:ok, users} <- list_all(uri, headers, api_token) do + with {:ok, users} <- list_all(uri, headers, provider) do active_users = Enum.filter(users, fn user -> user["status"] == "ACTIVE" @@ -52,7 +55,9 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do end end - def list_groups(endpoint, api_token) do + def list_groups(%Provider{} = provider) do + endpoint = provider.adapter_config["api_base_url"] + uri = URI.parse("#{endpoint}/api/v1/groups") |> URI.append_query( @@ -63,10 +68,12 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do headers = [] - list_all(uri, headers, api_token) + list_all(uri, headers, provider) end - def list_group_members(endpoint, api_token, group_id) do + def list_group_members(%Provider{} = provider, group_id) do + endpoint = provider.adapter_config["api_base_url"] + uri = URI.parse("#{endpoint}/api/v1/groups/#{group_id}/users") |> URI.append_query( @@ -77,7 +84,7 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do headers = [] - with {:ok, members} <- list_all(uri, headers, api_token) do + with {:ok, members} <- list_all(uri, headers, provider) do enabled_members = Enum.filter(members, fn member -> member["status"] == "ACTIVE" @@ -87,14 +94,14 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do end end - defp list_all(uri, headers, api_token, acc \\ []) do - case list(uri, headers, api_token) do + defp list_all(uri, headers, provider, acc \\ []) do + case list(uri, headers, provider) do {:ok, list, nil} -> {:ok, List.flatten(Enum.reverse([list | acc]))} {:ok, list, next_page_uri} -> URI.parse(next_page_uri) - |> list_all(headers, api_token, [list | acc]) + |> list_all(headers, provider, [list | acc]) {:error, reason} -> {:error, reason} @@ -108,7 +115,8 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do end # TODO: Need to catch 401/403 specifically when error message is in header - defp list(uri, headers, api_token) do + defp list(uri, headers, %Provider{} = provider) do + api_token = fetch_latest_access_token(provider) headers = headers ++ [{"Authorization", "Bearer #{api_token}"}] request = Finch.build(:get, uri, headers) @@ -135,17 +143,21 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do {:error, :invalid_response} - {:ok, %Finch.Response{body: raw_body, status: status}} when status in 400..499 -> + {:ok, %Finch.Response{body: raw_body, status: status, headers: headers}} + when status in 400..499 -> Logger.error("API request failed with 4xx status #{status}", response: inspect(response) ) case Jason.decode(raw_body) do {:ok, json_response} -> + # Errors are in JSON body {:error, {status, json_response}} _error -> - {:error, {status, response}} + # Errors should be in www-authenticate header + error_map = parse_headers_for_errors(headers) + {:error, {status, error_map}} end {:ok, %Finch.Response{status: status}} when status in 500..599 -> @@ -190,4 +202,82 @@ defmodule Domain.Auth.Adapters.Okta.APIClient do end defp parse_link_header(nil), do: nil + + defp parse_headers_for_errors(headers) do + headers + |> Enum.find({}, fn {key, _val} -> key == "www-authenticate" end) + |> parse_error_header() + end + + defp parse_error_header({"www-authenticate", errors}) do + String.split(errors, ",") + |> Enum.map(&String.trim/1) + |> Enum.filter(&String.starts_with?(&1, "error")) + |> Enum.map(&String.replace(&1, "\"", "")) + |> Enum.map(&String.split(&1, "=")) + |> Enum.into(%{}, fn [k, v] -> {k, v} end) + end + + defp parse_error_header(_) do + Logger.info("No www-authenticate header present") + %{"error" => "unknown", "error_message" => "no www-authenticate header present"} + end + + defp fetch_latest_access_token(provider) do + access_token = provider.adapter_state["access_token"] + + if access_token_active?(access_token) do + access_token + else + # Fetch provider from DB and return latest access token + {:ok, provider} = Domain.Auth.fetch_active_provider_by_id(provider.id) + provider.adapter_state["access_token"] + end + end + + defp access_token_active?(token) do + current_time = DateTime.utc_now() + + with {:ok, exp} <- fetch_exp(token), + {:ok, timestamp_time} <- DateTime.from_unix(exp) do + case DateTime.compare(current_time, timestamp_time) do + :lt -> + time_diff = DateTime.diff(timestamp_time, current_time) + time_diff >= 2 * 60 + + _gt_or_eq -> + false + end + else + {:error, msg} when is_binary(msg) -> + Logger.info(msg) + false + + unknown_error -> + Logger.warning("Error while checking access token expiration", + unknown_error: inspect(unknown_error) + ) + + false + end + end + + defp fetch_exp(token) do + with {:ok, decoded_jwt} <- parse_jwt(token), + fields when not is_nil(fields) <- decoded_jwt.fields, + exp when is_integer(exp) <- fields["exp"] do + {:ok, exp} + else + {:error, reason} -> {:error, reason} + _ -> {:error, "exp field is missing or invalid"} + end + end + + defp parse_jwt(token) do + {:ok, JOSE.JWT.peek(token)} + rescue + ArgumentError -> {:error, "Could not parse token"} + Jason.DecodeError -> {:error, "Could not decode token json"} + _ -> {:error, "Unknown error while parsing jwt"} + end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex b/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex index 8ad459083..a8588a42e 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/okta/jobs/sync_directory.ex @@ -24,21 +24,18 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectory do end def gather_provider_data(provider, task_supervisor_pid) do - endpoint = provider.adapter_config["api_base_url"] - access_token = provider.adapter_state["access_token"] - async_results = DirectorySync.run_async_requests(task_supervisor_pid, users: fn -> - Okta.APIClient.list_users(endpoint, access_token) + Okta.APIClient.list_users(provider) end, groups: fn -> - Okta.APIClient.list_groups(endpoint, access_token) + Okta.APIClient.list_groups(provider) end ) with {:ok, %{users: users, groups: groups}} <- async_results, - {:ok, membership_tuples} <- list_membership_tuples(endpoint, access_token, groups) do + {:ok, membership_tuples} <- list_membership_tuples(provider, groups) do identities_attrs = map_identity_attrs(users) actor_groups_attrs = map_group_attrs(groups) {:ok, {identities_attrs, actor_groups_attrs, membership_tuples}} @@ -66,10 +63,10 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectory do end end - defp list_membership_tuples(endpoint, access_token, groups) do + defp list_membership_tuples(provider, groups) do OpenTelemetry.Tracer.with_span "sync_provider.fetch_data.memberships" do Enum.reduce_while(groups, {:ok, []}, fn group, {:ok, tuples} -> - case Okta.APIClient.list_group_members(endpoint, access_token, group["id"]) do + case Okta.APIClient.list_group_members(provider, group["id"]) do {:ok, members} -> tuples = Enum.map(members, &{"G:" <> group["id"], &1["id"]}) ++ tuples {:cont, {:ok, tuples}} diff --git a/elixir/apps/domain/test/domain/auth/adapters/okta/api_client_test.exs b/elixir/apps/domain/test/domain/auth/adapters/okta/api_client_test.exs index 79d9b66c6..1af567b72 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/okta/api_client_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/okta/api_client_test.exs @@ -1,16 +1,50 @@ defmodule Domain.Auth.Adapters.Okta.APIClientTest do - use ExUnit.Case, async: true + use Domain.DataCase, async: true alias Domain.Mocks.OktaDirectory import Domain.Auth.Adapters.Okta.APIClient - describe "list_users/1" do - test "returns list of users" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" - OktaDirectory.mock_users_list_endpoint(bypass, 200) + setup do + jwk = %{ + "kty" => "oct", + "k" => :jose_base64url.encode("super_secret_key") + } - assert {:ok, users} = list_users(api_base_url, api_token) + jws = %{ + "alg" => "HS256" + } + + claims = %{ + "sub" => "1234567890", + "name" => "FooBar", + "iat" => DateTime.utc_now() |> DateTime.to_unix(), + "exp" => DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_unix() + } + + {_, jwt} = + JOSE.JWT.sign(jwk, jws, claims) + |> JOSE.JWS.compact() + + account = Fixtures.Accounts.create_account() + + {provider, bypass} = + Fixtures.Auth.start_and_create_okta_provider( + account: account, + access_token: jwt + ) + + %{ + account: account, + provider: provider, + bypass: bypass + } + end + + describe "list_users/1" do + test "returns list of users", %{provider: provider, bypass: bypass} do + OktaDirectory.mock_users_list_endpoint(bypass, 200) + api_token = provider.adapter_state["access_token"] + + assert {:ok, users} = list_users(provider) assert length(users) == 2 for user <- users do @@ -32,88 +66,78 @@ defmodule Domain.Auth.Adapters.Okta.APIClientTest do assert Plug.Conn.get_req_header(conn, "authorization") == ["Bearer #{api_token}"] end - test "returns error when Okta API is down" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns error when Okta API is down", %{provider: provider, bypass: bypass} do Bypass.down(bypass) - assert list_users(api_base_url, api_token) == + assert list_users(provider) == {:error, %Mint.TransportError{reason: :econnrefused}} end - test "returns invalid_response when api responds with unexpected 2xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns invalid_response when api responds with unexpected 2xx status", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_users_list_endpoint(bypass, 201) - assert list_users(api_base_url, api_token) == {:error, :invalid_response} + assert list_users(provider) == {:error, :invalid_response} end - test "returns invalid_response when api responds with unexpected 3xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns invalid_response when api responds with unexpected 3xx status", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_users_list_endpoint(bypass, 301) - assert list_users(api_base_url, api_token) == {:error, :invalid_response} + assert list_users(provider) == {:error, :invalid_response} end - test "returns error when api responds with 4xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" - + test "returns error when api responds with 4xx status", %{provider: provider, bypass: bypass} do OktaDirectory.mock_users_list_endpoint( bypass, 400, Jason.encode!(%{"error" => %{"code" => 400, "message" => "Bad Request"}}) ) - assert list_users(api_base_url, api_token) == + assert list_users(provider) == {:error, {400, %{"error" => %{"code" => 400, "message" => "Bad Request"}}}} end - test "returns retry_later when api responds with 5xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns retry_later when api responds with 5xx status", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_users_list_endpoint(bypass, 500) - assert list_users(api_base_url, api_token) == {:error, :retry_later} + assert list_users(provider) == {:error, :retry_later} end - test "returns invalid_response when api responds with unexpected data format" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" - + test "returns invalid_response when api responds with unexpected data format", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_users_list_endpoint( bypass, 200, Jason.encode!(%{"invalid" => "format"}) ) - assert list_users(api_base_url, api_token) == {:error, :invalid_response} + assert list_users(provider) == {:error, :invalid_response} end - test "returns error when api responds with invalid JSON" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns error when api responds with invalid JSON", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_users_list_endpoint(bypass, 200, "invalid json") assert {:error, %Jason.DecodeError{data: "invalid json"}} = - list_users(api_base_url, api_token) + list_users(provider) end end describe "list_groups/1" do - test "returns list of groups" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns list of groups", %{provider: provider, bypass: bypass} do OktaDirectory.mock_groups_list_endpoint(bypass, 200) + api_token = provider.adapter_state["access_token"] - assert {:ok, groups} = list_groups(api_base_url, api_token) + assert {:ok, groups} = list_groups(provider) assert length(groups) == 4 for group <- groups do @@ -134,90 +158,80 @@ defmodule Domain.Auth.Adapters.Okta.APIClientTest do assert Plug.Conn.get_req_header(conn, "authorization") == ["Bearer #{api_token}"] end - test "returns error when Okta API is down" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns error when Okta API is down", %{provider: provider, bypass: bypass} do Bypass.down(bypass) - assert list_groups(api_base_url, api_token) == + assert list_groups(provider) == {:error, %Mint.TransportError{reason: :econnrefused}} end - test "returns invalid_response when api responds with unexpected 2xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns invalid_response when api responds with unexpected 2xx status", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_groups_list_endpoint(bypass, 201) - assert list_groups(api_base_url, api_token) == {:error, :invalid_response} + assert list_groups(provider) == {:error, :invalid_response} end - test "returns invalid_response when api responds with unexpected 3xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns invalid_response when api responds with unexpected 3xx status", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_groups_list_endpoint(bypass, 301) - assert list_groups(api_base_url, api_token) == {:error, :invalid_response} + assert list_groups(provider) == {:error, :invalid_response} end - test "returns error when api responds with 4xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" - + test "returns error when api responds with 4xx status", %{provider: provider, bypass: bypass} do OktaDirectory.mock_groups_list_endpoint( bypass, 400, Jason.encode!(%{"error" => %{"code" => 400, "message" => "Bad Request"}}) ) - assert list_groups(api_base_url, api_token) == + assert list_groups(provider) == {:error, {400, %{"error" => %{"code" => 400, "message" => "Bad Request"}}}} end - test "returns retry_later when api responds with 5xx status" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns retry_later when api responds with 5xx status", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_groups_list_endpoint(bypass, 500) - assert list_groups(api_base_url, api_token) == {:error, :retry_later} + assert list_groups(provider) == {:error, :retry_later} end - test "returns invalid_response when api responds with unexpected data format" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" - + test "returns invalid_response when api responds with unexpected data format", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_groups_list_endpoint( bypass, 200, Jason.encode!(%{"invalid" => "format"}) ) - assert list_groups(api_base_url, api_token) == {:error, :invalid_response} + assert list_groups(provider) == {:error, :invalid_response} end - test "returns error when api responds with invalid JSON" do - api_token = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + test "returns error when api responds with invalid JSON", %{ + provider: provider, + bypass: bypass + } do OktaDirectory.mock_groups_list_endpoint(bypass, 200, "invalid json") assert {:error, %Jason.DecodeError{data: "invalid json"}} = - list_groups(api_base_url, api_token) + list_groups(provider) end end describe "list_group_members/1" do - test "returns list of group members" do - api_token = Ecto.UUID.generate() + test "returns list of group members", %{provider: provider, bypass: bypass} do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" OktaDirectory.mock_group_members_list_endpoint(bypass, group_id, 200) + api_token = provider.adapter_state["access_token"] - assert {:ok, members} = list_group_members(api_base_url, api_token, group_id) + assert {:ok, members} = list_group_members(provider, group_id) assert length(members) == 2 @@ -232,41 +246,36 @@ defmodule Domain.Auth.Adapters.Okta.APIClientTest do assert Plug.Conn.get_req_header(conn, "authorization") == ["Bearer #{api_token}"] end - test "returns error when Okta API is down" do - api_token = Ecto.UUID.generate() + test "returns error when Okta API is down", %{provider: provider, bypass: bypass} do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" Bypass.down(bypass) - assert list_group_members(api_base_url, api_token, group_id) == + assert list_group_members(provider, group_id) == {:error, %Mint.TransportError{reason: :econnrefused}} end - test "returns invalid_response when api responds with unexpected 2xx status" do - api_token = Ecto.UUID.generate() + test "returns invalid_response when api responds with unexpected 2xx status", %{ + provider: provider, + bypass: bypass + } do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" + OktaDirectory.mock_group_members_list_endpoint(bypass, group_id, 201) - assert list_group_members(api_base_url, api_token, group_id) == {:error, :invalid_response} + assert list_group_members(provider, group_id) == {:error, :invalid_response} end - test "returns invalid_response when api responds with unexpected 3xx status" do - api_token = Ecto.UUID.generate() + test "returns invalid_response when api responds with unexpected 3xx status", %{ + provider: provider, + bypass: bypass + } do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" OktaDirectory.mock_group_members_list_endpoint(bypass, group_id, 301) - assert list_group_members(api_base_url, api_token, group_id) == {:error, :invalid_response} + assert list_group_members(provider, group_id) == {:error, :invalid_response} end - test "returns error when api responds with 4xx status" do - api_token = Ecto.UUID.generate() + test "returns error when api responds with 4xx status", %{provider: provider, bypass: bypass} do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" OktaDirectory.mock_group_members_list_endpoint( bypass, @@ -275,24 +284,24 @@ defmodule Domain.Auth.Adapters.Okta.APIClientTest do Jason.encode!(%{"error" => %{"code" => 400, "message" => "Bad Request"}}) ) - assert list_group_members(api_base_url, api_token, group_id) == + assert list_group_members(provider, group_id) == {:error, {400, %{"error" => %{"code" => 400, "message" => "Bad Request"}}}} end - test "returns retry_later when api responds with 5xx status" do - api_token = Ecto.UUID.generate() + test "returns retry_later when api responds with 5xx status", %{ + provider: provider, + bypass: bypass + } do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" OktaDirectory.mock_group_members_list_endpoint(bypass, group_id, 500) - assert list_group_members(api_base_url, api_token, group_id) == {:error, :retry_later} + assert list_group_members(provider, group_id) == {:error, :retry_later} end - test "returns invalid_response when api responds with unexpected data format" do - api_token = Ecto.UUID.generate() + test "returns invalid_response when api responds with unexpected data format", %{ + provider: provider, + bypass: bypass + } do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" OktaDirectory.mock_group_members_list_endpoint( bypass, @@ -301,14 +310,14 @@ defmodule Domain.Auth.Adapters.Okta.APIClientTest do Jason.encode!(%{"invalid" => "data"}) ) - assert list_group_members(api_base_url, api_token, group_id) == {:error, :invalid_response} + assert list_group_members(provider, group_id) == {:error, :invalid_response} end - test "returns error when api responds with invalid JSON" do - api_token = Ecto.UUID.generate() + test "returns error when api responds with invalid JSON", %{ + provider: provider, + bypass: bypass + } do group_id = Ecto.UUID.generate() - bypass = Bypass.open() - api_base_url = "http://localhost:#{bypass.port}/" OktaDirectory.mock_group_members_list_endpoint( bypass, @@ -318,7 +327,7 @@ defmodule Domain.Auth.Adapters.Okta.APIClientTest do ) assert {:error, %Jason.DecodeError{data: "invalid json"}} = - list_group_members(api_base_url, api_token, group_id) + list_group_members(provider, group_id) end end end diff --git a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs index 1c4364628..ab74c2f72 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/okta/jobs/sync_directory_test.exs @@ -6,10 +6,30 @@ defmodule Domain.Auth.Adapters.Okta.Jobs.SyncDirectoryTest do describe "execute/1" do setup do + jwk = %{ + "kty" => "oct", + "k" => :jose_base64url.encode("super_secret_key") + } + + jws = %{ + "alg" => "HS256" + } + + claims = %{ + "sub" => "1234567890", + "name" => "FooBar", + "iat" => DateTime.utc_now() |> DateTime.to_unix(), + "exp" => DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_unix() + } + + {_, jwt} = + JOSE.JWT.sign(jwk, jws, claims) + |> JOSE.JWS.compact() + account = Fixtures.Accounts.create_account() {provider, bypass} = - Fixtures.Auth.start_and_create_okta_provider(account: account) + Fixtures.Auth.start_and_create_okta_provider(account: account, access_token: jwt) %{ bypass: bypass, diff --git a/elixir/apps/domain/test/support/fixtures/auth.ex b/elixir/apps/domain/test/support/fixtures/auth.ex index f6574ee0e..a842ba41b 100644 --- a/elixir/apps/domain/test/support/fixtures/auth.ex +++ b/elixir/apps/domain/test/support/fixtures/auth.ex @@ -302,12 +302,17 @@ defmodule Domain.Fixtures.Auth do Fixtures.Accounts.create_account(assoc_attrs) end) + {access_token, attrs} = + pop_assoc_fixture(attrs, :access_token, & &1) + {:ok, provider} = Auth.create_provider(account, attrs) + access_token = access_token || "OIDC_ACCESS_TOKEN" + update!(provider, disabled_at: nil, adapter_state: %{ - "access_token" => "OIDC_ACCESS_TOKEN", + "access_token" => access_token, "refresh_token" => "OIDC_REFRESH_TOKEN", "expires_at" => DateTime.utc_now() |> DateTime.add(1, :day), "claims" => "openid email profile offline_access"