Compare commits
13 Commits
website/do
...
dos-new-sc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18e7cc4378 | ||
|
|
ce3f826f57 | ||
|
|
e3483677d8 | ||
|
|
45f9b40877 | ||
|
|
1d3a0e9f0a | ||
|
|
a712e5bb2f | ||
|
|
4cfb61f83b | ||
|
|
30b82ea683 | ||
|
|
e0316ff2e8 | ||
|
|
2c3d11a4c3 | ||
|
|
a3c50ae92a | ||
|
|
3ef36b9e9e | ||
|
|
691e173cad |
2
.github/actions/setup/action.yml
vendored
@@ -64,7 +64,7 @@ runs:
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@e3134ec54b36203e18f2d1e80652058bd078dd91 # v2
|
||||
uses: taiki-e/install-action@ec28e287910af896fd98e04056d31fa68607e7ad # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
|
||||
20
.npmrc
Normal file
@@ -0,0 +1,20 @@
|
||||
# Block lifecycle scripts (preinstall/install/postinstall/prepare) from dependencies.
|
||||
# This neutralizes the dominant npm supply-chain attack vector.
|
||||
#
|
||||
# Packages that legitimately need a build step (e.g. esbuild, chromedriver, tree-sitter)
|
||||
# must be rebuilt explicitly:
|
||||
#
|
||||
# npm rebuild --foreground-scripts esbuild chromedriver tree-sitter tree-sitter-json
|
||||
ignore-scripts=true
|
||||
|
||||
# Fail fast if the active Node/npm doesn't match the "engines" field.
|
||||
engine-strict=true
|
||||
|
||||
# Pin exact versions so `npm install <pkg>` writes "1.2.3" not "^1.2.3".
|
||||
save-exact=true
|
||||
|
||||
# Surface CVE warnings during install; doesn't block.
|
||||
audit=true
|
||||
|
||||
# Suppress funding banners.
|
||||
fund=false
|
||||
@@ -34,6 +34,7 @@ packages/django-channels-postgres @goauthentik/backend
|
||||
packages/django-postgres-cache @goauthentik/backend
|
||||
packages/django-dramatiq-postgres @goauthentik/backend
|
||||
# Web packages
|
||||
.npmrc @goauthentik/frontend
|
||||
tsconfig.json @goauthentik/frontend
|
||||
package.json @goauthentik/frontend
|
||||
package-lock.json @goauthentik/frontend
|
||||
|
||||
18
Makefile
@@ -125,7 +125,7 @@ core-i18n-extract:
|
||||
--ignore website \
|
||||
-l en
|
||||
|
||||
install: node-install docs-install core-install ## Install all requires dependencies for `node`, `docs` and `core`
|
||||
install: node-install web-install core-install ## Install all requires dependencies for `node`, `web` and `core`
|
||||
|
||||
dev-drop-db:
|
||||
$(eval pg_user := $(shell $(UV) run python -m authentik.lib.config postgresql.user 2>/dev/null))
|
||||
@@ -228,14 +228,26 @@ gen-dev-config: ## Generate a local development config file
|
||||
## Node.js
|
||||
#########################
|
||||
|
||||
# Packages whose install/postinstall scripts are required for correct
|
||||
# operation (binary downloads, native bindings). The root .npmrc sets
|
||||
# `ignore-scripts=true` to block dependency lifecycle scripts by default;
|
||||
# this list is rebuilt explicitly with scripts re-enabled. Audit any
|
||||
# additions: each entry runs arbitrary code at install time.
|
||||
TRUSTED_INSTALL_SCRIPTS := esbuild chromedriver tree-sitter tree-sitter-json
|
||||
|
||||
node-install: ## Install the necessary libraries to build Node.js packages
|
||||
npm ci
|
||||
npm ci --prefix web
|
||||
|
||||
#########################
|
||||
## Web
|
||||
#########################
|
||||
|
||||
web-install: ## Install the necessary libraries to build the Authentik UI
|
||||
npm ci --prefix web
|
||||
|
||||
web-postinstall: ## Trigger postinstall scripts for packages with native bindings or binary downloads, which are blocked by default for security reasons.
|
||||
npm rebuild --prefix web --ignore-scripts=false --foreground-scripts $(TRUSTED_INSTALL_SCRIPTS)
|
||||
|
||||
web-build: node-install ## Build the Authentik UI
|
||||
npm run --prefix web build
|
||||
|
||||
@@ -268,7 +280,7 @@ web-i18n-extract:
|
||||
|
||||
docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Authentik docs source code, lint the code, and compile it
|
||||
|
||||
docs-install:
|
||||
docs-install: node-install
|
||||
npm ci --prefix website
|
||||
|
||||
docs-lint-fix: lint-spellcheck
|
||||
|
||||
@@ -36,6 +36,8 @@ Our [enterprise offering](https://goauthentik.io/pricing) is available for organ
|
||||
|
||||
See the [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/) for information about setting up local build environments, testing your contributions, and our contribution process.
|
||||
|
||||
When you contribute documentation, either to accompany a code change or as a standalone contribution, please be sure to follow our documentation [Style Guide](website/docs/developer-docs/docs/style-guide.mdx).
|
||||
|
||||
## Security
|
||||
|
||||
Please see [SECURITY.md](SECURITY.md).
|
||||
|
||||
@@ -31,7 +31,6 @@ class DeviceUser(VirtualUser):
|
||||
username = "authentik:endpoints:device"
|
||||
|
||||
def has_perm(self, perm: str, obj: Model | None = None) -> bool:
|
||||
print(perm)
|
||||
if perm in [
|
||||
"authentik_core.view_user",
|
||||
"authentik_core.view_group",
|
||||
|
||||
@@ -1,14 +1,72 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode, SCIMProvider
|
||||
from authentik.sources.oauth.models import UserOAuthSourceConnection
|
||||
|
||||
|
||||
class SCIMProviderSerializerMixin:
|
||||
|
||||
def _get_token(self, instance: SCIMProvider) -> UserOAuthSourceConnection | None:
|
||||
user = instance.auth_oauth_user
|
||||
conn = UserOAuthSourceConnection.objects.filter(
|
||||
user=user, source=instance.auth_oauth
|
||||
).first()
|
||||
return conn
|
||||
|
||||
def get_auth_oauth_token_last_updated(self, instance: SCIMProvider) -> datetime | None:
|
||||
conn = self._get_token(instance)
|
||||
return conn.last_updated if conn else None
|
||||
|
||||
def get_auth_oauth_token_expires(self, instance: SCIMProvider) -> datetime | None:
|
||||
conn = self._get_token(instance)
|
||||
return conn.expires if conn else None
|
||||
|
||||
def get_auth_oauth_url_callback(self, instance: SCIMProvider) -> str | None:
|
||||
if (
|
||||
instance.auth_mode
|
||||
in [
|
||||
SCIMAuthenticationMode.TOKEN,
|
||||
SCIMAuthenticationMode.OAUTH_SILENT,
|
||||
]
|
||||
or not instance.backchannel_application
|
||||
):
|
||||
return None
|
||||
relative_url = reverse(
|
||||
"authentik_enterprise_providers_scim:callback",
|
||||
kwargs={"application_slug": instance.backchannel_application.slug},
|
||||
)
|
||||
if "request" not in self.context:
|
||||
return relative_url
|
||||
return self.context["request"].build_absolute_uri(relative_url)
|
||||
|
||||
def get_auth_oauth_url_start(self, instance: SCIMProvider) -> str | None:
|
||||
if (
|
||||
instance.auth_mode
|
||||
in [
|
||||
SCIMAuthenticationMode.TOKEN,
|
||||
SCIMAuthenticationMode.OAUTH_SILENT,
|
||||
]
|
||||
or not instance.backchannel_application
|
||||
):
|
||||
return None
|
||||
relative_url = reverse(
|
||||
"authentik_enterprise_providers_scim:start",
|
||||
kwargs={"application_slug": instance.backchannel_application.slug},
|
||||
)
|
||||
if "request" not in self.context:
|
||||
return relative_url
|
||||
return self.context["request"].build_absolute_uri(relative_url)
|
||||
|
||||
def validate_auth_mode(self, auth_mode: SCIMAuthenticationMode) -> SCIMAuthenticationMode:
|
||||
if auth_mode == SCIMAuthenticationMode.OAUTH:
|
||||
if auth_mode in [
|
||||
SCIMAuthenticationMode.OAUTH_SILENT,
|
||||
SCIMAuthenticationMode.OAUTH_INTERACTIVE,
|
||||
]:
|
||||
if not LicenseKey.cached_summary().status.is_valid:
|
||||
raise ValidationError(_("Enterprise is required to use the OAuth mode."))
|
||||
return auth_mode
|
||||
|
||||
@@ -7,3 +7,4 @@ class AuthentikEnterpriseProviderSCIMConfig(EnterpriseConfig):
|
||||
label = "authentik_enterprise_providers_scim"
|
||||
verbose_name = "authentik Enterprise.Providers.SCIM"
|
||||
default = True
|
||||
mountpoint = "application/scim/"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from django.utils.timezone import now
|
||||
from requests import Request, RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.common.oauth.constants import GRANT_TYPE_PASSWORD, GRANT_TYPE_REFRESH_TOKEN
|
||||
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode
|
||||
from authentik.sources.oauth.clients.base import BaseOAuthClient
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,23 +20,26 @@ class SCIMOAuthException(SCIMRequestException):
|
||||
|
||||
|
||||
class SCIMOAuthAuth:
|
||||
|
||||
def __init__(self, provider: SCIMProvider):
|
||||
self.provider = provider
|
||||
self.user = provider.auth_oauth_user
|
||||
self.logger = get_logger().bind()
|
||||
self.connection = self.get_connection()
|
||||
|
||||
def retrieve_token(self):
|
||||
if not self.provider.auth_oauth:
|
||||
return None
|
||||
def retrieve_token(self, conn: UserOAuthSourceConnection | None) -> dict[str, Any]:
|
||||
source: OAuthSource = self.provider.auth_oauth
|
||||
client = OAuth2Client(source, None)
|
||||
client: BaseOAuthClient = source.source_type.callback_view(request=None).get_client(source)
|
||||
access_token_url = source.source_type.access_token_url or ""
|
||||
if source.source_type.urls_customizable and source.access_token_url:
|
||||
access_token_url = source.access_token_url
|
||||
data = client.get_access_token_args(None, None)
|
||||
data["grant_type"] = "password"
|
||||
if self.provider.auth_mode == SCIMAuthenticationMode.OAUTH_SILENT:
|
||||
data["grant_type"] = GRANT_TYPE_PASSWORD
|
||||
elif self.provider.auth_mode == SCIMAuthenticationMode.OAUTH_INTERACTIVE:
|
||||
data["grant_type"] = GRANT_TYPE_REFRESH_TOKEN
|
||||
if not conn:
|
||||
raise SCIMOAuthException(None, "Could not refresh SCIM OAuth token")
|
||||
data["refresh_token"] = conn.refresh_token
|
||||
data.update(self.provider.auth_oauth_params)
|
||||
try:
|
||||
response = client.do_request(
|
||||
@@ -54,12 +59,14 @@ class SCIMOAuthAuth:
|
||||
raise SCIMOAuthException(exc.response, message="Failed to get OAuth token") from exc
|
||||
|
||||
def get_connection(self):
|
||||
token = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.provider.auth_oauth, user=self.user, expires__gt=now()
|
||||
if not self.provider.auth_oauth:
|
||||
return None
|
||||
conn = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.provider.auth_oauth, user=self.user
|
||||
).first()
|
||||
if token and token.access_token:
|
||||
return token
|
||||
token = self.retrieve_token()
|
||||
if conn and conn.access_token and conn.expires > now():
|
||||
return conn
|
||||
token = self.retrieve_token(conn)
|
||||
access_token = token["access_token"]
|
||||
expires_in = int(token.get("expires_in", 0))
|
||||
token, _ = UserOAuthSourceConnection.objects.update_or_create(
|
||||
@@ -67,7 +74,10 @@ class SCIMOAuthAuth:
|
||||
user=self.user,
|
||||
defaults={
|
||||
"access_token": access_token,
|
||||
"refresh_token": token.get("refresh_token"),
|
||||
"expires": now() + timedelta(seconds=expires_in),
|
||||
# When using `update_or_create`, `last_updated` is not updated
|
||||
"last_updated": now(),
|
||||
},
|
||||
)
|
||||
return token
|
||||
|
||||
@@ -14,7 +14,10 @@ def scim_provider_post_save(sender: type[Model], instance: SCIMProvider, created
|
||||
"""Create service account before provider is saved"""
|
||||
identifier = f"ak-providers-scim-{instance.pk}"
|
||||
with audit_ignore():
|
||||
if instance.auth_mode == SCIMAuthenticationMode.OAUTH:
|
||||
if instance.auth_mode in [
|
||||
SCIMAuthenticationMode.OAUTH_SILENT,
|
||||
SCIMAuthenticationMode.OAUTH_INTERACTIVE,
|
||||
]:
|
||||
user, user_created = User.objects.update_or_create(
|
||||
username=identifier,
|
||||
defaults={
|
||||
|
||||
73
authentik/enterprise/providers/scim/tests/test_api.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""SCIM OAuth tests"""
|
||||
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.enterprise.tests.test_license import expiry_valid
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
|
||||
class TestSCIMOAuthAPI(APITestCase):
|
||||
"""SCIM User tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = OAuthSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
access_token_url="http://localhost/token", # nosec
|
||||
consumer_key=generate_id(),
|
||||
consumer_secret=generate_id(),
|
||||
provider_type="openidconnect",
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=expiry_valid,
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_api_create(self):
|
||||
License.objects.create(key=generate_id())
|
||||
self.client.force_login(create_test_admin_user())
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:scimprovider-list"),
|
||||
{
|
||||
"name": generate_id(),
|
||||
"url": "http://localhost",
|
||||
"auth_mode": "oauth",
|
||||
"auth_oauth": str(self.source.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.models.LicenseUsageStatus.is_valid",
|
||||
PropertyMock(return_value=False),
|
||||
)
|
||||
def test_api_create_no_license(self):
|
||||
self.client.force_login(create_test_admin_user())
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:scimprovider-list"),
|
||||
{
|
||||
"name": generate_id(),
|
||||
"url": "http://localhost",
|
||||
"auth_mode": "oauth",
|
||||
"auth_oauth": str(self.source.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content, {"auth_mode": ["Enterprise is required to use the OAuth mode."]}
|
||||
)
|
||||
100
authentik/enterprise/providers/scim/tests/test_auth.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""SCIM OAuth tests"""
|
||||
|
||||
from requests_mock import Mocker
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode, SCIMMapping, SCIMProvider
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class TestSCIMOAuthAuth(APITestCase):
|
||||
"""SCIM User tests"""
|
||||
|
||||
@apply_blueprint("system/providers-scim.yaml")
|
||||
def setUp(self) -> None:
|
||||
# Delete all users and groups as the mocked HTTP responses only return one ID
|
||||
# which will cause errors with multiple users
|
||||
Tenant.objects.update(avatars="none")
|
||||
User.objects.all().exclude_anonymous().delete()
|
||||
Group.objects.all().delete()
|
||||
self.source = OAuthSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
access_token_url="http://localhost/token", # nosec
|
||||
consumer_key=generate_id(),
|
||||
consumer_secret=generate_id(),
|
||||
provider_type="openidconnect",
|
||||
)
|
||||
self.provider = SCIMProvider.objects.create(
|
||||
name=generate_id(),
|
||||
url="https://localhost",
|
||||
auth_mode=SCIMAuthenticationMode.OAUTH_SILENT,
|
||||
auth_oauth=self.source,
|
||||
auth_oauth_params={
|
||||
"foo": "bar",
|
||||
},
|
||||
exclude_users_service_account=True,
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
self.provider.property_mappings.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
|
||||
)
|
||||
self.provider.property_mappings_group.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
|
||||
)
|
||||
|
||||
@Mocker()
|
||||
def test_user_create(self, mock: Mocker):
|
||||
"""Test user creation"""
|
||||
scim_id = generate_id()
|
||||
token = generate_id()
|
||||
mock.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
mock.post(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 3)
|
||||
self.assertEqual(mock.request_history[1].method, "GET")
|
||||
self.assertEqual(mock.request_history[2].method, "POST")
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[2].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
"primary": True,
|
||||
"type": "other",
|
||||
"value": f"{uid}@goauthentik.io",
|
||||
}
|
||||
],
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from base64 import b64encode
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
@@ -11,17 +11,14 @@ from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.enterprise.tests.test_license import expiry_valid
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode, SCIMMapping, SCIMProvider
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.tenants.models import Tenant
|
||||
from tests.live import create_test_admin_user
|
||||
|
||||
|
||||
class SCIMOAuthTests(APITestCase):
|
||||
class TestSCIMOAuthToken(APITestCase):
|
||||
"""SCIM User tests"""
|
||||
|
||||
@apply_blueprint("system/providers-scim.yaml")
|
||||
@@ -42,7 +39,7 @@ class SCIMOAuthTests(APITestCase):
|
||||
self.provider = SCIMProvider.objects.create(
|
||||
name=generate_id(),
|
||||
url="https://localhost",
|
||||
auth_mode=SCIMAuthenticationMode.OAUTH,
|
||||
auth_mode=SCIMAuthenticationMode.OAUTH_SILENT,
|
||||
auth_oauth=self.source,
|
||||
auth_oauth_params={
|
||||
"foo": "bar",
|
||||
@@ -60,8 +57,9 @@ class SCIMOAuthTests(APITestCase):
|
||||
self.provider.property_mappings_group.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
|
||||
)
|
||||
self.admin = create_test_admin_user()
|
||||
|
||||
def test_retrieve_token(self):
|
||||
def test_retrieve_token_silent(self):
|
||||
"""Test token retrieval"""
|
||||
with Mocker() as mocker:
|
||||
token = generate_id()
|
||||
@@ -86,6 +84,44 @@ class SCIMOAuthTests(APITestCase):
|
||||
)
|
||||
self.assertEqual(mocker.request_history[0].body, "grant_type=password&foo=bar")
|
||||
|
||||
def test_retrieve_token_interactive(self):
|
||||
"""Test token retrieval"""
|
||||
self.provider.auth_mode = SCIMAuthenticationMode.OAUTH_INTERACTIVE
|
||||
self.provider.save()
|
||||
refresh_token = generate_id()
|
||||
access_token = generate_id()
|
||||
UserOAuthSourceConnection.objects.create(
|
||||
user=self.provider.auth_oauth_user,
|
||||
source=self.source,
|
||||
refresh_token=refresh_token,
|
||||
access_token=access_token,
|
||||
)
|
||||
with Mocker() as mocker:
|
||||
token = generate_id()
|
||||
mocker.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
|
||||
self.provider.scim_auth()
|
||||
conn = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.source,
|
||||
user=self.provider.auth_oauth_user,
|
||||
).first()
|
||||
self.assertIsNotNone(conn)
|
||||
self.assertTrue(conn.is_valid)
|
||||
auth = (
|
||||
b64encode(
|
||||
b":".join((self.source.consumer_key.encode(), self.source.consumer_secret.encode()))
|
||||
)
|
||||
.strip()
|
||||
.decode()
|
||||
)
|
||||
self.assertEqual(
|
||||
mocker.request_history[0].headers["Authorization"],
|
||||
f"Basic {auth}",
|
||||
)
|
||||
self.assertEqual(
|
||||
mocker.request_history[0].body,
|
||||
f"grant_type=refresh_token&refresh_token={refresh_token}&foo=bar",
|
||||
)
|
||||
|
||||
def test_existing_token(self):
|
||||
"""Test existing token"""
|
||||
UserOAuthSourceConnection.objects.create(
|
||||
@@ -98,96 +134,54 @@ class SCIMOAuthTests(APITestCase):
|
||||
self.provider.scim_auth()
|
||||
self.assertEqual(len(mocker.request_history), 0)
|
||||
|
||||
@Mocker()
|
||||
def test_user_create(self, mock: Mocker):
|
||||
"""Test user creation"""
|
||||
scim_id = generate_id()
|
||||
token = generate_id()
|
||||
mock.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
mock.post(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 3)
|
||||
self.assertEqual(mock.request_history[1].method, "GET")
|
||||
self.assertEqual(mock.request_history[2].method, "POST")
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[2].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
"primary": True,
|
||||
"type": "other",
|
||||
"value": f"{uid}@goauthentik.io",
|
||||
}
|
||||
],
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
def test_interactive_start(self):
|
||||
self.client.force_login(self.admin)
|
||||
res = self.client.get(
|
||||
reverse(
|
||||
"authentik_enterprise_providers_scim:start",
|
||||
kwargs={
|
||||
"application_slug": self.app.slug,
|
||||
},
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=expiry_valid,
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_api_create(self):
|
||||
License.objects.create(key=generate_id())
|
||||
self.client.force_login(create_test_admin_user())
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:scimprovider-list"),
|
||||
{
|
||||
"name": generate_id(),
|
||||
"url": "http://localhost",
|
||||
"auth_mode": "oauth",
|
||||
"auth_oauth": str(self.source.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
query = parse_qs(urlparse(res.url).query)
|
||||
self.assertEqual(query["client_id"], [self.source.consumer_key])
|
||||
self.assertEqual(
|
||||
query["redirect_uri"],
|
||||
[f"http://testserver/application/scim/{self.app.slug}/oauth2/callback/"],
|
||||
)
|
||||
self.assertEqual(query["response_type"], ["code"])
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.models.LicenseUsageStatus.is_valid",
|
||||
PropertyMock(return_value=False),
|
||||
)
|
||||
def test_api_create_no_license(self):
|
||||
self.client.force_login(create_test_admin_user())
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:scimprovider-list"),
|
||||
{
|
||||
"name": generate_id(),
|
||||
"url": "http://localhost",
|
||||
"auth_mode": "oauth",
|
||||
"auth_oauth": str(self.source.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content, {"auth_mode": ["Enterprise is required to use the OAuth mode."]}
|
||||
def test_interactive_callback(self):
|
||||
self.client.force_login(self.admin)
|
||||
res = self.client.get(
|
||||
reverse(
|
||||
"authentik_enterprise_providers_scim:start",
|
||||
kwargs={
|
||||
"application_slug": self.app.slug,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
query = parse_qs(urlparse(res.url).query)
|
||||
|
||||
with Mocker() as mock:
|
||||
token = generate_id()
|
||||
mock.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
|
||||
|
||||
res = self.client.get(
|
||||
reverse(
|
||||
"authentik_enterprise_providers_scim:callback",
|
||||
kwargs={
|
||||
"application_slug": self.app.slug,
|
||||
},
|
||||
)
|
||||
+ "?"
|
||||
+ urlencode({"state": query["state"][0], "code": generate_id()})
|
||||
)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
|
||||
conn = UserOAuthSourceConnection.objects.filter(source=self.source).first()
|
||||
self.assertIsNotNone(conn)
|
||||
self.assertTrue(conn.is_valid)
|
||||
10
authentik/enterprise/providers/scim/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.providers.scim.views import SCIMOAuthStart, SCIMRedirectCallback
|
||||
|
||||
urlpatterns = [
|
||||
path("<slug:application_slug>/oauth2/start/", SCIMOAuthStart.as_view(), name="start"),
|
||||
path(
|
||||
"<slug:application_slug>/oauth2/callback/", SCIMRedirectCallback.as_view(), name="callback"
|
||||
),
|
||||
]
|
||||
70
authentik/enterprise/providers/scim/views.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
from authentik.sources.oauth.clients.base import BaseOAuthClient
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.registry import RequestKind, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
|
||||
class SCIMOAuthViewMixin:
|
||||
|
||||
provider: SCIMProvider
|
||||
|
||||
def get_client(self, source: OAuthSource, **kwargs) -> BaseOAuthClient:
|
||||
source: OAuthSource = self.provider.auth_oauth
|
||||
source_cls = registry.find(source.provider_type, kind=RequestKind.CALLBACK)
|
||||
if not source_cls.client_class:
|
||||
return super().get_client(source, **kwargs)
|
||||
return source_cls.client_class(source, self.request, **kwargs)
|
||||
|
||||
def _get_scim_provider(self, app_slug: str):
|
||||
app = Application.objects.filter(slug=app_slug).first()
|
||||
if not app:
|
||||
return None
|
||||
provider = SCIMProvider.objects.filter(backchannel_application=app)
|
||||
return provider.first()
|
||||
|
||||
def dispatch(self, request: HttpRequest, application_slug: str):
|
||||
if not request.user.is_authenticated:
|
||||
raise PermissionDenied()
|
||||
provider = self._get_scim_provider(application_slug)
|
||||
if not provider or not provider.auth_oauth:
|
||||
raise PermissionDenied()
|
||||
if not request.user.has_perm(
|
||||
"authentik_providers_scim.change_scimprovider",
|
||||
provider,
|
||||
):
|
||||
raise PermissionDenied()
|
||||
self.provider = provider
|
||||
return super().dispatch(request, source_slug=provider.auth_oauth.slug)
|
||||
|
||||
|
||||
class SCIMOAuthStart(SCIMOAuthViewMixin, OAuthRedirect):
|
||||
|
||||
def get_callback_url(self, source: OAuthSource):
|
||||
return reverse("authentik_enterprise_providers_scim:callback", kwargs=self.kwargs)
|
||||
|
||||
|
||||
class SCIMRedirectCallback(SCIMOAuthViewMixin, OAuthCallback):
|
||||
|
||||
def redirect_flow_manager(self, client: BaseOAuthClient):
|
||||
expires_in = int(self.token.get("expires_in", 0))
|
||||
UserOAuthSourceConnection.objects.update_or_create(
|
||||
source=self.provider.auth_oauth,
|
||||
user=self.provider.auth_oauth_user,
|
||||
defaults={
|
||||
"access_token": self.token.get("access_token"),
|
||||
"refresh_token": self.token.get("refresh_token"),
|
||||
"expires": now() + timedelta(seconds=expires_in),
|
||||
},
|
||||
)
|
||||
return redirect("authentik_core:if-admin")
|
||||
@@ -1,5 +1,6 @@
|
||||
"""SCIM Provider API Views"""
|
||||
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
@@ -16,6 +17,11 @@ class SCIMProviderSerializer(
|
||||
):
|
||||
"""SCIMProvider Serializer"""
|
||||
|
||||
auth_oauth_token_last_updated = SerializerMethodField()
|
||||
auth_oauth_token_expires = SerializerMethodField()
|
||||
auth_oauth_url_callback = SerializerMethodField()
|
||||
auth_oauth_url_start = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = SCIMProvider
|
||||
fields = [
|
||||
@@ -35,6 +41,10 @@ class SCIMProviderSerializer(
|
||||
"auth_mode",
|
||||
"auth_oauth",
|
||||
"auth_oauth_params",
|
||||
"auth_oauth_token_last_updated",
|
||||
"auth_oauth_token_expires",
|
||||
"auth_oauth_url_callback",
|
||||
"auth_oauth_url_start",
|
||||
"compatibility_mode",
|
||||
"service_provider_config_cache_timeout",
|
||||
"exclude_users_service_account",
|
||||
|
||||
@@ -102,4 +102,16 @@ class Migration(migrations.Migration):
|
||||
verbose_name="SCIM Compatibility Mode",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scimprovider",
|
||||
name="auth_mode",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("token", "Token"),
|
||||
("oauth", "OAuth (Silent)"),
|
||||
("oauth_interactive", "OAuth (interactive)"),
|
||||
],
|
||||
default="token",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -72,7 +72,8 @@ class SCIMAuthenticationMode(models.TextChoices):
|
||||
"""SCIM authentication modes"""
|
||||
|
||||
TOKEN = "token", _("Token")
|
||||
OAUTH = "oauth", _("OAuth")
|
||||
OAUTH_SILENT = "oauth", _("OAuth (Silent)")
|
||||
OAUTH_INTERACTIVE = "oauth_interactive", _("OAuth (interactive)")
|
||||
|
||||
|
||||
class SCIMCompatibilityMode(models.TextChoices):
|
||||
@@ -144,7 +145,10 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
)
|
||||
|
||||
def scim_auth(self) -> AuthBase:
|
||||
if self.auth_mode == SCIMAuthenticationMode.OAUTH:
|
||||
if self.auth_mode in [
|
||||
SCIMAuthenticationMode.OAUTH_SILENT,
|
||||
SCIMAuthenticationMode.OAUTH_INTERACTIVE,
|
||||
]:
|
||||
try:
|
||||
from authentik.enterprise.providers.scim.auth_oauth2 import SCIMOAuthAuth
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Source type manager"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
@@ -114,7 +113,7 @@ class SourceTypeRegistry:
|
||||
)
|
||||
return found_type
|
||||
|
||||
def find(self, type_name: str, kind: RequestKind) -> Callable:
|
||||
def find(self, type_name: str, kind: RequestKind) -> type[OAuthCallback | OAuthRedirect]:
|
||||
"""Find fitting Source Type"""
|
||||
found_type = self.find_type(type_name)
|
||||
if kind == RequestKind.CALLBACK:
|
||||
|
||||
@@ -15,6 +15,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.sources.oauth.clients.base import BaseOAuthClient
|
||||
from authentik.sources.oauth.models import (
|
||||
GroupOAuthSourceConnection,
|
||||
OAuthSource,
|
||||
@@ -29,7 +30,7 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
"Base OAuth callback view."
|
||||
|
||||
source: OAuthSource
|
||||
token: dict | None = None
|
||||
token: dict[str, Any] | None = None
|
||||
|
||||
def dispatch(self, request: HttpRequest, *_, **kwargs) -> HttpResponse:
|
||||
"""View Get handler"""
|
||||
@@ -49,20 +50,31 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
if "error" in self.token:
|
||||
return self.handle_login_failure(self.token["error"])
|
||||
# Fetch profile info
|
||||
try:
|
||||
res = self.redirect_flow_manager(client)
|
||||
except ValueError as exc:
|
||||
# if we're authenticated and not in a source stage and this new flag is enabled,
|
||||
# just continue
|
||||
if self.request.user.is_authenticated:
|
||||
pass
|
||||
return self.handle_login_failure(exc.args[0])
|
||||
return res
|
||||
|
||||
def redirect_flow_manager(self, client: BaseOAuthClient) -> HttpResponse:
|
||||
try:
|
||||
raw_info = client.get_profile_info(self.token)
|
||||
if raw_info is None:
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
raise ValueError("Could not retrieve profile.")
|
||||
except JSONDecodeError as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message="Failed to JSON-decode profile.",
|
||||
raw_profile=exc.doc,
|
||||
).from_http(self.request)
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
raise ValueError("Could not retrieve profile.") from None
|
||||
identifier = self.get_user_id(info=raw_info)
|
||||
if identifier is None:
|
||||
return self.handle_login_failure("Could not determine id.")
|
||||
raise ValueError("Could not determine id.")
|
||||
sfm = OAuthSourceFlowManager(
|
||||
source=self.source,
|
||||
request=self.request,
|
||||
|
||||
@@ -11203,7 +11203,8 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"token",
|
||||
"oauth"
|
||||
"oauth",
|
||||
"oauth_interactive"
|
||||
],
|
||||
"title": "Auth mode"
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
export const SCIMAuthenticationModeEnum = {
|
||||
Token: "token",
|
||||
Oauth: "oauth",
|
||||
OauthInteractive: "oauth_interactive",
|
||||
UnknownDefaultOpenApi: "11184809",
|
||||
} as const;
|
||||
export type SCIMAuthenticationModeEnum =
|
||||
|
||||
45
packages/client-ts/src/models/SCIMProvider.ts
generated
@@ -125,6 +125,30 @@ export interface SCIMProvider {
|
||||
* @memberof SCIMProvider
|
||||
*/
|
||||
authOauthParams?: { [key: string]: any };
|
||||
/**
|
||||
*
|
||||
* @type {Date}
|
||||
* @memberof SCIMProvider
|
||||
*/
|
||||
readonly authOauthTokenLastUpdated: Date | null;
|
||||
/**
|
||||
*
|
||||
* @type {Date}
|
||||
* @memberof SCIMProvider
|
||||
*/
|
||||
readonly authOauthTokenExpires: Date | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SCIMProvider
|
||||
*/
|
||||
readonly authOauthUrlCallback: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SCIMProvider
|
||||
*/
|
||||
readonly authOauthUrlStart: string | null;
|
||||
/**
|
||||
* Alter authentik behavior for vendor-specific SCIM implementations.
|
||||
* @type {CompatibilityModeEnum}
|
||||
@@ -190,6 +214,13 @@ export function instanceOfSCIMProvider(value: object): value is SCIMProvider {
|
||||
if (!("verboseNamePlural" in value) || value["verboseNamePlural"] === undefined) return false;
|
||||
if (!("metaModelName" in value) || value["metaModelName"] === undefined) return false;
|
||||
if (!("url" in value) || value["url"] === undefined) return false;
|
||||
if (!("authOauthTokenLastUpdated" in value) || value["authOauthTokenLastUpdated"] === undefined)
|
||||
return false;
|
||||
if (!("authOauthTokenExpires" in value) || value["authOauthTokenExpires"] === undefined)
|
||||
return false;
|
||||
if (!("authOauthUrlCallback" in value) || value["authOauthUrlCallback"] === undefined)
|
||||
return false;
|
||||
if (!("authOauthUrlStart" in value) || value["authOauthUrlStart"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -223,6 +254,16 @@ export function SCIMProviderFromJSONTyped(json: any, ignoreDiscriminator: boolea
|
||||
: SCIMAuthenticationModeEnumFromJSON(json["auth_mode"]),
|
||||
authOauth: json["auth_oauth"] == null ? undefined : json["auth_oauth"],
|
||||
authOauthParams: json["auth_oauth_params"] == null ? undefined : json["auth_oauth_params"],
|
||||
authOauthTokenLastUpdated:
|
||||
json["auth_oauth_token_last_updated"] == null
|
||||
? null
|
||||
: new Date(json["auth_oauth_token_last_updated"]),
|
||||
authOauthTokenExpires:
|
||||
json["auth_oauth_token_expires"] == null
|
||||
? null
|
||||
: new Date(json["auth_oauth_token_expires"]),
|
||||
authOauthUrlCallback: json["auth_oauth_url_callback"],
|
||||
authOauthUrlStart: json["auth_oauth_url_start"],
|
||||
compatibilityMode:
|
||||
json["compatibility_mode"] == null
|
||||
? undefined
|
||||
@@ -256,6 +297,10 @@ export function SCIMProviderToJSONTyped(
|
||||
| "verbose_name"
|
||||
| "verbose_name_plural"
|
||||
| "meta_model_name"
|
||||
| "auth_oauth_token_last_updated"
|
||||
| "auth_oauth_token_expires"
|
||||
| "auth_oauth_url_callback"
|
||||
| "auth_oauth_url_start"
|
||||
> | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
|
||||
@@ -85,7 +85,7 @@ dev = [
|
||||
"coverage[toml]==7.13.5",
|
||||
"daphne==4.2.1",
|
||||
"debugpy==1.8.20",
|
||||
"django-stubs[compatible-mypy]==6.0.3",
|
||||
"django-stubs[compatible-mypy]==6.0.4",
|
||||
"djangorestframework-stubs[compatible-mypy]==3.16.9",
|
||||
"drf-jsonschema-serializer==3.0.0",
|
||||
"freezegun==1.5.5",
|
||||
|
||||
23
schema.yml
@@ -54916,6 +54916,7 @@ components:
|
||||
enum:
|
||||
- token
|
||||
- oauth
|
||||
- oauth_interactive
|
||||
type: string
|
||||
SCIMMapping:
|
||||
type: object
|
||||
@@ -55050,6 +55051,24 @@ components:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
description: Additional OAuth parameters, such as grant_type
|
||||
auth_oauth_token_last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
readOnly: true
|
||||
auth_oauth_token_expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
readOnly: true
|
||||
auth_oauth_url_callback:
|
||||
type: string
|
||||
nullable: true
|
||||
readOnly: true
|
||||
auth_oauth_url_start:
|
||||
type: string
|
||||
nullable: true
|
||||
readOnly: true
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
@@ -55082,6 +55101,10 @@ components:
|
||||
required:
|
||||
- assigned_backchannel_application_name
|
||||
- assigned_backchannel_application_slug
|
||||
- auth_oauth_token_expires
|
||||
- auth_oauth_token_last_updated
|
||||
- auth_oauth_url_callback
|
||||
- auth_oauth_url_start
|
||||
- component
|
||||
- meta_model_name
|
||||
- name
|
||||
|
||||
56
uv.lock
generated
@@ -394,7 +394,7 @@ dev = [
|
||||
{ name = "coverage", extras = ["toml"], specifier = "==7.13.5" },
|
||||
{ name = "daphne", specifier = "==4.2.1" },
|
||||
{ name = "debugpy", specifier = "==1.8.20" },
|
||||
{ name = "django-stubs", extras = ["compatible-mypy"], specifier = "==6.0.3" },
|
||||
{ name = "django-stubs", extras = ["compatible-mypy"], specifier = "==6.0.4" },
|
||||
{ name = "djangorestframework-stubs", extras = ["compatible-mypy"], specifier = "==3.16.9" },
|
||||
{ name = "drf-jsonschema-serializer", specifier = "==3.0.0" },
|
||||
{ name = "freezegun", specifier = "==1.5.5" },
|
||||
@@ -1269,7 +1269,7 @@ s3 = [
|
||||
|
||||
[[package]]
|
||||
name = "django-stubs"
|
||||
version = "6.0.3"
|
||||
version = "6.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
@@ -1277,9 +1277,9 @@ dependencies = [
|
||||
{ name = "types-pyyaml" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/0c/8d0d875af79bf774c1c3997c84aa118dba3a77be12086b9c14e130e8ec72/django_stubs-6.0.3.tar.gz", hash = "sha256:ee895f403c373608eeb50822f0733f9d9ec5ab12731d4ab58956053bb95fdd9e", size = 278214, upload-time = "2026-04-18T15:11:22.327Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/82/ccf2a2dc9cdb4bd9cbe91f11e887589bf2da7609506db00ccbc73bd8a6da/django_stubs-6.0.4.tar.gz", hash = "sha256:7aee77e8de9c14c0d9cf84988befe826d93cbc15a87e0ade2943f14d553451cf", size = 280019, upload-time = "2026-05-09T21:24:30.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/a3/6751b7684d20fc4f228bdd3dd8341d382ab3faaf65d3d050c0d59ab0a1b0/django_stubs-6.0.3-py3-none-any.whl", hash = "sha256:5fee22bcbbad59a78c727a820b6f4e68ff442ca76a922b7002e57c25dd7cb390", size = 541570, upload-time = "2026-04-18T15:11:20.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e7/5128914ada94dd6277626ef5a4a5680a4def7d2f9366214d26c1cd86723b/django_stubs-6.0.4-py3-none-any.whl", hash = "sha256:e991c68f77239663577a5f4fc75e99c84f867f378cafc97cbf4acc5aff378279", size = 543791, upload-time = "2026-05-09T21:24:28.218Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -3743,32 +3743,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ujson"
|
||||
version = "5.12.0"
|
||||
version = "5.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/c35530c5ffc25b71c59ae0cd7b8f99df37313daa162ce1e2f7925f7c2877/ujson-5.12.0.tar.gz", hash = "sha256:14b2e1eb528d77bc0f4c5bd1a7ebc05e02b5b41beefb7e8567c9675b8b13bcf4", size = 7158451, upload-time = "2026-03-11T22:19:30.397Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/78/937198ea8708182dd1edbf0237bf255a96feab3f511691ad08b84da98e5d/ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35", size = 7164538, upload-time = "2026-05-05T22:05:01.354Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bd/9a8d693254bada62bfea75a507e014afcfdb6b9d047b6f8dd134bfefaf67/ujson-5.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85833bca01aa5cae326ac759276dc175c5fa3f7b3733b7d543cf27f2df12d1ef", size = 56499, upload-time = "2026-03-11T22:18:45.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2d/285a83df8176e18dcd675d1a4cff8f7620f003f30903ea43929406e98986/ujson-5.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d22cad98c2a10bbf6aa083a8980db6ed90d4285a841c4de892890c2b28286ef9", size = 53998, upload-time = "2026-03-11T22:18:47.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/8b/e2f09e16dabfa91f6a84555df34a4329fa7621e92ed054d170b9054b9bb2/ujson-5.12.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cc80facad240b0c2fb5a633044420878aac87a8e7c348b9486450cba93f27c", size = 57783, upload-time = "2026-03-11T22:18:48.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/fb/ba1d06f3658a0c36d0ab3869ec3914f202bad0a9bde92654e41516c7bb13/ujson-5.12.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:d1831c07bd4dce53c4b666fa846c7eba4b7c414f2e641a4585b7f50b72f502dc", size = 60011, upload-time = "2026-03-11T22:18:49.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/2b/3e322bf82d926d9857206cd5820438d78392d1f523dacecb8bd899952f73/ujson-5.12.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e00cec383eab2406c9e006bd4edb55d284e94bb943fda558326048178d26961", size = 57465, upload-time = "2026-03-11T22:18:50.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/fd/af72d69603f9885e5136509a529a4f6d88bf652b457263ff96aefcd3ab7d/ujson-5.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f19b3af31d02a2e79c5f9a6deaab0fb3c116456aeb9277d11720ad433de6dfc6", size = 1037275, upload-time = "2026-03-11T22:18:51.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/a7/a2411ec81aef7872578e56304c3e41b3a544a9809e95c8e1df46923fc40b/ujson-5.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bacbd3c69862478cbe1c7ed4325caedec580d8acf31b8ee1b9a1e02a56295cad", size = 1196758, upload-time = "2026-03-11T22:18:53.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/85/aa18ae175dd03a118555aa14304d4f466f9db61b924c97c6f84388ecacb1/ujson-5.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94c5f1621cbcab83c03be46441f090b68b9f307b6c7ec44d4e3f6d5997383df4", size = 1089760, upload-time = "2026-03-11T22:18:55.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/d4/4b40b67ac7e916ebffc3041ae2320c5c0b8a045300d4c542b6e50930cca5/ujson-5.12.0-cp314-cp314-win32.whl", hash = "sha256:e6369ac293d2cc40d52577e4fa3d75a70c1aae2d01fa3580a34a4e6eff9286b9", size = 41043, upload-time = "2026-03-11T22:18:56.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/38/a1496d2a3428981f2b3a2ffbb4656c2b05be6cc406301d6b10a6445f6481/ujson-5.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:31348a0ffbfc815ce78daac569d893349d85a0b57e1cd2cdbba50b7f333784da", size = 45303, upload-time = "2026-03-11T22:18:57.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d3/39dbd3159543d9c57ec3a82d36226152cf0d710784894ce5aa24b8220ac1/ujson-5.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:6879aed770557f0961b252648d36f6fdaab41079d37a2296b5649fd1b35608e0", size = 39860, upload-time = "2026-03-11T22:18:58.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/71/9b4dacb177d3509077e50497222d39eec04c8b41edb1471efc764d645237/ujson-5.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ddb08b3c2f9213df1f2e3eb2fbea4963d80ec0f8de21f0b59898e34f3b3d96d", size = 56845, upload-time = "2026-03-11T22:18:59.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/c2/8abffa3be1f3d605c4a62445fab232b3e7681512ce941c6b23014f404d36/ujson-5.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a3ae28f0b209be5af50b54ca3e2123a3de3a57d87b75f1e5aa3d7961e041983", size = 54463, upload-time = "2026-03-11T22:19:00.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/2e/60114a35d1d6796eb428f7affcba00a921831ff604a37d9142c3d8bbe5c5/ujson-5.12.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30ad4359413c8821cc7b3707f7ca38aa8bc852ba3b9c5a759ee2d7740157315", size = 58689, upload-time = "2026-03-11T22:19:01.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/ad/010925c2116c21ce119f9c2ff18d01f48a19ade3ff4c5795da03ce5829fc/ujson-5.12.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:02f93da7a4115e24f886b04fd56df1ee8741c2ce4ea491b7ab3152f744ad8f8e", size = 60618, upload-time = "2026-03-11T22:19:03.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/74/db7f638bf20282b1dccf454386cbd483faaaed3cdbb9cb27e06f74bb109e/ujson-5.12.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ff4ede90ed771140caa7e1890de17431763a483c54b3c1f88bd30f0cc1affc0", size = 58151, upload-time = "2026-03-11T22:19:04.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/7e/3ebaecfa70a2e8ce623db8e21bd5cb05d42a5ef943bcbb3309d71b5de68d/ujson-5.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bf9cc97f05048ac8f3e02cd58f0fe62b901453c24345bfde287f4305dcc31c", size = 1038117, upload-time = "2026-03-11T22:19:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/aa/e073eda7f0036c2973b28db7bb99faba17a932e7b52d801f9bb3e726271f/ujson-5.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2324d9a0502317ffc35d38e153c1b2fa9610ae03775c9d0f8d0cca7b8572b04e", size = 1197434, upload-time = "2026-03-11T22:19:06.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/01/b9a13f058fdd50c746b192c4447ca8d6352e696dcda912ccee10f032ff85/ujson-5.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:50524f4f6a1c839714dbaff5386a1afb245d2d5ec8213a01fbc99cea7307811e", size = 1090401, upload-time = "2026-03-11T22:19:08.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/37/3d1b4e0076b6e43379600b5229a5993db8a759ff2e1830ea635d876f6644/ujson-5.12.0-cp314-cp314t-win32.whl", hash = "sha256:f7a0430d765f9bda043e6aefaba5944d5f21ec43ff4774417d7e296f61917382", size = 41880, upload-time = "2026-03-11T22:19:09.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/c5/3c2a262a138b9f0014fe1134a6b5fdc2c54245030affbaac2fcbc0632138/ujson-5.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ccbfd94e59aad4a2566c71912b55f0547ac1680bfac25eb138e6703eb3dd434e", size = 46365, upload-time = "2026-03-11T22:19:10.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/40/956dc20b7e00dc0ff3259871864f18dab211837fce3478778bedb3132ac1/ujson-5.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:42d875388fbd091c7ea01edfff260f839ba303038ffb23475ef392012e4d63dd", size = 40398, upload-time = "2026-03-11T22:19:11.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ca/d88d86f90f8f237985f3e347b9a4f9fa24e8d30d19ec7d477ed18aa58393/ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc", size = 58099, upload-time = "2026-05-05T22:04:06.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/2d/a0a88407cee3550f7ed1e49b41157ee2d410f51905ed51fb134844255280/ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd", size = 55631, upload-time = "2026-05-05T22:04:07.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6d/12a3b8e72132db244ae048075e71a0079b3c5f61ff45b7ca81d5193ab3e7/ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc", size = 59469, upload-time = "2026-05-05T22:04:09.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/72/310f8c21737554f2d2b4f1883e1a71e8a6ab0d8f92f0feb8aaa85e0f4b66/ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508", size = 61611, upload-time = "2026-05-05T22:04:10.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/50/ab4b2f7bab6c7a67298c8f2aca80e2082eaf6f332cf2d099762647b5301e/ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f", size = 59122, upload-time = "2026-05-05T22:04:12.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/48/5d81cbe76fc2aa9e071aa489a3041cf0712f5e0663d60d501641f92b7bb4/ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb", size = 1038938, upload-time = "2026-05-05T22:04:13.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a7/abe1acb0e5d8b8d724b35533a44c89684c88100a5fd9f2fee7f7155528d5/ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0", size = 1198416, upload-time = "2026-05-05T22:04:15.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6e/087067d6ee22bd01bfba9fb1f32ce98c24ae2bcbab53bd2fbf8f7a80fe9e/ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33", size = 1091425, upload-time = "2026-05-05T22:04:17.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/d2/28938574b766980f873b68962abb4c68a944d939446768982934ad3bcd93/ujson-5.12.1-cp314-cp314-win32.whl", hash = "sha256:c8e626b6bc9bdd2e8f7393b7d99f3daa2ca4022e6203662e70de7bb3604b21b9", size = 42334, upload-time = "2026-05-05T22:04:19.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b0/0af30bf65d96b73c28054b344ebbe24bc96780ae8a7f2973f5dad979510a/ujson-5.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6d3bdd020333688ee60559437021ed68a98a28fdd609b5af16de5dd58f90cba", size = 46586, upload-time = "2026-05-05T22:04:21.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/3b/0ee2555823724e60cc847c715c299f5792aa444bdde69c51d4aa42d885c2/ujson-5.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:e3c9c894971f4ada3ded16a804ed4640e1f2b3e5239beaeec7c48296f39f4232", size = 41178, upload-time = "2026-05-05T22:04:22.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3d/7547835cd0b7fa22eb1122702f81b2403c38a0027a2cc0d75acc449a4a66/ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5", size = 58565, upload-time = "2026-05-05T22:04:23.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/1784e0b24aab50623eb47b2f7a8dc22c9d809d798854d2568a9cb7c3560f/ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102", size = 56157, upload-time = "2026-05-05T22:04:25.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2d/2c1b24df24eee309047d81460c3a1acf0d047207327edc6f3cab8a614985/ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c", size = 60288, upload-time = "2026-05-05T22:04:26.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/14/c0c603e3dff2ef98f7deee2df7795e6055abbc5825c6ef530024b3b06a15/ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430", size = 62302, upload-time = "2026-05-05T22:04:27.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/0d/889bbc044561d9adc9bf413620fbd9878f352c9fd36da829d319bca2f5ad/ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1", size = 59784, upload-time = "2026-05-05T22:04:28.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/35/3b1d8ff8cd6dc048f5c495af6ee6ded43055562610a7e9b78b438dc6421e/ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd", size = 1039759, upload-time = "2026-05-05T22:04:30.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/d8/3c66cdf839420a6da2d6140a54a882c15efd135bcced103bd4473d577636/ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682", size = 1199121, upload-time = "2026-05-05T22:04:31.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/51/c3d1b94a4ad27dc7532e9f7d00b869463157cede2295ba6d57566afeb8cd/ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b", size = 1092085, upload-time = "2026-05-05T22:04:33.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/52/4d4a6e78290a5eef3f576f6d281e6355535db903a08483fd1bb393bf8cb9/ujson-5.12.1-cp314-cp314t-win32.whl", hash = "sha256:4e12ca368b397aed7fa1eec534ea1ba8d94977b376f9df3e93ae1acfd004ec40", size = 43243, upload-time = "2026-05-05T22:04:35.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c8/849366785de52b513e5fc89d7aea0b531e71bb5641407cbdfdf47a99ede8/ujson-5.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cec6b9b539539affc1f01a795c99574592a635ce22331b64f2b42e0af570659e", size = 47662, upload-time = "2026-05-05T22:04:37.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/46/36a67f5a531a15308124786f3e2b7b96414b9d23dbcdc2a182dd3ffa2e1d/ujson-5.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:696224d4cfb8883fa5c0285dff31e5ce924704dd9ccd38e9ea8b5bf4a42b12fc", size = 41680, upload-time = "2026-05-05T22:04:39.083Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
This is the default UI for the authentik server. The documentation is going to be a little sparse
|
||||
for awhile, but at least let's get started.
|
||||
|
||||
# Setup
|
||||
|
||||
Install dependencies from the repo root with `make node-install` (or `make install` for the full
|
||||
Python + web + docs bootstrap). This wraps `npm ci` and explicitly rebuilds the small set of
|
||||
packages whose install scripts are required for the toolchain to function — currently `esbuild`,
|
||||
`chromedriver`, `tree-sitter`, and `tree-sitter-json`.
|
||||
|
||||
The repo-root `.npmrc` sets `ignore-scripts=true` to neutralize the dominant npm supply-chain
|
||||
attack vector. As a side effect, running `npm ci` directly in this directory will install
|
||||
dependencies but skip those rebuilds, leaving `esbuild` and `chromedriver` in a non-functional
|
||||
state. If you bypass `make`, run the rebuild step yourself:
|
||||
|
||||
```bash
|
||||
npm rebuild --ignore-scripts=false --foreground-scripts \
|
||||
esbuild chromedriver tree-sitter tree-sitter-json
|
||||
```
|
||||
|
||||
New dependencies that ship install scripts must be audited and added to `TRUSTED_INSTALL_SCRIPTS`
|
||||
in the repo-root `Makefile`. Each entry is arbitrary code that runs at install time, so the list
|
||||
is intentionally small.
|
||||
|
||||
# The Theory of the authentik UI
|
||||
|
||||
In Peter Naur's 1985 essay [Programming as Theory
|
||||
|
||||
@@ -93,6 +93,7 @@ export function renderAuth(provider?: Partial<SCIMProvider>, errors: ValidationE
|
||||
case SCIMAuthenticationModeEnum.Token:
|
||||
return renderAuthToken(provider, errors);
|
||||
case SCIMAuthenticationModeEnum.Oauth:
|
||||
case SCIMAuthenticationModeEnum.OauthInteractive:
|
||||
return renderAuthOAuth(provider, errors);
|
||||
}
|
||||
}
|
||||
@@ -160,12 +161,18 @@ export function renderForm({ provider, errors, update }: SCIMProviderFormProps)
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("OAuth"),
|
||||
label: msg("OAuth (Silent)"),
|
||||
value: SCIMAuthenticationModeEnum.Oauth,
|
||||
default: true,
|
||||
description: html`${msg("Authenticate SCIM requests using OAuth.")}
|
||||
<ak-license-notice></ak-license-notice>`,
|
||||
},
|
||||
{
|
||||
label: msg("OAuth (Interactive)"),
|
||||
value: SCIMAuthenticationModeEnum.OauthInteractive,
|
||||
description: html`${msg(
|
||||
"Authenticate SCIM requests using OAuth, interactively authorized.",
|
||||
)} <ak-license-notice></ak-license-notice>`,
|
||||
},
|
||||
]}
|
||||
></ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
@@ -13,6 +13,7 @@ import "#elements/buttons/ModalButton";
|
||||
import "#elements/sync/SyncStatusCard";
|
||||
import "#elements/tasks/ScheduleList";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "#elements/timestamp/ak-timestamp";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
@@ -20,7 +21,14 @@ import { EVENT_REFRESH } from "#common/constants";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { ModelEnum, ProvidersApi, SCIMProvider } from "@goauthentik/api";
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import {
|
||||
ModelEnum,
|
||||
ProvidersApi,
|
||||
SCIMAuthenticationModeEnum,
|
||||
SCIMProvider,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import MDSCIMProvider from "~docs/add-secure-apps/providers/scim/index.md";
|
||||
|
||||
@@ -154,6 +162,42 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
</main>`;
|
||||
}
|
||||
|
||||
renderSyncStatusExtra() {
|
||||
if (
|
||||
this.provider?.authMode !== SCIMAuthenticationModeEnum.Oauth &&
|
||||
this.provider?.authMode !== SCIMAuthenticationModeEnum.OauthInteractive
|
||||
)
|
||||
return nothing;
|
||||
return html`
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("OAuth Token last updated")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.provider?.authOauthTokenLastUpdated}
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("OAuth Token expires")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-timestamp
|
||||
.timestamp=${this.provider?.authOauthTokenExpires}
|
||||
></ak-timestamp>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderTabOverview(): SlottedTemplateResult {
|
||||
if (!this.provider) {
|
||||
return nothing;
|
||||
@@ -168,91 +212,94 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
: nothing}
|
||||
<div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl"
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-4-col-on-xl pf-m-4-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">${msg("Info")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Name")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.provider.name}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Assigned to application")}</span
|
||||
${renderDescriptionList([
|
||||
[msg("Name"), this.provider.name],
|
||||
[
|
||||
msg("Assigned to application"),
|
||||
html`<ak-provider-related-application
|
||||
mode="backchannel"
|
||||
.provider=${this.provider}
|
||||
></ak-provider-related-application>`,
|
||||
],
|
||||
[
|
||||
msg("Dry-run"),
|
||||
html`<ak-status-label
|
||||
?good=${!this.provider.dryRun}
|
||||
type="info"
|
||||
good-label=${msg("No")}
|
||||
bad-label=${msg("Yes")}
|
||||
></ak-status-label>`,
|
||||
],
|
||||
[msg("URL"), this.provider.url],
|
||||
[
|
||||
msg("Service Provider Config cache timeout"),
|
||||
this.provider.serviceProviderConfigCacheTimeout,
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update SCIM Provider")}</span>
|
||||
<ak-provider-scim-form
|
||||
slot="form"
|
||||
.instancePk=${this.provider.pk}
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-provider-related-application
|
||||
mode="backchannel"
|
||||
.provider=${this.provider}
|
||||
></ak-provider-related-application>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Dry-run")}</span
|
||||
</ak-provider-scim-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-status-label
|
||||
?good=${!this.provider.dryRun}
|
||||
type="info"
|
||||
good-label=${msg("No")}
|
||||
bad-label=${msg("Yes")}
|
||||
></ak-status-label>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("URL")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.provider.url}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">
|
||||
${msg("Service Provider Config cache timeout")}
|
||||
</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.provider.serviceProviderConfigCacheTimeout}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update SCIM Provider")}</span>
|
||||
<ak-provider-scim-form slot="form" .instancePk=${this.provider.pk}>
|
||||
</ak-provider-scim-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl"
|
||||
>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl pf-m-8-col-on-2xl">
|
||||
${this.provider.authMode === SCIMAuthenticationModeEnum.OauthInteractive
|
||||
? html`
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
${renderDescriptionList(
|
||||
[
|
||||
[
|
||||
msg("OAuth Status"),
|
||||
html`<ak-status-label
|
||||
?good=${this.provider
|
||||
.authOauthTokenLastUpdated !== null}
|
||||
good-label=${msg("Authenticated")}
|
||||
bad-label=${msg("No token saved")}
|
||||
></ak-status-label>
|
||||
<a
|
||||
class="pf-c-button pf-m-primary"
|
||||
href=${this.provider?.authOauthUrlStart ||
|
||||
""}
|
||||
target="_blank"
|
||||
>${msg("(Re-)authenticate")}</a
|
||||
>`,
|
||||
],
|
||||
[
|
||||
msg("OAuth Callback URL"),
|
||||
html`<input
|
||||
class="pf-c-form-control"
|
||||
readonly
|
||||
type="text"
|
||||
value="${this.provider.authOauthUrlCallback ||
|
||||
""}"
|
||||
/>`,
|
||||
],
|
||||
],
|
||||
{ horizontal: true },
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ak-sync-status-card
|
||||
.fetch=${() => {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersScimSyncStatusRetrieve(
|
||||
@@ -261,7 +308,9 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
},
|
||||
);
|
||||
}}
|
||||
></ak-sync-status-card>
|
||||
>
|
||||
${this.renderSyncStatusExtra()}
|
||||
</ak-sync-status-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-l-stack__item">
|
||||
<div class="pf-c-card">
|
||||
|
||||
@@ -90,6 +90,7 @@ export class SyncStatusCard extends AKElement {
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</dl>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ By default, if you click **New Application**, you are prompted to create the new
|
||||
|
||||
- **Configure Bindings**: to manage which applications a user can view and access via their **My applications** page, you can optionally create a [binding](../bindings-overview/index.md) between the application and a specific policy, group, or user. Note that if you do not define any bindings, then all users have access to the application. For more information about user access, refer to our documentation about [policy-driven authorization](#policy-driven-authorization), [using application entitlements](../applications/manage_apps.mdx#create-an-application-entitlement) and [hiding an application](#hide-applications).
|
||||
|
||||
4. On the **Review and Submit Application** panel, review the configuration for the new application and its provider, and then click **Submit**.
|
||||
4. On the **Review and Submit Application** panel, review the configuration for the new application and its provider, and then click **Create Application**.
|
||||
|
||||
## Use bindings to control access
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
title: Create an OAuth2 provider
|
||||
---
|
||||
|
||||
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **New Provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to solely create the provider by navigating to **Applications** > **Providers** and clicking **Create**.
|
||||
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **New Application**. We recommend this combined approach for most common use cases. (Alternatively, you can first create only the provider and then later pair it with an application, by navigating to **Applications** > **Providers** and clicking **New Provider**.)
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Applications** and click **New Provider** to create an application and provider pair.
|
||||
2. Navigate to **Applications > Applications** and click **New Application** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application settings, and then click **Next**.
|
||||
4. Select **OAuth2/OIDC** as the **Provider Type**, and then click **Next**.
|
||||
5. On the **Configure OAuth2/OpenID Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
|
||||
5. On the **Configure Provider** page, provide the required configuration settings.
|
||||
6. Click **Create Application** to create both the application and the provider.
|
||||
|
||||
:::info
|
||||
Optionally, configure the provider with the `offline_access` scope mapping. By default, applications only receive an access token. To receive a refresh token, applications and authentik must be configured to request the `offline_access` scope. Do this in the Scope mapping area on the **Configure OAuth2/OpenID Provider** page.
|
||||
|
||||
@@ -96,6 +96,8 @@ There are three general flows of OAuth 2.0:
|
||||
|
||||
Additionally, the [Refresh token](#refresh-token-grant) (grant type) is optionally used with any of the above flows, as well as the client credentials and device code flows.
|
||||
|
||||
You can define which grant types are available for your OAuth2 provider when you [create and configure the provider](./create-oauth2-provider.md). By default, all types are selected.
|
||||
|
||||
### 1: Web-based application authorization
|
||||
|
||||
The flows and grant types used in this case are those used for a typical authorization process, with a user and an application:
|
||||
|
||||
@@ -70,7 +70,7 @@ For more configuration options and full details about integrating with Grafana,
|
||||
|
||||
### 1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
|
||||
**A.** In the Admin interface, navigate to **Applications** > **Applications** and click **New Application** to create an application and provider pair.
|
||||
**A.** In the Admin interface, navigate to **Applications** > **Applications** and click **New Application** to create an application and provider pair.
|
||||
|
||||
:::tip About application and provider pairs
|
||||
Every application that you add to authentik requires a provider, which is used to configure the specific protocol between the application and authentik, for example OAuth2/OIDC, SAML, LDAP, or others.
|
||||
@@ -91,9 +91,7 @@ Every application that you add to authentik requires a provider, which is used t
|
||||
policies bound to the application must pass in order for a user to have access to the
|
||||
application.
|
||||
- **UI Settings**: optional UI settings that are displayed about the application, including the launch URL, and three settings to display extra information about the application on the **My Applications** page: an optional icon, the publisher of the application, and a brief description.
|
||||
|
||||
- **Choose a Provider type**: select **OAuth2/OpenID Connect** as the provider type.
|
||||
|
||||
- **Configure the Provider**:
|
||||
- **Name**: Provide a name (or accept the auto-provided name).
|
||||
- **Authorization flow**: Select the default `implicit` authorization flow to use for this provider.
|
||||
@@ -102,11 +100,12 @@ Every application that you add to authentik requires a provider, which is used t
|
||||
[_stages_](../../add-secure-apps/flows-stages/stages/index.md) of authorization are
|
||||
defined and executed. The defined set of stages construct the workflows of authentication,
|
||||
authorization, etc.
|
||||
- **Protocol settings** provide the following required configurations:
|
||||
- **Protocol settings**: provide the following required configurations:
|
||||
- Note the **Client ID**, **Client Secret**, and **Slug** values because they will be required later when you configure Grafana to use authentik.
|
||||
- Set a `Strict` redirect URI to `https://grafana.company/login/generic_oauth`.
|
||||
- <strong className="tip">TIP</strong>: The Redirect URI is where the application will
|
||||
go as soon as authentik's authorization flow is successfully completed.
|
||||
- Set the **Redirect URI** as a `Strict` redirect to `https://grafana.company/login/generic_oauth`.
|
||||
- <strong className="tip">TIP</strong>: The Redirect URI is where where a user is
|
||||
directed to, as soon as authentik's authorization flow is successfully completed.
|
||||
- **Grant Types** (required): Select at least one [grant type](../../add-secure-apps/providers/oauth2/#oauth-20-flows-and-grant-types) that the provider can use.
|
||||
- **Logout URI**: set to `https://grafana.company/logout`.
|
||||
- **Logout Method**: set to `Front-channel`.
|
||||
- <strong className="tip">TIP</strong>: With OAuth2, front-channel logout is considered the
|
||||
@@ -115,16 +114,14 @@ Every application that you add to authentik requires a provider, which is used t
|
||||
- <strong className="tip">TIP</strong>: authentik generates a key that you can use, called
|
||||
the `authentik Self-signed Certificate`, if you do not have a specific signing key for an
|
||||
application.
|
||||
|
||||
- **Configure Bindings** _(optional)_: for this tutorial, skip this step because you do not yet have a user. Later, after you create your first user, you can [create a binding](../../add-secure-apps/bindings-overview/work-with-bindings.md) to manage the display and access to applications on a user's **My applications** page.
|
||||
- <strong className="tip">TIP</strong>: By creating a binding between an application and a
|
||||
specific user, you are ensuring that the application is accessible only to that user and any
|
||||
other users or groups for whom you created a binding. Learn more about how bindings are used
|
||||
in authentik in our [Bindings overview](../../add-secure-apps/bindings-overview/index.md).
|
||||
in authentik in our [Bindings overview](../../add-secure-apps/bindings-overview/index.md). For
|
||||
any fields not mentioned above, you can leave the default value.
|
||||
|
||||
For any fields not mentioned above, you can leave the default value.
|
||||
|
||||
**C.** Click **Submit** to save the new application and provider.
|
||||
**C.** Click **Create Application** to save the new application and provider.
|
||||
|
||||
### 2. Configure Grafana to use authentik as its IdP
|
||||
|
||||
|
||||
@@ -24,4 +24,4 @@ Restrict API access to `/api/v3/oauth2/access_tokens/` for non-admin users, or r
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [[security@goauthentik.io](mailto:security@goauthentik.io)](mailto:security@goauthentik.io)
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
|
||||
|
||||
@@ -15,7 +15,7 @@ authentik_preview: true
|
||||
|
||||
## What is Apple Business Manager?
|
||||
|
||||
> Apple Business Manager is a web-based portal for IT administrators, managers, and procurement professionals to manage devices and automate device enrollment.
|
||||
> Apple Business Manager is a web-based portal for IT administrators, managers, and procurement professionals to manage devices, and automate device enrollment.
|
||||
>
|
||||
> Organizations using Apple Business Essentials can allow their users to authenticate into their Apple devices using their IdP credentials, typically their company email addresses.
|
||||
>
|
||||
@@ -35,7 +35,7 @@ While this integration guide focuses on Business Manager, the instructions are a
|
||||
|
||||
## Authentication flow
|
||||
|
||||
This sequence diagram shows a high-level flow between Apple device, authentik, and Apple Business Manager.
|
||||
This sequence diagram shows a high-level flow between the user's apple device, authentik, and Apple Business Manager.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -53,7 +53,8 @@ sequenceDiagram
|
||||
|
||||
```
|
||||
|
||||
In short, Apple Business Manager recognizes the email domain as a federated identity provider controlled by authentik. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
|
||||
In short, Apple Business Manager recognizes the email domain
|
||||
as a federated identity provider controlled by authentik. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
|
||||
|
||||
## Preparation
|
||||
|
||||
@@ -61,13 +62,21 @@ By the end of this integration, your users will be able to enroll their Apple de
|
||||
|
||||
You'll need to have an authentik instance running and accessible on an HTTPS domain, and an Apple Business Manager user with the role of Administrator or People Manager.
|
||||
|
||||
:::warning Apple Business Manager restrictions
|
||||
:::warning Caveats
|
||||
|
||||
Be aware that Apple Business Manager imposes the following restrictions on federated authentication:
|
||||
|
||||
- Federated authentication should use the user’s email address as their username. Aliases aren’t supported.
|
||||
- Existing users with an email address in the federated domain will automatically be converted to federated authentication, effectively _taking ownership_ of the account.
|
||||
- User accounts with the role of Administrator, Site Manager, or People Manager can’t sign in using federated authentication; they can only manage the federation process.
|
||||
:::
|
||||
|
||||
:::
|
||||
|
||||
### Placeholders
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `authentik.company`: The FQDN of the authentik installation.
|
||||
|
||||
## authentik configuration
|
||||
|
||||
@@ -85,18 +94,18 @@ Apple Business Manager requires that we create three scope mappings for our OIDC
|
||||
|
||||
#### User profile information
|
||||
|
||||
Apple Business Manager requires both a given name and family name in the OIDC claim. The example expression below assumes that the user's name is formatted with the given name first, followed by the family name, delimited by a space.
|
||||
1. From the authentik Admin interface, navigate to **Customization > Property Mappings** and click **Create**.
|
||||
|
||||
Consider adjusting the expression to match the name format used in your organization.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
3. Select **Scope Mapping** and set the following values:
|
||||
2. Select **Scope Mapping** and use the following values:
|
||||
- **Name**: `Apple Business Manager profile`
|
||||
- **Scope Name**: `profile`
|
||||
- **Description**: _[optional]_ Set to inform user
|
||||
- **Expression**:
|
||||
Apple Business Manager requires both a given name and family name in the OIDC claim. The example expression below assumes that the user's name is formatted with the given name first, followed by the family name, delimited by a space.
|
||||
|
||||
```python
|
||||
Consider adjusting the expression to match the name format used in your organization.
|
||||
|
||||
```py
|
||||
given_name, _, family_name = request.user.name.partition(" ")
|
||||
|
||||
return {
|
||||
@@ -105,129 +114,151 @@ Consider adjusting the expression to match the name format used in your organiza
|
||||
}
|
||||
```
|
||||
|
||||
4. Click **Finish**.
|
||||
3. Click **Finish** and confirm that new scope mapping is listed in the **Property Mappings** overview.
|
||||
|
||||
#### Read access
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
3. Select **Scope Mapping** and set the following values:
|
||||
1. On the **Property Mappings** list, click **Create**.
|
||||
|
||||
2. Select **Scope Mapping** and use the following values:
|
||||
- **Name**: `Apple Business Manager ssf.read`
|
||||
- **Scope Name**: `ssf.read`
|
||||
- **Description**: _[optional]_ Set to inform user
|
||||
- **Expression**: `return {}`
|
||||
|
||||
4. Click **Finish**.
|
||||
3. Click **Finish** and confirm that new scope mapping is listed in the **Property Mappings** overview.
|
||||
|
||||
#### Management access
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
3. Select **Scope Mapping** and set the following values:
|
||||
1. On the **Property Mappings** list, click **Create**.
|
||||
|
||||
2. Select **Scope Mapping** and use the following values:
|
||||
- **Name**: `Apple Business Manager ssf.manage`
|
||||
- **Scope Name**: `ssf.manage`
|
||||
- **Description**: _[optional]_ Set to inform user
|
||||
- **Expression**: `return {}`
|
||||
|
||||
4. Click **Finish**.
|
||||
3. Click **Finish** and confirm that new scope mapping is listed in the **Property Mappings** overview.
|
||||
|
||||
### 2. Create signing key
|
||||
### 2. Create signing keys
|
||||
|
||||
You will need to create a **Signing Key** to sign Security Event Tokens (SET).
|
||||
You will need to create **Signing Key** to sign Security Event Tokens (SET).
|
||||
This key is used to both sign and verify the SETs that are sent between authentik and Apple Business Manager.
|
||||
|
||||
You can either generate a new key or import an existing one. It is recommended to use the same key for both the OIDC and SSF providers.
|
||||
You can either generate a new key or import an existing one.
|
||||
|
||||
#### Generate a new key
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **System** > **Certificates** and click **Generate Certificate-Key Pair**.
|
||||
3. Provide a **Certificate Name** and click **Generate Certificate-Key Pair**.
|
||||
1. From the Admin interface, navigate to **System > Certificates**
|
||||
2. Click **Generate**, select **Signing Key**, and use the following values:
|
||||
- **Common Name**: `apple-business-manager`
|
||||
|
||||
3. Click **Generate** and confirm that the new key is listed in the **Certificates** overview.
|
||||
|
||||
#### Import an existing key
|
||||
|
||||
Alternatively, you can import an existing key.
|
||||
Alternatively, you can use an existing key if you have one available.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **System** > **Certificates** and click **Import Existing Certificate-Key Pair**.
|
||||
3. Provide a **Certificate Name**, paste the contents of your **Certificate**, and _optionally_ paste your **Private Key**.
|
||||
4. Click **Import Certificate-Key Pair**.
|
||||
1. From the Admin interface, navigate to **System > Certificates**.
|
||||
2. Click **Create** and use the following values:
|
||||
- **Name**: `apple-business-manager`
|
||||
- **Certificate**: Paste in your certificate
|
||||
- **Private Key**: _[optional]_ Paste in your private key
|
||||
|
||||
3. Click **Create** and confirm that the new key is listed in the **Certificates** overview.
|
||||
|
||||
### 3. Create OIDC provider
|
||||
|
||||
You will need to create an [OAuth2/OpenID Provider](/docs/add-secure-apps/providers/oauth2/) to handle the authentication flow between authentik and Apple Business Manager.
|
||||
:::tip Keep your text editor ready
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers** and click **New Provider** to open the provider wizard.
|
||||
- **Choose a Provider type**: select **OAuth2/OpenID Provider** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Note the **Client ID** and **Client Secret** values because they will be required later.
|
||||
- Set a `Strict` redirect URI to `https://gsa-ws.apple.com/grandslam/GsService2/acs`.
|
||||
- Select any available signing key.
|
||||
- Under **Advanced protocol settings**, in addition to the default scopes, add the four following **Selected Scopes** to the provider.
|
||||
- `Apple Business Manager ssf.manage`
|
||||
- `Apple Business Manager ssf.read`
|
||||
- `Apple Business Manager profile`
|
||||
- `authentik default OAuth Mapping: OpenID 'offline_access'`
|
||||
authentik will automatically generate the **Client ID** and **Client Secret** values for the new provider. You'll need these values when configuring Apple Business Manager.
|
||||
|
||||
3. Click **Create**.
|
||||
You can always find your provider's generated values by navigating to **Providers**, selecting the provider by name, and clicking the **Edit** button.
|
||||
|
||||
:::
|
||||
|
||||
1. From the authentik Admin interface, navigate to **Applications > Providers** and click **Create**.
|
||||
2. For the **Provider Type** select **OAuth2/OpenID Provider**, click **Next**, and use the following values.
|
||||
- **Name**: `Apple Business Manager`
|
||||
- **Authorization flow**: Select a flow that suits your organization's requirements.
|
||||
- **Protocol settings**:
|
||||
- **Client ID**: Copy the generated value to your text editor.
|
||||
- **Client Secret**: Copy the generated value to your text editor.
|
||||
- **Redirect URIs/Origins**:
|
||||
- `Strict`
|
||||
- **URL**: `https://gsa-ws.apple.com/grandslam/GsService2/acs`
|
||||
- **Signing Key**: Select a certificate to sign the OpenID Connect tokens.
|
||||
- **Advanced protocol settings**:
|
||||
Any fields that can be left as their default values are omitted from the list.
|
||||
- **Scopes**: Add four **Selected Scopes** to the provider.
|
||||
- [x] `Apple Business Manager ssf.manage`
|
||||
- [x] `Apple Business Manager ssf.read`
|
||||
- [x] `Apple Business Manager profile`
|
||||
- [x] `authentik default OAuth Mapping: OpenID 'profile'`
|
||||
|
||||
3. Click **Finish** and confirm that `Apple Business Manager` is listed in the provider overview.
|
||||
|
||||
4. Navigate to **Applications > Providers** and click `Apple Business Manager`.
|
||||
5. Copy the **OpenID Configuration URL** field to your text editor.
|
||||
|
||||
### 4. Create Shared Signals Framework provider
|
||||
|
||||
While the OIDC provider handles the authentication flow, you'll need to create a [Shared Signals Framework provider](/docs/add-secure-apps/providers/ssf/) to handle the backchannel communication between authentik and Apple Business Manager.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Providers** and click **New Provider** to open the provider wizard.
|
||||
- **Choose a Provider type**: select **Shared Signals Framework Provider** and the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), and the following required configurations.
|
||||
- Select the same signing key that you selected for the OIDC provider.
|
||||
1. From the authentik Admin interface, navigate to **Applications > Providers** and click **Create**.
|
||||
2. Select **Shared Signals Framework Provider** and use the following values.
|
||||
Any fields that can be left as their default values are omitted from the list.
|
||||
- **Name** `Apple Business Manager SSF`
|
||||
- **Signing Key**: `[Your Signing Key]`
|
||||
- **Event Retention**: `days=30`
|
||||
|
||||
3. Click **Create**.
|
||||
3. Click **Finish** and confirm that the new SSF provider is listed in the overview.
|
||||
|
||||
:::note A Blank SSF Config URL is expected
|
||||
The **SSF Config URL** will be blank until the SSF provider is assigned to an application as a backchannel provider. We'll return to collect this URL after creating the application.
|
||||
:::
|
||||
:::tip A blank SSF Config URL is expected
|
||||
|
||||
Keep in mind the **SSF Config URL** will be blank until the SSF provider is assigned to an application as a backchannel provider. We'll return to collect this URL after creating the application.
|
||||
|
||||
:::
|
||||
|
||||
### 5. Assign SSF permissions
|
||||
|
||||
The authentik user you will use to test the stream connection to Apple Business Manager must either have the role of superuser (such as the default `akadmin` account) or have permission to **Add stream to SSF provider**.
|
||||
The authentik user you will use to test the stream connection to Apple Business Manager must either have the role of superuser or have permission to add streams to the SSF provider.
|
||||
|
||||
If not using a superuser account, you can assign the correct permission by following these steps:
|
||||
1. From authentik the Admin interface, navigate to **Applications > Providers** and click the Apple Business Manager SSF provider.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Directory** > **Roles** and click **New Role**.
|
||||
3. Provide a name for the new role and click **Create Role**.
|
||||
4. Click on the name of the newly created role and open the **Users** tab.
|
||||
5. Add whichever user you want to have the permission.
|
||||
6. Navigate to **Applications** > **Providers** and click on the name of the SSF provider.
|
||||
7. Open the **Permissions** tab and click **Assign Role Object Permission**.
|
||||
8. Select the newly created role, toggle on **Add stream to SSF provider**, and click **Assign Role Object Permission**.
|
||||
2. Click the **Permissions** tab, select **User Object Permissions**, and click **Assign to new user**.
|
||||
|
||||
3. In the **User** field, enter the object name of the test user performing the initial connection to Apple Business Manager.
|
||||
|
||||
4. Set the **Add stream to SSF provider** permission toggle to **On**
|
||||
|
||||
5. Click **Assign** and confirm that the user is listed in the **User Object Permissions** list.
|
||||
|
||||
### 6. Create application
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Application**, click the **New Application** dropdown, click **with Existing Provider**, and set the following required values:
|
||||
- **Application Name**: `Apple Business Manager`
|
||||
1. From the authentik Admin interface, navigate to **Applications > Applications**, click **Create**, and use the following values:
|
||||
- **Name**: Apple Business Manager
|
||||
- **Slug**: `abm`
|
||||
- **Provider**: Select the OIDC provider that you created
|
||||
- **Backchannel Provider:** Select the SSF provider that you created
|
||||
- **Provider**: `Apple Business Manager`
|
||||
- **Backchannel Provider:** `Apple Business Manager SSF`
|
||||
|
||||
3. Click **Create application**.
|
||||
4. Navigate to **Application** > **Providers** and click on the name of the SSF provider.
|
||||
5. On the **Overview** tab, take note of the `SSF Config URL` value.
|
||||
6. Navigate to **Application** > **Providers** and click on the name of the OIDC provider.
|
||||
7. On the **Overview** tab, take note of the `OpenID Configuration URL` value.
|
||||
2. Click **Create** and confirm that the application is listed in the overview page.
|
||||
|
||||
3. Navigate to **Providers > Apple Business Manager SSF**
|
||||
- On the **Overview** tab copy the `SSF Config URL` value to your text editor.
|
||||
|
||||
### 7. Confirm and modify copied authentik values
|
||||
|
||||
Before proceeding to Apple Business Manager, let's go over the values that you have copied from authentik.
|
||||
|
||||
- Verify that you have all the necessary values:
|
||||
- From the OIDC provider:
|
||||
- `Client ID`
|
||||
- `Client Secret`
|
||||
- `OpenID Configuration URL`
|
||||
- Verify that you have all the necessary values in your text editor:
|
||||
- From the `Apple Business Manager` provider:
|
||||
- [x] `Client ID`
|
||||
- [x] `Client Secret`
|
||||
- [x] `OpenID Configuration URL`
|
||||
|
||||
- From the SSF provider:
|
||||
- `SSF Config URL`
|
||||
- From the `Apple Business Manager SSF` provider:
|
||||
- [x] `SSF Config URL`
|
||||
|
||||
## Apple Business Manager configuration
|
||||
|
||||
@@ -239,58 +270,67 @@ Similar to a personal Apple account, a _Managed Apple Account_ uses an email add
|
||||
|
||||
By verifying the domain, Apple Business Manager will delegate ownership of any accounts with a matching email address to the organization, allowing for centralized management of devices, apps, and services.
|
||||
|
||||
1. Log in to the [Apple Business Manager dashboard](https://business.apple.com/) as an administrator.
|
||||
2. Click **your account name** in the sidebar, then select **Preferences**.
|
||||
3. From the Preferences page, select **Managed Apple Accounts** tab, click **Add Domain**, and then provide your domain name.
|
||||
1. From the [Apple Business Manager dashboard](https://business.apple.com/), click **your account name** on the sidebar, then select **Preferences**.
|
||||
|
||||
2. From the Preferences page, select **Managed Apple Accounts** tab, click **Add Domain** and then provide your domain name.
|
||||
Apple will generate a DNS TXT record that you'll need to add to your domain's DNS settings.
|
||||
4. Wait for DNS propagation and click **Verify** to complete the domain verification process.
|
||||
|
||||
3. Wait for DNS propagation and click **Verify** to complete the domain verification process.
|
||||
|
||||
A confirmation dialog will prompt you to lock your domain before you can proceed with the next steps.
|
||||
|
||||
:::warning Locking your domain affects all enrolled users
|
||||
|
||||
Locking your domain ensures that only your organization can use your domain for federated authentication.
|
||||
|
||||
**Once locked, your enrolled users will not be able to access Apple services until you complete the next steps to configure federated authentication.**
|
||||
|
||||
**Only lock your domain when you're ready to proceed with the next steps.**
|
||||
|
||||
:::
|
||||
|
||||
5. In the confirmation dialog, set the **Lock Domain** toggle to **On** and confirm that the domain displays as locked in the **Managed Apple Accounts** tab.
|
||||
4. In the confirmation dialog, set the **Lock Domain** toggle to **On** and confirm that the domain displays as locked in the **Managed Apple Accounts** tab.
|
||||
|
||||
### 2. Capture all accounts _(optional)_
|
||||
### 2. Capture all accounts
|
||||
|
||||
Optionally, you may choose to [capture all accounts](https://support.apple.com/guide/apple-business-manager/capture-a-domain-axm512ce43c3/1/web/1), which will convert all existing accounts with an email address in the federated domain to _Managed Apple Accounts_. You can also choose to capture all accounts at a later time when you're ready to manage all users in the domain.
|
||||
|
||||
:::danger Account capture is one-way migration
|
||||
|
||||
Choosing to capture all accounts will affect all users with an email address in the federated domain, regardless of their enrollment status or device ownership.
|
||||
**Once captured, the accounts can't be reverted to personal Apple accounts – even if the domain is unlocked.**
|
||||
|
||||
**Only capture accounts if you're sure that every user in the domain should be managed by Apple Business Manager.**
|
||||
|
||||
:::
|
||||
|
||||
1. Log in to the [Apple Business Manager dashboard](https://business.apple.com/) as an administrator.
|
||||
2. Click **your account name** in the sidebar, then select **Preferences**.
|
||||
3. From the Preferences page, select **Managed Apple Accounts** tab, and click **Manage** next to the domain you've verified.
|
||||
4. In your domain's management dialog, ensure you understand the implications of capturing all accounts and then click **Capture All Accounts**.
|
||||
5. Wait for Apple to complete the account capture process, and confirm that all accounts are now managed by Apple Business Manager.
|
||||
1. From the [Apple Business Manager dashboard](https://business.apple.com/), click **your account name** on the sidebar, then select **Preferences**.
|
||||
|
||||
2. From the Preferences page, select **Managed Apple Accounts** tab, and click **Manage** next to the domain you've verified.
|
||||
|
||||
3. In your domain's management dialog, ensure you understand the implications of capturing all accounts and then click **Capture All Accounts**.
|
||||
|
||||
4. Wait for Apple to complete the account capture process, and confirm that all accounts are now managed by Apple Business Manager.
|
||||
|
||||
### 3. Enable federated authentication
|
||||
|
||||
You're now ready to configure federated authentication with authentik.
|
||||
|
||||
1. Log in to the [Apple Business Manager dashboard](https://business.apple.com/) as an administrator.
|
||||
2. Click **your account name** in the sidebar, then select **Preferences**.
|
||||
3. From the Preferences page, select **Managed Apple Accounts** tab, and click **Get Started** under the "User sign in and directory sync" section.
|
||||
4. To define how you want users to sign in, choose **Custom Identity Provider** and click **Continue**.
|
||||
5. On the **Set up your Custom Identity Provider** page, use the following values:
|
||||
- **Name**: `authentik`
|
||||
- **Client ID**: `Client ID` from authentik
|
||||
- **Client Secret**: `Client Secret` from authentik
|
||||
- **SSF Config URL**: `SSF Config URL` from authentik
|
||||
- **OpenID Config URL**: `OpenID Configuration URL` from authentik
|
||||
1. From the Apple Business Manager dashboard, click **your account name** on the sidebar, then select **Preferences**.
|
||||
|
||||
6. Click **Continue** to begin Apple's verification of your configuration.
|
||||
7. When prompted to authenticate through your authentik instance, provide your credentials and click **Log In**.
|
||||
2. From the Preferences page, select **Managed Apple Accounts** tab, and click **Get Started** under the "User sign in and directory sync" section.
|
||||
|
||||
3. To define how you want users to sign in, choose **Custom Identity Provider** and click **Continue**.
|
||||
|
||||
4. On the **Set up your Custom Identity Provider** page, use the following values:
|
||||
- **Name**: `authentik`
|
||||
- **Client ID**: _`Your Client ID`_
|
||||
- **Client Secret**: _`Your Client Secret`_
|
||||
- **SSF Config URL**: **_`Your SSF Config URL with 443 port`_**
|
||||
- **OpenID Config URL**: **_`Your OpenID Config URL with 443 port`_**
|
||||
|
||||
5. Click **Continue** to begin Apple's verification of your configuration.
|
||||
6. When prompted to authenticate through your authentik instance, provide your credentials and click **Log In**.
|
||||
|
||||
When the test finishes, click **Done** to complete the configuration.
|
||||
|
||||
@@ -302,17 +342,18 @@ If the connection test fails, your configuration may be incorrect. Here are some
|
||||
- [x] Verify that the Client ID and Client Secret values are correct.
|
||||
- [x] Verify that scope mappings are created and all assigned to the OIDC provider.
|
||||
- [x] Verify that the SSF provider is assigned to the application.
|
||||
- [x] Ensure that the SSF Config URL and OpenID Configuration URL are accurate.
|
||||
- [x] Ensure that the OAuth and SSF providers both have signing keys set. Ideally the same certificate should be used for both.
|
||||
- [x] Ensure that the SSF Config URL and OpenID Configuration URL include the port number `443`.
|
||||
|
||||
If you're still having issues, check your authentik instance's log for any errors that might have occurred during the authentication process. If Apple can reach your authentik instance, you should see logs indicating Apple's attempts to test the authentication flow.
|
||||
|
||||
## Configuration verification
|
||||
|
||||
:::warning Administrators cannot use federated authentication
|
||||
|
||||
Apple Business Manager does not allow users with the role of Administrator, Site Manager, or People Manager to log in using federated authentication.
|
||||
|
||||
When creating test users, ensure that their role is set to Standard (or Student) to test federated authentication with authentik.
|
||||
|
||||
:::
|
||||
|
||||
### 1. Create a test user
|
||||
@@ -325,14 +366,17 @@ When creating test users, ensure that their role is set to Standard (or Student)
|
||||
- **Role**: `Standard`
|
||||
|
||||
3. Click **Save** to create the user account, and then click **Create Sign-In** in the user's profile.
|
||||
|
||||
4. When prompted to choose a delivery method, select **Create a downloadable PDF and CSV** and click **Continue**. Note the temporary password provided on the next page, optionally downloading the PDF and CSV files for future reference.
|
||||
|
||||
5. Confirm the user is created from the authentik Admin interface by navigating to the **Users** page and searching for the account by their email address. Note that this may take a few minutes to synchronize.
|
||||
|
||||
### 2. Test the authentication flow
|
||||
|
||||
1. Confirm that the test user is synchronized in authentik.
|
||||
1. Confirmed the test user in synchronized in authentik.
|
||||
2. Open a private browsing window and navigate to the [Apple Business Manager](https://business.apple.com/).
|
||||
3. In the email field, provide the email address assigned to the test user.
|
||||
3. In the email field, provide the email address assigned to test user.
|
||||
|
||||
4. Submit the form to trigger the authentication flow.
|
||||
|
||||
You should be redirected to authentik for authentication and then back to Apple Business Manager to manage the test user's account.
|
||||
|
||||
1
website/package-lock.json
generated
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "@goauthentik/docs",
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"vendored/*",
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
"build:integrations": "npm run build -w integrations",
|
||||
"check-types": "tsc -b",
|
||||
"docusaurus": "docusaurus",
|
||||
"preinstall": "npm ci --prefix ..",
|
||||
"lint": "eslint --fix .",
|
||||
"lint:lockfile": "echo 'Skipping lockfile linting'",
|
||||
"lint-check": "eslint --max-warnings 0 .",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier-check": "prettier --check .",
|
||||
"prettier-check": "npm run prettier-prepare && prettier --check .",
|
||||
"prettier-prepare": "npm ci --prefix ../packages/prettier-config",
|
||||
"start": "npm start -w docs",
|
||||
"test": "node --test"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 575 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 576 KiB After Width: | Height: | Size: 1.0 MiB |