mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user