diff --git a/cookiecutter.json b/cookiecutter.json index d5e1803..e498ade 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -13,6 +13,9 @@ "use_flower": "n", "use_fingerprinting": "n", "use_channels": "y", + "use_allauth": "n", + "allauth_trust_external_emails": "y", + "allauth_providers": "google", "sentry_dsn": "", "csp_enabled": "n", "csp_report_only": "y", diff --git a/{{cookiecutter.repostory_name}}/README.md b/{{cookiecutter.repostory_name}}/README.md index 3d41972..e877781 100644 --- a/{{cookiecutter.repostory_name}}/README.md +++ b/{{cookiecutter.repostory_name}}/README.md @@ -103,6 +103,48 @@ git push --force production local-branch-to-deploy:master +{% if cookiecutter.use_allauth == 'y' %} +# External auth (OAuth, OpenID connect etc.) +To configure an external authentication mechanism, usually you must acquire a "client ID" and a "client secret". +This usually requires registering your application on the provider's website. Look at allauth's documentation for the +specific provider to see how to do that: + +[https://docs.allauth.org/en/latest/socialaccount/providers/index.html](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) + +After acquiring the id and secret, simply fill in the env vars for the provider. +{% if cookiecutter.allauth_trust_external_emails == "y" %} + +> ⚠️ Caution: the SSO provider is trusted to have verified the ownership of user's email address. +> This will allow a user to log in to any account that matches the email address returned by the +> SSO provider, whether the account is connected with the provider or not. + +{% endif %} + +{%- if 'openid_connect' in cookiecutter.allauth_providers %} +## Setting up a generic OpenID Connect service +
+If an SSO provider supports the OIDC protocol, it can be set up as a generic OIDC provider here: + +1. Come up with a new `provider_id` + - it's just an arbitrary alphanumerical string to identify the provider in the app + - it must be unique in the scope of the app + - it should not collide with the name of an installed provider type - so don't use `gitlab`, `google` or similar + - something like `rt_keycloak` would be OK +2. Register the app with the provider to acquire a `client_id`, a `secret` and the URL for the openid config (e.g. https://gitlab.com/.well-known/openid-configuration) + - When asked for callback / redirect url, use `https://{domain}/accounts/oidc/{provider_id}/login/callback/` + - For development, usually http://127.0.0.1:8000 can be used as the base URL here +3. Fill in the `OPENID_CONNECT_*` env vars + - `OPENID_CONNECT_NICE_NAME` is just a human-readable name, it will be later shown on login form (Log in with {name}...) + - the `OPENID_CONNECT_SERVER_URL` value is just the URL **before** the .well-known part, so for https://gitlab.com/.well-known/openid-configuration this is just https://gitlab.com +
+ +{% endif %} +## Allauth users in django +1. Allauth does not disable django's authentication. It lives next to it as an alternative. You can still access django admin login. +2. Allauth "social users" are just an extension to regular django users. When someone logs in via allauth, a django user model will also be created for them. +3. A "profile" page is available at `/accounts/` + +{% endif %} {% if cookiecutter.monitoring == 'y' %} # Monitoring diff --git a/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/settings.py b/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/settings.py index 84ab290..e2886c2 100644 --- a/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/settings.py +++ b/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/settings.py @@ -13,6 +13,9 @@ {% if cookiecutter.use_celery == "y" -%} # from celery.schedules import crontab +{% endif %} +{%- if cookiecutter.use_allauth == "y" -%} +from django.urls import reverse_lazy {% endif -%} import structlog @@ -59,6 +62,13 @@ def wrapped(*args, **kwargs): ALLOWED_HOSTS = ["*"] +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + {%- if cookiecutter.use_allauth == "y" %} + "allauth.account.auth_backends.AuthenticationBackend", + {%- endif %} +] + INSTALLED_APPS = [ {%- if cookiecutter.use_channels == "y" %} "daphne", @@ -87,9 +97,44 @@ def wrapped(*args, **kwargs): "django_probes", "django_structlog", "constance", - {% if cookiecutter.use_fingerprinting == "y" -%} + {%- if cookiecutter.use_fingerprinting == "y" %} "fingerprint", - {% endif -%} + {%- endif %} + {%- if cookiecutter.use_allauth == "y" %} + "allauth", + "allauth.account", + "allauth.socialaccount", + {%- if 'apple' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.apple", + {%- endif %} + {%- if 'atlassian' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.atlassian", + {%- endif %} + {%- if 'discord' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.discord", + {%- endif %} + {%- if 'facebook' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.facebook", + {%- endif %} + {%- if 'github' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.github", + {%- endif %} + {%- if 'gitlab' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.gitlab", + {%- endif %} + {%- if 'google' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.google", + {%- endif %} + {%- if 'microsoft' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.microsoft", + {%- endif %} + {%- if 'openid_connect' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.openid_connect", + {%- endif %} + {%- if 'twitter' in cookiecutter.allauth_providers -%} + "allauth.socialaccount.providers.twitter_oauth2", + {%- endif %} + {%- endif %} "{{cookiecutter.django_project_name}}.{{cookiecutter.django_default_app_name}}", ] @@ -131,6 +176,9 @@ def wrapped(*args, **kwargs): "django_prometheus.middleware.PrometheusAfterMiddleware", {%- endif %} "django_structlog.middlewares.RequestMiddleware", + {%- if cookiecutter.use_allauth == "y" -%} + "allauth.account.middleware.AccountMiddleware", + {%- endif %} ] @@ -297,6 +345,8 @@ def wrapped(*args, **kwargs): CELERY_RESULT_SERIALIZER = "json" CELERY_WORKER_PREFETCH_MULTIPLIER = env.int("CELERY_WORKER_PREFETCH_MULTIPLIER", default=10) CELERY_BROKER_POOL_LIMIT = env.int("CELERY_BROKER_POOL_LIMIT", default=50) + +DJANGO_STRUCTLOG_CELERY_ENABLED = True {%- endif %} EMAIL_BACKEND = env("EMAIL_BACKEND") @@ -355,7 +405,6 @@ def wrapped(*args, **kwargs): }, }, } -DJANGO_STRUCTLOG_CELERY_ENABLED = True def configure_structlog(): @@ -382,7 +431,9 @@ def configure_structlog(): # Sentry if SENTRY_DSN := env("SENTRY_DSN", default=""): import sentry_sdk + {% if cookiecutter.use_celery == "y" -%} from sentry_sdk.integrations.celery import CeleryIntegration + {% endif -%} from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger from sentry_sdk.integrations.redis import RedisIntegration @@ -392,7 +443,9 @@ def configure_structlog(): environment=ENV, integrations=[ DjangoIntegration(), + {% if cookiecutter.use_celery == "y" -%} CeleryIntegration(), + {% endif -%} RedisIntegration(), LoggingIntegration( level=logging.INFO, # Capture info and above as breadcrumbs @@ -401,3 +454,112 @@ def configure_structlog(): ], ) ignore_logger("django.security.DisallowedHost") +{% if cookiecutter.use_allauth == "y" -%} +LOGIN_URL = reverse_lazy("account_login") +LOGIN_REDIRECT_URL = "/" +ACCOUNT_AUTHENTICATION_METHOD = "email" +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = "mandatory" +ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_CHANGE_EMAIL = False +ACCOUNT_MAX_EMAIL_ADDRESSES = 1 +{%- if cookiecutter.allauth_trust_external_emails == "y" %} +# Trust, that the configured SSO providers verify that the users own the addresses that we get from the SSO flow. +# This allows users to log in to any existing account with any configured provider if the email addresses match. +SOCIALACCOUNT_EMAIL_AUTHENTICATION: True +{%- endif %} +SOCIALACCOUNT_PROVIDERS = { + {%- if 'apple' in cookiecutter.allauth_providers %} + "apple": { + "APP": { + "client_id": env("APPLE_LOGIN_CLIENT_ID"), + "secret": env("APPLE_LOGIN_SECRET"), + "key": env("APPLE_LOGIN_KEY"), + "settings": { + "certificate_key": env("APPLE_LOGIN_CERTIFICATE_PRIVATE_KEY"), + }, + }, + }, + {%- endif %} + {%- if 'atlassian' in cookiecutter.allauth_providers %} + "atlassian": { + "APP": { + "client_id": env("ATLASSIAN_LOGIN_CLIENT_ID"), + "secret": env("ATLASSIAN_LOGIN_SECRET"), + }, + }, + {%- endif %} + {%- if 'discord' in cookiecutter.allauth_providers %} + "discord": { + "APP": { + "client_id": env("DISCORD_LOGIN_CLIENT_ID"), + "secret": env("DISCORD_LOGIN_SECRET"), + }, + }, + {%- endif %} + {%- if 'facebook' in cookiecutter.allauth_providers %} + "facebook": { + "APP": { + "client_id": env("FACEBOOK_LOGIN_CLIENT_ID"), + "secret": env("FACEBOOK_LOGIN_SECRET"), + }, + }, + {%- endif %} + {%- if 'github' in cookiecutter.allauth_providers %} + "github": { + "APP": { + "client_id": env("GITHUB_LOGIN_CLIENT_ID"), + "secret": env("GITHUB_LOGIN_SECRET"), + }, + }, + {%- endif %} + {%- if 'gitlab' in cookiecutter.allauth_providers %} + "gitlab": { + "APP": { + "client_id": env("GITLAB_LOGIN_CLIENT_ID"), + "secret": env("GITLAB_LOGIN_SECRET"), + }, + }, + {%- endif %} + {%- if 'google' in cookiecutter.allauth_providers %} + "google": { + "APP": { + "client_id": env("GOOGLE_LOGIN_CLIENT_ID"), + "secret": env("GOOGLE_LOGIN_SECRET"), + }, + }, + {%- endif %} + {%- if 'microsoft' in cookiecutter.allauth_providers %} + "microsoft": { + "APP": { + "client_id": env("MICROSOFT_LOGIN_CLIENT_ID"), + "secret": env("MICROSOFT_LOGIN_SECRET"), + "settings": { + "tenant": "organizations", + }, + }, + }, + {%- endif %} + {%- if 'twitter' in cookiecutter.allauth_providers %} + "twitter_oauth2": { + "APP": { + "client_id": env("TWITTER_LOGIN_CLIENT_ID"), + "secret": env("TWITTER_LOGIN_SECRET"), + }, + }, + {%- endif %} + {%- if 'openid_connect' in cookiecutter.allauth_providers %} + "openid_connect": { + "APP": { + "client_id": "oidc", + "name": env("OPENID_CONNECT_NICE_NAME"), + "secret": env("OPENID_CONNECT_LOGIN_SECRET"), + "settings": { + "server_url": env("OPENID_CONNECT_SERVER_URL") + } + }, + }, + {%- endif %} +} +{%- endif %} \ No newline at end of file diff --git a/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/urls.py b/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/urls.py index 7305cd0..24d22cc 100644 --- a/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/urls.py +++ b/{{cookiecutter.repostory_name}}/app/src/{{cookiecutter.django_project_name}}/urls.py @@ -16,7 +16,6 @@ urlpatterns = [ path("admin/", site.urls), - path("", include("django.contrib.auth.urls")), {%- if cookiecutter.use_fingerprinting == "y" %} path("redirect/", FingerprintView.as_view(), name="fingerprint"), {%- endif %} @@ -25,6 +24,11 @@ path("business-metrics", metrics_manager.view, name="prometheus-business-metrics"), path("healthcheck/", include("health_check.urls")), {%- endif %} + {%- if cookiecutter.use_allauth == "y" %} + path('accounts/', include('allauth.urls')), + {%- else %} + path("", include("django.contrib.auth.urls")), + {%- endif %} ] {%- if cookiecutter.use_channels == "y" %} diff --git a/{{cookiecutter.repostory_name}}/envs/dev/.env.template b/{{cookiecutter.repostory_name}}/envs/dev/.env.template index 07649d1..d8f8e29 100644 --- a/{{cookiecutter.repostory_name}}/envs/dev/.env.template +++ b/{{cookiecutter.repostory_name}}/envs/dev/.env.template @@ -70,3 +70,50 @@ BACKUP_B2_BUCKET= BACKUP_B2_KEY_ID= BACKUP_B2_KEY_SECRET= BACKUP_LOCAL_ROTATE_KEEP_LAST= + +{% if cookiecutter.use_allauth == "y" -%} +{%- if 'apple' in cookiecutter.allauth_providers -%} +APPLE_LOGIN_CLIENT_ID= +APPLE_LOGIN_SECRET= +APPLE_LOGIN_KEY= +APPLE_LOGIN_CERTIFICATE_PRIVATE_KEY= +{% endif %} +{%- if 'discord' in cookiecutter.allauth_providers -%} +DISCORD_LOGIN_CLIENT_ID= +DISCORD_LOGIN_SECRET= +{% endif %} +{%- if 'facebook' in cookiecutter.allauth_providers -%} +FACEBOOK_LOGIN_CLIENT_ID= +FACEBOOK_LOGIN_SECRET= +{% endif %} +{%- if 'github' in cookiecutter.allauth_providers -%} +GITHUB_LOGIN_CLIENT_ID= +GITHUB_LOGIN_SECRET= +{% endif %} +{%- if 'gitlab' in cookiecutter.allauth_providers -%} +GITLAB_LOGIN_CLIENT_ID= +GITLAB_LOGIN_SECRET= +{% endif %} +{%- if 'google' in cookiecutter.allauth_providers -%} +GOOGLE_LOGIN_CLIENT_ID= +GOOGLE_LOGIN_SECRET= +{% endif %} +{%- if 'microsoft' in cookiecutter.allauth_providers -%} +MICROSOFT_LOGIN_CLIENT_ID= +MICROSOFT_LOGIN_SECRET= +{% endif %} +{%- if 'openid_connect' in cookiecutter.allauth_providers -%} +OPENID_CONNECT_NICE_NAME= +OPENID_CONNECT_LOGIN_CLIENT_ID= +OPENID_CONNECT_LOGIN_SECRET= +OPENID_CONNECT_SERVER_URL= +{% endif %} +{%- if 'twitter' in cookiecutter.allauth_providers -%} +TWITTER_LOGIN_CLIENT_ID= +TWITTER_LOGIN_SECRET= +{% endif %} +{%- if 'atlassian' in cookiecutter.allauth_providers -%} +ATLASSIAN_LOGIN_CLIENT_ID= +ATLASSIAN_LOGIN_SECRET= +{% endif %} +{% endif %} \ No newline at end of file diff --git a/{{cookiecutter.repostory_name}}/envs/prod/.env.template b/{{cookiecutter.repostory_name}}/envs/prod/.env.template index 47fb77a..ded54dc 100644 --- a/{{cookiecutter.repostory_name}}/envs/prod/.env.template +++ b/{{cookiecutter.repostory_name}}/envs/prod/.env.template @@ -79,3 +79,50 @@ BACKUP_B2_BUCKET= BACKUP_B2_KEY_ID= BACKUP_B2_KEY_SECRET= BACKUP_LOCAL_ROTATE_KEEP_LAST= + +{% if cookiecutter.use_allauth == "y" -%} +{%- if 'apple' in cookiecutter.allauth_providers -%} +APPLE_LOGIN_CLIENT_ID= +APPLE_LOGIN_SECRET= +APPLE_LOGIN_KEY= +APPLE_LOGIN_CERTIFICATE_PRIVATE_KEY= +{% endif %} +{%- if 'discord' in cookiecutter.allauth_providers -%} +DISCORD_LOGIN_CLIENT_ID= +DISCORD_LOGIN_SECRET= +{% endif %} +{%- if 'facebook' in cookiecutter.allauth_providers -%} +FACEBOOK_LOGIN_CLIENT_ID= +FACEBOOK_LOGIN_SECRET= +{% endif %} +{%- if 'github' in cookiecutter.allauth_providers -%} +GITHUB_LOGIN_CLIENT_ID= +GITHUB_LOGIN_SECRET= +{% endif %} +{%- if 'gitlab' in cookiecutter.allauth_providers -%} +GITLAB_LOGIN_CLIENT_ID= +GITLAB_LOGIN_SECRET= +{% endif %} +{%- if 'google' in cookiecutter.allauth_providers -%} +GOOGLE_LOGIN_CLIENT_ID= +GOOGLE_LOGIN_SECRET= +{% endif %} +{%- if 'microsoft' in cookiecutter.allauth_providers -%} +MICROSOFT_LOGIN_CLIENT_ID= +MICROSOFT_LOGIN_SECRET= +{% endif %} +{%- if 'openid_connect' in cookiecutter.allauth_providers -%} +OPENID_CONNECT_NICE_NAME= +OPENID_CONNECT_LOGIN_CLIENT_ID= +OPENID_CONNECT_LOGIN_SECRET= +OPENID_CONNECT_SERVER_URL= +{% endif %} +{%- if 'twitter' in cookiecutter.allauth_providers -%} +TWITTER_LOGIN_CLIENT_ID= +TWITTER_LOGIN_SECRET= +{% endif %} +{%- if 'atlassian' in cookiecutter.allauth_providers -%} +ATLASSIAN_LOGIN_CLIENT_ID= +ATLASSIAN_LOGIN_SECRET= +{% endif %} +{% endif %} \ No newline at end of file diff --git a/{{cookiecutter.repostory_name}}/pyproject.toml b/{{cookiecutter.repostory_name}}/pyproject.toml index b7d8f3f..af7fe09 100644 --- a/{{cookiecutter.repostory_name}}/pyproject.toml +++ b/{{cookiecutter.repostory_name}}/pyproject.toml @@ -42,6 +42,9 @@ dependencies = [ "uvicorn[standard]==0.29", "pydantic~=2.0", {% endif -%} + {% if cookiecutter.use_allauth == "y" -%} + "django-allauth[socialaccount]~=0.63.1", + {% endif -%} ] [build-system]