Repository: goauthentik/authentik

Issue:
Support for Mailcow OAuth
**Is your feature request related to a problem? Please describe.**
I'd like to use [Mailcow](https://github.com/mailcow/mailcow-dockerized) as an authentication source (via "Federation&social login"). However, they don't seem to support OpenID connect, just some generic OAuth which [works with Nextcloud](https://mailcow.github.io/mailcow-dockerized-docs/third_party/third_party-nextcloud/) but not many other services. Because of this, I'd like to use Authentik as a bridge between Mailcow and other services.

**Describe the solution you'd like**
It would be nice if Mailcow could be supported like other special providers.

**Additional context**
Mailcow only supports the scope `profile`.
This is what the oauth settings page in mailcow looks like:
![image](https://user-images.githubusercontent.com/30153207/155430853-17f29a81-8763-41d3-881d-8642fa69e04f.png)



Discussion:

Resolving patch:
diff --git a/authentik/sources/oauth/apps.py b/authentik/sources/oauth/apps.py
--- a/authentik/sources/oauth/apps.py
+++ b/authentik/sources/oauth/apps.py
@@ -17,6 +17,7 @@
     "authentik.sources.oauth.types.okta",
     "authentik.sources.oauth.types.reddit",
     "authentik.sources.oauth.types.twitter",
+    "authentik.sources.oauth.types.mailcow",
 ]
 
 
diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py
--- a/authentik/sources/oauth/models.py
+++ b/authentik/sources/oauth/models.py
@@ -111,6 +111,16 @@ class Meta:
         verbose_name_plural = _("GitHub OAuth Sources")
 
 
+class MailcowOAuthSource(OAuthSource):
+    """Social Login using Mailcow."""
+
+    class Meta:
+
+        abstract = True
+        verbose_name = _("Mailcow OAuth Source")
+        verbose_name_plural = _("Mailcow OAuth Sources")
+
+
 class TwitterOAuthSource(OAuthSource):
     """Social Login using Twitter.com"""
 
diff --git a/authentik/sources/oauth/types/mailcow.py b/authentik/sources/oauth/types/mailcow.py
new file mode 100644
--- /dev/null
+++ b/authentik/sources/oauth/types/mailcow.py
@@ -0,0 +1,69 @@
+"""Mailcow OAuth Views"""
+from typing import Any, Optional
+
+from requests.exceptions import RequestException
+from structlog.stdlib import get_logger
+
+from authentik.sources.oauth.clients.oauth2 import OAuth2Client
+from authentik.sources.oauth.types.manager import MANAGER, SourceType
+from authentik.sources.oauth.views.callback import OAuthCallback
+from authentik.sources.oauth.views.redirect import OAuthRedirect
+
+LOGGER = get_logger()
+
+
+class MailcowOAuthRedirect(OAuthRedirect):
+    """Mailcow OAuth2 Redirect"""
+
+    def get_additional_parameters(self, source):  # pragma: no cover
+        return {
+            "scope": ["profile"],
+        }
+
+
+class MailcowOAuth2Client(OAuth2Client):
+    """MailcowOAuth2Client, for some reason, mailcow does not like the default headers"""
+
+    def get_profile_info(self, token: dict[str, str]) -> Optional[dict[str, Any]]:
+        "Fetch user profile information."
+        profile_url = self.source.type.profile_url or ""
+        if self.source.type.urls_customizable and self.source.profile_url:
+            profile_url = self.source.profile_url
+        try:
+            response = self.session.request(
+                "get",
+                f"{profile_url}?access_token={token['access_token']}",
+            )
+            response.raise_for_status()
+        except RequestException as exc:
+            LOGGER.warning("Unable to fetch user profile", exc=exc)
+            return None
+        else:
+            return response.json()
+
+
+class MailcowOAuth2Callback(OAuthCallback):
+    """Mailcow OAuth2 Callback"""
+
+    client_class = MailcowOAuth2Client
+
+    def get_user_enroll_context(
+        self,
+        info: dict[str, Any],
+    ) -> dict[str, Any]:
+        return {
+            "email": info.get("email"),
+            "name": info.get("full_name"),
+        }
+
+
+@MANAGER.type()
+class MailcowType(SourceType):
+    """Mailcow Type definition"""
+
+    callback_view = MailcowOAuth2Callback
+    redirect_view = MailcowOAuthRedirect
+    name = "Mailcow"
+    slug = "mailcow"
+
+    urls_customizable = True


Test patch:
diff --git a/authentik/sources/oauth/tests/test_type_mailcow.py b/authentik/sources/oauth/tests/test_type_mailcow.py
new file mode 100644
--- /dev/null
+++ b/authentik/sources/oauth/tests/test_type_mailcow.py
@@ -0,0 +1,38 @@
+"""Mailcow Type tests"""
+from django.test import TestCase
+
+from authentik.sources.oauth.models import OAuthSource
+from authentik.sources.oauth.types.mailcow import MailcowOAuth2Callback
+
+# https://community.mailcow.email/d/13-mailcow-oauth-json-format/2
+MAILCOW_USER = {
+    "success": True,
+    "username": "email@example.com",
+    "identifier": "email@example.com",
+    "email": "email@example.com",
+    "full_name": "Example User",
+    "displayName": "Example User",
+    "created": "2020-05-15 11:33:08",
+    "modified": "2020-05-15 12:23:31",
+    "active": 1,
+}
+
+
+class TestTypeMailcow(TestCase):
+    """OAuth Source tests"""
+
+    def setUp(self):
+        self.source = OAuthSource.objects.create(
+            name="test",
+            slug="test",
+            provider_type="mailcow",
+            authorization_url="",
+            profile_url="",
+            consumer_key="",
+        )
+
+    def test_enroll_context(self):
+        """Test mailcow Enrollment context"""
+        ak_context = MailcowOAuth2Callback().get_user_enroll_context(MAILCOW_USER)
+        self.assertEqual(ak_context["email"], MAILCOW_USER["email"])
+        self.assertEqual(ak_context["name"], MAILCOW_USER["full_name"])