feat: dynamic auth providers + Auth0 support

- Add /api/1/auth/providers endpoint that returns only configured providers
- Frontend fetches available providers dynamically instead of hardcoding
- Only providers with credentials set (KEY + SECRET) appear as login options
- Add Auth0 as a supported authentication provider
- Add python-jose[cryptography] dependency for Auth0 JWT verification
- Show helpful message when no providers are configured

This makes the login page modular - administrators only see buttons for
providers they've actually configured, avoiding confusion from dead buttons.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Brian Olson
2025-12-31 15:57:48 -05:00
parent 16847cea93
commit 3aa321aff8
8 changed files with 135 additions and 12 deletions

View File

@@ -11,6 +11,7 @@ router.register(
urlpatterns = [
path("api/1/auth/logout", auth.LogoutView.as_view()),
path("api/1/auth/providers", auth.ProvidersView.as_view()),
path("api/1/user", user.UserViewSet.as_view({"get": "retrieve", "put": "update"})),
path(
"api/1/gateways/choose",

View File

@@ -1,9 +1,73 @@
from django.conf import settings
from django.contrib.auth import logout
from rest_framework.response import Response
from rest_framework.views import APIView
# Provider configuration: maps backend names to their display info
# and the environment variable prefix used to detect if they're configured
PROVIDER_CONFIG = {
'github': {
'name': 'GitHub',
'icon': 'github',
'cls': 'btn-primary',
'env_prefix': 'SOCIAL_AUTH_GITHUB',
},
'gitlab': {
'name': 'GitLab',
'icon': 'gitlab',
'cls': 'btn-warning',
'env_prefix': 'SOCIAL_AUTH_GITLAB',
},
'google-oauth2': {
'name': 'Google',
'icon': 'google',
'cls': 'btn-secondary',
'env_prefix': 'SOCIAL_AUTH_GOOGLE_OAUTH2',
},
'microsoft-graph': {
'name': 'Microsoft',
'icon': 'microsoft',
'cls': 'btn-light',
'env_prefix': 'SOCIAL_AUTH_MICROSOFT_GRAPH',
},
'auth0': {
'name': 'Auth0',
'icon': 'key', # Using key icon as Auth0 doesn't have a FA brand icon
'cls': 'btn-dark',
'env_prefix': 'SOCIAL_AUTH_AUTH0',
},
}
def is_provider_configured(env_prefix: str) -> bool:
"""Check if a provider has both KEY and SECRET configured."""
key = getattr(settings, f'{env_prefix}_KEY', None)
secret = getattr(settings, f'{env_prefix}_SECRET', None)
# For Auth0, also need DOMAIN
if env_prefix == 'SOCIAL_AUTH_AUTH0':
domain = getattr(settings, f'{env_prefix}_DOMAIN', None)
return bool(key and secret and domain)
return bool(key and secret)
class LogoutView(APIView):
def post(self, request, format=None):
logout(request)
return Response(None)
class ProvidersView(APIView):
"""Returns list of configured authentication providers."""
def get(self, request, format=None):
providers = []
for provider_id, config in PROVIDER_CONFIG.items():
if is_provider_configured(config['env_prefix']):
providers.append({
'id': provider_id,
'name': config['name'],
'icon': config['icon'],
'cls': config['cls'],
})
return Response(providers)

View File

@@ -140,10 +140,15 @@ AUTHENTICATION_BACKENDS = (
"social_core.backends.azuread.AzureADOAuth2",
"social_core.backends.microsoft.MicrosoftOAuth2",
"social_core.backends.google.GoogleOAuth2",
"social_core.backends.auth0.Auth0OAuth2",
"django.contrib.auth.backends.ModelBackend",
)
SOCIAL_AUTH_GITHUB_SCOPE = ["read:user", "user:email"]
SOCIAL_AUTH_AUTH0_SCOPE = ["openid", "profile", "email"]
SOCIAL_AUTH_AUTH0_EXTRA_DATA = ["id_token"]
# Auth0 uses RS256 for ID tokens
SOCIAL_AUTH_ALLOWED_REDIRECT_HOSTS = ["*"]
SOCIAL_AUTH_PIPELINE = (
"social_core.pipeline.social_auth.social_details",
"social_core.pipeline.social_auth.social_uid",
@@ -180,6 +185,9 @@ for key in [
"SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET",
"SOCIAL_AUTH_MICROSOFT_GRAPH_KEY",
"SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET",
"SOCIAL_AUTH_AUTH0_DOMAIN",
"SOCIAL_AUTH_AUTH0_KEY",
"SOCIAL_AUTH_AUTH0_SECRET",
"CONNECTION_GATEWAY_AUTH_CA",
"CONNECTION_GATEWAY_AUTH_CERTIFICATE",
"CONNECTION_GATEWAY_AUTH_KEY",