fix: social login providers now work from DB config without restart

- Extract all OAuth registration into _setup_social_providers() with a
  _register_oauth_client() helper that clears the authlib _clients cache
  so credentials can change without a restart
- Add refresh_social_providers() public function called after every
  settings reload (lifespan startup + settings_sync live reload)
- Fix connections page linked status to use _get_effective() (DB-aware)
  instead of the stale startup-time SOCIAL_PROVIDERS dict
- Fix oauth_configured template variable similarly
- Add tests: DB-driven linked status, stale-provider clearing,
  register_oauth_client cache-clear, refresh function coverage

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/ef15910f-fd25-469a-814b-9e1fb40659c9
This commit is contained in:
copilot-swe-agent[bot]
2026-03-22 18:42:18 +00:00
parent 33a0e49acd
commit 0c7ea6748d
5 changed files with 467 additions and 161 deletions
+219 -147
View File
@@ -45,78 +45,27 @@ OAUTH_PROVIDER_NAME = "Single Sign-On"
# Social login providers that are enabled and registered
SOCIAL_PROVIDERS: dict[str, dict[str, str]] = {}
if AUTH_ENABLED and settings.authentik_client_id and settings.authentik_client_secret:
oauth.register(
name="authentik",
client_id=settings.authentik_client_id,
client_secret=settings.authentik_client_secret,
server_metadata_url=settings.authentik_config_url,
client_kwargs={"scope": "openid profile email"},
)
OAUTH_CONFIGURED = True
OAUTH_PROVIDER_NAME = settings.oauth_provider_name or "Authentik SSO"
# --- Social Login Providers ---------------------------------------------------
if AUTH_ENABLED and settings.social_auth_google_enabled:
# Determine which credentials to use for Google social login
_google_client_id = settings.social_auth_google_client_id
_google_client_secret = settings.social_auth_google_client_secret
if settings.social_auth_google_use_global_credentials and not (_google_client_id and _google_client_secret):
_google_client_id = settings.google_drive_client_id
_google_client_secret = settings.google_drive_client_secret
# ---------------------------------------------------------------------------
# Helpers for dynamic (re-)registration of OAuth providers
# ---------------------------------------------------------------------------
if _google_client_id and _google_client_secret:
oauth.register(
name="google",
client_id=_google_client_id,
client_secret=_google_client_secret,
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email"},
)
SOCIAL_PROVIDERS["google"] = {"name": "Google", "icon": "fab fa-google", "color": "red"}
logger.info("Social login provider registered: Google")
else:
logger.warning("SOCIAL_AUTH_GOOGLE_ENABLED=true but client ID/secret not configured")
if AUTH_ENABLED and settings.social_auth_microsoft_enabled:
# Determine which credentials to use for Microsoft social login
_microsoft_client_id = settings.social_auth_microsoft_client_id
_microsoft_client_secret = settings.social_auth_microsoft_client_secret
if settings.social_auth_microsoft_use_global_credentials and not (
_microsoft_client_id and _microsoft_client_secret
):
_microsoft_client_id = settings.onedrive_client_id
_microsoft_client_secret = settings.onedrive_client_secret
def _register_oauth_client(name: str, **kwargs: object) -> None:
"""Register (or re-register) an authlib OAuth client, clearing any cached instance.
if _microsoft_client_id and _microsoft_client_secret:
tenant = settings.social_auth_microsoft_tenant or "common"
oauth.register(
name="microsoft",
client_id=_microsoft_client_id,
client_secret=_microsoft_client_secret,
server_metadata_url=f"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email"},
)
SOCIAL_PROVIDERS["microsoft"] = {"name": "Microsoft", "icon": "fab fa-microsoft", "color": "blue"}
logger.info("Social login provider registered: Microsoft (tenant=%s)", tenant)
else:
logger.warning("SOCIAL_AUTH_MICROSOFT_ENABLED=true but client ID/secret not configured")
authlib caches the constructed client object in ``oauth._clients`` after the
first ``register()`` call. Subsequent ``register()`` calls overwrite the
registry entry but the stale cached client is still returned by
``create_client()`` / ``__getattr__``. Popping the name from ``_clients``
before re-registering ensures the new credentials are picked up immediately.
if AUTH_ENABLED and settings.social_auth_apple_enabled:
if settings.social_auth_apple_client_id and settings.social_auth_apple_team_id:
oauth.register(
name="apple",
client_id=settings.social_auth_apple_client_id,
server_metadata_url="https://appleid.apple.com/.well-known/openid-configuration",
client_kwargs={
"scope": "openid name email",
"response_mode": "form_post",
},
)
SOCIAL_PROVIDERS["apple"] = {"name": "Apple", "icon": "fab fa-apple", "color": "gray"}
logger.info("Social login provider registered: Apple")
else:
logger.warning("SOCIAL_AUTH_APPLE_ENABLED=true but client ID/team ID not configured")
Args:
name: Provider name (e.g. ``"google"``, ``"github"``).
**kwargs: Keyword arguments forwarded verbatim to ``oauth.register()``.
"""
oauth._clients.pop(name, None)
oauth.register(name, **kwargs)
def _dropbox_userinfo_compliance_fix(client, user_cls, token, data):
@@ -145,92 +94,215 @@ def _dropbox_userinfo_compliance_fix(client, user_cls, token, data):
return data
if AUTH_ENABLED and settings.social_auth_dropbox_enabled:
# Determine which credentials to use for Dropbox social login
_dropbox_client_id = settings.social_auth_dropbox_client_id
_dropbox_client_secret = settings.social_auth_dropbox_client_secret
if settings.social_auth_dropbox_use_global_credentials and not (_dropbox_client_id and _dropbox_client_secret):
_dropbox_client_id = settings.dropbox_app_key
_dropbox_client_secret = settings.dropbox_app_secret
def _setup_social_providers() -> None:
"""Register all configured OAuth / social-login providers from current settings.
if _dropbox_client_id and _dropbox_client_secret:
oauth.register(
name="dropbox",
client_id=_dropbox_client_id,
client_secret=_dropbox_client_secret,
authorize_url="https://www.dropbox.com/oauth2/authorize",
access_token_url="https://api.dropboxapi.com/oauth2/token",
userinfo_endpoint="https://api.dropboxapi.com/2/users/get_current_account",
userinfo_compliance_fix=_dropbox_userinfo_compliance_fix,
client_kwargs={
"token_endpoint_auth_method": "client_secret_post",
"token_access_type": "offline",
},
)
SOCIAL_PROVIDERS["dropbox"] = {"name": "Dropbox", "icon": "fab fa-dropbox", "color": "blue"}
logger.info("Social login provider registered: Dropbox")
else:
logger.warning("SOCIAL_AUTH_DROPBOX_ENABLED=true but client ID/secret not configured")
This function is **idempotent**: it clears ``SOCIAL_PROVIDERS``,
``OAUTH_CONFIGURED``, and ``OAUTH_PROVIDER_NAME`` before rebuilding them,
and calls :func:`_register_oauth_client` (which also clears the authlib
client cache) so that credential changes in the database are reflected
without an application restart.
if AUTH_ENABLED and settings.social_auth_github_enabled:
if settings.social_auth_github_client_id and settings.social_auth_github_client_secret:
oauth.register(
name="github",
client_id=settings.social_auth_github_client_id,
client_secret=settings.social_auth_github_client_secret,
authorize_url="https://github.com/login/oauth/authorize",
access_token_url="https://github.com/login/oauth/access_token",
userinfo_endpoint="https://api.github.com/user",
client_kwargs={"scope": "read:user user:email"},
)
SOCIAL_PROVIDERS["github"] = {"name": "GitHub", "icon": "fab fa-github", "color": "gray"}
logger.info("Social login provider registered: GitHub")
else:
logger.warning("SOCIAL_AUTH_GITHUB_ENABLED=true but client ID/secret not configured")
Can safely be called multiple times, e.g. after a settings reload.
"""
global OAUTH_CONFIGURED, OAUTH_PROVIDER_NAME
if AUTH_ENABLED and settings.social_auth_keycloak_enabled:
_kc_server = settings.social_auth_keycloak_server_url
_kc_realm = settings.social_auth_keycloak_realm
if (
settings.social_auth_keycloak_client_id
and settings.social_auth_keycloak_client_secret
and _kc_server
and _kc_realm
):
_kc_base = f"{_kc_server.rstrip('/')}/realms/{_kc_realm}"
oauth.register(
name="keycloak",
client_id=settings.social_auth_keycloak_client_id,
client_secret=settings.social_auth_keycloak_client_secret,
server_metadata_url=f"{_kc_base}/.well-known/openid-configuration",
SOCIAL_PROVIDERS.clear()
OAUTH_CONFIGURED = False
OAUTH_PROVIDER_NAME = "Single Sign-On"
if not AUTH_ENABLED:
return
# --- Authentik / OIDC ---
if settings.authentik_client_id and settings.authentik_client_secret:
_register_oauth_client(
"authentik",
client_id=settings.authentik_client_id,
client_secret=settings.authentik_client_secret,
server_metadata_url=settings.authentik_config_url,
client_kwargs={"scope": "openid profile email"},
)
SOCIAL_PROVIDERS["keycloak"] = {"name": "Keycloak", "icon": "fas fa-key", "color": "gray"}
logger.info("Social login provider registered: Keycloak (realm=%s)", _kc_realm)
else:
logger.warning("SOCIAL_AUTH_KEYCLOAK_ENABLED=true but required settings not configured")
OAUTH_CONFIGURED = True
OAUTH_PROVIDER_NAME = settings.oauth_provider_name or "Authentik SSO"
if AUTH_ENABLED and settings.social_auth_generic_oauth2_enabled:
if (
settings.social_auth_generic_oauth2_client_id
and settings.social_auth_generic_oauth2_client_secret
and settings.social_auth_generic_oauth2_authorize_url
and settings.social_auth_generic_oauth2_token_url
):
oauth.register(
name="generic_oauth2",
client_id=settings.social_auth_generic_oauth2_client_id,
client_secret=settings.social_auth_generic_oauth2_client_secret,
authorize_url=settings.social_auth_generic_oauth2_authorize_url,
access_token_url=settings.social_auth_generic_oauth2_token_url,
userinfo_endpoint=settings.social_auth_generic_oauth2_userinfo_url,
client_kwargs={"scope": settings.social_auth_generic_oauth2_scope},
)
_generic_name = settings.social_auth_generic_oauth2_name or "OAuth2"
SOCIAL_PROVIDERS["generic_oauth2"] = {"name": _generic_name, "icon": "fas fa-sign-in-alt", "color": "indigo"}
logger.info("Social login provider registered: Generic OAuth2 (%s)", _generic_name)
else:
logger.warning("SOCIAL_AUTH_GENERIC_OAUTH2_ENABLED=true but required settings not configured")
# --- Social Login Providers ---
# Google
if settings.social_auth_google_enabled:
_google_client_id = settings.social_auth_google_client_id
_google_client_secret = settings.social_auth_google_client_secret
if settings.social_auth_google_use_global_credentials and not (_google_client_id and _google_client_secret):
_google_client_id = settings.google_drive_client_id
_google_client_secret = settings.google_drive_client_secret
if _google_client_id and _google_client_secret:
_register_oauth_client(
"google",
client_id=_google_client_id,
client_secret=_google_client_secret,
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email"},
)
SOCIAL_PROVIDERS["google"] = {"name": "Google", "icon": "fab fa-google", "color": "red"}
logger.info("Social login provider registered: Google")
else:
logger.warning("SOCIAL_AUTH_GOOGLE_ENABLED=true but client ID/secret not configured")
# Microsoft
if settings.social_auth_microsoft_enabled:
_microsoft_client_id = settings.social_auth_microsoft_client_id
_microsoft_client_secret = settings.social_auth_microsoft_client_secret
if settings.social_auth_microsoft_use_global_credentials and not (
_microsoft_client_id and _microsoft_client_secret
):
_microsoft_client_id = settings.onedrive_client_id
_microsoft_client_secret = settings.onedrive_client_secret
if _microsoft_client_id and _microsoft_client_secret:
tenant = settings.social_auth_microsoft_tenant or "common"
_register_oauth_client(
"microsoft",
client_id=_microsoft_client_id,
client_secret=_microsoft_client_secret,
server_metadata_url=f"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email"},
)
SOCIAL_PROVIDERS["microsoft"] = {"name": "Microsoft", "icon": "fab fa-microsoft", "color": "blue"}
logger.info("Social login provider registered: Microsoft (tenant=%s)", tenant)
else:
logger.warning("SOCIAL_AUTH_MICROSOFT_ENABLED=true but client ID/secret not configured")
# Apple
if settings.social_auth_apple_enabled:
if settings.social_auth_apple_client_id and settings.social_auth_apple_team_id:
_register_oauth_client(
"apple",
client_id=settings.social_auth_apple_client_id,
server_metadata_url="https://appleid.apple.com/.well-known/openid-configuration",
client_kwargs={
"scope": "openid name email",
"response_mode": "form_post",
},
)
SOCIAL_PROVIDERS["apple"] = {"name": "Apple", "icon": "fab fa-apple", "color": "gray"}
logger.info("Social login provider registered: Apple")
else:
logger.warning("SOCIAL_AUTH_APPLE_ENABLED=true but client ID/team ID not configured")
# Dropbox
if settings.social_auth_dropbox_enabled:
_dropbox_client_id = settings.social_auth_dropbox_client_id
_dropbox_client_secret = settings.social_auth_dropbox_client_secret
if settings.social_auth_dropbox_use_global_credentials and not (_dropbox_client_id and _dropbox_client_secret):
_dropbox_client_id = settings.dropbox_app_key
_dropbox_client_secret = settings.dropbox_app_secret
if _dropbox_client_id and _dropbox_client_secret:
_register_oauth_client(
"dropbox",
client_id=_dropbox_client_id,
client_secret=_dropbox_client_secret,
authorize_url="https://www.dropbox.com/oauth2/authorize",
access_token_url="https://api.dropboxapi.com/oauth2/token",
userinfo_endpoint="https://api.dropboxapi.com/2/users/get_current_account",
userinfo_compliance_fix=_dropbox_userinfo_compliance_fix,
client_kwargs={
"token_endpoint_auth_method": "client_secret_post",
"token_access_type": "offline",
},
)
SOCIAL_PROVIDERS["dropbox"] = {"name": "Dropbox", "icon": "fab fa-dropbox", "color": "blue"}
logger.info("Social login provider registered: Dropbox")
else:
logger.warning("SOCIAL_AUTH_DROPBOX_ENABLED=true but client ID/secret not configured")
# GitHub
if settings.social_auth_github_enabled:
if settings.social_auth_github_client_id and settings.social_auth_github_client_secret:
_register_oauth_client(
"github",
client_id=settings.social_auth_github_client_id,
client_secret=settings.social_auth_github_client_secret,
authorize_url="https://github.com/login/oauth/authorize",
access_token_url="https://github.com/login/oauth/access_token",
userinfo_endpoint="https://api.github.com/user",
client_kwargs={"scope": "read:user user:email"},
)
SOCIAL_PROVIDERS["github"] = {"name": "GitHub", "icon": "fab fa-github", "color": "gray"}
logger.info("Social login provider registered: GitHub")
else:
logger.warning("SOCIAL_AUTH_GITHUB_ENABLED=true but client ID/secret not configured")
# Keycloak
if settings.social_auth_keycloak_enabled:
_kc_server = settings.social_auth_keycloak_server_url
_kc_realm = settings.social_auth_keycloak_realm
if (
settings.social_auth_keycloak_client_id
and settings.social_auth_keycloak_client_secret
and _kc_server
and _kc_realm
):
_kc_base = f"{_kc_server.rstrip('/')}/realms/{_kc_realm}"
_register_oauth_client(
"keycloak",
client_id=settings.social_auth_keycloak_client_id,
client_secret=settings.social_auth_keycloak_client_secret,
server_metadata_url=f"{_kc_base}/.well-known/openid-configuration",
client_kwargs={"scope": "openid profile email"},
)
SOCIAL_PROVIDERS["keycloak"] = {"name": "Keycloak", "icon": "fas fa-key", "color": "gray"}
logger.info("Social login provider registered: Keycloak (realm=%s)", _kc_realm)
else:
logger.warning("SOCIAL_AUTH_KEYCLOAK_ENABLED=true but required settings not configured")
# Generic OAuth2
if settings.social_auth_generic_oauth2_enabled:
if (
settings.social_auth_generic_oauth2_client_id
and settings.social_auth_generic_oauth2_client_secret
and settings.social_auth_generic_oauth2_authorize_url
and settings.social_auth_generic_oauth2_token_url
):
_register_oauth_client(
"generic_oauth2",
client_id=settings.social_auth_generic_oauth2_client_id,
client_secret=settings.social_auth_generic_oauth2_client_secret,
authorize_url=settings.social_auth_generic_oauth2_authorize_url,
access_token_url=settings.social_auth_generic_oauth2_token_url,
userinfo_endpoint=settings.social_auth_generic_oauth2_userinfo_url,
client_kwargs={"scope": settings.social_auth_generic_oauth2_scope},
)
_generic_name = settings.social_auth_generic_oauth2_name or "OAuth2"
SOCIAL_PROVIDERS["generic_oauth2"] = {
"name": _generic_name,
"icon": "fas fa-sign-in-alt",
"color": "indigo",
}
logger.info("Social login provider registered: Generic OAuth2 (%s)", _generic_name)
else:
logger.warning("SOCIAL_AUTH_GENERIC_OAUTH2_ENABLED=true but required settings not configured")
def refresh_social_providers() -> None:
"""Re-register all OAuth providers from the *current* settings object.
Call this after loading or reloading settings from the database so that
providers configured (or updated) through the admin UI take effect
immediately — **no application restart required**.
This function is safe to call multiple times and is idempotent.
"""
logger.info("Refreshing social login provider registrations from current settings")
_setup_social_providers()
# Perform the initial registration from environment / default settings at
# import time. The lifespan hook and settings_sync will call
# refresh_social_providers() again after DB settings are loaded so that
# any providers configured only in the database are also active.
_setup_social_providers()
router = APIRouter()