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:
parent
a596392bc3
commit
212220554f
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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"),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
12
schema.yml
12
schema.yml
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Reference in a new issue