diff --git a/.env.example b/.env.example index d9bc5ee..f379fe5 100644 --- a/.env.example +++ b/.env.example @@ -19,9 +19,9 @@ KEYWARDEN_ADMIN_EMAIL=admin@example.com KEYWARDEN_ADMIN_PASSWORD=password # Auth mode: native | oidc | hybrid -# native: Uses JWT authentication based on Username / Password -# oidc: Disables in-build auth, exclusively uses OIDC configuration -# hybrid: Allows native auth, prefers oidc +# native: Uses Keywarden username/password login only. OIDC/social options are hidden and disabled. +# oidc: Disables built-in login and uses OIDC only. Users can be auto-onboarded from IdP claims. +# hybrid: Supports OIDC + Keywarden native login; login screen prefers OIDC/social options first. KEYWARDEN_AUTH_MODE=native @@ -41,14 +41,16 @@ KEYWARDEN_AUTH_MODE=native # KEYWARDEN_OIDC_EMAIL_CLAIM=email # KEYWARDEN_OIDC_EMAIL_VERIFIED_CLAIM=email_verified # KEYWARDEN_OIDC_USERNAME_CLAIM=preferred_username +# KEYWARDEN_OIDC_ALLOW_AUTO_ONBOARDING=true # KEYWARDEN_OIDC_GROUPS_CLAIM=groups -# KEYWARDEN_OIDC_SYNC_ADMIN_FROM_GROUPS=false +# KEYWARDEN_OIDC_SYNC_ADMIN_FROM_GROUPS=true # defaults to true when KEYWARDEN_AUTH_MODE=oidc # KEYWARDEN_OIDC_ADMIN_GROUPS=authentik Admins # KEYWARDEN_OIDC_ADMIN_DEMOTE_ON_MISS=false # Social login (optional) # KEYWARDEN_SITE_ID=1 # KEYWARDEN_SOCIAL_REQUIRE_VERIFIED_EMAIL=true +# KEYWARDEN_SOCIAL_ALLOW_AUTO_ONBOARDING=false # KEYWARDEN_SOCIAL_GOOGLE_ENABLED=false # KEYWARDEN_SOCIAL_GOOGLE_CLIENT_ID= # KEYWARDEN_SOCIAL_GOOGLE_CLIENT_SECRET= @@ -61,4 +63,4 @@ KEYWARDEN_AUTH_MODE=native # Authentik Secrets AUTHENTIK_PG_PASS= -AUTHENTIK_SECRET_KEY= \ No newline at end of file +AUTHENTIK_SECRET_KEY= diff --git a/app/apps/accounts/social_auth.py b/app/apps/accounts/social_auth.py index 66759c3..208e2d9 100644 --- a/app/apps/accounts/social_auth.py +++ b/app/apps/accounts/social_auth.py @@ -61,6 +61,13 @@ class KeywardenSocialAccountAdapter(DefaultSocialAccountAdapter): if user: sociallogin.connect(request, user) self._sync_identity(user, sociallogin) + return + + if not getattr(settings, "KEYWARDEN_SOCIAL_ALLOW_AUTO_ONBOARDING", False): + self._reject( + request, + "Social auto-onboarding is disabled. Ask an administrator to create/link your account.", + ) def populate_user(self, request, sociallogin, data): user = super().populate_user(request, sociallogin, data) diff --git a/app/apps/accounts/views.py b/app/apps/accounts/views.py index 96f1cf7..5da2ff6 100644 --- a/app/apps/accounts/views.py +++ b/app/apps/accounts/views.py @@ -133,7 +133,7 @@ def profile(request): context = { "user": request.user, "auth_mode": getattr(settings, "KEYWARDEN_AUTH_MODE", "hybrid"), - "oidc_enabled": bool(getattr(settings, "KEYWARDEN_OIDC_ENABLED", False)), + "oidc_enabled": bool(getattr(settings, "KEYWARDEN_OIDC_LOGIN_ENABLED", False)), "oidc_identity": oidc_identity, "oidc_provider_id": getattr(settings, "KEYWARDEN_OIDC_PROVIDER_ID", "oidc"), "oidc_issuer": getattr(settings, "KEYWARDEN_OIDC_ISSUER", ""), @@ -149,25 +149,32 @@ def profile(request): def login_view(request): auth_mode = getattr(settings, "KEYWARDEN_AUTH_MODE", "hybrid") - if auth_mode == "oidc": + oidc_login_enabled = bool(getattr(settings, "KEYWARDEN_OIDC_LOGIN_ENABLED", False)) + social_providers = ( + getattr(settings, "KEYWARDEN_SOCIAL_LOGIN_PROVIDERS", []) + if auth_mode == "hybrid" + else [] + ) + + if auth_mode == "oidc" and oidc_login_enabled: return redirect("/oidc/authenticate/") has_sso_options = bool( - getattr(settings, "KEYWARDEN_OIDC_ENABLED", False) - or getattr(settings, "KEYWARDEN_SOCIAL_LOGIN_PROVIDERS", []) + oidc_login_enabled + or social_providers ) show_native_login = ( request.method == "POST" or request.GET.get("native") == "1" - or (auth_mode == "native" and not has_sso_options) + or auth_mode == "native" ) # native or hybrid -> render Django's built-in login view return auth_views.LoginView.as_view( template_name="accounts/login.html", extra_context={ - "social_providers": getattr(settings, "KEYWARDEN_SOCIAL_LOGIN_PROVIDERS", []), - "oidc_enabled": bool(getattr(settings, "KEYWARDEN_OIDC_ENABLED", False)), + "social_providers": social_providers, + "oidc_enabled": oidc_login_enabled, "oidc_login_url": "/oidc/authenticate/", "oidc_provider_label": _oidc_provider_label(), "show_native_login": show_native_login, diff --git a/app/keywarden/settings/base.py b/app/keywarden/settings/base.py index 4d74fc6..d957433 100644 --- a/app/keywarden/settings/base.py +++ b/app/keywarden/settings/base.py @@ -22,6 +22,12 @@ def env_csv(name: str, default: str = "") -> list[str]: return [item.strip() for item in raw.split(",") if item.strip()] +# Auth mode: native | oidc | hybrid +KEYWARDEN_AUTH_MODE_REQUESTED = os.getenv("KEYWARDEN_AUTH_MODE", "hybrid").lower() +if KEYWARDEN_AUTH_MODE_REQUESTED not in {"native", "oidc", "hybrid"}: + KEYWARDEN_AUTH_MODE_REQUESTED = "hybrid" + + BASE_DIR = Path(__file__).resolve().parent.parent.parent SECRET_KEY = os.getenv("KEYWARDEN_SECRET_KEY") @@ -70,12 +76,15 @@ KEYWARDEN_SOCIAL_APPLE_CONFIGURED = bool( and KEYWARDEN_SOCIAL_APPLE_CLIENT_SECRET ) -KEYWARDEN_SOCIAL_AUTH_ENABLED = any( +KEYWARDEN_SOCIAL_AUTH_ENABLED = ( + KEYWARDEN_AUTH_MODE_REQUESTED == "hybrid" + and any( ( KEYWARDEN_SOCIAL_GOOGLE_CONFIGURED, KEYWARDEN_SOCIAL_GITHUB_CONFIGURED, KEYWARDEN_SOCIAL_APPLE_CONFIGURED, ) + ) ) # Default to secure cookies and respect TLS termination headers. @@ -331,7 +340,8 @@ OIDC_OP_JWKS_ENDPOINT = os.getenv("KEYWARDEN_OIDC_JWKS_ENDPOINT") OIDC_RP_SIGN_ALGO = os.getenv("KEYWARDEN_OIDC_SIGN_ALGO", "RS256") OIDC_RP_SCOPES = os.getenv("KEYWARDEN_OIDC_SCOPES", "openid email profile") OIDC_STORE_ACCESS_TOKEN = env_bool("KEYWARDEN_OIDC_STORE_ACCESS_TOKEN", default=False) -OIDC_CREATE_USER = True +KEYWARDEN_OIDC_ALLOW_AUTO_ONBOARDING = env_bool("KEYWARDEN_OIDC_ALLOW_AUTO_ONBOARDING", default=True) +OIDC_CREATE_USER = KEYWARDEN_OIDC_ALLOW_AUTO_ONBOARDING KEYWARDEN_OIDC_PROVIDER_ID = os.getenv("KEYWARDEN_OIDC_PROVIDER_ID", "oidc") KEYWARDEN_OIDC_ISSUER = os.getenv("KEYWARDEN_OIDC_ISSUER", "").strip() @@ -340,7 +350,10 @@ KEYWARDEN_OIDC_EMAIL_VERIFIED_CLAIM = os.getenv("KEYWARDEN_OIDC_EMAIL_VERIFIED_C KEYWARDEN_OIDC_REQUIRE_VERIFIED_EMAIL = env_bool("KEYWARDEN_OIDC_REQUIRE_VERIFIED_EMAIL", default=True) KEYWARDEN_OIDC_USERNAME_CLAIM = os.getenv("KEYWARDEN_OIDC_USERNAME_CLAIM", "preferred_username") KEYWARDEN_OIDC_GROUPS_CLAIM = os.getenv("KEYWARDEN_OIDC_GROUPS_CLAIM", "groups") -KEYWARDEN_OIDC_SYNC_ADMIN_FROM_GROUPS = env_bool("KEYWARDEN_OIDC_SYNC_ADMIN_FROM_GROUPS", default=False) +KEYWARDEN_OIDC_SYNC_ADMIN_FROM_GROUPS = env_bool( + "KEYWARDEN_OIDC_SYNC_ADMIN_FROM_GROUPS", + default=(KEYWARDEN_AUTH_MODE_REQUESTED == "oidc"), +) KEYWARDEN_OIDC_ADMIN_DEMOTE_ON_MISS = env_bool("KEYWARDEN_OIDC_ADMIN_DEMOTE_ON_MISS", default=False) KEYWARDEN_OIDC_ADMIN_GROUPS = env_csv("KEYWARDEN_OIDC_ADMIN_GROUPS") @@ -358,16 +371,18 @@ KEYWARDEN_OIDC_ENABLED = bool( ) ) +KEYWARDEN_OIDC_LOGIN_ENABLED = KEYWARDEN_OIDC_ENABLED and KEYWARDEN_AUTH_MODE_REQUESTED in {"oidc", "hybrid"} + KEYWARDEN_SOCIAL_LOGIN_PROVIDERS = [] -if KEYWARDEN_SOCIAL_GOOGLE_CONFIGURED: +if KEYWARDEN_SOCIAL_AUTH_ENABLED and KEYWARDEN_SOCIAL_GOOGLE_CONFIGURED: KEYWARDEN_SOCIAL_LOGIN_PROVIDERS.append( {"id": "google", "name": "Google", "login_url": "/accounts/sso/google/login/"} ) -if KEYWARDEN_SOCIAL_GITHUB_CONFIGURED: +if KEYWARDEN_SOCIAL_AUTH_ENABLED and KEYWARDEN_SOCIAL_GITHUB_CONFIGURED: KEYWARDEN_SOCIAL_LOGIN_PROVIDERS.append( {"id": "github", "name": "GitHub", "login_url": "/accounts/sso/github/login/"} ) -if KEYWARDEN_SOCIAL_APPLE_CONFIGURED: +if KEYWARDEN_SOCIAL_AUTH_ENABLED and KEYWARDEN_SOCIAL_APPLE_CONFIGURED: KEYWARDEN_SOCIAL_LOGIN_PROVIDERS.append( {"id": "apple", "name": "Apple", "login_url": "/accounts/sso/apple/login/"} ) @@ -378,7 +393,8 @@ if KEYWARDEN_SOCIAL_AUTH_ENABLED: ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_AUTHENTICATION_METHOD = "email" - SOCIALACCOUNT_AUTO_SIGNUP = True + KEYWARDEN_SOCIAL_ALLOW_AUTO_ONBOARDING = env_bool("KEYWARDEN_SOCIAL_ALLOW_AUTO_ONBOARDING", default=False) + SOCIALACCOUNT_AUTO_SIGNUP = KEYWARDEN_SOCIAL_ALLOW_AUTO_ONBOARDING SOCIALACCOUNT_EMAIL_AUTHENTICATION = False SOCIALACCOUNT_ADAPTER = "apps.accounts.social_auth.KeywardenSocialAccountAdapter" SOCIALACCOUNT_LOGIN_ON_GET = True @@ -426,28 +442,37 @@ KEYWARDEN_SOCIAL_REQUIRE_VERIFIED_EMAIL = env_bool( ) # Auth mode: native | oidc | hybrid -AUTH_MODE = os.getenv("KEYWARDEN_AUTH_MODE", "hybrid").lower() -if AUTH_MODE not in {"native", "oidc", "hybrid"}: - AUTH_MODE = "hybrid" +AUTH_MODE = KEYWARDEN_AUTH_MODE_REQUESTED if AUTH_MODE == "oidc" and not KEYWARDEN_OIDC_ENABLED: AUTH_MODE = "native" KEYWARDEN_AUTH_MODE = AUTH_MODE -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "guardian.backends.ObjectPermissionBackend", -] -if KEYWARDEN_SOCIAL_AUTH_ENABLED: - AUTHENTICATION_BACKENDS.append("allauth.account.auth_backends.AuthenticationBackend") -if KEYWARDEN_OIDC_ENABLED: - AUTHENTICATION_BACKENDS.append("apps.accounts.oidc.KeywardenOIDCAuthenticationBackend") +if AUTH_MODE == "oidc": + AUTHENTICATION_BACKENDS = [ + "apps.accounts.oidc.KeywardenOIDCAuthenticationBackend", + "guardian.backends.ObjectPermissionBackend", + ] +elif AUTH_MODE == "hybrid": + AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "guardian.backends.ObjectPermissionBackend", + ] + if KEYWARDEN_SOCIAL_AUTH_ENABLED: + AUTHENTICATION_BACKENDS.append("allauth.account.auth_backends.AuthenticationBackend") + if KEYWARDEN_OIDC_ENABLED: + AUTHENTICATION_BACKENDS.append("apps.accounts.oidc.KeywardenOIDCAuthenticationBackend") +else: + AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "guardian.backends.ObjectPermissionBackend", + ] if AUTH_MODE == "oidc": # OIDC-only: enforce identity provider logins. LOGIN_URL = "/oidc/authenticate/" else: LOGIN_URL = "/accounts/login/" -LOGOUT_URL = "/oidc/logout/" if KEYWARDEN_OIDC_ENABLED else "/accounts/logout/" +LOGOUT_URL = "/oidc/logout/" if AUTH_MODE in {"oidc", "hybrid"} and KEYWARDEN_OIDC_ENABLED else "/accounts/logout/" LOGIN_REDIRECT_URL = "/servers/" LOGOUT_REDIRECT_URL = "/" diff --git a/app/keywarden/urls.py b/app/keywarden/urls.py index a7dca6d..fc2e26d 100644 --- a/app/keywarden/urls.py +++ b/app/keywarden/urls.py @@ -7,9 +7,11 @@ from keywarden.api import api as ninja_api, api_v1 as ninja_api_v1 urlpatterns = [ path("admin/", admin.site.urls), - path("oidc/", include("mozilla_django_oidc.urls")), ] +if getattr(settings, "KEYWARDEN_OIDC_LOGIN_ENABLED", False): + urlpatterns.append(path("oidc/", include("mozilla_django_oidc.urls"))) + if getattr(settings, "KEYWARDEN_SOCIAL_AUTH_ENABLED", False): urlpatterns.append(path("accounts/sso/", include("allauth.urls")))