diff --git a/authentik/sources/oauth/api/source.py b/authentik/sources/oauth/api/source.py index db2e38384..b288b909d 100644 --- a/authentik/sources/oauth/api/source.py +++ b/authentik/sources/oauth/api/source.py @@ -74,6 +74,7 @@ class OAuthSourceSerializer(SourceSerializer): "consumer_key", "consumer_secret", "callback_url", + "additional_scopes", "type", ] extra_kwargs = {"consumer_secret": {"write_only": True}} @@ -99,6 +100,7 @@ class OAuthSourceViewSet(UsedByMixin, ModelViewSet): "access_token_url", "profile_url", "consumer_key", + "additional_scopes", ] ordering = ["name"] diff --git a/authentik/sources/oauth/clients/base.py b/authentik/sources/oauth/clients/base.py index 401f39656..33e05d2be 100644 --- a/authentik/sources/oauth/clients/base.py +++ b/authentik/sources/oauth/clients/base.py @@ -58,6 +58,9 @@ class BaseOAuthClient: args = self.get_redirect_args() additional = parameters or {} args.update(additional) + # Special handling for scope, since it's set as array + # to make additional scopes easier + args["scope"] = " ".join(sorted(set(args["scope"]))) params = urlencode(args, quote_via=quote) LOGGER.info("redirect args", **args) authorization_url = self.source.type.authorization_url or "" diff --git a/authentik/sources/oauth/migrations/0006_oauthsource_additional_scopes.py b/authentik/sources/oauth/migrations/0006_oauthsource_additional_scopes.py new file mode 100644 index 000000000..6574716e2 --- /dev/null +++ b/authentik/sources/oauth/migrations/0006_oauthsource_additional_scopes.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0 on 2022-01-03 14:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_sources_oauth", "0005_update_provider_type_names"), + ] + + operations = [ + migrations.AddField( + model_name="oauthsource", + name="additional_scopes", + field=models.TextField(default="", verbose_name="Additional Scopes"), + ), + ] diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index de7156442..18d96311c 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -44,6 +44,7 @@ class OAuthSource(Source): verbose_name=_("Profile URL"), help_text=_("URL used by authentik to get user information."), ) + additional_scopes = models.TextField(default="", verbose_name=_("Additional Scopes")) consumer_key = models.TextField() consumer_secret = models.TextField() diff --git a/authentik/sources/oauth/tests/test_type_google.py b/authentik/sources/oauth/tests/test_type_google.py index 8c7b8fe05..b31a9a513 100644 --- a/authentik/sources/oauth/tests/test_type_google.py +++ b/authentik/sources/oauth/tests/test_type_google.py @@ -1,8 +1,11 @@ """google Type tests""" +from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase +from django.test.client import RequestFactory +from authentik.lib.tests.utils import dummy_get_response from authentik.sources.oauth.models import OAuthSource -from authentik.sources.oauth.types.google import GoogleOAuth2Callback +from authentik.sources.oauth.types.google import GoogleOAuth2Callback, GoogleOAuthRedirect # https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en GOOGLE_USER = { @@ -21,17 +24,51 @@ class TestTypeGoogle(TestCase): """OAuth Source tests""" def setUp(self): - self.source = OAuthSource.objects.create( + self.source: OAuthSource = OAuthSource.objects.create( name="test", slug="test", provider_type="google", authorization_url="", profile_url="", - consumer_key="", + consumer_key="foo", ) + self.request_factory = RequestFactory() def test_enroll_context(self): """Test Google Enrollment context""" ak_context = GoogleOAuth2Callback().get_user_enroll_context(GOOGLE_USER) self.assertEqual(ak_context["email"], GOOGLE_USER["email"]) self.assertEqual(ak_context["name"], GOOGLE_USER["name"]) + + def test_authorize_url(self): + """Test authorize URL""" + request = self.request_factory.get("/") + middleware = SessionMiddleware(dummy_get_response) + middleware.process_request(request) + request.session.save() + redirect = GoogleOAuthRedirect(request=request).get_redirect_url( + source_slug=self.source.slug + ) + self.assertEqual( + redirect, + ( + f"https://accounts.google.com/o/oauth2/auth?client_id={self.source.consumer_key}&re" + "direct_uri=http%3A%2F%2Ftestserver%2Fsource%2Foauth%2Fcallback%2Ftest%2F&response_" + f"type=code&state={request.session['oauth-client-test-request-state']}&scope=" + "email%20profile" + ), + ) + self.source.additional_scopes = "foo" + self.source.save() + redirect = GoogleOAuthRedirect(request=request).get_redirect_url( + source_slug=self.source.slug + ) + self.assertEqual( + redirect, + ( + f"https://accounts.google.com/o/oauth2/auth?client_id={self.source.consumer_key}&re" + "direct_uri=http%3A%2F%2Ftestserver%2Fsource%2Foauth%2Fcallback%2Ftest%2F&response_" + f"type=code&state={request.session['oauth-client-test-request-state']}&scope=" + "email%20foo%20profile" + ), + ) diff --git a/authentik/sources/oauth/types/apple.py b/authentik/sources/oauth/types/apple.py index 6265b6ccd..452531828 100644 --- a/authentik/sources/oauth/types/apple.py +++ b/authentik/sources/oauth/types/apple.py @@ -78,7 +78,7 @@ class AppleOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source: OAuthSource): # pragma: no cover return { - "scope": "name email", + "scope": ["name", "email"], "response_mode": "form_post", } diff --git a/authentik/sources/oauth/types/azure_ad.py b/authentik/sources/oauth/types/azure_ad.py index cb0252454..02d0d18bf 100644 --- a/authentik/sources/oauth/types/azure_ad.py +++ b/authentik/sources/oauth/types/azure_ad.py @@ -17,7 +17,7 @@ class AzureADOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source): # pragma: no cover return { - "scope": "openid https://graph.microsoft.com/User.Read", + "scope": ["openid", "https://graph.microsoft.com/User.Read"], } diff --git a/authentik/sources/oauth/types/discord.py b/authentik/sources/oauth/types/discord.py index cee0aa07b..5bf1a47b1 100644 --- a/authentik/sources/oauth/types/discord.py +++ b/authentik/sources/oauth/types/discord.py @@ -11,7 +11,7 @@ class DiscordOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source): # pragma: no cover return { - "scope": "email identify", + "scope": ["email", "identify"], "prompt": "none", } diff --git a/authentik/sources/oauth/types/facebook.py b/authentik/sources/oauth/types/facebook.py index 8efe16102..2297b69ff 100644 --- a/authentik/sources/oauth/types/facebook.py +++ b/authentik/sources/oauth/types/facebook.py @@ -14,7 +14,7 @@ class FacebookOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source): # pragma: no cover return { - "scope": "email", + "scope": ["email"], } diff --git a/authentik/sources/oauth/types/google.py b/authentik/sources/oauth/types/google.py index 2819c3153..5d6865e70 100644 --- a/authentik/sources/oauth/types/google.py +++ b/authentik/sources/oauth/types/google.py @@ -11,7 +11,7 @@ class GoogleOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source): # pragma: no cover return { - "scope": "email profile", + "scope": ["email", "profile"], } diff --git a/authentik/sources/oauth/types/oidc.py b/authentik/sources/oauth/types/oidc.py index 309dbeb95..59ed16b08 100644 --- a/authentik/sources/oauth/types/oidc.py +++ b/authentik/sources/oauth/types/oidc.py @@ -12,7 +12,7 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source: OAuthSource): # pragma: no cover return { - "scope": "openid email profile", + "scope": ["openid", "email", "profile"], } diff --git a/authentik/sources/oauth/types/okta.py b/authentik/sources/oauth/types/okta.py index 5f03bda53..7bb3b97be 100644 --- a/authentik/sources/oauth/types/okta.py +++ b/authentik/sources/oauth/types/okta.py @@ -13,7 +13,7 @@ class OktaOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source: OAuthSource): # pragma: no cover return { - "scope": "openid email profile", + "scope": ["openid", "email", "profile"], } diff --git a/authentik/sources/oauth/types/reddit.py b/authentik/sources/oauth/types/reddit.py index 53757b38e..ff58d3a3c 100644 --- a/authentik/sources/oauth/types/reddit.py +++ b/authentik/sources/oauth/types/reddit.py @@ -14,7 +14,7 @@ class RedditOAuthRedirect(OAuthRedirect): def get_additional_parameters(self, source): # pragma: no cover return { - "scope": "identity", + "scope": ["identity"], "duration": "permanent", } diff --git a/authentik/sources/oauth/views/redirect.py b/authentik/sources/oauth/views/redirect.py index 39f1faa8b..8e58a3a67 100644 --- a/authentik/sources/oauth/views/redirect.py +++ b/authentik/sources/oauth/views/redirect.py @@ -34,7 +34,7 @@ class OAuthRedirect(OAuthClientMixin, RedirectView): "Build redirect url for a given source." slug = kwargs.get("source_slug", "") try: - source = OAuthSource.objects.get(slug=slug) + source: OAuthSource = OAuthSource.objects.get(slug=slug) except OAuthSource.DoesNotExist: raise Http404(f"Unknown OAuth source '{slug}'.") else: @@ -42,4 +42,7 @@ class OAuthRedirect(OAuthClientMixin, RedirectView): raise Http404(f"source {slug} is not enabled.") client = self.get_client(source, callback=self.get_callback_url(source)) params = self.get_additional_parameters(source) + params.setdefault("scope", []) + if source.additional_scopes != "": + params["scope"] += source.additional_scopes.split(" ") return client.get_redirect_url(params) diff --git a/schema.yml b/schema.yml index f71cbfbf0..2d084975a 100644 --- a/schema.yml +++ b/schema.yml @@ -12437,6 +12437,10 @@ paths: name: access_token_url schema: type: string + - in: query + name: additional_scopes + schema: + type: string - in: query name: authentication_flow schema: @@ -23342,6 +23346,8 @@ components: callback_url: type: string readOnly: true + additional_scopes: + type: string type: allOf: - $ref: '#/components/schemas/SourceType' @@ -23425,6 +23431,9 @@ components: type: string writeOnly: true minLength: 1 + additional_scopes: + type: string + minLength: 1 required: - consumer_key - consumer_secret @@ -27645,6 +27654,9 @@ components: type: string writeOnly: true minLength: 1 + additional_scopes: + type: string + minLength: 1 PatchedOutpostRequest: type: object description: Outpost Serializer diff --git a/web/src/locales/en.po b/web/src/locales/en.po index fdc85fdae..364a72ae7 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -208,6 +208,10 @@ msgstr "Addition Group DN" msgid "Addition User DN" msgstr "Addition User DN" +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional Scope" +msgstr "Additional Scope" + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional group DN, prepended to the Base DN." msgstr "Additional group DN, prepended to the Base DN." @@ -216,6 +220,10 @@ msgstr "Additional group DN, prepended to the Base DN." msgid "Additional scope mappings, which are passed to the proxy." msgstr "Additional scope mappings, which are passed to the proxy." +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional scopes to be passed to the OAuth Provider, separated by space." +msgstr "Additional scopes to be passed to the OAuth Provider, separated by space." + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional settings" msgstr "Additional settings" diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index b7a1a17f6..de7d1134c 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -213,6 +213,10 @@ msgstr "Préfixe DN groupes" msgid "Addition User DN" msgstr "Préfixe DN utilisateurs" +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional Scope" +msgstr "" + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional group DN, prepended to the Base DN." msgstr "DN à préfixer au DN de base pour les groupes" @@ -221,6 +225,10 @@ msgstr "DN à préfixer au DN de base pour les groupes" msgid "Additional scope mappings, which are passed to the proxy." msgstr "" +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional scopes to be passed to the OAuth Provider, separated by space." +msgstr "" + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional settings" msgstr "Paramètres supplémentaire" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index cb2dfd868..fea023855 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -208,6 +208,10 @@ msgstr "" msgid "Addition User DN" msgstr "" +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional Scope" +msgstr "" + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional group DN, prepended to the Base DN." msgstr "" @@ -216,6 +220,10 @@ msgstr "" msgid "Additional scope mappings, which are passed to the proxy." msgstr "" +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional scopes to be passed to the OAuth Provider, separated by space." +msgstr "" + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional settings" msgstr "" diff --git a/web/src/locales/tr.po b/web/src/locales/tr.po index 6f5110e37..0fc2cb55f 100644 --- a/web/src/locales/tr.po +++ b/web/src/locales/tr.po @@ -210,6 +210,10 @@ msgstr "Toplama Grubu DN" msgid "Addition User DN" msgstr "Ekleme Kullanıcı DN" +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional Scope" +msgstr "" + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional group DN, prepended to the Base DN." msgstr "Ek grup DN, Base DN için eklenmiş." @@ -218,6 +222,10 @@ msgstr "Ek grup DN, Base DN için eklenmiş." msgid "Additional scope mappings, which are passed to the proxy." msgstr "Proxy'ye iletilen ek kapsam eşlemeleri." +#: src/pages/sources/oauth/OAuthSourceForm.ts +msgid "Additional scopes to be passed to the OAuth Provider, separated by space." +msgstr "" + #: src/pages/sources/ldap/LDAPSourceForm.ts msgid "Additional settings" msgstr "Ek ayarlar" diff --git a/web/src/pages/sources/oauth/OAuthSourceForm.ts b/web/src/pages/sources/oauth/OAuthSourceForm.ts index 517a50c15..9a61c1a46 100644 --- a/web/src/pages/sources/oauth/OAuthSourceForm.ts +++ b/web/src/pages/sources/oauth/OAuthSourceForm.ts @@ -52,7 +52,7 @@ export class OAuthSourceForm extends ModelForm { } @property({ attribute: false }) - providerType?: SourceType; + providerType: SourceType | null = null; getSuccessMessage(): string { if (this.instance) { @@ -257,6 +257,21 @@ export class OAuthSourceForm extends ModelForm { ${this.renderUrlOptions()} + + +

+ ${t`Additional scopes to be passed to the OAuth Provider, separated by space.`} +

+
${t`Flow settings`}