diff --git a/authentik/sources/oauth/apps.py b/authentik/sources/oauth/apps.py index b3d6fd67e..04a9a506d 100644 --- a/authentik/sources/oauth/apps.py +++ b/authentik/sources/oauth/apps.py @@ -12,12 +12,13 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [ "authentik.sources.oauth.types.facebook", "authentik.sources.oauth.types.github", "authentik.sources.oauth.types.google", + "authentik.sources.oauth.types.mailcow", "authentik.sources.oauth.types.oidc", "authentik.sources.oauth.types.okta", + "authentik.sources.oauth.types.patreon", "authentik.sources.oauth.types.reddit", - "authentik.sources.oauth.types.twitter", - "authentik.sources.oauth.types.mailcow", "authentik.sources.oauth.types.twitch", + "authentik.sources.oauth.types.twitter", ] diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index 80d55f147..602cde4ea 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -163,6 +163,15 @@ class DiscordOAuthSource(OAuthSource): verbose_name_plural = _("Discord OAuth Sources") +class PatreonOAuthSource(OAuthSource): + """Social Login using Patreon.""" + + class Meta: + abstract = True + verbose_name = _("Patreon OAuth Source") + verbose_name_plural = _("Patreon OAuth Sources") + + class GoogleOAuthSource(OAuthSource): """Social Login using Google or Google Workspace (GSuite).""" diff --git a/authentik/sources/oauth/tests/test_type_patreon.py b/authentik/sources/oauth/tests/test_type_patreon.py new file mode 100644 index 000000000..670b7d665 --- /dev/null +++ b/authentik/sources/oauth/tests/test_type_patreon.py @@ -0,0 +1,67 @@ +"""Patreon Type tests""" +from django.test import RequestFactory, TestCase + +from authentik.sources.oauth.models import OAuthSource +from authentik.sources.oauth.types.patreon import PatreonOAuthCallback + +PATREON_USER = { + "data": { + "attributes": { + "about": None, + "created": "2017-10-20T21:36:23+00:00", + "discord_id": None, + "email": "corgi@example.com", + "facebook": None, + "facebook_id": None, + "first_name": "Corgi", + "full_name": "Corgi The Dev", + "gender": 0, + "has_password": True, + "image_url": "https://c8.patreon.com/2/400/0000000", + "is_deleted": False, + "is_email_verified": False, + "is_nuked": False, + "is_suspended": False, + "last_name": "The Dev", + "social_connections": { + "deviantart": None, + "discord": None, + "facebook": None, + "reddit": None, + "spotify": None, + "twitch": None, + "twitter": None, + "youtube": None, + }, + "thumb_url": "https://c8.patreon.com/2/100/0000000", + "twitch": None, + "twitter": None, + "url": "https://www.patreon.com/corgithedev", + "vanity": "corgithedev", + "youtube": None, + }, + "id": "0000000", + "relationships": {"pledges": {"data": []}}, + "type": "user", + }, + "links": {"self": "https://www.patreon.com/api/user/0000000"}, +} + + +class TestTypePatreon(TestCase): + """OAuth Source tests""" + + def setUp(self): + self.source = OAuthSource.objects.create( + name="test", + slug="test", + provider_type="Patreon", + ) + self.factory = RequestFactory() + + def test_enroll_context(self): + """Test Patreon Enrollment context""" + ak_context = PatreonOAuthCallback().get_user_enroll_context(PATREON_USER) + self.assertEqual(ak_context["username"], PATREON_USER["data"]["attributes"]["vanity"]) + self.assertEqual(ak_context["email"], PATREON_USER["data"]["attributes"]["email"]) + self.assertEqual(ak_context["name"], PATREON_USER["data"]["attributes"]["full_name"]) diff --git a/authentik/sources/oauth/types/patreon.py b/authentik/sources/oauth/types/patreon.py new file mode 100644 index 000000000..2b54533ff --- /dev/null +++ b/authentik/sources/oauth/types/patreon.py @@ -0,0 +1,50 @@ +"""Patreon OAuth Views""" +from typing import Any + +from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient +from authentik.sources.oauth.models import OAuthSource +from authentik.sources.oauth.types.registry import SourceType, registry +from authentik.sources.oauth.views.callback import OAuthCallback +from authentik.sources.oauth.views.redirect import OAuthRedirect + + +class PatreonOAuthRedirect(OAuthRedirect): + """Patreon OAuth2 Redirect""" + + def get_additional_parameters(self, source: OAuthSource): # pragma: no cover + return { + "scope": ["openid", "email", "profile"], + } + + +class PatreonOAuthCallback(OAuthCallback): + """Patreon OAuth2 Callback""" + + client_class: UserprofileHeaderAuthClient + + def get_user_id(self, info: dict[str, str]) -> str: + return info.get("data", {}).get("id") + + def get_user_enroll_context( + self, + info: dict[str, Any], + ) -> dict[str, Any]: + return { + "username": info.get("data", {}).get("attributes", {}).get("vanity"), + "email": info.get("data", {}).get("attributes", {}).get("email"), + "name": info.get("data", {}).get("attributes", {}).get("full_name"), + } + + +@registry.register() +class PatreonType(SourceType): + """OpenIDConnect Type definition""" + + callback_view = PatreonOAuthCallback + redirect_view = PatreonOAuthRedirect + name = "Patreon" + slug = "patreon" + + authorization_url = "https://www.patreon.com/oauth2/authorize" + access_token_url = "https://www.patreon.com/api/oauth2/token" # nosec + profile_url = "https://www.patreon.com/api/oauth2/api/current_user" diff --git a/web/authentik/sources/patreon.svg b/web/authentik/sources/patreon.svg new file mode 100644 index 000000000..1056fa48e --- /dev/null +++ b/web/authentik/sources/patreon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file