Compare commits
97 Commits
import-org
...
eap-but-ac
Author | SHA1 | Date | |
---|---|---|---|
06848be14b | |||
4bae3bbe60 | |||
e33f839d7f | |||
f5eb827d14 | |||
9045f5ba73 | |||
7b97e92094 | |||
3027cdcc4b | |||
67f627a925 | |||
f1101e0c01 | |||
fb01a117ad | |||
fad18db70b | |||
e0c837257c | |||
2a567ccc85 | |||
e36373ceab | |||
d8a625be03 | |||
4d944f7444 | |||
c49274042b | |||
10fc15ffe0 | |||
7c996d9d9d | |||
5d25f68b71 | |||
8da54d5811 | |||
4571f5e644 | |||
ee234ea3aa | |||
82c177b7eb | |||
1155ccb3e8 | |||
1575b96262 | |||
19bb77638a | |||
d6cf129eaa | |||
b6686cff14 | |||
8cf8f1e199 | |||
50c50c4109 | |||
51f4a8d83d | |||
3ada3a7e0e | |||
fa06c9fe4e | |||
2a024238fe | |||
91c87b7c3c | |||
318443f270 | |||
ac88784089 | |||
855afa7b9f | |||
240abfef41 | |||
03075f1890 | |||
5bc0ed6e11 | |||
8f4cfc28c7 | |||
6d77eaaab7 | |||
9cee59537c | |||
fc5c0e2789 | |||
573446689f | |||
fd4bfe604d | |||
06e76a5b37 | |||
3c228bf5c3 | |||
8a80f07db2 | |||
ae59a3e576 | |||
df21e678d6 | |||
a71532b3e3 | |||
d7cb0b3ea1 | |||
ba8f137885 | |||
958ff66070 | |||
ad57c66a32 | |||
2bba0ddd74 | |||
767c0a8e45 | |||
b10c795a26 | |||
8088e08fd9 | |||
eab6e288d7 | |||
91c2863358 | |||
1638e95bc7 | |||
8f75131541 | |||
c85471575a | |||
5d00dc7e9e | |||
6982e7d1c9 | |||
c7fe987c5a | |||
e48739c8a0 | |||
b2ee585c43 | |||
97e8ea8e76 | |||
1f1e0c9db1 | |||
ca47a803fe | |||
c606eb53b0 | |||
62357133b0 | |||
99d2d91257 | |||
69d9363fce | |||
cfc7f6b993 | |||
bebbbe9b90 | |||
188d3c69c1 | |||
877f312145 | |||
f471a98bc7 | |||
e874cfc21d | |||
ec7bdf74aa | |||
e87bc94b95 | |||
a3865abaa9 | |||
7100d3c674 | |||
c0c2d2ad3c | |||
dc287989db | |||
03204f6943 | |||
fcd369e466 | |||
cb79407bc1 | |||
04a88daf34 | |||
c6a49da5c3 | |||
bfeeecf3fa |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.6.2
|
||||
current_version = 2025.6.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
@ -38,6 +38,8 @@ jobs:
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-qemu-action@v3.6.0
|
||||
|
1
.github/workflows/ci-main-daily.yml
vendored
1
.github/workflows/ci-main-daily.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
4
.github/workflows/ci-main.yml
vendored
4
.github/workflows/ci-main.yml
vendored
@ -247,11 +247,13 @@ jobs:
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
needs: ci-core-mark
|
||||
uses: ./.github/workflows/_reusable-docker-build.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_name: ghcr.io/goauthentik/dev-server
|
||||
image_name: ${{ github.repository == 'goauthentik/authentik-internal' && 'ghcr.io/goauthentik/internal-server' || 'ghcr.io/goauthentik/dev-server' }}
|
||||
release: false
|
||||
pr-comment:
|
||||
needs:
|
||||
|
1
.github/workflows/ci-outpost.yml
vendored
1
.github/workflows/ci-outpost.yml
vendored
@ -59,6 +59,7 @@ jobs:
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
build-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
timeout-minutes: 120
|
||||
needs:
|
||||
- ci-outpost-mark
|
||||
|
2
.github/workflows/ci-website.yml
vendored
2
.github/workflows/ci-website.yml
vendored
@ -63,6 +63,7 @@ jobs:
|
||||
working-directory: website/
|
||||
run: npm run ${{ matrix.job }}
|
||||
build-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload container images to ghcr.io
|
||||
@ -122,3 +123,4 @@ jobs:
|
||||
- uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
allowed-skips: ${{ github.repository == 'goauthentik/authentik-internal' && 'build-container' || '[]' }}
|
||||
|
21
.github/workflows/repo-mirror-cleanup.yml
vendored
Normal file
21
.github/workflows/repo-mirror-cleanup.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: "authentik-repo-mirror-cleanup"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
to_internal:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
|
||||
with:
|
||||
target_repo_url: git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
|
||||
args: --tags --force --prune
|
||||
env:
|
||||
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}
|
9
.github/workflows/repo-mirror.yml
vendored
9
.github/workflows/repo-mirror.yml
vendored
@ -11,11 +11,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
uses: pixta-dev/repository-mirroring-action@v1
|
||||
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
|
||||
with:
|
||||
target_repo_url:
|
||||
git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GH_MIRROR_KEY }}
|
||||
target_repo_url: git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
|
||||
args: --tags --force
|
||||
env:
|
||||
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}
|
||||
|
@ -16,6 +16,7 @@ env:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
|
@ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.15 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.17 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.6.2"
|
||||
__version__ = "2025.6.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -5,7 +5,6 @@ from collections.abc import Callable
|
||||
from django.apps import apps
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.v1.importer import is_model_allowed
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.providers.oauth2.models import RefreshToken
|
||||
|
||||
@ -22,10 +21,13 @@ def serializer_tester_factory(test_model: type[SerializerModel]) -> Callable:
|
||||
return
|
||||
model_class = test_model()
|
||||
self.assertTrue(isinstance(model_class, SerializerModel))
|
||||
# Models that have subclasses don't have to have a serializer
|
||||
if len(test_model.__subclasses__()) > 0:
|
||||
return
|
||||
self.assertIsNotNone(model_class.serializer)
|
||||
if model_class.serializer.Meta().model == RefreshToken:
|
||||
return
|
||||
self.assertEqual(model_class.serializer.Meta().model, test_model)
|
||||
self.assertTrue(issubclass(test_model, model_class.serializer.Meta().model))
|
||||
|
||||
return tester
|
||||
|
||||
@ -34,6 +36,6 @@ for app in apps.get_app_configs():
|
||||
if not app.label.startswith("authentik"):
|
||||
continue
|
||||
for model in app.get_models():
|
||||
if not is_model_allowed(model):
|
||||
if not issubclass(model, SerializerModel):
|
||||
continue
|
||||
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
||||
|
@ -1082,6 +1082,12 @@ class AuthenticatedSession(SerializerModel):
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionSerializer
|
||||
|
||||
return AuthenticatedSessionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Authenticated Session")
|
||||
verbose_name_plural = _("Authenticated Sessions")
|
||||
|
@ -6,7 +6,7 @@ from djangoql.ast import Name
|
||||
from djangoql.exceptions import DjangoQLError
|
||||
from djangoql.queryset import apply_search
|
||||
from djangoql.schema import DjangoQLSchema
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.filters import BaseFilterBackend, SearchFilter
|
||||
from rest_framework.request import Request
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -39,19 +39,21 @@ class BaseSchema(DjangoQLSchema):
|
||||
return super().resolve_name(name)
|
||||
|
||||
|
||||
class QLSearch(SearchFilter):
|
||||
class QLSearch(BaseFilterBackend):
|
||||
"""rest_framework search filter which uses DjangoQL"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._fallback = SearchFilter()
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return apps.get_app_config("authentik_enterprise").enabled()
|
||||
|
||||
def get_search_terms(self, request) -> str:
|
||||
"""
|
||||
Search terms are set by a ?search=... query parameter,
|
||||
and may be comma and/or whitespace delimited.
|
||||
"""
|
||||
params = request.query_params.get(self.search_param, "")
|
||||
def get_search_terms(self, request: Request) -> str:
|
||||
"""Search terms are set by a ?search=... query parameter,
|
||||
and may be comma and/or whitespace delimited."""
|
||||
params = request.query_params.get("search", "")
|
||||
params = params.replace("\x00", "") # strip null characters
|
||||
return params
|
||||
|
||||
@ -70,9 +72,9 @@ class QLSearch(SearchFilter):
|
||||
search_query = self.get_search_terms(request)
|
||||
schema = self.get_schema(request, view)
|
||||
if len(search_query) == 0 or not self.enabled:
|
||||
return super().filter_queryset(request, queryset, view)
|
||||
return self._fallback.filter_queryset(request, queryset, view)
|
||||
try:
|
||||
return apply_search(queryset, search_query, schema=schema)
|
||||
except DjangoQLError as exc:
|
||||
LOGGER.debug("Failed to parse search expression", exc=exc)
|
||||
return super().filter_queryset(request, queryset, view)
|
||||
return self._fallback.filter_queryset(request, queryset, view)
|
||||
|
@ -57,7 +57,7 @@ class QLTest(APITestCase):
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
content = loads(res.content)
|
||||
self.assertGreaterEqual(content["pagination"]["count"], 1)
|
||||
self.assertEqual(content["pagination"]["count"], 1)
|
||||
self.assertEqual(content["results"][0]["username"], self.user.username)
|
||||
|
||||
def test_search_json(self):
|
||||
|
@ -66,7 +66,10 @@ class RACClientConsumer(AsyncWebsocketConsumer):
|
||||
def init_outpost_connection(self):
|
||||
"""Initialize guac connection settings"""
|
||||
self.token = (
|
||||
ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"])
|
||||
ConnectionToken.filter_not_expired(
|
||||
token=self.scope["url_route"]["kwargs"]["token"],
|
||||
session__session__session_key=self.scope["session"].session_key,
|
||||
)
|
||||
.select_related("endpoint", "provider", "session", "session__user")
|
||||
.first()
|
||||
)
|
||||
|
@ -87,3 +87,22 @@ class TestRACViews(APITestCase):
|
||||
)
|
||||
body = loads(flow_response.content)
|
||||
self.assertEqual(body["component"], "ak-stage-access-denied")
|
||||
|
||||
def test_different_session(self):
|
||||
"""Test request"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_providers_rac:start",
|
||||
kwargs={"app": self.app.slug, "endpoint": str(self.endpoint.pk)},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
flow_response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
)
|
||||
body = loads(flow_response.content)
|
||||
next_url = body["to"]
|
||||
self.client.logout()
|
||||
final_response = self.client.get(next_url)
|
||||
self.assertEqual(final_response.url, reverse("authentik_core:if-user"))
|
||||
|
@ -68,7 +68,10 @@ class RACInterface(InterfaceView):
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
# Early sanity check to ensure token still exists
|
||||
token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first()
|
||||
token = ConnectionToken.filter_not_expired(
|
||||
token=self.kwargs["token"],
|
||||
session__session__session_key=request.session.session_key,
|
||||
).first()
|
||||
if not token:
|
||||
return redirect("authentik_core:if-user")
|
||||
self.token = token
|
||||
|
@ -44,6 +44,7 @@ class RadiusProviderSerializer(ProviderSerializer):
|
||||
"shared_secret",
|
||||
"outpost_set",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
@ -79,6 +80,7 @@ class RadiusOutpostConfigSerializer(ModelSerializer):
|
||||
"client_networks",
|
||||
"shared_secret",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-16 13:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0004_alter_certificatekeypair_name"),
|
||||
("authentik_providers_radius", "0004_alter_radiusproviderpropertymapping_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="radiusprovider",
|
||||
name="certificate",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
]
|
@ -1,11 +1,14 @@
|
||||
"""Radius Provider"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import PropertyMapping, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
@ -38,6 +41,10 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
),
|
||||
)
|
||||
|
||||
certificate = models.ForeignKey(
|
||||
CertificateKeyPair, on_delete=models.CASCADE, default=None, null=True
|
||||
)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> str | None:
|
||||
"""Radius never has a launch URL"""
|
||||
@ -57,6 +64,12 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
|
||||
return RadiusProviderSerializer
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
required_models = [self, "authentik_stages_mtls.pass_outpost_certificate"]
|
||||
if self.certificate is not None:
|
||||
required_models.append(self.certificate)
|
||||
return required_models
|
||||
|
||||
def __str__(self):
|
||||
return f"Radius Provider {self.name}"
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -27,7 +27,6 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<td>
|
||||
{% endblock %}
|
||||
|
||||
{% block sub_content %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.6.2 Blueprint schema",
|
||||
"title": "authentik 2025.6.3 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -8953,6 +8953,11 @@
|
||||
"type": "boolean",
|
||||
"title": "MFA Support",
|
||||
"description": "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon."
|
||||
},
|
||||
"certificate": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Certificate"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -55,7 +55,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
2
go.mod
2
go.mod
@ -29,7 +29,7 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025062.6
|
||||
goauthentik.io/api/v3 v3.2025063.1
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.15.0
|
||||
|
4
go.sum
4
go.sum
@ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025062.6 h1:rlChhGP2vJufYCaTMb4sbRBEE1p2uL5T4HzMqF1AJ4A=
|
||||
goauthentik.io/api/v3 v3.2025062.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025063.1 h1:zvKhZTESgMY/SNiLuTs7G0YleBnev1v7+S9Xd6PZ9bc=
|
||||
goauthentik.io/api/v3 v3.2025063.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -33,4 +33,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.6.2"
|
||||
const VERSION = "2025.6.3"
|
||||
|
@ -34,9 +34,10 @@ var (
|
||||
type SolverFunction func(*api.ChallengeTypes, api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error)
|
||||
|
||||
type FlowExecutor struct {
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
InteractiveSolver SolverFunction
|
||||
|
||||
solvers map[StageComponent]SolverFunction
|
||||
|
||||
@ -94,6 +95,10 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
|
||||
return fe
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) AddHeader(name string, value string) {
|
||||
fe.api.GetConfig().AddDefaultHeader(name, value)
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := fe.transport.RoundTrip(req)
|
||||
if res != nil {
|
||||
@ -110,7 +115,7 @@ func (fe *FlowExecutor) ApiClient() *api.APIClient {
|
||||
return fe.api
|
||||
}
|
||||
|
||||
type challengeCommon interface {
|
||||
type ChallengeCommon interface {
|
||||
GetComponent() string
|
||||
GetResponseErrors() map[string][]api.ErrorDetail
|
||||
}
|
||||
@ -165,7 +170,7 @@ func (fe *FlowExecutor) getInitialChallenge() (*api.ChallengeTypes, error) {
|
||||
if i == nil {
|
||||
return nil, errors.New("response instance was null")
|
||||
}
|
||||
ch := i.(challengeCommon)
|
||||
ch := i.(ChallengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got challenge")
|
||||
gcsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
gcsp.Finish()
|
||||
@ -184,7 +189,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response request instance was null")
|
||||
}
|
||||
ch := i.(challengeCommon)
|
||||
ch := i.(ChallengeCommon)
|
||||
|
||||
// Check for any validation errors that we might've gotten
|
||||
if len(ch.GetResponseErrors()) > 0 {
|
||||
@ -201,11 +206,17 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
case string(StageRedirect):
|
||||
return true, nil
|
||||
default:
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
var err error
|
||||
var rr api.FlowChallengeResponseRequest
|
||||
if fe.InteractiveSolver != nil {
|
||||
rr, err = fe.InteractiveSolver(challenge, responseReq)
|
||||
} else {
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
}
|
||||
rr, err = solver(challenge, responseReq)
|
||||
}
|
||||
rr, err := solver(challenge, responseReq)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -220,7 +231,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response instance was null")
|
||||
}
|
||||
ch = i.(challengeCommon)
|
||||
ch = i.(ChallengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got response")
|
||||
scsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
scsp.Finish()
|
||||
|
@ -8,6 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
var a challengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
var a ChallengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
assert.NotNil(t, a)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func parseCIDRs(raw string) []*net.IPNet {
|
||||
@ -41,26 +42,28 @@ func (rs *RadiusServer) Refresh() error {
|
||||
if len(apiProviders) < 1 {
|
||||
return errors.New("no radius provider defined")
|
||||
}
|
||||
providers := make([]*ProviderInstance, len(apiProviders))
|
||||
for idx, provider := range apiProviders {
|
||||
providers := make(map[int32]*ProviderInstance)
|
||||
for _, provider := range apiProviders {
|
||||
existing, ok := rs.providers[provider.Pk]
|
||||
state := map[string]*protocol.State{}
|
||||
if ok {
|
||||
state = existing.eapState
|
||||
}
|
||||
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
|
||||
providers[idx] = &ProviderInstance{
|
||||
providers[provider.Pk] = &ProviderInstance{
|
||||
SharedSecret: []byte(provider.GetSharedSecret()),
|
||||
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
|
||||
MFASupport: provider.GetMfaSupport(),
|
||||
appSlug: provider.ApplicationSlug,
|
||||
flowSlug: provider.AuthFlowSlug,
|
||||
certId: provider.GetCertificate(),
|
||||
providerId: provider.Pk,
|
||||
s: rs,
|
||||
log: logger,
|
||||
eapState: state,
|
||||
}
|
||||
}
|
||||
rs.providers = providers
|
||||
rs.log.Info("Update providers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) StartRadiusServer() error {
|
||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||
return rs.s.ListenAndServe()
|
||||
}
|
||||
|
44
internal/outpost/radius/eap/README.md
Normal file
44
internal/outpost/radius/eap/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
# EAP protocol implementation
|
||||
|
||||
Install `eapol_test` (`sudo apt install eapoltest`)
|
||||
|
||||
Both PEAP and EAP-TLS require a minimal PKI setup. A CA, a certificate for the server and for EAP-TLS a client certificate need to be provided.
|
||||
|
||||
Save either of the config files below and run eapoltest like so:
|
||||
|
||||
```
|
||||
# peap.conf is the config file under the PEAP testing section
|
||||
# foo is the shared RADIUS secret
|
||||
# 1.2.3.4 is the IP of the RADIUS server
|
||||
eapol_test -c peap.conf -s foo -a 1.2.3.4
|
||||
```
|
||||
|
||||
### PEAP testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=PEAP
|
||||
identity="foo"
|
||||
password="bar"
|
||||
ca_cert="ca.pem"
|
||||
phase2="auth=MSCHAPV2"
|
||||
}
|
||||
```
|
||||
|
||||
### EAP-TLS testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=TLS
|
||||
identity="foo"
|
||||
ca_cert="ca.pem"
|
||||
client_cert="cert_client.pem"
|
||||
private_key="cert_client.key"
|
||||
eapol_flags=3
|
||||
eap_workaround=0
|
||||
}
|
||||
```
|
55
internal/outpost/radius/eap/context.go
Normal file
55
internal/outpost/radius/eap/context.go
Normal file
@ -0,0 +1,55 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
req *radius.Request
|
||||
rootPayload protocol.Payload
|
||||
typeState map[protocol.Type]any
|
||||
log *log.Entry
|
||||
settings interface{}
|
||||
parent *context
|
||||
endStatus protocol.Status
|
||||
handleInner func(protocol.Payload, protocol.StateManager, protocol.Context) (protocol.Payload, error)
|
||||
}
|
||||
|
||||
func (ctx *context) RootPayload() protocol.Payload { return ctx.rootPayload }
|
||||
func (ctx *context) Packet() *radius.Request { return ctx.req }
|
||||
func (ctx *context) ProtocolSettings() any { return ctx.settings }
|
||||
func (ctx *context) GetProtocolState(p protocol.Type) any { return ctx.typeState[p] }
|
||||
func (ctx *context) SetProtocolState(p protocol.Type, st any) { ctx.typeState[p] = st }
|
||||
func (ctx *context) IsProtocolStart(p protocol.Type) bool { return ctx.typeState[p] == nil }
|
||||
func (ctx *context) Log() *log.Entry { return ctx.log }
|
||||
func (ctx *context) HandleInnerEAP(p protocol.Payload, st protocol.StateManager) (protocol.Payload, error) {
|
||||
return ctx.handleInner(p, st, ctx)
|
||||
}
|
||||
func (ctx *context) Inner(p protocol.Payload, t protocol.Type) protocol.Context {
|
||||
nctx := &context{
|
||||
req: ctx.req,
|
||||
rootPayload: ctx.rootPayload,
|
||||
typeState: ctx.typeState,
|
||||
log: ctx.log.WithField("type", fmt.Sprintf("%T", p)).WithField("code", t),
|
||||
settings: ctx.settings,
|
||||
parent: ctx,
|
||||
handleInner: ctx.handleInner,
|
||||
}
|
||||
nctx.log.Debug("Creating inner context")
|
||||
return nctx
|
||||
}
|
||||
func (ctx *context) EndInnerProtocol(st protocol.Status) {
|
||||
ctx.log.Info("Ending protocol")
|
||||
if ctx.parent != nil {
|
||||
ctx.parent.EndInnerProtocol(st)
|
||||
return
|
||||
}
|
||||
if ctx.endStatus != protocol.StatusUnknown {
|
||||
return
|
||||
}
|
||||
ctx.endStatus = st
|
||||
}
|
13
internal/outpost/radius/eap/debug/debug.go
Normal file
13
internal/outpost/radius/eap/debug/debug.go
Normal file
@ -0,0 +1,13 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FormatBytes(d []byte) string {
|
||||
b := d
|
||||
if len(b) > 32 {
|
||||
b = b[:32]
|
||||
}
|
||||
return fmt.Sprintf("% x", b)
|
||||
}
|
182
internal/outpost/radius/eap/handler.go
Normal file
182
internal/outpost/radius/eap/handler.go
Normal file
@ -0,0 +1,182 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2865"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
func sendErrorResponse(w radius.ResponseWriter, r *radius.Request) {
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
err := w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) HandleRadiusPacket(w radius.ResponseWriter, r *radius.Request) {
|
||||
p.r = r
|
||||
rst := rfc2865.State_GetString(r.Packet)
|
||||
if rst == "" {
|
||||
rst = base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(12))
|
||||
}
|
||||
p.state = rst
|
||||
|
||||
rp := &Packet{r: r}
|
||||
rep, err := p.handleEAP(p.eap, p.stm, nil)
|
||||
rp.eap = rep
|
||||
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
if err == nil {
|
||||
switch rp.eap.Code {
|
||||
case protocol.CodeRequest:
|
||||
rres.Code = radius.CodeAccessChallenge
|
||||
case protocol.CodeFailure:
|
||||
rres.Code = radius.CodeAccessReject
|
||||
case protocol.CodeSuccess:
|
||||
rres.Code = radius.CodeAccessAccept
|
||||
}
|
||||
} else {
|
||||
rres.Code = radius.CodeAccessReject
|
||||
log.WithError(err).Debug("Rejecting request")
|
||||
}
|
||||
for _, mod := range p.responseModifiers {
|
||||
err := mod.ModifyRADIUSResponse(rres, r.Packet)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Root-EAP: failed to modify response packet")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rfc2865.State_SetString(rres, p.state)
|
||||
eapEncoded, err := rp.Encode()
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to encode response")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
log.WithField("length", len(eapEncoded)).WithField("type", fmt.Sprintf("%T", rp.eap.Payload)).Debug("Root-EAP: encapsulated challenge")
|
||||
rfc2869.EAPMessage_Set(rres, eapEncoded)
|
||||
err = p.setMessageAuthenticator(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send message authenticator")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
err = w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) handleEAP(pp protocol.Payload, stm protocol.StateManager, parentContext *context) (*eap.Payload, error) {
|
||||
st := stm.GetEAPState(p.state)
|
||||
if st == nil {
|
||||
log.Debug("Root-EAP: blank state")
|
||||
st = protocol.BlankState(stm.GetEAPSettings())
|
||||
}
|
||||
|
||||
nextChallengeToOffer, err := st.GetNextProtocol()
|
||||
if err != nil {
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeFailure,
|
||||
ID: p.eap.ID,
|
||||
}, err
|
||||
}
|
||||
|
||||
next := func() (*eap.Payload, error) {
|
||||
st.ProtocolIndex += 1
|
||||
st.TypeState = map[protocol.Type]any{}
|
||||
stm.SetEAPState(p.state, st)
|
||||
return p.handleEAP(pp, stm, nil)
|
||||
}
|
||||
|
||||
if n, ok := pp.(*eap.Payload).Payload.(*legacy_nak.Payload); ok {
|
||||
log.WithField("desired", n.DesiredType).Debug("Root-EAP: received NAK, trying next protocol")
|
||||
pp.(*eap.Payload).Payload = nil
|
||||
return next()
|
||||
}
|
||||
|
||||
np, t, _ := eap.EmptyPayload(stm.GetEAPSettings(), nextChallengeToOffer)
|
||||
|
||||
var ctx *context
|
||||
if parentContext != nil {
|
||||
ctx = parentContext.Inner(np, t).(*context)
|
||||
ctx.settings = stm.GetEAPSettings().ProtocolSettings[np.Type()]
|
||||
} else {
|
||||
ctx = &context{
|
||||
req: p.r,
|
||||
rootPayload: p.eap,
|
||||
typeState: st.TypeState,
|
||||
log: log.WithField("type", fmt.Sprintf("%T", np)).WithField("code", t),
|
||||
settings: stm.GetEAPSettings().ProtocolSettings[t],
|
||||
}
|
||||
ctx.handleInner = func(pp protocol.Payload, sm protocol.StateManager, ctx protocol.Context) (protocol.Payload, error) {
|
||||
// cctx := ctx.Inner(np, np.Type(), nil).(*context)
|
||||
return p.handleEAP(pp, sm, ctx.(*context))
|
||||
}
|
||||
}
|
||||
if !np.Offerable() {
|
||||
ctx.Log().Debug("Root-EAP: protocol not offerable, skipping")
|
||||
return next()
|
||||
}
|
||||
ctx.Log().Debug("Root-EAP: Passing to protocol")
|
||||
|
||||
res := &eap.Payload{
|
||||
Code: protocol.CodeRequest,
|
||||
ID: p.eap.ID + 1,
|
||||
MsgType: t,
|
||||
}
|
||||
var payload any
|
||||
if reflect.TypeOf(pp.(*eap.Payload).Payload) == reflect.TypeOf(np) {
|
||||
np.Decode(pp.(*eap.Payload).RawPayload)
|
||||
}
|
||||
payload = np.Handle(ctx)
|
||||
if payload != nil {
|
||||
res.Payload = payload.(protocol.Payload)
|
||||
}
|
||||
|
||||
stm.SetEAPState(p.state, st)
|
||||
|
||||
if rm, ok := np.(protocol.ResponseModifier); ok {
|
||||
ctx.log.Debug("Root-EAP: Registered response modifier")
|
||||
p.responseModifiers = append(p.responseModifiers, rm)
|
||||
}
|
||||
|
||||
switch ctx.endStatus {
|
||||
case protocol.StatusSuccess:
|
||||
res.Code = protocol.CodeSuccess
|
||||
res.ID -= 1
|
||||
case protocol.StatusError:
|
||||
res.Code = protocol.CodeFailure
|
||||
res.ID -= 1
|
||||
case protocol.StatusNextProtocol:
|
||||
ctx.log.Debug("Root-EAP: Protocol ended, starting next protocol")
|
||||
return next()
|
||||
case protocol.StatusUnknown:
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *Packet) setMessageAuthenticator(rp *radius.Packet) error {
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
|
||||
hash := hmac.New(md5.New, rp.Secret)
|
||||
encode, err := rp.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash.Write(encode)
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
|
||||
return nil
|
||||
}
|
34
internal/outpost/radius/eap/packet.go
Normal file
34
internal/outpost/radius/eap/packet.go
Normal file
@ -0,0 +1,34 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Packet struct {
|
||||
r *radius.Request
|
||||
eap *eap.Payload
|
||||
stm protocol.StateManager
|
||||
state string
|
||||
responseModifiers []protocol.ResponseModifier
|
||||
}
|
||||
|
||||
func Decode(stm protocol.StateManager, raw []byte) (*Packet, error) {
|
||||
packet := &Packet{
|
||||
eap: &eap.Payload{
|
||||
Settings: stm.GetEAPSettings(),
|
||||
},
|
||||
stm: stm,
|
||||
responseModifiers: []protocol.ResponseModifier{},
|
||||
}
|
||||
err := packet.eap.Decode(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func (p *Packet) Encode() ([]byte, error) {
|
||||
return p.eap.Encode()
|
||||
}
|
32
internal/outpost/radius/eap/protocol/context.go
Normal file
32
internal/outpost/radius/eap/protocol/context.go
Normal file
@ -0,0 +1,32 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusUnknown Status = iota
|
||||
StatusSuccess
|
||||
StatusError
|
||||
StatusNextProtocol
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
Packet() *radius.Request
|
||||
RootPayload() Payload
|
||||
|
||||
ProtocolSettings() interface{}
|
||||
|
||||
GetProtocolState(p Type) interface{}
|
||||
SetProtocolState(p Type, s interface{})
|
||||
IsProtocolStart(p Type) bool
|
||||
|
||||
HandleInnerEAP(Payload, StateManager) (Payload, error)
|
||||
Inner(Payload, Type) Context
|
||||
EndInnerProtocol(Status)
|
||||
|
||||
Log() *log.Entry
|
||||
}
|
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
@ -0,0 +1,23 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func EmptyPayload(settings protocol.Settings, t protocol.Type) (protocol.Payload, protocol.Type, error) {
|
||||
for _, cons := range settings.Protocols {
|
||||
np := cons()
|
||||
if np.Type() == t {
|
||||
return np, np.Type(), nil
|
||||
}
|
||||
// If the protocol has an inner protocol, return the original type but the code for the inner protocol
|
||||
if i, ok := np.(protocol.Inner); ok {
|
||||
if ii := i.HasInner(); ii != nil {
|
||||
return np, ii.Type(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, protocol.Type(0), fmt.Errorf("unsupported EAP type %d", t)
|
||||
}
|
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
@ -0,0 +1,96 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeEAP protocol.Type = 0
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Code protocol.Code
|
||||
ID uint8
|
||||
Length uint16
|
||||
MsgType protocol.Type
|
||||
Payload protocol.Payload
|
||||
RawPayload []byte
|
||||
|
||||
Settings protocol.Settings
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeEAP
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Code = protocol.Code(raw[0])
|
||||
p.ID = raw[1]
|
||||
p.Length = binary.BigEndian.Uint16(raw[2:])
|
||||
if p.Length != uint16(len(raw)) {
|
||||
return fmt.Errorf("mismatched packet length; got %d, expected %d", p.Length, uint16(len(raw)))
|
||||
}
|
||||
if len(raw) > 4 && (p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse) {
|
||||
p.MsgType = protocol.Type(raw[4])
|
||||
}
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Trace("EAP: decode raw")
|
||||
p.RawPayload = raw[5:]
|
||||
if p.Payload == nil {
|
||||
pp, _, err := EmptyPayload(p.Settings, p.MsgType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Payload = pp
|
||||
}
|
||||
err := p.Payload.Decode(raw[5:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
buff := make([]byte, 4)
|
||||
buff[0] = uint8(p.Code)
|
||||
buff[1] = uint8(p.ID)
|
||||
|
||||
if p.Payload != nil {
|
||||
payloadBuffer, err := p.Payload.Encode()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
if p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse {
|
||||
buff = append(buff, uint8(p.MsgType))
|
||||
}
|
||||
buff = append(buff, payloadBuffer...)
|
||||
}
|
||||
binary.BigEndian.PutUint16(buff[2:], uint16(len(buff)))
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
ctx.Log().Debug("EAP: Handle")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<EAP Packet Code=%d, ID=%d, Type=%d, Length=%d, Payload=%T>",
|
||||
p.Code,
|
||||
p.ID,
|
||||
p.MsgType,
|
||||
p.Length,
|
||||
p.Payload,
|
||||
)
|
||||
}
|
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
@ -0,0 +1,5 @@
|
||||
package eap
|
||||
|
||||
type State struct {
|
||||
PacketID uint8
|
||||
}
|
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
@ -0,0 +1,61 @@
|
||||
package gtc
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeGTC protocol.Type = 6
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Challenge []byte
|
||||
|
||||
st *State
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeGTC
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.raw = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return p.Challenge, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeGTC, p.st)
|
||||
}()
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
if ctx.IsProtocolStart(TypeGTC) {
|
||||
g, v := settings.ChallengeHandler(ctx)
|
||||
p.st = &State{
|
||||
getChallenge: g,
|
||||
validateResponse: v,
|
||||
}
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeGTC).(*State)
|
||||
p.st.validateResponse(p.raw)
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return "<GTC Packet>"
|
||||
}
|
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
@ -0,0 +1,10 @@
|
||||
package gtc
|
||||
|
||||
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
|
||||
type GetChallenge func() []byte
|
||||
type ValidateResponse func(answer []byte)
|
||||
|
||||
type Settings struct {
|
||||
ChallengeHandler func(ctx protocol.Context) (GetChallenge, ValidateResponse)
|
||||
}
|
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
@ -0,0 +1,6 @@
|
||||
package gtc
|
||||
|
||||
type State struct {
|
||||
getChallenge GetChallenge
|
||||
validateResponse ValidateResponse
|
||||
}
|
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
@ -0,0 +1,48 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeIdentity protocol.Type = 1
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Identity string
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeIdentity
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Identity = string(raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeIdentity) {
|
||||
ctx.EndInnerProtocol(protocol.StatusNextProtocol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Identity Packet Identity=%s>",
|
||||
p.Identity,
|
||||
)
|
||||
}
|
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
@ -0,0 +1,48 @@
|
||||
package legacy_nak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeLegacyNAK protocol.Type = 3
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
DesiredType protocol.Type
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeLegacyNAK
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.DesiredType = protocol.Type(raw[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{byte(p.DesiredType)}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeLegacyNAK) {
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Legacy NAK Packet DesiredType=%d>",
|
||||
p.DesiredType,
|
||||
)
|
||||
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
@ -0,0 +1,23 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Challenge []byte
|
||||
NTResponse []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func ParseResponse(raw []byte) (*Response, error) {
|
||||
res := &Response{}
|
||||
res.Challenge = raw[:challengeValueSize]
|
||||
if !bytes.Equal(raw[challengeValueSize:challengeValueSize+responseReservedSize], make([]byte, 8)) {
|
||||
return nil, errors.New("MSCHAPv2: Reserved bytes not empty?")
|
||||
}
|
||||
res.NTResponse = raw[challengeValueSize+responseReservedSize : challengeValueSize+responseReservedSize+responseNTResponseSize]
|
||||
res.Flags = (raw[challengeValueSize+responseReservedSize+responseNTResponseSize])
|
||||
return res, nil
|
||||
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
@ -0,0 +1,23 @@
|
||||
package mschapv2
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
type SuccessRequest struct {
|
||||
*Payload
|
||||
Authenticator []byte
|
||||
}
|
||||
|
||||
// A success request is encoded slightly differently, it doesn't have a challenge and as such
|
||||
// doesn't need to encode the length of it
|
||||
func (sr *SuccessRequest) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(sr.OpCode),
|
||||
sr.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
encoded = append(encoded, sr.Authenticator...)
|
||||
sr.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], sr.MSLength)
|
||||
return encoded, nil
|
||||
}
|
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
@ -0,0 +1,196 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/vendors/microsoft"
|
||||
)
|
||||
|
||||
const TypeMSCHAPv2 protocol.Type = 26
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
const (
|
||||
challengeValueSize = 16
|
||||
responseValueSize = 49
|
||||
responseReservedSize = 8
|
||||
responseNTResponseSize = 24
|
||||
)
|
||||
|
||||
type OpCode uint8
|
||||
|
||||
const (
|
||||
OpChallenge OpCode = 1
|
||||
OpResponse OpCode = 2
|
||||
OpSuccess OpCode = 3
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
OpCode OpCode
|
||||
MSCHAPv2ID uint8
|
||||
MSLength uint16
|
||||
ValueSize uint8
|
||||
|
||||
Challenge []byte
|
||||
Response []byte
|
||||
|
||||
Name []byte
|
||||
|
||||
st *State
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeMSCHAPv2
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debugf("MSCHAPv2: decode raw")
|
||||
p.OpCode = OpCode(raw[0])
|
||||
if p.OpCode == OpSuccess {
|
||||
return nil
|
||||
}
|
||||
// TODO: Validate against root EAP packet
|
||||
p.MSCHAPv2ID = raw[1]
|
||||
p.MSLength = binary.BigEndian.Uint16(raw[2:])
|
||||
|
||||
p.ValueSize = raw[4]
|
||||
if p.ValueSize != responseValueSize {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect value size: %d", p.ValueSize)
|
||||
}
|
||||
p.Response = raw[5 : p.ValueSize+5]
|
||||
p.Name = raw[5+p.ValueSize:]
|
||||
if int(p.MSLength) != len(raw) {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect MS-Length: %d, should be %d", p.MSLength, len(raw))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(p.OpCode),
|
||||
p.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
byte(len(p.Challenge)),
|
||||
}
|
||||
encoded = append(encoded, p.Challenge...)
|
||||
encoded = append(encoded, p.Name...)
|
||||
p.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], p.MSLength)
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeMSCHAPv2, p.st)
|
||||
}()
|
||||
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
|
||||
if ctx.IsProtocolStart(TypeMSCHAPv2) {
|
||||
ctx.Log().Debug("MSCHAPv2: Empty state, starting")
|
||||
p.st = &State{
|
||||
Challenge: securecookie.GenerateRandomKey(challengeValueSize),
|
||||
}
|
||||
return &Payload{
|
||||
OpCode: OpChallenge,
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
Challenge: p.st.Challenge,
|
||||
Name: []byte("authentik"),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeMSCHAPv2).(*State)
|
||||
|
||||
response := &Payload{
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
}
|
||||
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
|
||||
ctx.Log().Debugf("MSCHAPv2: OpCode: %d", p.OpCode)
|
||||
if p.OpCode == OpResponse {
|
||||
res, err := ParseResponse(p.Response)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to parse response")
|
||||
return nil
|
||||
}
|
||||
p.st.PeerChallenge = res.Challenge
|
||||
auth, err := settings.AuthenticateRequest(AuthRequest{
|
||||
Challenge: p.st.Challenge,
|
||||
PeerChallenge: p.st.PeerChallenge,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to check password")
|
||||
return nil
|
||||
}
|
||||
if !bytes.Equal(auth.NTResponse, res.NTResponse) {
|
||||
ctx.Log().Warning("MSCHAPv2: NT response mismatch")
|
||||
return nil
|
||||
}
|
||||
ctx.Log().Info("MSCHAPv2: Successfully checked password")
|
||||
p.st.AuthResponse = auth
|
||||
succ := &SuccessRequest{
|
||||
Payload: &Payload{
|
||||
OpCode: OpSuccess,
|
||||
},
|
||||
Authenticator: []byte(auth.AuthenticatorResponse),
|
||||
}
|
||||
return succ
|
||||
} else if p.OpCode == OpSuccess && p.st.AuthResponse != nil {
|
||||
ep := &peap.ExtensionPayload{
|
||||
AVPs: []peap.ExtensionAVP{
|
||||
{
|
||||
Mandatory: true,
|
||||
Type: peap.AVPAckResult,
|
||||
Value: []byte{0, 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
p.st.IsProtocolEnded = true
|
||||
return ep
|
||||
} else if p.st.IsProtocolEnded {
|
||||
ctx.EndInnerProtocol(protocol.StatusSuccess)
|
||||
return &Payload{}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||
if p.st == nil || p.st.AuthResponse == nil {
|
||||
return nil
|
||||
}
|
||||
if r.Code != radius.CodeAccessAccept {
|
||||
return nil
|
||||
}
|
||||
log.Debug("MSCHAPv2: Radius modifier")
|
||||
if len(microsoft.MSMPPERecvKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPERecvKey_Set(r, p.st.AuthResponse.RecvKey)
|
||||
}
|
||||
if len(microsoft.MSMPPESendKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPESendKey_Set(r, p.st.AuthResponse.SendKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<MSCHAPv2 Packet OpCode=%d, MSCHAPv2ID=%d>",
|
||||
p.OpCode,
|
||||
p.MSCHAPv2ID,
|
||||
)
|
||||
}
|
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
@ -0,0 +1,50 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"layeh.com/radius/rfc2759"
|
||||
"layeh.com/radius/rfc3079"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
AuthenticateRequest func(req AuthRequest) (*AuthResponse, error)
|
||||
}
|
||||
|
||||
type AuthRequest struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
NTResponse []byte
|
||||
RecvKey []byte
|
||||
SendKey []byte
|
||||
AuthenticatorResponse string
|
||||
}
|
||||
|
||||
func DebugStaticCredentials(user, password []byte) func(req AuthRequest) (*AuthResponse, error) {
|
||||
return func(req AuthRequest) (*AuthResponse, error) {
|
||||
res := &AuthResponse{}
|
||||
ntResponse, err := rfc2759.GenerateNTResponse(req.Challenge, req.PeerChallenge, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.NTResponse = ntResponse
|
||||
|
||||
res.RecvKey, err = rfc3079.MakeKey(ntResponse, password, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.SendKey, err = rfc3079.MakeKey(ntResponse, password, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.AuthenticatorResponse, err = rfc2759.GenerateAuthenticatorResponse(req.Challenge, req.PeerChallenge, ntResponse, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
}
|
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
@ -0,0 +1,8 @@
|
||||
package mschapv2
|
||||
|
||||
type State struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
IsProtocolEnded bool
|
||||
AuthResponse *AuthResponse
|
||||
}
|
31
internal/outpost/radius/eap/protocol/packet.go
Normal file
31
internal/outpost/radius/eap/protocol/packet.go
Normal file
@ -0,0 +1,31 @@
|
||||
package protocol
|
||||
|
||||
import "layeh.com/radius"
|
||||
|
||||
type Type uint8
|
||||
|
||||
type Code uint8
|
||||
|
||||
const (
|
||||
CodeRequest Code = 1
|
||||
CodeResponse Code = 2
|
||||
CodeSuccess Code = 3
|
||||
CodeFailure Code = 4
|
||||
)
|
||||
|
||||
type Payload interface {
|
||||
Decode(raw []byte) error
|
||||
Encode() ([]byte, error)
|
||||
Handle(ctx Context) Payload
|
||||
Type() Type
|
||||
Offerable() bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type Inner interface {
|
||||
HasInner() Payload
|
||||
}
|
||||
|
||||
type ResponseModifier interface {
|
||||
ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error
|
||||
}
|
59
internal/outpost/radius/eap/protocol/peap/extension.go
Normal file
59
internal/outpost/radius/eap/protocol/peap/extension.go
Normal file
@ -0,0 +1,59 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypePEAPExtension protocol.Type = 33
|
||||
|
||||
type ExtensionPayload struct {
|
||||
AVPs []ExtensionAVP
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debugf("PEAP-Extension: decode raw")
|
||||
ep.AVPs = []ExtensionAVP{}
|
||||
offset := 0
|
||||
for {
|
||||
if len(raw[offset:]) < 4 {
|
||||
return nil
|
||||
}
|
||||
len := binary.BigEndian.Uint16(raw[offset+2:offset+2+2]) + ExtensionHeaderSize
|
||||
avp := &ExtensionAVP{}
|
||||
err := avp.Decode(raw[offset : offset+int(len)])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ep.AVPs = append(ep.AVPs, *avp)
|
||||
offset = offset + int(len)
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Encode() ([]byte, error) {
|
||||
log.Debug("PEAP-Extension: encode")
|
||||
buff := []byte{}
|
||||
for _, avp := range ep.AVPs {
|
||||
buff = append(buff, avp.Encode()...)
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Handle(protocol.Context) protocol.Payload {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) String() string {
|
||||
return "<PEAP Extension Payload>"
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Type() protocol.Type {
|
||||
return TypePEAPExtension
|
||||
}
|
62
internal/outpost/radius/eap/protocol/peap/extension_avp.go
Normal file
62
internal/outpost/radius/eap/protocol/peap/extension_avp.go
Normal file
@ -0,0 +1,62 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AVPType uint16
|
||||
|
||||
const (
|
||||
AVPAckResult AVPType = 3
|
||||
)
|
||||
|
||||
const ExtensionHeaderSize = 4
|
||||
|
||||
type ExtensionAVP struct {
|
||||
Mandatory bool
|
||||
Type AVPType // 14-bit field
|
||||
Length uint16
|
||||
Value []byte
|
||||
}
|
||||
|
||||
var (
|
||||
ErrorReservedBitSet = errors.New("PEAP-Extension: Reserved bit is not 0")
|
||||
)
|
||||
|
||||
func (eavp *ExtensionAVP) Decode(raw []byte) error {
|
||||
typ := binary.BigEndian.Uint16(raw[:2])
|
||||
if typ>>15 == 1 {
|
||||
eavp.Mandatory = true
|
||||
}
|
||||
if typ>>14&1 != 0 {
|
||||
return ErrorReservedBitSet
|
||||
}
|
||||
eavp.Type = AVPType(typ & 0b0011111111111111)
|
||||
eavp.Length = binary.BigEndian.Uint16(raw[2:4])
|
||||
val := raw[4:]
|
||||
if eavp.Length != uint16(len(val)) {
|
||||
return fmt.Errorf("PEAP-Extension: Invalid length: %d, should be %d", eavp.Length, len(val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (eavp ExtensionAVP) Encode() []byte {
|
||||
buff := []byte{
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
t := uint16(eavp.Type)
|
||||
// Type is a 14-bit number, the highest bit is the mandatory flag
|
||||
if eavp.Mandatory {
|
||||
t = t | 0b1000000000000000
|
||||
}
|
||||
// The next bit is reserved and should always be set to 0
|
||||
t = t & 0b1011111111111111
|
||||
binary.BigEndian.PutUint16(buff[0:], t)
|
||||
binary.BigEndian.PutUint16(buff[2:], uint16(len(eavp.Value)))
|
||||
return append(buff, eavp.Value...)
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package peap_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
)
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{
|
||||
Mandatory: true,
|
||||
Type: peap.AVPType(3),
|
||||
}
|
||||
assert.Equal(t, []byte{0x80, 0x3, 0x0, 0x0}, eavp.Encode())
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, eavp.Mandatory)
|
||||
assert.Equal(t, peap.AVPType(3), eavp.Type)
|
||||
}
|
||||
|
||||
func TestDecode_Invalid_ReservedBitSet(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0xc0, 0x3, 0x0, 0x0})
|
||||
assert.ErrorIs(t, err, peap.ErrorReservedBitSet)
|
||||
}
|
||||
|
||||
func TestDecode_Invalid_Length(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0, 0x0})
|
||||
assert.NotNil(t, err)
|
||||
}
|
167
internal/outpost/radius/eap/protocol/peap/payload.go
Normal file
167
internal/outpost/radius/eap/protocol/peap/payload.go
Normal file
@ -0,0 +1,167 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||
)
|
||||
|
||||
const TypePEAP protocol.Type = 25
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &tls.Payload{
|
||||
Inner: &Payload{
|
||||
Inner: &eap.Payload{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Inner protocol.Payload
|
||||
|
||||
eap *eap.Payload
|
||||
st *State
|
||||
settings Settings
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypePEAP
|
||||
}
|
||||
|
||||
func (p *Payload) HasInner() protocol.Payload {
|
||||
return p.Inner
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debug("PEAP: Decode")
|
||||
p.raw = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
log.Debug("PEAP: Encoding inner EAP")
|
||||
if p.eap.Payload == nil {
|
||||
return []byte{}, errors.New("PEAP: no payload in response eap packet")
|
||||
}
|
||||
payload, err := p.eap.Payload.Encode()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
encoded := []byte{
|
||||
byte(p.eap.MsgType),
|
||||
}
|
||||
return append(encoded, payload...), nil
|
||||
}
|
||||
|
||||
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||
func (p *Payload) eapInnerDecode(ctx protocol.Context) (*eap.Payload, error) {
|
||||
ep := &eap.Payload{
|
||||
Settings: p.GetEAPSettings(),
|
||||
}
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
fixedRaw := []byte{
|
||||
byte(rootEap.Code),
|
||||
rootEap.ID,
|
||||
// 2 byte space for length
|
||||
0,
|
||||
0,
|
||||
}
|
||||
fullLength := len(p.raw) + len(fixedRaw)
|
||||
binary.BigEndian.PutUint16(fixedRaw[2:], uint16(fullLength))
|
||||
fixedRaw = append(fixedRaw, p.raw...)
|
||||
// If the raw data has a msgtype set to type 33 (EAP extension), decode differently
|
||||
if len(p.raw) > 5 && p.raw[4] == byte(TypePEAPExtension) {
|
||||
ep.Payload = &ExtensionPayload{}
|
||||
// Pass original raw data to EAP as extension payloads are encoded like normal EAP packets
|
||||
fixedRaw = p.raw
|
||||
}
|
||||
err := ep.Decode(fixedRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypePEAP, p.st)
|
||||
}()
|
||||
p.settings = ctx.ProtocolSettings().(Settings)
|
||||
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
|
||||
if ctx.IsProtocolStart(TypePEAP) {
|
||||
ctx.Log().Debug("PEAP: Protocol start")
|
||||
p.st = &State{
|
||||
SubState: make(map[string]*protocol.State),
|
||||
}
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeRequest,
|
||||
ID: rootEap.ID + 1,
|
||||
MsgType: identity.TypeIdentity,
|
||||
Payload: &identity.Payload{},
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypePEAP).(*State)
|
||||
|
||||
ep, err := p.eapInnerDecode(ctx)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("PEAP: failed to decode inner EAP")
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeFailure,
|
||||
ID: rootEap.ID + 1,
|
||||
}
|
||||
}
|
||||
p.eap = ep
|
||||
ctx.Log().Debugf("PEAP: Decoded inner EAP to %s", ep.String())
|
||||
|
||||
res, err := ctx.HandleInnerEAP(ep, p)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("PEAP: failed to handle inner EAP")
|
||||
return nil
|
||||
}
|
||||
// Normal payloads need to be wrapped in PEAP to use the correct encoding (see Encode() above)
|
||||
// Extension payloads handle encoding differently
|
||||
pres := res.(*eap.Payload)
|
||||
if _, ok := pres.Payload.(*ExtensionPayload); ok {
|
||||
// HandleInnerEAP will set the MsgType to the PEAP type, however we need to override that
|
||||
pres.MsgType = TypePEAPExtension
|
||||
ctx.Log().Debug("PEAP: Encoding response as extension")
|
||||
return res
|
||||
}
|
||||
return &Payload{eap: pres}
|
||||
}
|
||||
|
||||
func (p *Payload) GetEAPSettings() protocol.Settings {
|
||||
return p.settings.InnerProtocols
|
||||
}
|
||||
|
||||
func (p *Payload) GetEAPState(key string) *protocol.State {
|
||||
return p.st.SubState[key]
|
||||
}
|
||||
|
||||
func (p *Payload) SetEAPState(key string, st *protocol.State) {
|
||||
p.st.SubState[key] = st
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<PEAP Packet Wrapping=%s>",
|
||||
p.eap.String(),
|
||||
)
|
||||
}
|
16
internal/outpost/radius/eap/protocol/peap/settings.go
Normal file
16
internal/outpost/radius/eap/protocol/peap/settings.go
Normal file
@ -0,0 +1,16 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Config *tls.Config
|
||||
InnerProtocols protocol.Settings
|
||||
}
|
||||
|
||||
func (s Settings) TLSConfig() *tls.Config {
|
||||
return s.Config
|
||||
}
|
7
internal/outpost/radius/eap/protocol/peap/state.go
Normal file
7
internal/outpost/radius/eap/protocol/peap/state.go
Normal file
@ -0,0 +1,7 @@
|
||||
package peap
|
||||
|
||||
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
|
||||
type State struct {
|
||||
SubState map[string]*protocol.State
|
||||
}
|
42
internal/outpost/radius/eap/protocol/state.go
Normal file
42
internal/outpost/radius/eap/protocol/state.go
Normal file
@ -0,0 +1,42 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type StateManager interface {
|
||||
GetEAPSettings() Settings
|
||||
GetEAPState(string) *State
|
||||
SetEAPState(string, *State)
|
||||
}
|
||||
|
||||
type ProtocolConstructor func() Payload
|
||||
|
||||
type Settings struct {
|
||||
Protocols []ProtocolConstructor
|
||||
ProtocolPriority []Type
|
||||
ProtocolSettings map[Type]interface{}
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Protocols []ProtocolConstructor
|
||||
ProtocolIndex int
|
||||
ProtocolPriority []Type
|
||||
TypeState map[Type]any
|
||||
}
|
||||
|
||||
func (st *State) GetNextProtocol() (Type, error) {
|
||||
if st.ProtocolIndex >= len(st.ProtocolPriority) {
|
||||
return Type(0), errors.New("no more protocols to offer")
|
||||
}
|
||||
return st.ProtocolPriority[st.ProtocolIndex], nil
|
||||
}
|
||||
|
||||
func BlankState(settings Settings) *State {
|
||||
return &State{
|
||||
Protocols: slices.Clone(settings.Protocols),
|
||||
ProtocolPriority: slices.Clone(settings.ProtocolPriority),
|
||||
TypeState: map[Type]any{},
|
||||
}
|
||||
}
|
111
internal/outpost/radius/eap/protocol/tls/buff_conn.go
Normal file
111
internal/outpost/radius/eap/protocol/tls/buff_conn.go
Normal file
@ -0,0 +1,111 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BuffConn struct {
|
||||
reader *bytes.Buffer
|
||||
writer *bytes.Buffer
|
||||
|
||||
ctx context.Context
|
||||
|
||||
expectedWriterByteCount int
|
||||
writtenByteCount int
|
||||
|
||||
retryOptions []retry.Option
|
||||
}
|
||||
|
||||
func NewBuffConn(initialData []byte, ctx context.Context) *BuffConn {
|
||||
c := &BuffConn{
|
||||
reader: bytes.NewBuffer(initialData),
|
||||
writer: bytes.NewBuffer([]byte{}),
|
||||
ctx: ctx,
|
||||
retryOptions: []retry.Option{
|
||||
retry.Context(ctx),
|
||||
retry.Delay(10 * time.Microsecond),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.MaxDelay(100 * time.Millisecond),
|
||||
retry.Attempts(0),
|
||||
},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
var errStall = errors.New("Stall")
|
||||
|
||||
func (conn BuffConn) OutboundData() []byte {
|
||||
d, _ := retry.DoWithData(
|
||||
func() ([]byte, error) {
|
||||
b := conn.writer.Bytes()
|
||||
if len(b) < 1 {
|
||||
return nil, errStall
|
||||
}
|
||||
return b, nil
|
||||
},
|
||||
conn.retryOptions...,
|
||||
)
|
||||
return d
|
||||
}
|
||||
|
||||
func (conn *BuffConn) UpdateData(data []byte) {
|
||||
conn.reader.Write(data)
|
||||
conn.writtenByteCount += len(data)
|
||||
log.Debugf("TLS(buffcon): Appending new data %d (total %d, expecting %d)", len(data), conn.writtenByteCount, conn.expectedWriterByteCount)
|
||||
}
|
||||
|
||||
func (conn BuffConn) NeedsMoreData() bool {
|
||||
if conn.expectedWriterByteCount > 0 {
|
||||
return conn.reader.Len() < int(conn.expectedWriterByteCount)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (conn *BuffConn) Read(p []byte) (int, error) {
|
||||
d, err := retry.DoWithData(
|
||||
func() (int, error) {
|
||||
if conn.reader.Len() == 0 {
|
||||
log.Debugf("TLS(buffcon): Attempted read %d from empty buffer, stalling...", len(p))
|
||||
return 0, errStall
|
||||
}
|
||||
if conn.expectedWriterByteCount > 0 {
|
||||
// If we're waiting for more data, we need to stall
|
||||
if conn.writtenByteCount < int(conn.expectedWriterByteCount) {
|
||||
log.Debugf("TLS(buffcon): Attempted read %d while waiting for bytes %d, stalling...", len(p), conn.expectedWriterByteCount-conn.reader.Len())
|
||||
return 0, errStall
|
||||
}
|
||||
// If we have all the data, reset how much we're expecting to still get
|
||||
if conn.writtenByteCount == int(conn.expectedWriterByteCount) {
|
||||
conn.expectedWriterByteCount = 0
|
||||
}
|
||||
}
|
||||
if conn.reader.Len() == 0 {
|
||||
conn.writtenByteCount = 0
|
||||
}
|
||||
n, err := conn.reader.Read(p)
|
||||
log.Debugf("TLS(buffcon): Read: %d into %d (total %d)", n, len(p), conn.reader.Len())
|
||||
return n, err
|
||||
},
|
||||
conn.retryOptions...,
|
||||
)
|
||||
return d, err
|
||||
}
|
||||
|
||||
func (conn BuffConn) Write(p []byte) (int, error) {
|
||||
log.Debugf("TLS(buffcon): Write: %d", len(p))
|
||||
return conn.writer.Write(p)
|
||||
}
|
||||
|
||||
func (conn BuffConn) Close() error { return nil }
|
||||
func (conn BuffConn) LocalAddr() net.Addr { return nil }
|
||||
func (conn BuffConn) RemoteAddr() net.Addr { return nil }
|
||||
func (conn BuffConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (conn BuffConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (conn BuffConn) SetWriteDeadline(t time.Time) error { return nil }
|
10
internal/outpost/radius/eap/protocol/tls/flags.go
Normal file
10
internal/outpost/radius/eap/protocol/tls/flags.go
Normal file
@ -0,0 +1,10 @@
|
||||
package tls
|
||||
|
||||
type Flag byte
|
||||
|
||||
const (
|
||||
FlagLengthIncluded Flag = 1 << 7
|
||||
FlagMoreFragments Flag = 1 << 6
|
||||
FlagTLSStart Flag = 1 << 5
|
||||
FlagNone Flag = 0
|
||||
)
|
39
internal/outpost/radius/eap/protocol/tls/inner.go
Normal file
39
internal/outpost/radius/eap/protocol/tls/inner.go
Normal file
@ -0,0 +1,39 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func (p *Payload) innerHandler(ctx protocol.Context) {
|
||||
d := make([]byte, 1024)
|
||||
if !ctx.IsProtocolStart(p.Inner.Type()) {
|
||||
ctx.Log().Debug("TLS: Reading from TLS for inner protocol")
|
||||
n, err := p.st.TLS.Read(d)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: Failed to read from TLS connection")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
// Truncate data to the size we read
|
||||
d = d[:n]
|
||||
}
|
||||
err := p.Inner.Decode(d)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to decode inner protocol")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
pl := p.Inner.Handle(ctx.Inner(p.Inner, p.Inner.Type()))
|
||||
enc, err := pl.Encode()
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to encode inner protocol")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
_, err = p.st.TLS.Write(enc)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to write to TLS")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
}
|
279
internal/outpost/radius/eap/protocol/tls/payload.go
Normal file
279
internal/outpost/radius/eap/protocol/tls/payload.go
Normal file
@ -0,0 +1,279 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/vendors/microsoft"
|
||||
)
|
||||
|
||||
const maxChunkSize = 1000
|
||||
const staleConnectionTimeout = 10
|
||||
|
||||
const TypeTLS protocol.Type = 13
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Flags Flag
|
||||
Length uint32
|
||||
Data []byte
|
||||
|
||||
st *State
|
||||
Inner protocol.Payload
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeTLS
|
||||
}
|
||||
|
||||
func (p *Payload) HasInner() protocol.Payload {
|
||||
return p.Inner
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Flags = Flag(raw[0])
|
||||
raw = raw[1:]
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
if len(raw) < 4 {
|
||||
return errors.New("invalid size")
|
||||
}
|
||||
p.Length = binary.BigEndian.Uint32(raw)
|
||||
p.Data = raw[4:]
|
||||
} else {
|
||||
p.Data = raw[0:]
|
||||
}
|
||||
log.WithField("raw", debug.FormatBytes(p.Data)).WithField("size", len(p.Data)).WithField("flags", p.Flags).Trace("TLS: decode raw")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
l := 1
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
l += 4
|
||||
}
|
||||
buff := make([]byte, len(p.Data)+l)
|
||||
buff[0] = byte(p.Flags)
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
buff[1] = byte(p.Length >> 24)
|
||||
buff[2] = byte(p.Length >> 16)
|
||||
buff[3] = byte(p.Length >> 8)
|
||||
buff[4] = byte(p.Length)
|
||||
}
|
||||
if len(p.Data) > 0 {
|
||||
copy(buff[5:], p.Data)
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeTLS, p.st)
|
||||
}()
|
||||
if ctx.IsProtocolStart(TypeTLS) {
|
||||
p.st = NewState(ctx).(*State)
|
||||
return &Payload{
|
||||
Flags: FlagTLSStart,
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeTLS).(*State)
|
||||
|
||||
if p.st.TLS == nil {
|
||||
p.tlsInit(ctx)
|
||||
} else if len(p.Data) > 0 {
|
||||
ctx.Log().Debug("TLS: Updating buffer with new TLS data from packet")
|
||||
if p.Flags&FlagLengthIncluded != 0 && p.st.Conn.expectedWriterByteCount == 0 {
|
||||
ctx.Log().Debugf("TLS: Expecting %d total bytes, will buffer", p.Length)
|
||||
p.st.Conn.expectedWriterByteCount = int(p.Length)
|
||||
} else if p.Flags&FlagLengthIncluded != 0 {
|
||||
ctx.Log().Debug("TLS: No length included, not buffering")
|
||||
p.st.Conn.expectedWriterByteCount = 0
|
||||
}
|
||||
p.st.Conn.UpdateData(p.Data)
|
||||
if !p.st.Conn.NeedsMoreData() && !p.st.HandshakeDone {
|
||||
// Wait for outbound data to be available
|
||||
p.st.Conn.OutboundData()
|
||||
}
|
||||
}
|
||||
// If we need more data, send the client the go-ahead
|
||||
if p.st.Conn.NeedsMoreData() {
|
||||
return &Payload{
|
||||
Flags: FlagNone,
|
||||
Length: 0,
|
||||
Data: []byte{},
|
||||
}
|
||||
}
|
||||
if p.st.HasMore() {
|
||||
return p.sendNextChunk()
|
||||
}
|
||||
if p.st.Conn.writer.Len() == 0 && p.st.HandshakeDone {
|
||||
if p.Inner != nil {
|
||||
ctx.Log().Debug("TLS: Handshake is done, delegating to inner protocol")
|
||||
p.innerHandler(ctx)
|
||||
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||
}
|
||||
defer p.st.ContextCancel()
|
||||
// If we don't have a final status from the handshake finished function, stall for time
|
||||
pst, _ := retry.DoWithData(
|
||||
func() (protocol.Status, error) {
|
||||
if p.st.FinalStatus == protocol.StatusUnknown {
|
||||
return p.st.FinalStatus, errStall
|
||||
}
|
||||
return p.st.FinalStatus, nil
|
||||
},
|
||||
retry.Context(p.st.Context),
|
||||
retry.Delay(10*time.Microsecond),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.MaxDelay(100*time.Millisecond),
|
||||
retry.Attempts(0),
|
||||
)
|
||||
ctx.EndInnerProtocol(pst)
|
||||
return nil
|
||||
}
|
||||
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||
}
|
||||
|
||||
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||
if r.Code != radius.CodeAccessAccept {
|
||||
return nil
|
||||
}
|
||||
if p.st == nil || !p.st.HandshakeDone {
|
||||
return nil
|
||||
}
|
||||
log.Debug("TLS: Adding MPPE Keys")
|
||||
// TLS overrides other protocols' MPPE keys
|
||||
if len(microsoft.MSMPPERecvKey_Get(r, q)) > 0 {
|
||||
microsoft.MSMPPERecvKey_Del(r)
|
||||
}
|
||||
if len(microsoft.MSMPPESendKey_Get(r, q)) > 0 {
|
||||
microsoft.MSMPPESendKey_Del(r)
|
||||
}
|
||||
microsoft.MSMPPERecvKey_Set(r, p.st.MPPEKey[:32])
|
||||
microsoft.MSMPPESendKey_Set(r, p.st.MPPEKey[64:64+32])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) tlsInit(ctx protocol.Context) {
|
||||
ctx.Log().Debug("TLS: no TLS connection in state yet, starting connection")
|
||||
p.st.Context, p.st.ContextCancel = context.WithTimeout(context.Background(), staleConnectionTimeout*time.Second)
|
||||
p.st.Conn = NewBuffConn(p.Data, p.st.Context)
|
||||
cfg := ctx.ProtocolSettings().(TLSConfig).TLSConfig().Clone()
|
||||
|
||||
if klp, ok := os.LookupEnv("SSLKEYLOGFILE"); ok {
|
||||
kl, err := os.OpenFile(klp, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg.KeyLogWriter = kl
|
||||
}
|
||||
|
||||
cfg.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
ctx.Log().Debugf("TLS: ClientHello: %+v\n", chi)
|
||||
p.st.ClientHello = chi
|
||||
return nil, nil
|
||||
}
|
||||
p.st.TLS = tls.Server(p.st.Conn, cfg)
|
||||
p.st.TLS.SetDeadline(time.Now().Add(staleConnectionTimeout * time.Second))
|
||||
go func() {
|
||||
err := p.st.TLS.HandshakeContext(p.st.Context)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Debug("TLS: Handshake error")
|
||||
p.st.FinalStatus = protocol.StatusError
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
ctx.Log().Debug("TLS: handshake done")
|
||||
p.tlsHandshakeFinished(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Payload) tlsHandshakeFinished(ctx protocol.Context) {
|
||||
cs := p.st.TLS.ConnectionState()
|
||||
label := "client EAP encryption"
|
||||
var context []byte
|
||||
switch cs.Version {
|
||||
case tls.VersionTLS10:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.0)", cs.Version)
|
||||
case tls.VersionTLS11:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.1)", cs.Version)
|
||||
case tls.VersionTLS12:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.2)", cs.Version)
|
||||
case tls.VersionTLS13:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.3)", cs.Version)
|
||||
label = "EXPORTER_EAP_TLS_Key_Material"
|
||||
context = []byte{byte(TypeTLS)}
|
||||
}
|
||||
ksm, err := cs.ExportKeyingMaterial(label, context, 64+64)
|
||||
ctx.Log().Debugf("TLS: ksm % x %v", ksm, err)
|
||||
p.st.MPPEKey = ksm
|
||||
p.st.HandshakeDone = true
|
||||
if p.Inner == nil {
|
||||
p.st.FinalStatus = ctx.ProtocolSettings().(Settings).HandshakeSuccessful(ctx, cs.PeerCertificates)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) startChunkedTransfer(data []byte) *Payload {
|
||||
if len(data) > maxChunkSize {
|
||||
log.WithField("length", len(data)).Debug("TLS: Data needs to be chunked")
|
||||
p.st.RemainingChunks = append(p.st.RemainingChunks, slices.Collect(slices.Chunk(data, maxChunkSize))...)
|
||||
p.st.TotalPayloadSize = len(data)
|
||||
return p.sendNextChunk()
|
||||
}
|
||||
log.WithField("length", len(data)).Debug("TLS: Sending data un-chunked")
|
||||
p.st.Conn.writer.Reset()
|
||||
return &Payload{
|
||||
Flags: FlagLengthIncluded,
|
||||
Length: uint32(len(data)),
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) sendNextChunk() *Payload {
|
||||
nextChunk := p.st.RemainingChunks[0]
|
||||
log.WithField("raw", debug.FormatBytes(nextChunk)).Debug("TLS: Sending next chunk")
|
||||
p.st.RemainingChunks = p.st.RemainingChunks[1:]
|
||||
flags := FlagLengthIncluded
|
||||
if p.st.HasMore() {
|
||||
log.WithField("chunks", len(p.st.RemainingChunks)).Debug("TLS: More chunks left")
|
||||
flags += FlagMoreFragments
|
||||
} else {
|
||||
// Last chunk, reset the connection buffers and pending payload size
|
||||
defer func() {
|
||||
log.Debug("TLS: Sent last chunk")
|
||||
p.st.Conn.writer.Reset()
|
||||
p.st.TotalPayloadSize = 0
|
||||
}()
|
||||
}
|
||||
log.WithField("length", p.st.TotalPayloadSize).Debug("TLS: Total payload size")
|
||||
return &Payload{
|
||||
Flags: flags,
|
||||
Length: uint32(p.st.TotalPayloadSize),
|
||||
Data: nextChunk,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<TLS Packet HandshakeDone=%t, FinalStatus=%d, ClientHello=%v>",
|
||||
p.st.HandshakeDone,
|
||||
p.st.FinalStatus,
|
||||
p.st.ClientHello,
|
||||
)
|
||||
}
|
21
internal/outpost/radius/eap/protocol/tls/settings.go
Normal file
21
internal/outpost/radius/eap/protocol/tls/settings.go
Normal file
@ -0,0 +1,21 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type TLSConfig interface {
|
||||
TLSConfig() *tls.Config
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Config *tls.Config
|
||||
HandshakeSuccessful func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status
|
||||
}
|
||||
|
||||
func (s Settings) TLSConfig() *tls.Config {
|
||||
return s.Config
|
||||
}
|
32
internal/outpost/radius/eap/protocol/tls/state.go
Normal file
32
internal/outpost/radius/eap/protocol/tls/state.go
Normal file
@ -0,0 +1,32 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
RemainingChunks [][]byte
|
||||
HandshakeDone bool
|
||||
FinalStatus protocol.Status
|
||||
ClientHello *tls.ClientHelloInfo
|
||||
MPPEKey []byte
|
||||
TotalPayloadSize int
|
||||
TLS *tls.Conn
|
||||
Conn *BuffConn
|
||||
Context context.Context
|
||||
ContextCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewState(c protocol.Context) interface{} {
|
||||
c.Log().Debug("TLS: new state")
|
||||
return &State{
|
||||
RemainingChunks: make([][]byte, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (s State) HasMore() bool {
|
||||
return len(s.RemainingChunks) > 0
|
||||
}
|
@ -1,17 +1,44 @@
|
||||
package radius
|
||||
|
||||
import (
|
||||
"context"
|
||||
ttls "crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net/url"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/outpost/flow"
|
||||
"goauthentik.io/internal/outpost/radius/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/gtc"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/mschapv2"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||
"goauthentik.io/internal/outpost/radius/metrics"
|
||||
"goauthentik.io/internal/utils"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2865"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
eap := rfc2869.EAPMessage_Get(r.Packet)
|
||||
if len(eap) > 0 {
|
||||
rs.log.Trace("EAP request")
|
||||
rs.Handle_AccessRequest_EAP(w, r)
|
||||
} else {
|
||||
rs.log.Trace("PAP request")
|
||||
rs.Handle_AccessRequest_PAP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest_PAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
username := rfc2865.UserName_GetString(r.Packet)
|
||||
|
||||
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
@ -87,3 +114,164 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
|
||||
res.Add(attr.Type, attr.Attribute)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest_EAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
er := rfc2869.EAPMessage_Get(r.Packet)
|
||||
ep, err := eap.Decode(r.pi, er)
|
||||
if err != nil {
|
||||
rs.log.WithError(err).Warning("failed to parse EAP packet")
|
||||
return
|
||||
}
|
||||
ep.HandleRadiusPacket(w, r.Request)
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetEAPState(key string) *protocol.State {
|
||||
return pi.eapState[key]
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) SetEAPState(key string, state *protocol.State) {
|
||||
pi.eapState[key] = state
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetEAPSettings() protocol.Settings {
|
||||
protocols := []protocol.ProtocolConstructor{
|
||||
identity.Protocol,
|
||||
legacy_nak.Protocol,
|
||||
}
|
||||
|
||||
certId := pi.certId
|
||||
if certId == "" {
|
||||
return protocol.Settings{
|
||||
Protocols: protocols,
|
||||
}
|
||||
}
|
||||
|
||||
cert := pi.s.cryptoStore.Get(certId)
|
||||
if cert == nil {
|
||||
return protocol.Settings{
|
||||
Protocols: protocols,
|
||||
}
|
||||
}
|
||||
|
||||
return protocol.Settings{
|
||||
Protocols: append(protocols, tls.Protocol, peap.Protocol),
|
||||
ProtocolPriority: []protocol.Type{tls.TypeTLS, peap.TypePEAP},
|
||||
ProtocolSettings: map[protocol.Type]interface{}{
|
||||
tls.TypeTLS: tls.Settings{
|
||||
Config: &ttls.Config{
|
||||
Certificates: []ttls.Certificate{*cert},
|
||||
ClientAuth: ttls.RequireAnyClientCert,
|
||||
},
|
||||
HandshakeSuccessful: func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status {
|
||||
ctx.Log().Debug("Starting authn flow")
|
||||
pem := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certs[0].Raw,
|
||||
})
|
||||
|
||||
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||
})
|
||||
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
fe.AddHeader("X-Authentik-Outpost-Certificate", url.QueryEscape(string(pem)))
|
||||
|
||||
passed, err := fe.Execute()
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||
return protocol.StatusError
|
||||
}
|
||||
ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||
if passed {
|
||||
return protocol.StatusSuccess
|
||||
} else {
|
||||
return protocol.StatusError
|
||||
}
|
||||
},
|
||||
},
|
||||
peap.TypePEAP: peap.Settings{
|
||||
Config: &ttls.Config{
|
||||
Certificates: []ttls.Certificate{*cert},
|
||||
},
|
||||
InnerProtocols: protocol.Settings{
|
||||
Protocols: append(protocols, gtc.Protocol, mschapv2.Protocol),
|
||||
ProtocolPriority: []protocol.Type{gtc.TypeGTC, mschapv2.TypeMSCHAPv2},
|
||||
ProtocolSettings: map[protocol.Type]interface{}{
|
||||
mschapv2.TypeMSCHAPv2: mschapv2.Settings{
|
||||
AuthenticateRequest: mschapv2.DebugStaticCredentials(
|
||||
[]byte("foo"), []byte("bar"),
|
||||
),
|
||||
},
|
||||
gtc.TypeGTC: gtc.Settings{
|
||||
ChallengeHandler: func(ctx protocol.Context) (gtc.GetChallenge, gtc.ValidateResponse) {
|
||||
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||
})
|
||||
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
var ch []byte = nil
|
||||
var ans []byte = nil
|
||||
fe.InteractiveSolver = func(ct *api.ChallengeTypes, afesr api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
|
||||
comp := ct.GetActualInstance().(flow.ChallengeCommon).GetComponent()
|
||||
ch = []byte(comp)
|
||||
for {
|
||||
if ans == nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
switch comp {
|
||||
case string(flow.StageIdentification):
|
||||
r := api.NewIdentificationChallengeResponseRequest(string(ans))
|
||||
return api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||
case string(flow.StagePassword):
|
||||
r := api.NewPasswordChallengeResponseRequest(string(ans))
|
||||
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||
}
|
||||
panic(comp)
|
||||
}
|
||||
passed := false
|
||||
done := false
|
||||
go func() {
|
||||
var err error
|
||||
passed, err = fe.Execute()
|
||||
done = true
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||
// return protocol.StatusError
|
||||
}
|
||||
// ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||
// if passed {
|
||||
// return protocol.StatusSuccess
|
||||
// } else {
|
||||
// return protocol.StatusError
|
||||
// }
|
||||
}()
|
||||
return func() []byte {
|
||||
if done {
|
||||
status := protocol.StatusError
|
||||
if passed {
|
||||
status = protocol.StatusSuccess
|
||||
}
|
||||
ctx.EndInnerProtocol(status)
|
||||
}
|
||||
for {
|
||||
if ch == nil {
|
||||
continue
|
||||
}
|
||||
defer func() {
|
||||
ch = nil
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
}, func(answer []byte) {
|
||||
ans = answer
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package radius
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
@ -35,12 +36,32 @@ func (r *RadiusRequest) ID() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
type LogWriter struct {
|
||||
w radius.ResponseWriter
|
||||
l *log.Entry
|
||||
}
|
||||
|
||||
func (lw LogWriter) Write(packet *radius.Packet) error {
|
||||
lw.l.WithField("code", packet.Code.String()).Info("Radius Response")
|
||||
return lw.w.Write(packet)
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
|
||||
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
|
||||
sentry.WithTransactionName("authentik.providers.radius.connect"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
rl := rs.log.WithField("code", r.Code.String()).WithField("request", rid)
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr.String())
|
||||
if err != nil {
|
||||
rs.log.WithError(err).Warning("Failed to get remote IP")
|
||||
return
|
||||
}
|
||||
rl := rs.log.WithFields(log.Fields{
|
||||
"code": r.Code.String(),
|
||||
"request": rid,
|
||||
"ip": host,
|
||||
"id": r.Identifier,
|
||||
})
|
||||
selectedApp := ""
|
||||
defer func() {
|
||||
span.Finish()
|
||||
@ -58,6 +79,7 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
}
|
||||
|
||||
rl.Info("Radius Request")
|
||||
ww := LogWriter{w, rl}
|
||||
|
||||
// Lookup provider by shared secret
|
||||
var pi *ProviderInstance
|
||||
@ -72,12 +94,12 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
hs := sha512.Sum512([]byte(r.Secret))
|
||||
bs := hex.EncodeToString(hs[:])
|
||||
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
_ = ww.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
nr.pi = pi
|
||||
|
||||
if nr.Code == radius.CodeAccessRequest {
|
||||
rs.Handle_AccessRequest(w, nr)
|
||||
rs.Handle_AccessRequest(ww, nr)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/metrics"
|
||||
|
||||
"layeh.com/radius"
|
||||
@ -22,23 +23,27 @@ type ProviderInstance struct {
|
||||
appSlug string
|
||||
flowSlug string
|
||||
providerId int32
|
||||
certId string
|
||||
s *RadiusServer
|
||||
log *log.Entry
|
||||
eapState map[string]*protocol.State
|
||||
}
|
||||
|
||||
type RadiusServer struct {
|
||||
s radius.PacketServer
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
s radius.PacketServer
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
cryptoStore *ak.CryptoStore
|
||||
|
||||
providers []*ProviderInstance
|
||||
providers map[int32]*ProviderInstance
|
||||
}
|
||||
|
||||
func NewServer(ac *ak.APIController) ak.Outpost {
|
||||
rs := &RadiusServer{
|
||||
log: log.WithField("logger", "authentik.outpost.radius"),
|
||||
ac: ac,
|
||||
providers: []*ProviderInstance{},
|
||||
log: log.WithField("logger", "authentik.outpost.radius"),
|
||||
ac: ac,
|
||||
providers: map[int32]*ProviderInstance{},
|
||||
cryptoStore: ak.NewCryptoStore(ac.Client.CryptoApi),
|
||||
}
|
||||
rs.s = radius.PacketServer{
|
||||
Handler: rs,
|
||||
@ -85,7 +90,7 @@ func (rs *RadiusServer) RADIUSSecret(ctx context.Context, remoteAddr net.Addr) (
|
||||
return bi < bj
|
||||
})
|
||||
candidate := matchedPrefixes[0]
|
||||
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).Debug("Matched CIDR")
|
||||
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).WithField("instance", candidate.p.appSlug).Debug("Matched CIDR")
|
||||
return candidate.p.SharedSecret, nil
|
||||
}
|
||||
|
||||
@ -98,7 +103,8 @@ func (rs *RadiusServer) Start() error {
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := rs.StartRadiusServer()
|
||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||
err := rs.s.ListenAndServe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.6.2
|
||||
Default: 2025.6.3
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
Binary file not shown.
@ -11,18 +11,18 @@
|
||||
# Nicola Mersi, 2024
|
||||
# tmassimi, 2024
|
||||
# Marc Schmitt, 2024
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2024
|
||||
# Matteo Piccina <altermatte@gmail.com>, 2025
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
|
||||
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
|
||||
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2025\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -116,7 +116,7 @@ msgstr "Certificato Web utilizzato dal server Web authentik Core."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid "Certificates used for client authentication."
|
||||
msgstr ""
|
||||
msgstr "Certificati utilizzati per l'autenticazione del client."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid "Brand"
|
||||
@ -130,10 +130,6 @@ msgstr "Brands"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "L'utente non ha accesso all'applicazione."
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "Descrizione extra non disponibile"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "Impossibile impostare il gruppo come padre di se stesso."
|
||||
@ -294,15 +290,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Collegamento a un utente con indirizzo email identico. Può avere "
|
||||
"implicazioni sulla sicurezza quando una fonte non convalida gli indirizzi "
|
||||
"e-mail."
|
||||
"email."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"Use the user's email address, but deny enrollment when the email address "
|
||||
"already exists."
|
||||
msgstr ""
|
||||
"Usa l'indirizzo e-mail dell'utente, ma nega l'iscrizione quando l'indirizzo "
|
||||
"e-mail esiste già."
|
||||
"Usa l'indirizzo email dell'utente, ma nega l'iscrizione quando l'indirizzo "
|
||||
"email esiste già."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
@ -682,26 +678,29 @@ msgid ""
|
||||
"option has a higher priority than the `client_certificate` option on "
|
||||
"`Brand`."
|
||||
msgstr ""
|
||||
"Configura le autorità di certificazione per convalidare il certificato. "
|
||||
"Questa opzione ha una priorità maggiore rispetto all'opzione "
|
||||
"`client_certificate` su `Brand`."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Mutual TLS Stage"
|
||||
msgstr ""
|
||||
msgstr "Fase di TLS reciproca"
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Mutual TLS Stages"
|
||||
msgstr ""
|
||||
msgstr "Fasi di TLS reciproche"
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Permissions to pass Certificates for outposts."
|
||||
msgstr ""
|
||||
msgstr " Permessi di trasmissione dei Certificati per gli avamposti."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/stage.py
|
||||
msgid "Certificate required but no certificate was given."
|
||||
msgstr ""
|
||||
msgstr " Il certificato è stato richiesto ma non è stato consegnato."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/stage.py
|
||||
msgid "No user found for certificate."
|
||||
msgstr ""
|
||||
msgstr "Nessun utente trovato per il certificato."
|
||||
|
||||
#: authentik/enterprise/stages/source/models.py
|
||||
msgid ""
|
||||
@ -834,6 +833,14 @@ msgstr ""
|
||||
"Definisci a quale gruppo di utenti deve essere inviata e mostrata questa "
|
||||
"notifica. Se lasciato vuoto, la notifica non verrà inviata."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr ""
|
||||
"Se abilitata, la notifica verrà inviata all'utente che ha attivato l'evento."
|
||||
" Se destination_group è configurato, la notifica verrà inviata a entrambi."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "Regola di notifica"
|
||||
@ -1050,16 +1057,16 @@ msgstr "Avvio della sincronizzazione completa del provider"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing users"
|
||||
msgstr ""
|
||||
msgstr "Sincronizzazione degli utenti"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing groups"
|
||||
msgstr ""
|
||||
msgstr "Sincronizzazione dei gruppi"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "Sincronizzando pagina {page} dei gruppi"
|
||||
msgid "Syncing page {page} of {object_type}"
|
||||
msgstr "Sincronizzazione della pagina {page} di {object_type}"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
@ -2461,6 +2468,10 @@ msgstr "Gruppo di aggiunta DN"
|
||||
msgid "Consider Objects matching this filter to be Users."
|
||||
msgstr "Considerare gli oggetti corrispondenti a questo filtro come Utenti."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Attribute which matches the value of `group_membership_field`."
|
||||
msgstr "Attributo che corrisponde al valore di `group_membership_field`."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Field which contains members of a group."
|
||||
msgstr "Campo che contiene i membri di un gruppo."
|
||||
@ -2502,6 +2513,8 @@ msgid ""
|
||||
"Delete authentik users and groups which were previously supplied by this "
|
||||
"source, but are now missing from it."
|
||||
msgstr ""
|
||||
"Elimina gli utenti e i gruppi authentik precedentemente forniti da questa "
|
||||
"fonte, ma che ora mancano."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "LDAP Source"
|
||||
@ -2523,6 +2536,8 @@ msgstr "Mappature delle proprietà della sorgente LDAP"
|
||||
msgid ""
|
||||
"Unique ID used while checking if this object still exists in the directory."
|
||||
msgstr ""
|
||||
"ID univoco utilizzato per verificare se questo oggetto esiste ancora nella "
|
||||
"directory."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "User LDAP Source Connection"
|
||||
@ -2920,7 +2935,7 @@ msgstr "Connessioni sorgente SAML di gruppo"
|
||||
#: authentik/sources/saml/views.py
|
||||
#, python-brace-format
|
||||
msgid "Continue to {source_name}"
|
||||
msgstr ""
|
||||
msgstr "Continua su {source_name}"
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Source"
|
||||
@ -2988,8 +3003,8 @@ msgstr "Fasi di configurazione dell'autenticatore email"
|
||||
#: authentik/stages/email/stage.py
|
||||
msgid "Exception occurred while rendering E-mail template"
|
||||
msgstr ""
|
||||
"Eccezione verificatasi durante la visualizzazione del modello di posta "
|
||||
"elettronica"
|
||||
"Si è verificata un'eccezione durante la visualizzazione del modello di posta"
|
||||
" elettronica"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Device"
|
||||
@ -3028,7 +3043,7 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Codice MFA via e-mail.\n"
|
||||
" Codice MFA via email.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.html
|
||||
@ -3054,7 +3069,7 @@ msgid ""
|
||||
"Email MFA code\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Codice e-mail MFA\n"
|
||||
"Codice email MFA\n"
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
|
||||
#, python-format
|
||||
@ -3321,7 +3336,7 @@ msgstr "Consensi utente"
|
||||
|
||||
#: authentik/stages/consent/stage.py
|
||||
msgid "Invalid consent token, re-showing prompt"
|
||||
msgstr ""
|
||||
msgstr "Token di consenso non valido, viene nuovamente visualizzato il prompt"
|
||||
|
||||
#: authentik/stages/deny/models.py
|
||||
msgid "Deny Stage"
|
||||
@ -3341,11 +3356,11 @@ msgstr "Fasi fittizie"
|
||||
|
||||
#: authentik/stages/email/flow.py
|
||||
msgid "Continue to confirm this email address."
|
||||
msgstr ""
|
||||
msgstr "Continua per confermare questo indirizzo email."
|
||||
|
||||
#: authentik/stages/email/flow.py
|
||||
msgid "Link was already used, please request a new link."
|
||||
msgstr ""
|
||||
msgstr "Il collegamento è già stato utilizzato. Richiedine uno nuovo."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Password Reset"
|
||||
@ -3365,7 +3380,7 @@ msgstr "Fase email"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stages"
|
||||
msgstr "Fasi Email"
|
||||
msgstr "Fasi email"
|
||||
|
||||
#: authentik/stages/email/stage.py
|
||||
msgid "Successfully verified Email."
|
||||
@ -3467,7 +3482,7 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n"
|
||||
" Se non hai richiesto una modifica della password, ignora questa email. Il link sopra è valido per %(expires)s.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt
|
||||
@ -3485,11 +3500,11 @@ msgid ""
|
||||
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n"
|
||||
"Se non hai richiesto una modifica della password, ignora questa email. Il link sopra è valido per %(expires)s.\n"
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.html
|
||||
msgid "authentik Test-Email"
|
||||
msgstr "e-mail di prova di authentik"
|
||||
msgstr "email di prova di authentik"
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.html
|
||||
msgid ""
|
||||
@ -3498,7 +3513,7 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Questa è un'e-mail di prova per informarti che hai configurato correttamente le e-mail di authentik.\n"
|
||||
" Questa è un'email di prova per informarti che hai configurato correttamente le email di authentik.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.txt
|
||||
@ -3507,7 +3522,7 @@ msgid ""
|
||||
"This is a test email to inform you, that you've successfully configured authentik emails.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Questa è un'e-mail di prova per informarti che hai configurato correttamente le e-mail di authentik.\n"
|
||||
"Questa è un'email di prova per informarti che hai configurato correttamente le email di authentik.\n"
|
||||
|
||||
#: authentik/stages/identification/api.py
|
||||
msgid "When no user fields are selected, at least one source must be selected"
|
||||
@ -3710,7 +3725,7 @@ msgstr ""
|
||||
|
||||
#: authentik/stages/prompt/models.py
|
||||
msgid "Email: Text field with Email type."
|
||||
msgstr "E-mail: Campo di testo con il tipo di e-mail."
|
||||
msgstr "Email: Campo di testo con il tipo di email."
|
||||
|
||||
#: authentik/stages/prompt/models.py
|
||||
msgid ""
|
||||
@ -3865,10 +3880,6 @@ msgstr "Fasi di accesso utente"
|
||||
msgid "No Pending user to login."
|
||||
msgstr "Nessun utente in attesa di accesso."
|
||||
|
||||
#: authentik/stages/user_login/stage.py
|
||||
msgid "Successfully logged in!"
|
||||
msgstr "Accesso effettuato!"
|
||||
|
||||
#: authentik/stages/user_logout/models.py
|
||||
msgid "User Logout Stage"
|
||||
msgstr "Fase di disconnessione dell'utente"
|
||||
|
Binary file not shown.
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
|
||||
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -118,10 +118,6 @@ msgstr "品牌"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "用户没有访问此应用程序的权限。"
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "额外描述不可用"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "无法设置组自身为父级。"
|
||||
@ -775,6 +771,12 @@ msgid ""
|
||||
"If left empty, Notification won't ben sent."
|
||||
msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr "启用时,通知会被发送到触发事件的用户。当配置了 destination_group 时,通知也会同时发送到对应组。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "通知规则"
|
||||
|
Binary file not shown.
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
|
||||
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -117,10 +117,6 @@ msgstr "品牌"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "用户没有访问此应用程序的权限。"
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "额外描述不可用"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "无法设置组自身为父级。"
|
||||
@ -774,6 +770,12 @@ msgid ""
|
||||
"If left empty, Notification won't ben sent."
|
||||
msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr "启用时,通知会被发送到触发事件的用户。当配置了 destination_group 时,通知也会同时发送到对应组。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "通知规则"
|
||||
|
4
notes.md
Normal file
4
notes.md
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
eapol_test -s foo -a 192.168.68.1 -c config
|
||||
|
||||
sudo tcpdump -i bridge100 port 1812 -w eap.pcap
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.6.2",
|
||||
"version": "2025.6.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.6.2",
|
||||
"version": "2025.6.3",
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"prettier": "^3.3.3",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.6.2",
|
||||
"version": "2025.6.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
@ -2,11 +2,8 @@
|
||||
* @file Prettier configuration for authentik.
|
||||
*
|
||||
* @import { Config as PrettierConfig } from "prettier";
|
||||
*/
|
||||
|
||||
import { importsPlugin } from "./imports.js";
|
||||
|
||||
/**
|
||||
* @import { PluginConfig as SortPluginConfig } from "@trivago/prettier-plugin-sort-imports";
|
||||
*
|
||||
* @typedef {object} PackageJSONPluginConfig
|
||||
* @property {string[]} [packageSortOrder] Custom ordering array.
|
||||
*/
|
||||
@ -14,7 +11,7 @@ import { importsPlugin } from "./imports.js";
|
||||
/**
|
||||
* authentik Prettier configuration.
|
||||
*
|
||||
* @type {PrettierConfig & PackageJSONPluginConfig}
|
||||
* @type {PrettierConfig & SortPluginConfig & PackageJSONPluginConfig}
|
||||
* @internal
|
||||
*/
|
||||
export const AuthentikPrettierConfig = {
|
||||
@ -37,8 +34,32 @@ export const AuthentikPrettierConfig = {
|
||||
plugins: [
|
||||
// ---
|
||||
"prettier-plugin-packagejson",
|
||||
importsPlugin(),
|
||||
"@trivago/prettier-plugin-sort-imports",
|
||||
],
|
||||
importOrder: [
|
||||
// ---
|
||||
|
||||
"^(@goauthentik/|#)common.+",
|
||||
"^(@goauthentik/|#)elements.+",
|
||||
"^(@goauthentik/|#)components.+",
|
||||
"^(@goauthentik/|#)user.+",
|
||||
"^(@goauthentik/|#)admin.+",
|
||||
"^(@goauthentik/|#)flow.+",
|
||||
|
||||
"^#.+",
|
||||
"^@goauthentik.+",
|
||||
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
|
||||
"^(@?)lit(.*)$",
|
||||
"\\.css$",
|
||||
"^@goauthentik/api$",
|
||||
"^[./]",
|
||||
],
|
||||
importOrderSideEffects: false,
|
||||
importOrderSeparation: true,
|
||||
importOrderSortSpecifiers: true,
|
||||
importOrderParserPlugins: ["typescript", "jsx", "classProperties", "decorators-legacy"],
|
||||
overrides: [
|
||||
{
|
||||
files: "schemas/**/*.json",
|
||||
|
@ -1,172 +0,0 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import { formatSourceFromFile } from "format-imports";
|
||||
import { parsers as babelParsers } from "prettier/plugins/babel";
|
||||
/**
|
||||
* @file Prettier import plugin.
|
||||
*
|
||||
* @import { Plugin, ParserOptions } from "prettier";
|
||||
*/
|
||||
import { parsers as typescriptParsers } from "prettier/plugins/typescript";
|
||||
|
||||
const require = createRequire(process.cwd() + "/");
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string | null}
|
||||
*/
|
||||
function resolveModule(name) {
|
||||
try {
|
||||
return require.resolve(name);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const webSubmodules = [
|
||||
// ---
|
||||
"common",
|
||||
"elements",
|
||||
"components",
|
||||
"user",
|
||||
"admin",
|
||||
"flow",
|
||||
];
|
||||
|
||||
/**
|
||||
* Ensure that every import without an extension adds one.
|
||||
* @param {string} input
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeExtensions(input) {
|
||||
return input.replace(/(?:import|from)\s*["']((?:\.\.?\/).*?)(?<!\.\w+)["']/gm, (line, path) => {
|
||||
return line.replace(path, `${path}.js`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filepath
|
||||
* @param {string} input
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeImports(filepath, input) {
|
||||
let output = input;
|
||||
|
||||
// Replace all TypeScript imports with the paths resolved by Node/Browser import maps.
|
||||
|
||||
for (const submodule of webSubmodules) {
|
||||
const legacyPattern = new RegExp(
|
||||
[
|
||||
// ---
|
||||
`(?:import|from)`,
|
||||
`\\\(?\\n?\\s*`,
|
||||
`"(?<suffix>@goauthentik\/${submodule}\/)`,
|
||||
|
||||
`(?<path>[^"'.]+)`,
|
||||
`(?:\.[^"']+)?["']`,
|
||||
`\\n?\\s*\\\)?;`,
|
||||
].join(""),
|
||||
"gm",
|
||||
);
|
||||
|
||||
output = output.replace(
|
||||
legacyPattern,
|
||||
/**
|
||||
* @param {string} line
|
||||
* @param {string} suffix
|
||||
* @param {string} path
|
||||
*/
|
||||
(line, suffix, path) => {
|
||||
const exported = `@goauthentik/web/${submodule}/${path}`;
|
||||
let imported = `#${submodule}/${path}`;
|
||||
|
||||
let module = resolveModule(`${exported}.ts`);
|
||||
|
||||
if (!module) {
|
||||
module = resolveModule(`${exported}/index.ts`);
|
||||
imported += "/index";
|
||||
}
|
||||
|
||||
if (imported.endsWith(".css")) {
|
||||
imported += ".js";
|
||||
}
|
||||
|
||||
if (!module) {
|
||||
console.warn(`\nCannot resolve module ${exported} from ${filepath}`, {
|
||||
line,
|
||||
path,
|
||||
exported,
|
||||
imported,
|
||||
module,
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return (
|
||||
line
|
||||
// ---
|
||||
.replace(suffix + path, imported)
|
||||
.replace(`${imported}.js`, imported)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Plugin}
|
||||
*/
|
||||
export function importsPlugin({
|
||||
useLegacyCleanup = process.env.AK_FIX_LEGACY_IMPORTS === "true",
|
||||
} = {}) {
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {ParserOptions} options
|
||||
*/
|
||||
const preprocess = (input, { filepath, printWidth }) => {
|
||||
let output = input;
|
||||
|
||||
if (useLegacyCleanup) {
|
||||
output = normalizeExtensions(input);
|
||||
output = normalizeImports(filepath, output);
|
||||
}
|
||||
|
||||
const value = formatSourceFromFile.sync(output, filepath, {
|
||||
nodeProtocol: "always",
|
||||
maxLineLength: printWidth,
|
||||
wrappingStyle: "prettier",
|
||||
groupRules: [
|
||||
"^node:",
|
||||
...webSubmodules.map((submodule) => `^(@goauthentik/|#)${submodule}.+`),
|
||||
|
||||
"^#.+",
|
||||
"^@goauthentik.+",
|
||||
|
||||
{}, // Other imports.
|
||||
|
||||
"^(@?)lit(.*)$",
|
||||
"\\.css$",
|
||||
"^@goauthentik/api$",
|
||||
"^[./]",
|
||||
],
|
||||
});
|
||||
|
||||
return value || input;
|
||||
};
|
||||
|
||||
return {
|
||||
parsers: {
|
||||
typescript: {
|
||||
...typescriptParsers.typescript,
|
||||
preprocess,
|
||||
},
|
||||
babel: {
|
||||
...babelParsers.babel,
|
||||
preprocess,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
1830
packages/prettier-config/package-lock.json
generated
1830
packages/prettier-config/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/prettier-config",
|
||||
"version": "3.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "authentik's Prettier config",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@ -10,19 +10,19 @@
|
||||
},
|
||||
"type": "module",
|
||||
"exports": "./index.js",
|
||||
"dependencies": {
|
||||
"format-imports": "^4.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@goauthentik/tsconfig": "^1.0.1",
|
||||
"@types/node": "^24.0.4",
|
||||
"prettier": "^3.6.1",
|
||||
"prettier-plugin-packagejson": "^2.5.16",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.15",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.6.1",
|
||||
"prettier-plugin-packagejson": "^2.5.16"
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2025.6.2"
|
||||
version = "2025.6.3"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.13.*"
|
||||
@ -43,7 +43,7 @@ dependencies = [
|
||||
"jwcrypto==1.5.6",
|
||||
"kubernetes==33.1.0",
|
||||
"ldap3==2.9.1",
|
||||
"lxml==5.4.0",
|
||||
"lxml==6.0.0",
|
||||
"msgraph-sdk==1.35.0",
|
||||
"opencontainers==0.0.14",
|
||||
"packaging==25.0",
|
||||
@ -57,7 +57,7 @@ dependencies = [
|
||||
"pyyaml==6.0.2",
|
||||
"requests-oauthlib==2.0.0",
|
||||
"scim2-filter-parser==0.7.0",
|
||||
"sentry-sdk==2.31.0",
|
||||
"sentry-sdk==2.32.0",
|
||||
"service-identity==24.2.0",
|
||||
"setproctitle==1.3.6",
|
||||
"structlog==25.4.0",
|
||||
@ -67,7 +67,7 @@ dependencies = [
|
||||
"ua-parser==1.0.1",
|
||||
"unidecode==1.4.0",
|
||||
"urllib3<3",
|
||||
"uvicorn[standard]==0.34.3",
|
||||
"uvicorn[standard]==0.35.0",
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.6.0",
|
||||
"wsproto==1.2.0",
|
||||
|
18
schema.yml
18
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.6.2
|
||||
version: 2025.6.3
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -54849,6 +54849,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
PatchedRedirectStageRequest:
|
||||
type: object
|
||||
description: RedirectStage Serializer
|
||||
@ -57302,6 +57306,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- application_slug
|
||||
- auth_flow_slug
|
||||
@ -57388,6 +57396,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
@ -57512,6 +57524,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
|
@ -7,7 +7,7 @@ services:
|
||||
network_mode: host
|
||||
restart: always
|
||||
mailpit:
|
||||
image: docker.io/axllent/mailpit:v1.26.2
|
||||
image: docker.io/axllent/mailpit:v1.27.0
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
|
53
uv.lock
generated
53
uv.lock
generated
@ -165,7 +165,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authentik"
|
||||
version = "2025.6.2"
|
||||
version = "2025.6.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "argon2-cffi" },
|
||||
@ -305,7 +305,7 @@ requires-dist = [
|
||||
{ name = "jwcrypto", specifier = "==1.5.6" },
|
||||
{ name = "kubernetes", specifier = "==33.1.0" },
|
||||
{ name = "ldap3", specifier = "==2.9.1" },
|
||||
{ name = "lxml", specifier = "==5.4.0" },
|
||||
{ name = "lxml", specifier = "==6.0.0" },
|
||||
{ name = "msgraph-sdk", specifier = "==1.35.0" },
|
||||
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
|
||||
{ name = "packaging", specifier = "==25.0" },
|
||||
@ -319,7 +319,7 @@ requires-dist = [
|
||||
{ name = "pyyaml", specifier = "==6.0.2" },
|
||||
{ name = "requests-oauthlib", specifier = "==2.0.0" },
|
||||
{ name = "scim2-filter-parser", specifier = "==0.7.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.31.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.32.0" },
|
||||
{ name = "service-identity", specifier = "==24.2.0" },
|
||||
{ name = "setproctitle", specifier = "==1.3.6" },
|
||||
{ name = "structlog", specifier = "==25.4.0" },
|
||||
@ -329,7 +329,7 @@ requires-dist = [
|
||||
{ name = "ua-parser", specifier = "==1.0.1" },
|
||||
{ name = "unidecode", specifier = "==1.4.0" },
|
||||
{ name = "urllib3", specifier = "<3" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.3" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.35.0" },
|
||||
{ name = "watchdog", specifier = "==6.0.0" },
|
||||
{ name = "webauthn", specifier = "==2.6.0" },
|
||||
{ name = "wsproto", specifier = "==1.2.0" },
|
||||
@ -1824,27 +1824,22 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.4.0"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2961,15 +2956,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.31.0"
|
||||
version = "2.32.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/45/c7ef7e12d8434fda8b61cdab432d8af64fb832480c93cdaf4bdcab7f5597/sentry_sdk-2.31.0.tar.gz", hash = "sha256:fed6d847f15105849cdf5dfdc64dcec356f936d41abb8c9d66adae45e60959ec", size = 334167, upload-time = "2025-06-24T16:36:26.066Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/59/eb90c45cb836cf8bec973bba10230ddad1c55e2b2e9ffa9d7d7368948358/sentry_sdk-2.32.0.tar.gz", hash = "sha256:9016c75d9316b0f6921ac14c8cd4fb938f26002430ac5be9945ab280f78bec6b", size = 334932, upload-time = "2025-06-27T08:10:02.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a2/9b6d8cc59f03251c583b3fec9d2f075dc09c0f6e030e0e0a3b223c6e64b2/sentry_sdk-2.31.0-py2.py3-none-any.whl", hash = "sha256:e953f5ab083e6599bab255b75d6829b33b3ddf9931a27ca00b4ab0081287e84f", size = 355638, upload-time = "2025-06-24T16:36:24.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a1/fc4856bd02d2097324fb7ce05b3021fb850f864b83ca765f6e37e92ff8ca/sentry_sdk-2.32.0-py2.py3-none-any.whl", hash = "sha256:6cf51521b099562d7ce3606da928c473643abe99b00ce4cb5626ea735f4ec345", size = 356122, upload-time = "2025-06-27T08:10:01.424Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3322,15 +3317,15 @@ socks = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.3"
|
||||
version = "0.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
@ -4,6 +4,7 @@
|
||||
* @import { InlineConfig, Plugin } from "vite";
|
||||
*/
|
||||
import postcssLit from "rollup-plugin-postcss-lit";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
|
||||
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
|
||||
@ -60,7 +61,7 @@ const config = {
|
||||
*/
|
||||
const overrides = {
|
||||
define: createBundleDefinitions(),
|
||||
plugins: [inlineCSSPlugin, postcssLit()],
|
||||
plugins: [inlineCSSPlugin, postcssLit(), tsconfigPaths()],
|
||||
};
|
||||
|
||||
return mergeConfig(config, overrides);
|
||||
|
@ -4,7 +4,6 @@
|
||||
* @import { ThemeVarsPartial } from "storybook/internal/theming";
|
||||
*/
|
||||
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
|
||||
|
||||
import { addons } from "@storybook/manager-api";
|
||||
import { create } from "@storybook/theming/create";
|
||||
|
||||
|
@ -10,11 +10,10 @@
|
||||
* PluginBuild
|
||||
* } from "esbuild"
|
||||
*/
|
||||
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
||||
|
||||
/**
|
||||
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData Data passed to `onload`.
|
||||
*
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
// @ts-check
|
||||
|
46
web/package-lock.json
generated
46
web/package-lock.json
generated
@ -128,6 +128,7 @@
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.35.0",
|
||||
"vite-plugin-lit-css": "^2.0.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"wireit": "^0.14.12"
|
||||
},
|
||||
"engines": {
|
||||
@ -16313,6 +16314,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globrex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
||||
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -26423,6 +26430,26 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
|
||||
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w=="
|
||||
},
|
||||
"node_modules/tsconfck": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.3.tgz",
|
||||
"integrity": "sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsconfck": "bin/tsconfck.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
@ -27745,6 +27772,25 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-tsconfig-paths": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz",
|
||||
"integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"globrex": "^0.1.2",
|
||||
"tsconfck": "^3.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
|
@ -148,7 +148,7 @@
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.0.5",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
"@goauthentik/prettier-config": "^3.0.0",
|
||||
"@goauthentik/prettier-config": "^1.0.5",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@hcaptcha/types": "^1.0.4",
|
||||
"@lit/localize-tools": "^0.8.0",
|
||||
@ -199,6 +199,7 @@
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.35.0",
|
||||
"vite-plugin-lit-css": "^2.0.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"wireit": "^0.14.12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
@ -7,4 +7,6 @@
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
||||
export default {};
|
||||
|
@ -3,12 +3,9 @@
|
||||
*
|
||||
* @runtime node
|
||||
*/
|
||||
|
||||
import { MonoRepoRoot } from "#paths/node";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
import { MonoRepoRoot } from "#paths/node";
|
||||
|
||||
// ts-import-sorter: disable
|
||||
import PackageJSON from "../../../../package.json" with { type: "json" };
|
||||
|
||||
/**
|
||||
|
@ -1,11 +1,10 @@
|
||||
/**
|
||||
* @file Rollup configuration for the SFE package.
|
||||
*/
|
||||
import { resolve as resolvePath } from "node:path";
|
||||
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import swc from "@rollup/plugin-swc";
|
||||
import { resolve as resolvePath } from "node:path";
|
||||
import copy from "rollup-plugin-copy";
|
||||
|
||||
export async function createConfig() {
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { fromByteArray } from "base64-js";
|
||||
import "formdata-polyfill";
|
||||
import $ from "jquery";
|
||||
import "weakmap-polyfill";
|
||||
|
||||
import {
|
||||
@ -14,9 +16,6 @@ import {
|
||||
type RedirectChallenge,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { fromByteArray } from "base64-js";
|
||||
import $ from "jquery";
|
||||
|
||||
interface GlobalAuthentik {
|
||||
brand: {
|
||||
branding_logo: string;
|
||||
|
@ -3,12 +3,10 @@
|
||||
*
|
||||
* @runtime node
|
||||
*/
|
||||
|
||||
import { DistDirectoryName } from "#paths";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { DistDirectoryName } from "#paths";
|
||||
|
||||
const relativeDirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
//#region Base paths
|
||||
|
@ -9,15 +9,14 @@
|
||||
* long spew of "this string is not translated" and replacing it with a
|
||||
* summary of how many strings are missing with respect to the source locale.
|
||||
*
|
||||
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config.js"
|
||||
* @import { Stats } from "node:fs";
|
||||
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config"
|
||||
* @import { Stats } from "fs";
|
||||
*/
|
||||
import { PackageRoot } from "#paths/node";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { PackageRoot } from "#paths/node";
|
||||
|
||||
/**
|
||||
* @type {ConfigFile}
|
||||
*/
|
||||
|
@ -1,25 +1,21 @@
|
||||
/// <reference types="../types/esbuild.js" />
|
||||
/**
|
||||
* @file ESBuild script for building the authentik web UI.
|
||||
*
|
||||
* @import { BuildOptions } from "esbuild";
|
||||
*/
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { mdxPlugin } from "#bundler/mdx-plugin/node";
|
||||
import { createBundleDefinitions } from "#bundler/utils/node";
|
||||
import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node";
|
||||
|
||||
import { NodeEnvironment } from "@goauthentik/core/environment/node";
|
||||
import { MonoRepoRoot, resolvePackage } from "@goauthentik/core/paths/node";
|
||||
import { readBuildIdentifier } from "@goauthentik/core/version/node";
|
||||
|
||||
import { deepmerge } from "deepmerge-ts";
|
||||
import esbuild from "esbuild";
|
||||
import { copy } from "esbuild-plugin-copy";
|
||||
import copy from "esbuild-plugin-copy";
|
||||
import { polyfillNode } from "esbuild-plugin-polyfill-node";
|
||||
|
||||
/// <reference types="../types/esbuild.js" />
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
const logPrefix = "[Build]";
|
||||
|
||||
|
@ -6,12 +6,9 @@
|
||||
* @import { ProgramMessage } from "@lit/localize-tools/src/messages.js"
|
||||
* @import { Locale } from "@lit/localize-tools/src/types/locale.js"
|
||||
*/
|
||||
|
||||
import { PackageRoot } from "#paths/node";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { PackageRoot } from "#paths/node";
|
||||
|
||||
import pseudolocale from "pseudolocale";
|
||||
|
||||
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
|
||||
|
@ -1,21 +1,19 @@
|
||||
import "#elements/EmptyState";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
|
||||
import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html, TemplateResult } from "lit";
|
||||
import { TemplateResult, css, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css";
|
||||
|
||||
import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-about-modal")
|
||||
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
|
||||
static get styles() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user