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_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"]
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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"),
|
||||
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()
|
||||
|
||||
|
|
|
@ -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"
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class FacebookOAuthRedirect(OAuthRedirect):
|
|||
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": "email",
|
||||
"scope": ["email"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ class GoogleOAuthRedirect(OAuthRedirect):
|
|||
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": "email profile",
|
||||
"scope": ["email", "profile"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class RedditOAuthRedirect(OAuthRedirect):
|
|||
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": "identity",
|
||||
"scope": ["identity"],
|
||||
"duration": "permanent",
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
12
schema.yml
12
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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -52,7 +52,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
|||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
providerType?: SourceType;
|
||||
providerType: SourceType | null = null;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance) {
|
||||
|
@ -257,6 +257,21 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
|||
</div>
|
||||
</ak-form-group>
|
||||
${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>
|
||||
<span slot="header"> ${t`Flow settings`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
|
|
Reference in a new issue