Compare commits

...

62 Commits

Author SHA1 Message Date
06848be14b fix nak in peap
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:04 +02:00
4bae3bbe60 most of gtc
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:04 +02:00
e33f839d7f add basic testing readme
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:04 +02:00
f5eb827d14 start reworking response modification
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:03 +02:00
9045f5ba73 add tests for peap-extensions
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:03 +02:00
7b97e92094 hmm
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:03 +02:00
3027cdcc4b mschapv2 working
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:03 +02:00
67f627a925 mostly working
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:03 +02:00
f1101e0c01 mostly parsing eavp extensions
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:03 +02:00
fb01a117ad encode extension AVPs
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:03 +02:00
fad18db70b more mschap v2, start peap extension type 33
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:02 +02:00
e0c837257c fix decode not called in inner protocol
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:02 +02:00
2a567ccc85 peap: fix encode
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:02 +02:00
e36373ceab cleanup parsing
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:02 +02:00
d8a625be03 fix a bunch of stuff ig
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:02 +02:00
4d944f7444 eap/tls: trunc data to size we read
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:02 +02:00
c49274042b slightly better decoding
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:01 +02:00
10fc15ffe0 more debug tools
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:01 +02:00
7c996d9d9d start handling inner
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:01 +02:00
5d25f68b71 start inner STM
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:01 +02:00
8da54d5811 more refactor
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:00 +02:00
4571f5e644 working PEAP decode
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:00 +02:00
ee234ea3aa simplify
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:00 +02:00
82c177b7eb try to make this work
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:00 +02:00
1155ccb3e8 support SSLKEYLOGFILE
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:43:00 +02:00
1575b96262 separate eap logic into protocol
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:59 +02:00
19bb77638a folder structure to prepare eap in eap
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:59 +02:00
d6cf129eaa attempt peap
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:59 +02:00
b6686cff14 refactor v1, start support for more protocols and implement nak
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:59 +02:00
8cf8f1e199 keep eap state when refreshing
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:59 +02:00
50c50c4109 remove panic
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:59 +02:00
51f4a8d83d fix outpost not having perms for cert
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:58 +02:00
3ada3a7e0e make certificate configurable
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:58 +02:00
fa06c9fe4e start tying it into the flow
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:58 +02:00
2a024238fe slightly better logging
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:58 +02:00
91c87b7c3c ok this works kinda
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:58 +02:00
318443f270 hmmm idk
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:57 +02:00
ac88784089 maybe?
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:57 +02:00
855afa7b9f slight read refactor (seems to fix flaky issues?)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:57 +02:00
240abfef41 use tighter retry that cancels and backs off
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:57 +02:00
03075f1890 slight refactor
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:57 +02:00
5bc0ed6e11 apparently it works now
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:57 +02:00
8f4cfc28c7 fix outgoing buffer not cleared when sending unchunked
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:57 +02:00
6d77eaaab7 deduplicate
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:56 +02:00
9cee59537c prep ctx
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:56 +02:00
fc5c0e2789 generate MPPE key
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:56 +02:00
573446689f fix remaning tls data not sent
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:56 +02:00
fd4bfe604d more fixup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:56 +02:00
06e76a5b37 it's almost working
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:56 +02:00
3c228bf5c3 try to make the finish work
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:55 +02:00
8a80f07db2 this might actually be cooking
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:55 +02:00
ae59a3e576 we're getting somewhere
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:55 +02:00
df21e678d6 fix a bunch more
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:55 +02:00
a71532b3e3 refactor more
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:55 +02:00
d7cb0b3ea1 fixup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:54 +02:00
ba8f137885 keep track of total payload size
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:54 +02:00
958ff66070 fix parsing when lengincluded is not set
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:54 +02:00
ad57c66a32 better log
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:54 +02:00
2bba0ddd74 might actually happen?
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 22:42:54 +02:00
767c0a8e45 website/docs: enhance customization docs (#15081)
* drafty

* rearrange everything

* fix links

* fixed link

* more links to fix

* tweaks

* not sure

* Optimised images with calibre/image-actions

* Update website/docs/add-secure-apps/providers/rac/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/_global/global.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/sys-mgmt/brands.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/_global/global.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/add-secure-apps/providers/rac/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/_global/global.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/user-admin/customization.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/user-admin/customization.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/user-admin/customization.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/user-admin/customization.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/user-admin/customization.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/netlify.toml

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/flow/customization.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* tweak

* separated admin and user again

* so many busted things, now unbusted

* redirect yesterday's redirects

* dewi edits

* tweak to bump build

* revert package.json change

* links, tweaks

* Optimised images with calibre/image-actions

* Update website/docs/customize/interfaces/admin/customization_admin_ui.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/admin/customization_admin_ui.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/flow/customization_flow.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/flow/customization_flow.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/user/customization_user_ui.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/customize/interfaces/admin/customization_admin_ui.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* simplify sidebar

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix redirect indent

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rename files

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* flows -> flow interface

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 15:24:56 -05:00
b10c795a26 website: bump the build group across 1 directory with 9 updates (#15332)
Bumps the build group with 9 updates in the /website directory:

| Package | From | To |
| --- | --- | --- |
| [@rspack/binding-darwin-arm64](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.4.1` | `1.4.2` |
| [@rspack/binding-linux-arm64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.4.1` | `1.4.2` |
| [@rspack/binding-linux-x64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.4.1` | `1.4.2` |
| [@swc/core-darwin-arm64](https://github.com/swc-project/swc) | `1.12.7` | `1.12.9` |
| [@swc/core-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.12.7` | `1.12.9` |
| [@swc/core-linux-x64-gnu](https://github.com/swc-project/swc) | `1.12.7` | `1.12.9` |
| [@swc/html-darwin-arm64](https://github.com/swc-project/swc) | `1.12.7` | `1.12.9` |
| [@swc/html-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.12.7` | `1.12.9` |
| [@swc/html-linux-x64-gnu](https://github.com/swc-project/swc) | `1.12.7` | `1.12.9` |



Updates `@rspack/binding-darwin-arm64` from 1.4.1 to 1.4.2
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.4.2/packages/rspack)

Updates `@rspack/binding-linux-arm64-gnu` from 1.4.1 to 1.4.2
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.4.2/packages/rspack)

Updates `@rspack/binding-linux-x64-gnu` from 1.4.1 to 1.4.2
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.4.2/packages/rspack)

Updates `@swc/core-darwin-arm64` from 1.12.7 to 1.12.9
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.7...v1.12.9)

Updates `@swc/core-linux-arm64-gnu` from 1.12.7 to 1.12.9
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.7...v1.12.9)

Updates `@swc/core-linux-x64-gnu` from 1.12.7 to 1.12.9
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.7...v1.12.9)

Updates `@swc/html-darwin-arm64` from 1.12.7 to 1.12.9
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.7...v1.12.9)

Updates `@swc/html-linux-arm64-gnu` from 1.12.7 to 1.12.9
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.7...v1.12.9)

Updates `@swc/html-linux-x64-gnu` from 1.12.7 to 1.12.9
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.7...v1.12.9)

---
updated-dependencies:
- dependency-name: "@rspack/binding-darwin-arm64"
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-arm64-gnu"
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-x64-gnu"
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.12.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.12.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.12.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-version: 1.12.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-version: 1.12.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-version: 1.12.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 20:16:11 +02:00
8088e08fd9 website/docs: re-add gtag (#15334)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-07-01 19:32:39 +02:00
67 changed files with 2460 additions and 203 deletions

View File

@ -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",
]

View File

@ -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",
),
),
]

View File

@ -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}"

View File

@ -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": []

View File

@ -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()

View File

@ -8,6 +8,6 @@ import (
)
func TestConvert(t *testing.T) {
var a challengeCommon = api.NewIdentificationChallengeWithDefaults()
var a ChallengeCommon = api.NewIdentificationChallengeWithDefaults()
assert.NotNil(t, a)
}

View File

@ -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()
}

View 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
}
```

View 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
}

View 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)
}

View 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
}

View 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()
}

View 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
}

View 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)
}

View 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,
)
}

View File

@ -0,0 +1,5 @@
package eap
type State struct {
PacketID uint8
}

View 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>"
}

View 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)
}

View File

@ -0,0 +1,6 @@
package gtc
type State struct {
getChallenge GetChallenge
validateResponse ValidateResponse
}

View 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,
)
}

View 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,
)
}

View 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
}

View 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
}

View 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,
)
}

View 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
}
}

View File

@ -0,0 +1,8 @@
package mschapv2
type State struct {
Challenge []byte
PeerChallenge []byte
IsProtocolEnded bool
AuthResponse *AuthResponse
}

View 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
}

View 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
}

View 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...)
}

View File

@ -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)
}

View 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(),
)
}

View 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
}

View File

@ -0,0 +1,7 @@
package peap
import "goauthentik.io/internal/outpost/radius/eap/protocol"
type State struct {
SubState map[string]*protocol.State
}

View 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{},
}
}

View 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 }

View 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
)

View 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
}
}

View 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,
)
}

View 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
}

View 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
}

View File

@ -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
}
},
},
},
},
},
},
}
}

View File

@ -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)
}
}

View File

@ -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)
}

4
notes.md Normal file
View 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

View File

@ -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

View File

@ -1,3 +1,4 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
@ -93,6 +94,14 @@ export function renderForm(
help=${clientNetworksHelp}
input-hint="code"
></ak-text-input>
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.certificate ?? undefined)}
singleton
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Certificate used for EAP-TLS.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"

View File

@ -45,11 +45,11 @@ Configuration details such as credentials can be specified through _settings_, w
### Connection settings
Each connection is authorized through authentik Policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
Each connection is authorized through authentik policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user/customization.mdx). Once the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually.
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
Additionally it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.

View File

@ -6,8 +6,8 @@ You can customize the behaviour, look, and available resources for your authenti
- [Policies](./policies/working_with_policies.md)
- Interfaces:
- [Flows](./interfaces/flow/customization.mdx)
- [User interface](./interfaces/user/customization.mdx)
- [Admin interface](./interfaces/admin/customization.mdx)
- [Flow interface](./interfaces/flow)
- [User interface](./interfaces/user)
- [Admin interface](./interfaces/admin)
- [Blueprints](./blueprints/index.mdx)
- [Branding](./branding.md)

View File

@ -0,0 +1,19 @@
### Enabling/disabling features
The features listed below can be enabled or disabled through attributes set on the Brand. By default, all of the listed features are enabled. To disable a specific feature, set its value to `false`.
#### `settings.enabledFeatures.apiDrawer`
Display the API Request drawer in the upper tool bar.
#### `settings.enabledFeatures.notificationDrawer`
Display the Notification drawer in the upper tool bar.
#### `settings.enabledFeatures.settings`
Display the Settings link in the upper tool bar.
#### `settings.enabledFeatures.search`
Display the Search bar in the upper tool bar.

View File

@ -0,0 +1,36 @@
### General settings (both Admin and User interfaces)
#### `settings.navbar.userDisplay`
Configure what is shown in the top right corner. Defaults to `username`. Available options: `username`, `name`, `email`
#### `settings.theme.base`
Configure the base color scheme or toggle between dark and light modes. The default setting is `automatic`, which adapts based on the users browser preference. Available options: `automatic`, `dark`, `light`.
**Example**:
```
settings:
theme:
base: dark
```
#### `settings.theme.background`
Optional CSS that is applied to the background of the User interface, for example to set a custom background color, gradient, or image.
```yaml
settings:
theme:
background: >
background: url('https://picsum.photos/1920/1080');
filter: blur(8px);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
```
#### `settings.locale`
The locale which can be configured in the user settings by default. This can be used to preset locales for groups of users, but still let them choose their own preferred locale.

View File

@ -1,3 +1,11 @@
### Global customization
## Global customization
See [Brand Settings](../../../sys-mgmt/brands.md#branding-settings)
To customize the following brand settings, log in to the Admin interface and navigate to **System > Brands > Brand settings**.
- Title
- Logo
- Favicon
- Default flow background image
- Custom CSS
For more details, see the [Brand settings](../../../sys-mgmt/brands.md#branding-settings) documentation.

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -1,17 +0,0 @@
# Customization
### `settings.pagination.perPage`
How many items should be retrieved per page. Defaults to 20.
### `settings.defaults.userPath`
Default user path which is opened when opening the user list. Defaults to `users`.
### `settings.theme.base`
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
import Global from "../_global/global.mdx";
<Global />

View File

@ -0,0 +1,46 @@
---
title: Customize the Admin interface
sidebar_label: Admin interface
---
The Admin interface can be customized using attributes configured in [Brands](../../../sys-mgmt/brands.md)
To add, remove, or modify attributes for a brand, log in to the Admin interface and navigate to **System > Brands > Other global settings > Attributes**.
Most attributes defined in a brand apply to _both_ the User and Admin interfaces. However, any settings that are specific to only the Admin interface are explicitly noted as such below.
The following screenshot shows the syntax for setting several attributes for a brand: dark mode, a 3-column display of applications on **My applications** page of the User interface, and hiding the API and Notifications drawers from the Admin interface tool bar.
![](./admin-interface-attributes.png)
## Custom settings
The following settings for attributes are grouped by:
- `enabledFeatures` settings
- General settings (used on both the Admin interface and the User interface)
- Admin interface only
import Enabledfeatureslist from "../\_enabledfeatureslist.mdx";
<Enabledfeatureslist />
import Generalattributes from "../\_generalattributes.mdx";
<Generalattributes />
### Settings for the Admin interface only
The following settings can only be used to customize the Admin interface, not the User interface.
#### `settings.pagination.perPage`
How many items should be retrieved per page. Defaults to 20.
#### `settings.defaults.userPath`
Default user path which is used when opening the user list. Defaults to `users`.
import Global from "../_global/global.mdx";
<Global />

View File

@ -1,11 +0,0 @@
# Customization
Since flows can be executed authenticated or unauthenticated, the default settings can be set via brands _attributes_.
### `settings.theme.base`
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
import Global from "../_global/global.mdx";
<Global />

View File

@ -0,0 +1,19 @@
---
title: Customize a flow
sidebar_label: Flow interface
---
Typically, settings for flows are defined as defaults in the [Brand settings](../../../sys-mgmt/brands.md). However, its important to note that some flows are executed before the specific user is authenticated and thus before authentik can determine which user is viewing the flow (for example, the `default-authentication-flow`!). Consequently, using default settings for all flows ensures a more consistent user experience.
Two settings that you can configure per flow are the _background image_ for the flow, and the _layout_.
## Customize a flow's background image
You can define a:
- Default background image for all flows, set in the instance's [brand](../../../sys-mgmt/brands.md)
- A background image for [one or more specific flows](../../../add-secure-apps/flows-stages/flow/index.md#flow-configuration-options) (overrides the default)
## Set the layout for a flow
To define the layout for a flow, edit the flow and under **Appearance settings > Layout** select how the UI displays the flow when it is executed; with stacked elements, content left or right, and sidebar left or right.

View File

@ -1,64 +0,0 @@
# Customization
The user interface can be customized through attributes, and will be inherited from a users' groups.
## Enabling/disabling features
The following features can be enabled/disabled. By default, all of them are enabled:
- `settings.enabledFeatures.apiDrawer`
API Request drawer in navbar
- `settings.enabledFeatures.notificationDrawer`
Notification drawer in navbar
- `settings.enabledFeatures.settings`
Settings link in navbar
- `settings.enabledFeatures.applicationEdit`
Application edit in library (only shown when user is superuser)
- `settings.enabledFeatures.search`
Search bar
## Other configuration
### `settings.navbar.userDisplay`
Configure what is shown in the top right corner. Defaults to `username`. Choices: `username`, `name`, `email`
### `settings.theme.base`
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
### `settings.theme.background`
Optional CSS which is applied in the background of the background of the user interface; for example
```yaml
settings:
theme:
background: >
background: url('https://picsum.photos/1920/1080');
filter: blur(8px);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
```
### `settings.layout.type`
Which layout to use for the _My applications_ view. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
### `settings.locale`
The locale which can be configured in the user settings by default. This can be used to preset locales for groups of users, but still let them choose their own preferred locale
import Global from "../_global/global.mdx";
<Global />

View File

@ -0,0 +1,44 @@
---
title: Customize the User interface
sidebar_label: User interface
---
The User interface can be customized using attributes configured in [Brands](../../../sys-mgmt/brands.md).
To add, remove, or modify attributes for a brand, log in as an administrator and navigate to **System > Brands > Other global settings > Attributes**.
Most attributes defined in a brand apply to _both_ the User and Admin interfaces. However, any settings that are specific to only one interface are explicitly noted as such below.
The following screenshot shows the syntax for setting several attributes for a brand: light mode, a 3-column display of applications on **My applications** page, hiding the API drawer and the Notification drawer from the tool bar, and disallowing users to edit the applications on **My applications** page.
![](./user-interface-attributes.png)
## Custom settings
The following settings for attributes are grouped by:
- `enabledFeatures` settings
- General attributes (used on both the Admin interface and the User interface)
- User interface only
import Enabledfeatureslist from "../\_enabledfeatureslist.mdx";
<Enabledfeatureslist />
#### `settings.enabledFeatures.applicationEdit` (User interface only)
Display the Edit option for each application on the **My applications** page (only shown when user is superuser).
import Generalattributes from "../\_generalattributes.mdx";
<Generalattributes />
### Settings for the User interface only
#### `settings.layout.type`
Which layout to use for the **My applications** page. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
import Global from "../_global/global.mdx";
<Global />

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -16,8 +16,8 @@ slug: "/releases/2023.3"
Documentation: [SCIM Provider](../../add-secure-apps/providers/scim/index.md)
- Theming improvements
- The custom.css file is now loaded in ShadowDOMs, allowing for much greater customization, as previously it was only possible to style elements outside of the ShadowDOM. See docs for [Flow](../../customize/interfaces/flow/customization.mdx), [User](../../customize/interfaces/user/customization.mdx) and [Admin](../../customize/interfaces/admin/customization.mdx) interfaces.
- Previously, authentik would automatically switch between dark and light theme based on the users' browsers' settings. This can now be overridden to either force the light or dark theme, per user/group/tenant. See docs for [Flow](../../customize/interfaces/flow/customization.mdx), [User](../../customize/interfaces/user/customization.mdx) and [Admin](../../customize/interfaces/admin/customization.mdx) interfaces.
- The custom.css file is now loaded in ShadowDOMs, allowing for much greater customization, as previously it was only possible to style elements outside of the ShadowDOM. See docs for the [User interface](../../customize/interfaces/user/index.mdx) and [Admin interface](../../customize/interfaces/admin/index.mdx).
- Previously, authentik would automatically switch between dark and light theme based on the users' browsers' settings. This can now be overridden to either force the light or dark theme, per user/group/brand. See docs for the [User interface](../../customize/interfaces/user/index.mdx) and [Admin interface](../../customize/interfaces/admin/index.mdx).
## Upgrading

View File

@ -3,7 +3,7 @@ title: Brands
slug: /brands
---
As an authentik admin, you can customize your instance's appearance and behavior using brands. Brands apply to a single domain, a domain wildcard or can be set as default, in which case the brand will be used when no other brand matches the domain.
As an authentik administrator, you can customize your instance's appearance and behavior using brands. Brands apply to a single domain, a domain wildcard, or can be set as default, in which case the brand will be applied when no other brand matches the domain.
For an overview of branding and other customization options in authentik refer to [Customize your instance](../customize/index.md).
@ -71,4 +71,4 @@ When using the [Mutual TLS Stage](../add-secure-apps/flows-stages/stages/mtls/in
#### Attributes
Attributes such as locale, theme settings and custom attributes can be set to a per-brand default value here. Any custom attributes can be retrieved via [`group_attributes()`](../users-sources/user/user_ref.mdx#object-properties).
Attributes such as locale, theme settings (light/dark mode), and custom attributes can be set to a per-brand default value here. Any custom attributes can be retrieved via [`group_attributes()`](../users-sources/user/user_ref.mdx#object-properties).

View File

@ -131,6 +131,10 @@ const config = createDocusaurusConfig({
],
],
},
gtag: {
trackingID: ["G-9MVR9WZFZH"],
anonymizeIP: true,
},
theme: {
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
},

View File

@ -128,6 +128,10 @@ const config = createDocusaurusConfig({
],
],
},
gtag: {
trackingID: ["G-9MVR9WZFZH"],
anonymizeIP: true,
},
theme: {
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
},

View File

@ -86,6 +86,20 @@ package = "netlify-plugin-debug-cache"
to = "/docs/customize/branding"
status = 302
[[redirects]]
from = "/docs/customize/interfaces/admin/customization"
to = "/docs/customize/interfaces/admin"
status = 302
[[redirects]]
from = "/docs/customize/interfaces/user/customization"
to = "/docs/customize/interfaces/user"
status = 302
[[redirects]]
from = "/docs/customize/interfaces/flow/customization"
to = "/docs/customize/interfaces/flow"
status = 302
# Migration to new structure with script Sept 2025
[[redirects]]

View File

@ -19,7 +19,7 @@
"@goauthentik/docusaurus-config": "^1.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@mdx-js/react": "^3.1.0",
"@rspack/binding-linux-x64-gnu": "1.4.1",
"@swc/html-linux-x64-gnu": "1.12.9",
"clsx": "^2.1.1",
"docusaurus-plugin-openapi-docs": "^4.4.0",
"docusaurus-theme-openapi-docs": "^4.4.0",
@ -62,15 +62,15 @@
"node": ">=22.14.0"
},
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.4.1",
"@rspack/binding-linux-arm64-gnu": "1.4.1",
"@rspack/binding-linux-x64-gnu": "1.4.1",
"@swc/core-darwin-arm64": "1.12.7",
"@swc/core-linux-arm64-gnu": "1.12.7",
"@swc/core-linux-x64-gnu": "1.12.7",
"@swc/html-darwin-arm64": "1.12.7",
"@swc/html-linux-arm64-gnu": "1.12.7",
"@swc/html-linux-x64-gnu": "1.12.7",
"@rspack/binding-darwin-arm64": "1.4.2",
"@rspack/binding-linux-arm64-gnu": "1.4.2",
"@rspack/binding-linux-x64-gnu": "1.4.2",
"@swc/core-darwin-arm64": "1.12.9",
"@swc/core-linux-arm64-gnu": "1.12.9",
"@swc/core-linux-x64-gnu": "1.12.9",
"@swc/html-darwin-arm64": "1.12.9",
"@swc/html-linux-arm64-gnu": "1.12.9",
"@swc/html-linux-x64-gnu": "1.12.9",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"
@ -5034,9 +5034,9 @@
}
},
"node_modules/@rspack/binding-darwin-arm64": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.1.tgz",
"integrity": "sha512-enh5DYbpaexdEmjbcxj3BJDauP3w+20jFKWvKROtAQV350PUw0bf2b4WOgngIH9hBzlfjpXNYAk6T5AhVAlY3Q==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.2.tgz",
"integrity": "sha512-0fPOew7D0l/x6qFZYdyUqutbw15K98VLvES2/7x2LPssTgypE4rVmnQSmVBnge3Nr8Qs/9qASPRpMWXBaqMfOA==",
"cpu": [
"arm64"
],
@ -5060,9 +5060,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.1.tgz",
"integrity": "sha512-PJ5cHqvrj1bK7jH5DVrdKoR8Fy+p6l9baxXajq/6xWTxP+4YTdEtLsRZnpLMS1Ho2RRpkxDWJn+gdlKuleNioQ==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.2.tgz",
"integrity": "sha512-UHAzggS8Mc7b3Xguhj82HwujLqBZquCeo8qJj5XreNaMKGb6YRw/91dJOVmkNiLCB0bj71CRE1Cocd+Peq3N9A==",
"cpu": [
"arm64"
],
@ -5086,9 +5086,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.1.tgz",
"integrity": "sha512-jjTx53CpiYWK7fAv5qS8xHEytFK6gLfZRk+0kt2YII6uqez/xQ3SRcboreH8XbJcBoxINBzMNMf5/SeMBZ939A==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.2.tgz",
"integrity": "sha512-ucCCWdtH1tekZadrsYj6GNJ8EP21BM2uSE7MootbwLw8aBtgVTKUuRDQEps1h/rtrdthzd9XBX6Lc2N926gM+g==",
"cpu": [
"x64"
],
@ -5586,13 +5586,12 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.7.tgz",
"integrity": "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.9.tgz",
"integrity": "sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
@ -5634,13 +5633,12 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.7.tgz",
"integrity": "sha512-N15hKizSSh+hkZ2x3TDVrxq0TDcbvDbkQJi2ZrLb9fK+NdFUV/x+XF16ZDPlbxtrGXl1CT7VD439SNaMN9F7qw==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.9.tgz",
"integrity": "sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
@ -5666,13 +5664,12 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.7.tgz",
"integrity": "sha512-PR4tPVwU1BQBfFDk2XfzXxsEIjF3x/bOV1BzZpYvrlkU0TKUDbR4t2wzvsYwD/coW7/yoQmlL70/qnuPtTp1Zw==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz",
"integrity": "sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
@ -5824,13 +5821,12 @@
}
},
"node_modules/@swc/html-darwin-arm64": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.7.tgz",
"integrity": "sha512-4rHV4lW8PXSc7YfJ/c9Cj0xZWSJArkD/Yuax4plH6f4VtEcEAluZI3ryBG3Vh4VawQ1RMkytPQ2S65BbCyDIXg==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.9.tgz",
"integrity": "sha512-uQl0y9uOgqnYR6t+TgcwFeGv1TC48xHGBqw3MrOIQLc+tqavqhQsLkVEEz1yd1J0WW3cVAsNSQlbERiwQcXQXA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
@ -5872,13 +5868,12 @@
}
},
"node_modules/@swc/html-linux-arm64-gnu": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.7.tgz",
"integrity": "sha512-z66ejXsSwI0mKyDhLimG74+xZyvSQCrceSZv9jLHa23sn/di+07M9njZrj3SQKGfHoJqXsN1iPqDpvkVajNb9Q==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.9.tgz",
"integrity": "sha512-xX/S0galaqXMNc1olt1UOMcHXybDYGogGP90WheI6XD5zKVmbHdz9yU/nVeddZNUf5gZ011NCc5QSMB+2fh8EA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
@ -5904,13 +5899,12 @@
}
},
"node_modules/@swc/html-linux-x64-gnu": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.7.tgz",
"integrity": "sha512-5KFLil4ELKzCLjjvKpt+SMEU6uBDR/EL4e7eleybtYi1cU8Jzv0xnTvabsVDfpT8fsvJF3Mvach4F/ggH5+CDQ==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.9.tgz",
"integrity": "sha512-9tRAsVsjjyEUFMH5uNrcLxb+5q0l2PCgTH7pe48hjcshKFoZamp1aiwvNnJMMBan3Ny9vFG5jKMJKG3ZkYPYxg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"

View File

@ -75,15 +75,15 @@
"typescript-eslint": "^8.35.1"
},
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.4.1",
"@rspack/binding-linux-arm64-gnu": "1.4.1",
"@rspack/binding-linux-x64-gnu": "1.4.1",
"@swc/core-darwin-arm64": "1.12.7",
"@swc/core-linux-arm64-gnu": "1.12.7",
"@swc/core-linux-x64-gnu": "1.12.7",
"@swc/html-darwin-arm64": "1.12.7",
"@swc/html-linux-arm64-gnu": "1.12.7",
"@swc/html-linux-x64-gnu": "1.12.7",
"@rspack/binding-darwin-arm64": "1.4.2",
"@rspack/binding-linux-arm64-gnu": "1.4.2",
"@rspack/binding-linux-x64-gnu": "1.4.2",
"@swc/core-darwin-arm64": "1.12.9",
"@swc/core-linux-arm64-gnu": "1.12.9",
"@swc/core-linux-x64-gnu": "1.12.9",
"@swc/html-darwin-arm64": "1.12.9",
"@swc/html-linux-arm64-gnu": "1.12.9",
"@swc/html-linux-x64-gnu": "1.12.9",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"

View File

@ -408,21 +408,9 @@ const items = [
type: "category",
label: "Interfaces",
items: [
{
type: "category",
label: "Flow",
items: ["customize/interfaces/flow/customization"],
},
{
type: "category",
label: "User",
items: ["customize/interfaces/user/customization"],
},
{
type: "category",
label: "Admin",
items: ["customize/interfaces/admin/customization"],
},
"customize/interfaces/flow/index",
"customize/interfaces/user/index",
"customize/interfaces/admin/index",
],
},
{