diff --git a/authentik/sources/oauth/apps.py b/authentik/sources/oauth/apps.py index 1dbeb54e1..8ed2f295b 100644 --- a/authentik/sources/oauth/apps.py +++ b/authentik/sources/oauth/apps.py @@ -17,6 +17,7 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [ "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 index 87958a8a8..35888b3c3 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -111,6 +111,16 @@ class GitHubOAuthSource(OAuthSource): 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/tests/test_type_mailcow.py b/authentik/sources/oauth/tests/test_type_mailcow.py new file mode 100644 index 000000000..2687bf3cf --- /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"]) diff --git a/authentik/sources/oauth/types/mailcow.py b/authentik/sources/oauth/types/mailcow.py new file mode 100644 index 000000000..55bd5cc75 --- /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 diff --git a/schema.yml b/schema.yml index 675843f2b..2285fc88f 100644 --- a/schema.yml +++ b/schema.yml @@ -29017,6 +29017,7 @@ components: - okta - reddit - twitter + - mailcow type: string ProxyMode: enum: diff --git a/web/authentik/sources/mailcow.svg b/web/authentik/sources/mailcow.svg new file mode 100644 index 000000000..d1bab08ed --- /dev/null +++ b/web/authentik/sources/mailcow.svg @@ -0,0 +1,182 @@ + + + +image/svg+xml diff --git a/website/integrations/sources/mailcow/index.md b/website/integrations/sources/mailcow/index.md new file mode 100644 index 000000000..8f507e88a --- /dev/null +++ b/website/integrations/sources/mailcow/index.md @@ -0,0 +1,52 @@ +--- +title: Mailcow +--- + +Allows users to authenticate using their Mailcow credentials + +## Preparation + +The following placeholders will be used: + +- `authentik.company` is the FQDN of the authentik install. +- `mailcow.company` is the FQDN of the mailcow install. + +## Mailcow + +1. Log into mailcow as an admin and navigate to the OAuth2 Apps settings + +![OAuth2 Apps menu](mailcow1.png) + +2. Click "Add OAuth2 Client" + +3. Insert the redirect URL: `https://authentik.company/source/oauth/callback/mailcow` + +![Add OAuth2 CLient](mailcow2.png) + +4. Copy the **Client ID** and **Client secret** and _save it for later_ + +![ClientID and Secret](mailcow3.png) + +## Authentik + +5. Under _Directory -> Federation & Social login_ Click **Create > Mailcow OAuth Source** + +![Mailcow OAuth Source](mailcow4.png) + +6. **Name:** Choose a name (For the example I used Mailcow) +7. **Slug:** mailcow (You can choose a different slug, if you do you will need to update the Mailcow redirect URL and point it to the correct slug.) +8. **Consumer Key:** Client ID from step 4 +9. **Consumer Secret:** Client Secret from step 4 +10. **Authorization URL:** https://mailcow.company/oauth/authorize +11. **Access token URL:** https://mailcow.company/oauth/token +12. **Profile URL:** https://mailcow.company/oauth/profile + +Here is an example of a complete authentik Mailcow OAuth Source + +![Example Screen](mailcow5.png) + +Save, and you now have Mailcow as a source. + +:::note +For more details on how-to have the new source display on the Login Page see [here](../). +::: \ No newline at end of file diff --git a/website/integrations/sources/mailcow/mailcow1.png b/website/integrations/sources/mailcow/mailcow1.png new file mode 100644 index 000000000..a6fdbc53b Binary files /dev/null and b/website/integrations/sources/mailcow/mailcow1.png differ diff --git a/website/integrations/sources/mailcow/mailcow2.png b/website/integrations/sources/mailcow/mailcow2.png new file mode 100644 index 000000000..05f622c2b Binary files /dev/null and b/website/integrations/sources/mailcow/mailcow2.png differ diff --git a/website/integrations/sources/mailcow/mailcow3.png b/website/integrations/sources/mailcow/mailcow3.png new file mode 100644 index 000000000..b2a173081 Binary files /dev/null and b/website/integrations/sources/mailcow/mailcow3.png differ diff --git a/website/integrations/sources/mailcow/mailcow4.png b/website/integrations/sources/mailcow/mailcow4.png new file mode 100644 index 000000000..b4a494087 Binary files /dev/null and b/website/integrations/sources/mailcow/mailcow4.png differ diff --git a/website/integrations/sources/mailcow/mailcow5.png b/website/integrations/sources/mailcow/mailcow5.png new file mode 100644 index 000000000..6fa81f40d Binary files /dev/null and b/website/integrations/sources/mailcow/mailcow5.png differ diff --git a/website/sidebarsIntegrations.js b/website/sidebarsIntegrations.js index 8fdd4af9a..074147161 100644 --- a/website/sidebarsIntegrations.js +++ b/website/sidebarsIntegrations.js @@ -60,6 +60,7 @@ module.exports = { "sources/github/index", "sources/google/index", "sources/ldap/index", + "sources/mailcow/index", "sources/oauth/index", "sources/plex/index", "sources/saml/index",