sources/oauth: add additional scopes field to get additional data from provider

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2047
This commit is contained in:
Jens Langhammer 2022-01-03 16:43:52 +01:00
parent a596392bc3
commit 212220554f
20 changed files with 136 additions and 13 deletions

View file

@ -74,6 +74,7 @@ class OAuthSourceSerializer(SourceSerializer):
"consumer_key", "consumer_key",
"consumer_secret", "consumer_secret",
"callback_url", "callback_url",
"additional_scopes",
"type", "type",
] ]
extra_kwargs = {"consumer_secret": {"write_only": True}} extra_kwargs = {"consumer_secret": {"write_only": True}}
@ -99,6 +100,7 @@ class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
"access_token_url", "access_token_url",
"profile_url", "profile_url",
"consumer_key", "consumer_key",
"additional_scopes",
] ]
ordering = ["name"] ordering = ["name"]

View file

@ -58,6 +58,9 @@ class BaseOAuthClient:
args = self.get_redirect_args() args = self.get_redirect_args()
additional = parameters or {} additional = parameters or {}
args.update(additional) 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) params = urlencode(args, quote_via=quote)
LOGGER.info("redirect args", **args) LOGGER.info("redirect args", **args)
authorization_url = self.source.type.authorization_url or "" authorization_url = self.source.type.authorization_url or ""

View file

@ -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"),
),
]

View file

@ -44,6 +44,7 @@ class OAuthSource(Source):
verbose_name=_("Profile URL"), verbose_name=_("Profile URL"),
help_text=_("URL used by authentik to get user information."), help_text=_("URL used by authentik to get user information."),
) )
additional_scopes = models.TextField(default="", verbose_name=_("Additional Scopes"))
consumer_key = models.TextField() consumer_key = models.TextField()
consumer_secret = models.TextField() consumer_secret = models.TextField()

View file

@ -1,8 +1,11 @@
"""google Type tests""" """google Type tests"""
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase 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.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 # https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en
GOOGLE_USER = { GOOGLE_USER = {
@ -21,17 +24,51 @@ class TestTypeGoogle(TestCase):
"""OAuth Source tests""" """OAuth Source tests"""
def setUp(self): def setUp(self):
self.source = OAuthSource.objects.create( self.source: OAuthSource = OAuthSource.objects.create(
name="test", name="test",
slug="test", slug="test",
provider_type="google", provider_type="google",
authorization_url="", authorization_url="",
profile_url="", profile_url="",
consumer_key="", consumer_key="foo",
) )
self.request_factory = RequestFactory()
def test_enroll_context(self): def test_enroll_context(self):
"""Test Google Enrollment context""" """Test Google Enrollment context"""
ak_context = GoogleOAuth2Callback().get_user_enroll_context(GOOGLE_USER) ak_context = GoogleOAuth2Callback().get_user_enroll_context(GOOGLE_USER)
self.assertEqual(ak_context["email"], GOOGLE_USER["email"]) self.assertEqual(ak_context["email"], GOOGLE_USER["email"])
self.assertEqual(ak_context["name"], GOOGLE_USER["name"]) 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"
),
)

View file

@ -78,7 +78,7 @@ class AppleOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return { return {
"scope": "name email", "scope": ["name", "email"],
"response_mode": "form_post", "response_mode": "form_post",
} }

View file

@ -17,7 +17,7 @@ class AzureADOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source): # pragma: no cover def get_additional_parameters(self, source): # pragma: no cover
return { return {
"scope": "openid https://graph.microsoft.com/User.Read", "scope": ["openid", "https://graph.microsoft.com/User.Read"],
} }

View file

@ -11,7 +11,7 @@ class DiscordOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source): # pragma: no cover def get_additional_parameters(self, source): # pragma: no cover
return { return {
"scope": "email identify", "scope": ["email", "identify"],
"prompt": "none", "prompt": "none",
} }

View file

@ -14,7 +14,7 @@ class FacebookOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source): # pragma: no cover def get_additional_parameters(self, source): # pragma: no cover
return { return {
"scope": "email", "scope": ["email"],
} }

View file

@ -11,7 +11,7 @@ class GoogleOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source): # pragma: no cover def get_additional_parameters(self, source): # pragma: no cover
return { return {
"scope": "email profile", "scope": ["email", "profile"],
} }

View file

@ -12,7 +12,7 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return { return {
"scope": "openid email profile", "scope": ["openid", "email", "profile"],
} }

View file

@ -13,7 +13,7 @@ class OktaOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return { return {
"scope": "openid email profile", "scope": ["openid", "email", "profile"],
} }

View file

@ -14,7 +14,7 @@ class RedditOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source): # pragma: no cover def get_additional_parameters(self, source): # pragma: no cover
return { return {
"scope": "identity", "scope": ["identity"],
"duration": "permanent", "duration": "permanent",
} }

View file

@ -34,7 +34,7 @@ class OAuthRedirect(OAuthClientMixin, RedirectView):
"Build redirect url for a given source." "Build redirect url for a given source."
slug = kwargs.get("source_slug", "") slug = kwargs.get("source_slug", "")
try: try:
source = OAuthSource.objects.get(slug=slug) source: OAuthSource = OAuthSource.objects.get(slug=slug)
except OAuthSource.DoesNotExist: except OAuthSource.DoesNotExist:
raise Http404(f"Unknown OAuth source '{slug}'.") raise Http404(f"Unknown OAuth source '{slug}'.")
else: else:
@ -42,4 +42,7 @@ class OAuthRedirect(OAuthClientMixin, RedirectView):
raise Http404(f"source {slug} is not enabled.") raise Http404(f"source {slug} is not enabled.")
client = self.get_client(source, callback=self.get_callback_url(source)) client = self.get_client(source, callback=self.get_callback_url(source))
params = self.get_additional_parameters(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) return client.get_redirect_url(params)

View file

@ -12437,6 +12437,10 @@ paths:
name: access_token_url name: access_token_url
schema: schema:
type: string type: string
- in: query
name: additional_scopes
schema:
type: string
- in: query - in: query
name: authentication_flow name: authentication_flow
schema: schema:
@ -23342,6 +23346,8 @@ components:
callback_url: callback_url:
type: string type: string
readOnly: true readOnly: true
additional_scopes:
type: string
type: type:
allOf: allOf:
- $ref: '#/components/schemas/SourceType' - $ref: '#/components/schemas/SourceType'
@ -23425,6 +23431,9 @@ components:
type: string type: string
writeOnly: true writeOnly: true
minLength: 1 minLength: 1
additional_scopes:
type: string
minLength: 1
required: required:
- consumer_key - consumer_key
- consumer_secret - consumer_secret
@ -27645,6 +27654,9 @@ components:
type: string type: string
writeOnly: true writeOnly: true
minLength: 1 minLength: 1
additional_scopes:
type: string
minLength: 1
PatchedOutpostRequest: PatchedOutpostRequest:
type: object type: object
description: Outpost Serializer description: Outpost Serializer

View file

@ -208,6 +208,10 @@ msgstr "Addition Group DN"
msgid "Addition User DN" msgid "Addition User DN"
msgstr "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 #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional group DN, prepended to the Base DN." msgid "Additional group DN, prepended to the Base DN."
msgstr "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." msgid "Additional scope mappings, which are passed to the proxy."
msgstr "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 #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional settings" msgid "Additional settings"
msgstr "Additional settings" msgstr "Additional settings"

View file

@ -213,6 +213,10 @@ msgstr "Préfixe DN groupes"
msgid "Addition User DN" msgid "Addition User DN"
msgstr "Préfixe DN utilisateurs" msgstr "Préfixe DN utilisateurs"
#: src/pages/sources/oauth/OAuthSourceForm.ts
msgid "Additional Scope"
msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional group DN, prepended to the Base DN." msgid "Additional group DN, prepended to the Base DN."
msgstr "DN à préfixer au DN de base pour les groupes" 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." msgid "Additional scope mappings, which are passed to the proxy."
msgstr "" 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 #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional settings" msgid "Additional settings"
msgstr "Paramètres supplémentaire" msgstr "Paramètres supplémentaire"

View file

@ -208,6 +208,10 @@ msgstr ""
msgid "Addition User DN" msgid "Addition User DN"
msgstr "" msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts
msgid "Additional Scope"
msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional group DN, prepended to the Base DN." msgid "Additional group DN, prepended to the Base DN."
msgstr "" msgstr ""
@ -216,6 +220,10 @@ msgstr ""
msgid "Additional scope mappings, which are passed to the proxy." msgid "Additional scope mappings, which are passed to the proxy."
msgstr "" 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 #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional settings" msgid "Additional settings"
msgstr "" msgstr ""

View file

@ -210,6 +210,10 @@ msgstr "Toplama Grubu DN"
msgid "Addition User DN" msgid "Addition User DN"
msgstr "Ekleme Kullanıcı DN" msgstr "Ekleme Kullanıcı DN"
#: src/pages/sources/oauth/OAuthSourceForm.ts
msgid "Additional Scope"
msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional group DN, prepended to the Base DN." msgid "Additional group DN, prepended to the Base DN."
msgstr "Ek grup DN, Base DN için eklenmiş." 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." msgid "Additional scope mappings, which are passed to the proxy."
msgstr "Proxy'ye iletilen ek kapsam eşlemeleri." 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 #: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Additional settings" msgid "Additional settings"
msgstr "Ek ayarlar" msgstr "Ek ayarlar"

View file

@ -52,7 +52,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
} }
@property({ attribute: false }) @property({ attribute: false })
providerType?: SourceType; providerType: SourceType | null = null;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
@ -257,6 +257,21 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
</div> </div>
</ak-form-group> </ak-form-group>
${this.renderUrlOptions()} ${this.renderUrlOptions()}
<ak-form-element-horizontal
label=${t`Additional Scope`}
?required=${true}
name="additionalScopes"
>
<input
type="text"
value="${first(this.instance?.additionalScopes, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Additional scopes to be passed to the OAuth Provider, separated by space.`}
</p>
</ak-form-element-horizontal>
<ak-form-group> <ak-form-group>
<span slot="header"> ${t`Flow settings`} </span> <span slot="header"> ${t`Flow settings`} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">