Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 138 additions & 21 deletions lib/phoenix_kit/settings/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ defmodule PhoenixKit.Settings do
Gets OAuth credentials for a specific provider.

Returns a map with all credentials for the given provider.
Uses cache for performance - suitable for non-critical reads.

## Examples

Expand All @@ -487,6 +488,30 @@ defmodule PhoenixKit.Settings do
end
end

@doc """
Gets OAuth credentials directly from database, bypassing cache.

Use this for security-critical operations where fresh data is required,
such as configuring OAuth providers after settings update.

This prevents race conditions where cache invalidation hasn't completed
before the credentials are read.

## Examples

iex> PhoenixKit.Settings.get_oauth_credentials_direct(:google)
%{client_id: "google-client-id", client_secret: "google-client-secret"}
"""
def get_oauth_credentials_direct(provider)
when provider in [:google, :apple, :github, :facebook] do
case provider do
:google -> get_google_oauth_credentials_direct()
:apple -> get_apple_oauth_credentials_direct()
:github -> get_github_oauth_credentials_direct()
:facebook -> get_facebook_oauth_credentials_direct()
end
end

defp get_google_oauth_credentials do
keys = ["oauth_google_client_id", "oauth_google_client_secret"]
defaults = %{"oauth_google_client_id" => "", "oauth_google_client_secret" => ""}
Expand Down Expand Up @@ -545,9 +570,89 @@ defmodule PhoenixKit.Settings do
}
end

# Direct database reads for OAuth credentials (bypassing cache)
# Used by OAuthConfig.configure_providers() to avoid race conditions

defp get_google_oauth_credentials_direct do
keys = ["oauth_google_client_id", "oauth_google_client_secret"]
settings = get_settings_direct(keys)

%{
client_id: Map.get(settings, "oauth_google_client_id", ""),
client_secret: Map.get(settings, "oauth_google_client_secret", "")
}
end

defp get_apple_oauth_credentials_direct do
keys = [
"oauth_apple_client_id",
"oauth_apple_team_id",
"oauth_apple_key_id",
"oauth_apple_private_key"
]

settings = get_settings_direct(keys)

%{
client_id: Map.get(settings, "oauth_apple_client_id", ""),
team_id: Map.get(settings, "oauth_apple_team_id", ""),
key_id: Map.get(settings, "oauth_apple_key_id", ""),
private_key: Map.get(settings, "oauth_apple_private_key", "")
}
end

defp get_github_oauth_credentials_direct do
keys = ["oauth_github_client_id", "oauth_github_client_secret"]
settings = get_settings_direct(keys)

%{
client_id: Map.get(settings, "oauth_github_client_id", ""),
client_secret: Map.get(settings, "oauth_github_client_secret", "")
}
end

defp get_facebook_oauth_credentials_direct do
keys = ["oauth_facebook_app_id", "oauth_facebook_app_secret"]
settings = get_settings_direct(keys)

%{
app_id: Map.get(settings, "oauth_facebook_app_id", ""),
app_secret: Map.get(settings, "oauth_facebook_app_secret", "")
}
end

@doc """
Gets multiple settings directly from database, bypassing cache.

Use this for security-critical operations where fresh data is required.
Returns a map with setting keys and their values.

## Examples

iex> PhoenixKit.Settings.get_settings_direct(["oauth_google_client_id", "oauth_google_client_secret"])
%{"oauth_google_client_id" => "client-id", "oauth_google_client_secret" => "secret"}
"""
def get_settings_direct(keys) when is_list(keys) do
if repo_available?() do
Setting
|> where([s], s.key in ^keys)
|> select([s], {s.key, s.value})
|> repo().all()
|> Map.new()
else
%{}
end
rescue
error ->
Logger.warning("Failed to get settings directly from DB: #{inspect(error)}")
%{}
end

@doc """
Checks if OAuth credentials are configured for a provider.

Uses cache for performance - suitable for non-critical checks.

## Examples

iex> PhoenixKit.Settings.has_oauth_credentials?(:google)
Expand All @@ -564,6 +669,29 @@ defmodule PhoenixKit.Settings do
end
end

@doc """
Checks if OAuth credentials are configured for a provider, reading directly from database.

Bypasses cache to ensure fresh data. Use this when configuring OAuth providers
after settings update to avoid race conditions.

## Examples

iex> PhoenixKit.Settings.has_oauth_credentials_direct?(:google)
true
"""
def has_oauth_credentials_direct?(provider)
when provider in [:google, :apple, :github, :facebook] do
credentials = get_oauth_credentials_direct(provider)

case provider do
:google -> validate_google_credentials(credentials)
:apple -> validate_apple_credentials(credentials)
:github -> validate_github_credentials(credentials)
:facebook -> validate_facebook_credentials(credentials)
end
end

defp validate_google_credentials(credentials) do
credentials.client_id != "" and credentials.client_secret != ""
end
Expand Down Expand Up @@ -1333,32 +1461,21 @@ defmodule PhoenixKit.Settings do
end

@doc """
Warm cache with critical OAuth settings only.
Warm cache with critical settings only.

Returns map of critical OAuth settings for synchronous cache warming.
This is used during startup to ensure OAuth configuration is available
immediately, preventing race conditions with OAuthConfigLoader.
Returns map of critical settings for synchronous cache warming.
This is used during startup to ensure essential configuration is available
immediately.

Only loads OAuth-related settings that are required for provider configuration.
Note: OAuth credentials are NOT cached here because they are read directly
from the database via get_oauth_credentials_direct/1 to avoid race conditions
when credentials are updated through the admin UI.
"""
def warm_critical_cache do
# Critical OAuth keys that must be loaded synchronously at startup
# Critical keys that must be loaded synchronously at startup
# OAuth credentials are intentionally NOT included - they use direct DB reads
critical_keys = [
# Google OAuth
"oauth_google_client_id",
"oauth_google_client_secret",
# GitHub OAuth
"oauth_github_client_id",
"oauth_github_client_secret",
# Apple OAuth
"oauth_apple_client_id",
"oauth_apple_team_id",
"oauth_apple_key_id",
"oauth_apple_private_key_path",
# Facebook OAuth
"oauth_facebook_app_id",
"oauth_facebook_app_secret",
# OAuth general settings
# OAuth enabled flag only (not credentials)
"oauth_enabled"
]

Expand Down
19 changes: 11 additions & 8 deletions lib/phoenix_kit/users/oauth_availability.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,34 +49,37 @@ defmodule PhoenixKit.Users.OAuthAvailability do
try do
providers = []

# Check Google
# Check Google (direct DB read to avoid cache race conditions)
providers =
if provider_enabled?(:google) and PhoenixKit.Settings.has_oauth_credentials?(:google) do
if provider_enabled?(:google) and
PhoenixKit.Settings.has_oauth_credentials_direct?(:google) do
[:google | providers]
else
providers
end

# Check Apple
# Check Apple (direct DB read to avoid cache race conditions)
providers =
if provider_enabled?(:apple) and PhoenixKit.Settings.has_oauth_credentials?(:apple) do
if provider_enabled?(:apple) and
PhoenixKit.Settings.has_oauth_credentials_direct?(:apple) do
[:apple | providers]
else
providers
end

# Check GitHub
# Check GitHub (direct DB read to avoid cache race conditions)
providers =
if provider_enabled?(:github) and PhoenixKit.Settings.has_oauth_credentials?(:github) do
if provider_enabled?(:github) and
PhoenixKit.Settings.has_oauth_credentials_direct?(:github) do
[:github | providers]
else
providers
end

# Check Facebook
# Check Facebook (direct DB read to avoid cache race conditions)
providers =
if provider_enabled?(:facebook) and
PhoenixKit.Settings.has_oauth_credentials?(:facebook) do
PhoenixKit.Settings.has_oauth_credentials_direct?(:facebook) do
[:facebook | providers]
else
providers
Expand Down
32 changes: 19 additions & 13 deletions lib/phoenix_kit/users/oauth_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,39 +80,40 @@ defmodule PhoenixKit.Users.OAuthConfig do
end

# Build the list of available providers based on configured credentials
# Uses direct database reads to avoid cache race conditions
defp build_provider_list do
providers = %{}

# Add Google if credentials exist
# Add Google if credentials exist (direct DB read)
providers =
if Settings.has_oauth_credentials?(:google) and
if Settings.has_oauth_credentials_direct?(:google) and
Settings.get_boolean_setting("oauth_google_enabled", false) do
Map.put(providers, :google, {Ueberauth.Strategy.Google, []})
else
providers
end

# Add Apple if credentials exist
# Add Apple if credentials exist (direct DB read)
providers =
if Settings.has_oauth_credentials?(:apple) and
if Settings.has_oauth_credentials_direct?(:apple) and
Settings.get_boolean_setting("oauth_apple_enabled", false) do
Map.put(providers, :apple, {Ueberauth.Strategy.Apple, []})
else
providers
end

# Add GitHub if credentials exist
# Add GitHub if credentials exist (direct DB read)
providers =
if Settings.has_oauth_credentials?(:github) and
if Settings.has_oauth_credentials_direct?(:github) and
Settings.get_boolean_setting("oauth_github_enabled", false) do
Map.put(providers, :github, {Ueberauth.Strategy.Github, []})
else
providers
end

# Add Facebook if credentials exist
# Add Facebook if credentials exist (direct DB read)
providers =
if Settings.has_oauth_credentials?(:facebook) and
if Settings.has_oauth_credentials_direct?(:facebook) and
Settings.get_boolean_setting("oauth_facebook_enabled", false) do
Map.put(providers, :facebook, {Ueberauth.Strategy.Facebook, []})
else
Expand All @@ -123,9 +124,10 @@ defmodule PhoenixKit.Users.OAuthConfig do
end

# Configure Google OAuth
# Uses direct DB read to avoid cache race conditions after settings update
defp configure_google do
if Settings.get_boolean_setting("oauth_google_enabled", false) do
credentials = Settings.get_oauth_credentials(:google)
credentials = Settings.get_oauth_credentials_direct(:google)

if credentials.client_id != "" and credentials.client_secret != "" do
config = [
Expand All @@ -142,9 +144,10 @@ defmodule PhoenixKit.Users.OAuthConfig do
end

# Configure Apple OAuth
# Uses direct DB read to avoid cache race conditions after settings update
defp configure_apple do
if Settings.get_boolean_setting("oauth_apple_enabled", false) do
credentials = Settings.get_oauth_credentials(:apple)
credentials = Settings.get_oauth_credentials_direct(:apple)

if credentials.client_id != "" and
credentials.team_id != "" and
Expand All @@ -166,9 +169,10 @@ defmodule PhoenixKit.Users.OAuthConfig do
end

# Configure GitHub OAuth
# Uses direct DB read to avoid cache race conditions after settings update
defp configure_github do
if Settings.get_boolean_setting("oauth_github_enabled", false) do
credentials = Settings.get_oauth_credentials(:github)
credentials = Settings.get_oauth_credentials_direct(:github)

if credentials.client_id != "" and credentials.client_secret != "" do
config = [
Expand All @@ -185,9 +189,10 @@ defmodule PhoenixKit.Users.OAuthConfig do
end

# Configure Facebook OAuth
# Uses direct DB read to avoid cache race conditions after settings update
defp configure_facebook do
if Settings.get_boolean_setting("oauth_facebook_enabled", false) do
credentials = Settings.get_oauth_credentials(:facebook)
credentials = Settings.get_oauth_credentials_direct(:facebook)

if credentials.app_id != "" and credentials.app_secret != "" do
config = [
Expand All @@ -207,6 +212,7 @@ defmodule PhoenixKit.Users.OAuthConfig do
Validates OAuth credentials for a specific provider.

Returns `{:ok, provider}` if credentials are valid, or `{:error, reason}` if not.
Uses direct database read for accurate validation.

## Examples

Expand All @@ -217,7 +223,7 @@ defmodule PhoenixKit.Users.OAuthConfig do
{:error, "Missing Apple private key"}
"""
def validate_credentials(provider) when provider in [:google, :apple, :github, :facebook] do
credentials = Settings.get_oauth_credentials(provider)
credentials = Settings.get_oauth_credentials_direct(provider)

case provider do
:google -> validate_google_credentials(credentials)
Expand Down
10 changes: 5 additions & 5 deletions lib/phoenix_kit/workers/oauth_config_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,12 @@ defmodule PhoenixKit.Workers.OAuthConfigLoader do
# OAuth settings are already loaded via sync_init
oauth_enabled = PhoenixKit.Settings.get_setting("oauth_enabled", "false")

# Check that at least one provider's credentials are accessible
# Check that at least one provider's credentials are accessible (direct DB read)
has_any_oauth_data =
PhoenixKit.Settings.has_oauth_credentials?(:google) or
PhoenixKit.Settings.has_oauth_credentials?(:apple) or
PhoenixKit.Settings.has_oauth_credentials?(:github) or
PhoenixKit.Settings.has_oauth_credentials?(:facebook)
PhoenixKit.Settings.has_oauth_credentials_direct?(:google) or
PhoenixKit.Settings.has_oauth_credentials_direct?(:apple) or
PhoenixKit.Settings.has_oauth_credentials_direct?(:github) or
PhoenixKit.Settings.has_oauth_credentials_direct?(:facebook)

Logger.debug(
"OAuth configuration: enabled=#{oauth_enabled}, has_oauth_data=#{has_any_oauth_data}"
Expand Down
Loading
Loading