Skip to content

Commit 6549b30

Browse files
authored
enterprise/providers: SSF (#12327)
* init Signed-off-by: Jens Langhammer <[email protected]> * fix some other stuff Signed-off-by: Jens Langhammer <[email protected]> * more progress Signed-off-by: Jens Langhammer <[email protected]> * fix missing format Signed-off-by: Jens Langhammer <[email protected]> * make it work, send verification event Signed-off-by: Jens Langhammer <[email protected]> * progress Signed-off-by: Jens Langhammer <[email protected]> * more progress Signed-off-by: Jens Langhammer <[email protected]> * fix Signed-off-by: Jens Langhammer <[email protected]> * save iss Signed-off-by: Jens Langhammer <[email protected]> * add signals for MFA devices Signed-off-by: Jens Langhammer <[email protected]> * fix tests Signed-off-by: Jens Langhammer <[email protected]> * refactor more Signed-off-by: Jens Langhammer <[email protected]> * re-work auth Signed-off-by: Jens Langhammer <[email protected]> * add API to list ssf streams Signed-off-by: Jens Langhammer <[email protected]> * start rbac Signed-off-by: Jens Langhammer <[email protected]> * add ssf icon Signed-off-by: Jens Langhammer <[email protected]> * fix web Signed-off-by: Jens Langhammer <[email protected]> * fix bugs Signed-off-by: Jens Langhammer <[email protected]> * make events expire, rewrite sending logic Signed-off-by: Jens Langhammer <[email protected]> * add oidc token test Signed-off-by: Jens Langhammer <[email protected]> * add stream list Signed-off-by: Jens Langhammer <[email protected]> * add jwks tests and fixes Signed-off-by: Jens Langhammer <[email protected]> * update web ui Signed-off-by: Jens Langhammer <[email protected]> * fix Signed-off-by: Jens Langhammer <[email protected]> * fix configuration endpoint Signed-off-by: Jens Langhammer <[email protected]> * replace port number correctly Signed-off-by: Jens Langhammer <[email protected]> * better log what went wrong Signed-off-by: Jens Langhammer <[email protected]> * linter has opinions Signed-off-by: Jens Langhammer <[email protected]> * fix messages Signed-off-by: Jens Langhammer <[email protected]> * fix set status Signed-off-by: Jens Langhammer <[email protected]> * more debug logging Signed-off-by: Jens Langhammer <[email protected]> * fix issuer here too Signed-off-by: Jens Langhammer <[email protected]> * remove port :443...removal apparently apple's HTTP logic is wrong and includes the port in the Host header even if the default port is used (80 or 443), which then fails as the URL doesn't exactly match what the admin configured...so instead of trying to add magic about this we'll add it in the docs Signed-off-by: Jens Langhammer <[email protected]> * fix error when no request in context Signed-off-by: Jens Langhammer <[email protected]> * add signal for admin session revoke Signed-off-by: Jens Langhammer <[email protected]> * set txn based on request id Signed-off-by: Jens Langhammer <[email protected]> * validate method and endpoint url Signed-off-by: Jens Langhammer <[email protected]> * fix request ID detection Signed-off-by: Jens Langhammer <[email protected]> * add timestamp Signed-off-by: Jens Langhammer <[email protected]> * temp migration Signed-off-by: Jens Langhammer <[email protected]> * fix signal Signed-off-by: Jens Langhammer <[email protected]> * add signal tests Signed-off-by: Jens Langhammer <[email protected]> * the final commit Signed-off-by: Jens Langhammer <[email protected]> * ok actually the last commit Signed-off-by: Jens Langhammer <[email protected]> --------- Signed-off-by: Jens Langhammer <[email protected]>
1 parent e2d6d38 commit 6549b30

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2598
-19
lines changed

Makefile

+8-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ UID = $(shell id -u)
66
GID = $(shell id -g)
77
NPM_VERSION = $(shell python -m scripts.npm_version)
88
PY_SOURCES = authentik tests scripts lifecycle .github
9+
GO_SOURCES = cmd internal
10+
WEB_SOURCES = web/src web/packages
911
DOCKER_IMAGE ?= "authentik:test"
1012

1113
GEN_API_TS = "gen-ts-api"
@@ -19,11 +21,12 @@ pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
1921
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
2022
-I .github/codespell-words.txt \
2123
-S 'web/src/locales/**' \
22-
-S 'website/docs/developer-docs/api/reference/**' \
23-
authentik \
24-
internal \
25-
cmd \
26-
web/src \
24+
-S 'website/developer-docs/api/reference/**' \
25+
-S '**/node_modules/**' \
26+
-S '**/dist/**' \
27+
$(PY_SOURCES) \
28+
$(GO_SOURCES) \
29+
$(WEB_SOURCES) \
2730
website/src \
2831
website/blog \
2932
website/docs \

authentik/blueprints/v1/importer.py

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
MicrosoftEntraProviderUser,
5252
)
5353
from authentik.enterprise.providers.rac.models import ConnectionToken
54+
from authentik.enterprise.providers.ssf.models import StreamEvent
5455
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
5556
EndpointDevice,
5657
EndpointDeviceConnection,
@@ -131,6 +132,7 @@ def excluded_models() -> list[type[Model]]:
131132
EndpointDevice,
132133
EndpointDeviceConnection,
133134
DeviceToken,
135+
StreamEvent,
134136
)
135137

136138

authentik/core/models.py

+8
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,14 @@ def get_provider(self) -> Provider | None:
599599
return None
600600
return candidates[-1]
601601

602+
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
603+
"""Get Backchannel provider for a specific type"""
604+
providers = self.backchannel_providers.filter(
605+
**{f"{provider_type._meta.model_name}__isnull": False},
606+
**kwargs,
607+
)
608+
return getattr(providers.first(), provider_type._meta.model_name)
609+
602610
def __str__(self):
603611
return str(self.name)
604612

authentik/enterprise/providers/ssf/__init__.py

Whitespace-only changes.

authentik/enterprise/providers/ssf/api/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""SSF Provider API Views"""
2+
3+
from django.urls import reverse
4+
from rest_framework.fields import SerializerMethodField
5+
from rest_framework.request import Request
6+
from rest_framework.viewsets import ModelViewSet
7+
8+
from authentik.core.api.providers import ProviderSerializer
9+
from authentik.core.api.tokens import TokenSerializer
10+
from authentik.core.api.used_by import UsedByMixin
11+
from authentik.enterprise.api import EnterpriseRequiredMixin
12+
from authentik.enterprise.providers.ssf.models import SSFProvider
13+
14+
15+
class SSFProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
16+
"""SSFProvider Serializer"""
17+
18+
ssf_url = SerializerMethodField()
19+
token_obj = TokenSerializer(source="token", required=False, read_only=True)
20+
21+
def get_ssf_url(self, instance: SSFProvider) -> str | None:
22+
request: Request = self._context.get("request")
23+
if not request:
24+
return None
25+
if not instance.backchannel_application:
26+
return None
27+
return request.build_absolute_uri(
28+
reverse(
29+
"authentik_providers_ssf:configuration",
30+
kwargs={
31+
"application_slug": instance.backchannel_application.slug,
32+
},
33+
)
34+
)
35+
36+
class Meta:
37+
model = SSFProvider
38+
fields = [
39+
"pk",
40+
"name",
41+
"component",
42+
"verbose_name",
43+
"verbose_name_plural",
44+
"meta_model_name",
45+
"signing_key",
46+
"token_obj",
47+
"oidc_auth_providers",
48+
"ssf_url",
49+
"event_retention",
50+
]
51+
extra_kwargs = {}
52+
53+
54+
class SSFProviderViewSet(UsedByMixin, ModelViewSet):
55+
"""SSFProvider Viewset"""
56+
57+
queryset = SSFProvider.objects.all()
58+
serializer_class = SSFProviderSerializer
59+
filterset_fields = {
60+
"application": ["isnull"],
61+
"name": ["iexact"],
62+
}
63+
search_fields = ["name"]
64+
ordering = ["name"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""SSF Stream API Views"""
2+
3+
from rest_framework.viewsets import ReadOnlyModelViewSet
4+
5+
from authentik.core.api.utils import ModelSerializer
6+
from authentik.enterprise.providers.ssf.api.providers import SSFProviderSerializer
7+
from authentik.enterprise.providers.ssf.models import Stream
8+
9+
10+
class SSFStreamSerializer(ModelSerializer):
11+
"""SSFStream Serializer"""
12+
13+
provider_obj = SSFProviderSerializer(source="provider", read_only=True)
14+
15+
class Meta:
16+
model = Stream
17+
fields = [
18+
"pk",
19+
"provider",
20+
"provider_obj",
21+
"delivery_method",
22+
"endpoint_url",
23+
"events_requested",
24+
"format",
25+
"aud",
26+
"iss",
27+
]
28+
29+
30+
class SSFStreamViewSet(ReadOnlyModelViewSet):
31+
"""SSFStream Viewset"""
32+
33+
queryset = Stream.objects.all()
34+
serializer_class = SSFStreamSerializer
35+
filterset_fields = ["provider", "endpoint_url", "delivery_method"]
36+
search_fields = ["provider__name", "endpoint_url"]
37+
ordering = ["provider", "uuid"]
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""SSF app config"""
2+
3+
from authentik.enterprise.apps import EnterpriseConfig
4+
5+
6+
class AuthentikEnterpriseProviderSSF(EnterpriseConfig):
7+
"""authentik enterprise ssf app config"""
8+
9+
name = "authentik.enterprise.providers.ssf"
10+
label = "authentik_providers_ssf"
11+
verbose_name = "authentik Enterprise.Providers.SSF"
12+
default = True
13+
mountpoint = ""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Generated by Django 5.0.11 on 2025-02-05 16:20
2+
3+
import authentik.lib.utils.time
4+
import django.contrib.postgres.fields
5+
import django.db.models.deletion
6+
import uuid
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
initial = True
13+
14+
dependencies = [
15+
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
16+
("authentik_crypto", "0004_alter_certificatekeypair_name"),
17+
("authentik_providers_oauth2", "0027_accesstoken_authentik_p_expires_9f24a5_idx_and_more"),
18+
]
19+
20+
operations = [
21+
migrations.CreateModel(
22+
name="SSFProvider",
23+
fields=[
24+
(
25+
"provider_ptr",
26+
models.OneToOneField(
27+
auto_created=True,
28+
on_delete=django.db.models.deletion.CASCADE,
29+
parent_link=True,
30+
primary_key=True,
31+
serialize=False,
32+
to="authentik_core.provider",
33+
),
34+
),
35+
(
36+
"event_retention",
37+
models.TextField(
38+
default="days=30",
39+
validators=[authentik.lib.utils.time.timedelta_string_validator],
40+
),
41+
),
42+
(
43+
"oidc_auth_providers",
44+
models.ManyToManyField(
45+
blank=True, default=None, to="authentik_providers_oauth2.oauth2provider"
46+
),
47+
),
48+
(
49+
"signing_key",
50+
models.ForeignKey(
51+
help_text="Key used to sign the SSF Events.",
52+
on_delete=django.db.models.deletion.CASCADE,
53+
to="authentik_crypto.certificatekeypair",
54+
verbose_name="Signing Key",
55+
),
56+
),
57+
(
58+
"token",
59+
models.ForeignKey(
60+
default=None,
61+
null=True,
62+
on_delete=django.db.models.deletion.CASCADE,
63+
to="authentik_core.token",
64+
),
65+
),
66+
],
67+
options={
68+
"verbose_name": "Shared Signals Framework Provider",
69+
"verbose_name_plural": "Shared Signals Framework Providers",
70+
"permissions": [("add_stream", "Add stream to SSF provider")],
71+
},
72+
bases=("authentik_core.provider",),
73+
),
74+
migrations.CreateModel(
75+
name="Stream",
76+
fields=[
77+
(
78+
"uuid",
79+
models.UUIDField(
80+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
81+
),
82+
),
83+
(
84+
"delivery_method",
85+
models.TextField(
86+
choices=[
87+
(
88+
"https://schemas.openid.net/secevent/risc/delivery-method/push",
89+
"Risc Push",
90+
),
91+
(
92+
"https://schemas.openid.net/secevent/risc/delivery-method/poll",
93+
"Risc Poll",
94+
),
95+
]
96+
),
97+
),
98+
("endpoint_url", models.TextField(null=True)),
99+
(
100+
"events_requested",
101+
django.contrib.postgres.fields.ArrayField(
102+
base_field=models.TextField(
103+
choices=[
104+
(
105+
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
106+
"Caep Session Revoked",
107+
),
108+
(
109+
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
110+
"Caep Credential Change",
111+
),
112+
(
113+
"https://schemas.openid.net/secevent/ssf/event-type/verification",
114+
"Set Verification",
115+
),
116+
]
117+
),
118+
default=list,
119+
size=None,
120+
),
121+
),
122+
("format", models.TextField()),
123+
(
124+
"aud",
125+
django.contrib.postgres.fields.ArrayField(
126+
base_field=models.TextField(), default=list, size=None
127+
),
128+
),
129+
("iss", models.TextField()),
130+
(
131+
"provider",
132+
models.ForeignKey(
133+
on_delete=django.db.models.deletion.CASCADE,
134+
to="authentik_providers_ssf.ssfprovider",
135+
),
136+
),
137+
],
138+
options={
139+
"verbose_name": "SSF Stream",
140+
"verbose_name_plural": "SSF Streams",
141+
"default_permissions": ["change", "delete", "view"],
142+
},
143+
),
144+
migrations.CreateModel(
145+
name="StreamEvent",
146+
fields=[
147+
("created", models.DateTimeField(auto_now_add=True)),
148+
("last_updated", models.DateTimeField(auto_now=True)),
149+
("expires", models.DateTimeField(default=None, null=True)),
150+
("expiring", models.BooleanField(default=True)),
151+
(
152+
"uuid",
153+
models.UUIDField(
154+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
155+
),
156+
),
157+
(
158+
"status",
159+
models.TextField(
160+
choices=[
161+
("pending_new", "Pending New"),
162+
("pending_failed", "Pending Failed"),
163+
("sent", "Sent"),
164+
]
165+
),
166+
),
167+
(
168+
"type",
169+
models.TextField(
170+
choices=[
171+
(
172+
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
173+
"Caep Session Revoked",
174+
),
175+
(
176+
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
177+
"Caep Credential Change",
178+
),
179+
(
180+
"https://schemas.openid.net/secevent/ssf/event-type/verification",
181+
"Set Verification",
182+
),
183+
]
184+
),
185+
),
186+
("payload", models.JSONField(default=dict)),
187+
(
188+
"stream",
189+
models.ForeignKey(
190+
on_delete=django.db.models.deletion.CASCADE,
191+
to="authentik_providers_ssf.stream",
192+
),
193+
),
194+
],
195+
options={
196+
"verbose_name": "SSF Stream Event",
197+
"verbose_name_plural": "SSF Stream Events",
198+
"ordering": ("-created",),
199+
},
200+
),
201+
]

authentik/enterprise/providers/ssf/migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)