Compare commits

..

60 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
766 changed files with 8221 additions and 19091 deletions

View File

@ -78,13 +78,13 @@ updates:
patterns:
- "@goauthentik/*"
- package-ecosystem: npm
directory: "/docs"
directory: "/website"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "docs:"
prefix: "website:"
labels:
- dependencies
groups:

View File

@ -31,4 +31,4 @@ If changes to the frontend have been made
If applicable
- [ ] The documentation has been updated
- [ ] The documentation has been formatted (`make docs`)
- [ ] The documentation has been formatted (`make website`)

View File

@ -1,83 +0,0 @@
name: authentik-ci-api-docs
on:
push:
branches:
- main
- next
- version-*
pull_request:
branches:
- main
- version-*
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- prettier-check
steps:
- uses: actions/checkout@v4
- name: Install Dependencies
working-directory: docs/
run: npm ci
- name: Lint
working-directory: docs/
run: npm run ${{ matrix.command }}
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
run: npm ci
- name: Build API Docs via Docusaurus
working-directory: docs
run: npm run build -w api
- uses: actions/upload-artifact@v4
with:
name: api-docs
path: docs/api/build
deploy:
runs-on: ubuntu-latest
needs:
- lint
- build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: api-docs
path: docs/api/build
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
run: npm ci
- name: Deploy Netlify (Production)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
NETLIFY_SITE_ID: authentik-api-docs.netlify.app
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
working-directory: docs/api
run: npx netlify deploy --no-build --prod
- name: Deploy Netlify (Preview)
if: github.event_name == 'pull_request' || github.ref != 'refs/heads/main'
env:
NETLIFY_SITE_ID: authentik-api-docs.netlify.app
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
working-directory: docs/api
run: npx netlify deploy --no-build --alias=deploy-preview-${{ github.event.number }}

View File

@ -24,8 +24,8 @@ jobs:
run: |
# Create folder structure for go embeds
mkdir -p web/dist
mkdir -p docs/help
touch web/dist/test docs/help/test
mkdir -p website/help
touch web/dist/test website/help/test
- name: Generate API
run: make gen-client-go
- name: golangci-lint

View File

@ -1,4 +1,4 @@
name: authentik-ci-docs
name: authentik-ci-website
on:
push:
@ -18,47 +18,50 @@ jobs:
fail-fast: false
matrix:
command:
- lint:lockfile
- prettier-check
steps:
- uses: actions/checkout@v4
- name: Install dependencies
working-directory: docs/
- working-directory: website/
run: npm ci
- name: Lint
working-directory: docs/
working-directory: website/
run: npm run ${{ matrix.command }}
build-topics:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: Build Documentation via Docusaurus
working-directory: docs/
run: npm run build
build-integrations:
- name: test
working-directory: website/
run: npm test
build:
runs-on: ubuntu-latest
name: ${{ matrix.job }}
strategy:
fail-fast: false
matrix:
job:
- build
- build:integrations
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: Build Integrations via Docusaurus
working-directory: docs/
run: npm run build -w integrations
- name: build
working-directory: website/
run: npm run ${{ matrix.job }}
build-container:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
@ -95,7 +98,7 @@ jobs:
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: docs/Dockerfile
file: website/Dockerfile
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
platforms: linux/amd64,linux/arm64
context: .
@ -108,12 +111,12 @@ jobs:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
ci-docs-mark:
ci-website-mark:
if: always()
needs:
- lint
- build-topics
- build-integrations
- test
- build
- build-container
runs-on: ubuntu-latest
steps:

View File

@ -52,7 +52,7 @@ jobs:
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: docs/Dockerfile
file: website/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
context: .

View File

@ -10,7 +10,7 @@ coverage
dist
out
.docusaurus
docs/api/reference
website/docs/developer-docs/api/**/*
## Environment
*.env

44
.vscode/tasks.json vendored
View File

@ -4,7 +4,12 @@
{
"label": "authentik/core: make",
"command": "uv",
"args": ["run", "make", "lint-fix", "lint"],
"args": [
"run",
"make",
"lint-fix",
"lint"
],
"presentation": {
"panel": "new"
},
@ -13,7 +18,11 @@
{
"label": "authentik/core: run",
"command": "uv",
"args": ["run", "ak", "server"],
"args": [
"run",
"ak",
"server"
],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -23,13 +32,17 @@
{
"label": "authentik/web: make",
"command": "make",
"args": ["web"],
"args": [
"web"
],
"group": "build"
},
{
"label": "authentik/web: watch",
"command": "make",
"args": ["web-watch"],
"args": [
"web-watch"
],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -39,19 +52,26 @@
{
"label": "authentik: install",
"command": "make",
"args": ["install", "-j4"],
"args": [
"install",
"-j4"
],
"group": "build"
},
{
"label": "authentik/docs: make",
"label": "authentik/website: make",
"command": "make",
"args": ["docs"],
"args": [
"website"
],
"group": "build"
},
{
"label": "authentik/docs: watch",
"label": "authentik/website: watch",
"command": "make",
"args": ["docs-watch"],
"args": [
"website-watch"
],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -61,7 +81,11 @@
{
"label": "authentik/api: generate",
"command": "uv",
"args": ["run", "make", "gen"],
"args": [
"run",
"make",
"gen"
],
"group": "build"
}
]

View File

@ -32,8 +32,8 @@ tests/wdio/ @goauthentik/frontend
locale/ @goauthentik/backend @goauthentik/frontend
web/xliff/ @goauthentik/backend @goauthentik/frontend
# Docs & Website
docs/ @goauthentik/docs
website/ @goauthentik/docs
CODE_OF_CONDUCT.md @goauthentik/docs
# Security
SECURITY.md @goauthentik/security @goauthentik/docs
docs/security/ @goauthentik/security @goauthentik/docs
website/docs/security/ @goauthentik/security @goauthentik/docs

View File

@ -18,7 +18,7 @@ RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
COPY ./package.json /work
COPY ./web /work/web/
COPY ./docs /work/docs/
COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \

View File

@ -1,7 +1,7 @@
Copyright (c) 2023 Jens Langhammer
Portions of this software are licensed as follows:
* All content residing under the "docs/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All content residing under the "website/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All content that resides under the "authentik/enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "authentik/enterprise/LICENSE".
* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license.
* All third party components incorporated into the authentik are licensed under the original license provided by the owner of the applicable component.

View File

@ -1,4 +1,4 @@
.PHONY: gen dev-reset all clean test web docs
.PHONY: gen dev-reset all clean test web website
SHELL := /usr/bin/env bash
.SHELLFLAGS += ${SHELLFLAGS} -e -o pipefail
@ -70,10 +70,10 @@ core-i18n-extract:
--ignore internal \
--ignore ${GEN_API_TS} \
--ignore ${GEN_API_GO} \
--ignore docs \
--ignore website \
-l en
install: web-install docs-install core-install ## Install all requires dependencies for `web`, `docs` and `core`
install: web-install website-install core-install ## Install all requires dependencies for `web`, `website` and `core`
dev-drop-db:
dropdb -U ${pg_user} -h ${pg_host} ${pg_name}
@ -221,22 +221,22 @@ web-i18n-extract:
cd web && npm run extract-locales
#########################
## Docs
## Website
#########################
docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Authentik docs source code, lint the code, and compile it
website: website-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it
docs-install:
npm ci --prefix docs
website-install:
cd website && npm ci
docs-lint-fix: lint-codespell
npm run prettier --prefix docs
website-lint-fix: lint-codespell
cd website && npm run prettier
docs-build:
npm run build --prefix docs
website-build:
cd website && npm run build
docs-watch: ## Build and watch the documentation website, updating automatically
npm run watch --prefix docs
website-watch: ## Build and watch the documentation website, updating automatically
cd website && npm run watch
#########################
## Docker

View File

@ -8,12 +8,12 @@
# make gen-dev-config
# ```
#
# You may edit the generated file to override the configuration below.
# You may edit the generated file to override the configuration below.
#
# When making modifying the default configuration file,
# When making modifying the default configuration file,
# ensure that the corresponding documentation is updated to match.
#
# @see {@link ../../docs/topics/install-config/configuration/configuration.mdx Configuration documentation} for more information.
# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information.
postgresql:
host: localhost

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

@ -1,9 +0,0 @@
[production]
> 0.2%
not dead
not op_mini all
[development]
last 1 chrome version
last 1 firefox version
last 1 safari version

View File

@ -1,20 +0,0 @@
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-slim AS docs-builder
ENV NODE_ENV=production
WORKDIR /work/docs
COPY ./docs/package.json ./docs/package-lock.json /work/docs/
RUN npm ci --include=dev
COPY ./docs /work/docs/
COPY ./blueprints /work/blueprints/
COPY ./schema.yml /work/
COPY ./SECURITY.md /work/
RUN npm run build
FROM docker.io/library/nginx:1.29.0
COPY --from=docs-builder /work/docs/topics/build /usr/share/nginx/html

View File

@ -1,18 +0,0 @@
---
title: Authentication
sidebar_position: 1
---
For any of the token-based methods, set the `Authorization` header to `Bearer <token>`.
### Session
When authenticating with a flow, you'll get an authenticated Session cookie, that can be used for authentication. Keep in mind that in this context, a CSRF header is also required.
### API Token
Users can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate.
### JWT Token
OAuth2 clients can request the scope `goauthentik.io/api`, which allows their OAuth Access token to be used to authenticate to the API.

View File

@ -1,15 +0,0 @@
---
title: API Client Overview
---
import DocCardList from "@theme/DocCardList";
These API clients are officially supported and maintained.
:::info
These API clients are primarily built around creating/updating/deleting configuration objects in authentik, and in most cases can **not** be used to implemented SSO into your application.
:::
<DocCardList />

View File

@ -1,17 +0,0 @@
---
title: Go API Client
sidebar_label: Golang
description: A Golang client for the authentik API.
---
The [Go API client](https://pkg.go.dev/goauthentik.io/api/v3) is generated using the [OpenAPI Generator](https://openapi-generator.tech/) and the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
```bash
go get goauthentik.io/api/v3
```
## Building the Go Client
The Go client is used by the Outpost to communicate with the backend authentik server. To build the go client, run `make gen-client-go`.
The generated files are stored in `/gen-go-api` in the root of the repository.

View File

@ -1,33 +0,0 @@
---
title: Node.js API Client
sidebar_label: Node.js
description: A TypeScript client for the authentik API.
---
The [Node.js API client](https://www.npmjs.com/package/@goauthentik/api) is generated using the [OpenAPI Generator](https://openapi-generator.tech/) and the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
```bash npm2yarn
npm install @goauthentik/api
```
## Usage
```ts
import { AdminApi, Configuration } from "@goauthentik/api";
const config = new Configuration({
basePath: "authentik.company/api/v3",
});
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
```
## Building the Node.js Client
The web client is used by the web-interface and web-FlowExecutor to communicate with authentik. To build the client, run `make gen-client-ts`.
Since the client is normally distributed as an npm package, running `make gen-client-ts` will overwrite the locally installed client with the newly built one.
:::caution
Running `npm i` in the `/web` folder after using `make gen-client-ts` will overwrite the custom client and revert to the upstream client.
:::

View File

@ -1,13 +0,0 @@
---
title: Python API Client
sidebar_label: Python
description: A Python client for the authentik API.
---
The [Python API client](https://pypi.org/project/authentik-client/) is generated using the [OpenAPI Generator](https://openapi-generator.tech/) and the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
```bash
pip install authentik-client
# Or
uv pip install authentik-client
```

View File

@ -1 +0,0 @@
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);

View File

@ -1,161 +0,0 @@
/**
* @file Docusaurus config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
* @import * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
*/
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
import { GlobExcludeDefault } from "@docusaurus/utils";
import { createApiPageMD } from "docusaurus-plugin-openapi-docs/lib/markdown/index.js";
import { cp } from "node:fs/promises";
import { createRequire } from "node:module";
import { basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { gzip } from "pako";
const require = createRequire(import.meta.url);
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const rootStaticDirectory = resolve(__dirname, "..", "static");
//#region Copy static files
const authentikModulePath = resolve(__dirname, "..", "..");
const files = [
resolve(authentikModulePath, "docker-compose.yml"),
resolve(authentikModulePath, "schema.yml"),
];
await Promise.all(
files.map((file) => {
const fileName = basename(file);
const destPath = resolve(rootStaticDirectory, fileName);
return cp(file, destPath, {
recursive: true,
});
}),
);
//#endregion
//#region Configuration
//#region Configuration
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
const config = {
staticDirectories: [
// ---
"static",
rootStaticDirectory,
],
onBrokenAnchors: "ignore",
onBrokenLinks: "ignore",
onBrokenMarkdownLinks: "ignore",
onDuplicateRoutes: "ignore",
themes: ["@docusaurus/theme-mermaid", "docusaurus-theme-openapi-docs"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
navbarReplacements: {
DOCS_URL: "/",
},
docs: {
sidebar: {
hideable: true,
},
},
}),
plugins: [
[
"@docusaurus/theme-classic",
{
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
},
],
//#region Docs Content Plugin
[
"@docusaurus/plugin-content-docs",
/** @type {DocsPluginOptions} */ ({
showLastUpdateAuthor: false,
showLastUpdateTime: false,
numberPrefixParser: false,
id: "docs",
routeBasePath: "/",
path: ".",
exclude: [...GlobExcludeDefault],
include: ["**/*.mdx", "**/*.md"],
sidebarPath: "./sidebar.mjs",
docItemComponent: "@theme/ApiItem",
beforeDefaultRemarkPlugins: [
remarkLinkRewrite([
// ---
["/integrations", "https://integrations.goauthentik.io"],
["/docs", "https://docs.goauthentik.io"],
]),
],
}),
],
//#endregion
//#region OpenAPI Docs Plugin
[
"docusaurus-plugin-openapi-docs",
{
id: "open-api-docs",
docsPluginId: "docs",
config: {
authentik: /** @type {OpenApiPlugin.Options} */ ({
specPath: resolve("..", "..", "schema.yml"),
outputDir: "./reference",
hideSendButton: true,
disableCompression: true,
sidebarOptions: {
groupPathsBy: "tag",
},
template: "src/templates/api.mustache",
markdownGenerators: {
createApiPageMD: (pageData) => {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
postman,
...coreAPI
} = pageData.api;
return [
createApiPageMD(pageData),
`export const api = "${btoa(
String.fromCharCode(
...gzip(JSON.stringify(coreAPI), {
level: 9,
}),
),
)}";`,
].join("\n");
},
},
}),
},
},
],
],
//#endregion
};
//#endregion
export default createDocusaurusConfig(config);

View File

@ -1,11 +0,0 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
try {
require.resolve("#reference/sidebar");
} catch (_error) {
console.error(
"\n⛔ API Reference sidebar not found.\n\nRun `npm run build:api` to generate files.",
);
process.exit(1);
}

View File

@ -1,16 +0,0 @@
---
title: API Overview
sidebar_label: Overview
---
Our API reference documentation is generated from the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
You can also access your installation's own, instance-specific API Browser. Starting with 2021.3.5, every authentik instance has a built-in API browser, which can be accessed at <code>https://<em>authentik.company</em>/api/v3/</code>.
To generate an API client you can use the OpenAPI v3 schema at <code>https://<em>authentik.company</em>/api/v3/schema/</code>.
## Making schema changes
Some backend changes might require new/different fields or remove other fields. To create a new schema after changing a Serializer, run `make gen-build`.
This will update the `schema.yml` file in the root of the repository.

View File

@ -1,30 +0,0 @@
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [".docusaurus", ".cache", 'node_modules/.cache']
[[plugins]]
package = "netlify-plugin-debug-cache"
[build]
base = "docs"
package = "api"
command = "npm run build -w api"
publish = "api/build"
[dev]
command = "npm start"
targetPort = 3000
publish = "api/build"
[context.production.environment]
NODE_ENV = "production"
[context.dev.environment]
NODE_ENV = "development"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"

View File

@ -1,24 +0,0 @@
{
"name": "@goauthentik/api-docs",
"version": "0.0.0",
"description": "API Documentation",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:api build:types build:docusaurus",
"build:api": "docusaurus gen-api-docs all",
"build:docusaurus": "docusaurus build",
"build:types": "tsc -b .",
"deploy": "docusaurus deploy",
"docusaurus": "docusaurus",
"serve": "docusaurus serve",
"start": "docusaurus start",
"swizzle": "docusaurus swizzle"
},
"imports": {
"#reference/sidebar": "./reference/sidebar.ts"
},
"dependencies": {
"@goauthentik/docusaurus-theme": "*"
}
}

View File

@ -1,65 +0,0 @@
/**
* @file Sidebar configuration for documentation entries.
*
* @import { SidebarItemConfig } from "@docusaurus/plugin-content-docs/src/sidebars/types.js"
*/
import "./ensure-reference-sidebar.mjs";
// No file extensions for Docusaurus's automatic resolution.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Allows for project-wide type checking when partially building docs.
import apiReference from "./reference/sidebar";
const DOCS_URL = process.env.DOCS_URL || "https://docs.goauthentik.io";
/**
* @type {SidebarItemConfig}
*/
const sidebar = {
reference: [
{
type: "link",
label: "← Back to Developer Docs",
href: new URL("/developer-docs", DOCS_URL).href,
className: "navbar-sidebar__upwards",
},
{
type: "doc",
label: "API Overview",
className: "api-overview",
id: "index",
},
{
type: "category",
label: "Clients",
collapsed: false,
collapsible: false,
link: {
type: "doc",
id: "clients",
},
items: [
{
type: "autogenerated",
dirName: "clients",
},
],
},
{
type: "category",
label: "API Reference",
className: "api-reference",
collapsed: false,
collapsible: false,
link: {
type: "doc",
id: apiReference[0].id,
},
items: apiReference.slice(1),
},
],
};
export default sidebar;

View File

@ -1,25 +0,0 @@
---
id: {{{id}}}
title: "{{{title}}}"
description: "{{{frontMatter.description}}}"
{{^api}}
sidebar_label: Introduction
sidebar_position: 0
{{/api}}
hide_title: true
{{#api}}
hide_table_of_contents: true
{{/api}}
{{#json}}
api: true
{{/json}}
{{#api.method}}
sidebar_class_name: "{{{api.method}}} api-method"
{{/api.method}}
{{#infoPath}}
info_path: {{{infoPath}}}
{{/infoPath}}
hide_send_button: true
---
{{{markdown}}}

View File

@ -1,75 +0,0 @@
import { useDoc } from "@docusaurus/plugin-content-docs/client";
import { useWindowSize } from "@docusaurus/theme-common";
import type { Props } from "@theme/ApiItem/Layout";
import ContentVisibility from "@theme/ContentVisibility";
import DocBreadcrumbs from "@theme/DocBreadcrumbs";
import DocItemContent from "@theme/DocItem/Content";
import DocItemFooter from "@theme/DocItem/Footer";
import DocItemPaginator from "@theme/DocItem/Paginator";
import DocItemTOCDesktop from "@theme/DocItem/TOC/Desktop";
import DocItemTOCMobile from "@theme/DocItem/TOC/Mobile";
import DocVersionBadge from "@theme/DocVersionBadge";
import DocVersionBanner from "@theme/DocVersionBanner";
import clsx from "clsx";
import React, { type JSX } from "react";
import styles from "./styles.module.css";
/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const { frontMatter, toc } = useDoc();
const windowSize = useWindowSize();
const hidden = frontMatter.hide_table_of_contents;
const canRender = !hidden && toc.length > 0;
const mobile = canRender ? <DocItemTOCMobile /> : undefined;
const desktop =
canRender && (windowSize === "desktop" || windowSize === "ssr") ? (
<DocItemTOCDesktop />
) : undefined;
return {
hidden,
mobile,
desktop,
};
}
export default function DocItemLayout({ children }: Props): JSX.Element {
const docTOC = useDocTOC();
const { metadata, frontMatter } = useDoc() as DocContextValue;
const { api, schema } = frontMatter;
return (
<div className="row">
<div className={clsx("col", !docTOC.hidden && styles.docItemCol)}>
<ContentVisibility metadata={metadata} />
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<div className="row">
<div className={clsx("col", api || schema ? "col--7" : "col--12")}>
<DocItemFooter />
</div>
</div>
</article>
<div className="row">
<div className={clsx("col", api || schema ? "col--7" : "col--12")}>
<DocItemPaginator />
</div>
</div>
</div>
</div>
{docTOC.desktop ? <div className="col col--3">{docTOC.desktop}</div> : null}
</div>
);
}

View File

@ -1,10 +0,0 @@
.docItemContainer header + *,
.docItemContainer article > *:first-child {
margin-top: 0;
}
@media (min-width: 997px) {
.docItemCol {
max-width: 75% !important;
}
}

View File

@ -1,245 +0,0 @@
import BrowserOnly from "@docusaurus/BrowserOnly";
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
import { DocProvider } from "@docusaurus/plugin-content-docs/client";
import { HtmlClassNameProvider } from "@docusaurus/theme-common";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import useIsBrowser from "@docusaurus/useIsBrowser";
import type { ApiExplorerProps } from "@theme/APIExplorer";
import { createAuth } from "@theme/ApiExplorer/Authorization/slice";
import { createPersistanceMiddleware } from "@theme/ApiExplorer/persistanceMiddleware";
import DocItemLayout from "@theme/ApiItem/Layout";
import CodeBlock from "@theme/CodeBlock";
import DocItemMetadata from "@theme/DocItem/Metadata";
import SkeletonLoader from "@theme/SkeletonLoader";
import clsx from "clsx";
import { ParameterObject, ServerObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
import type { ApiItem as ApiItemType } from "docusaurus-plugin-openapi-docs/src/types";
import type { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types";
import { ungzip } from "pako";
import React from "react";
import { Provider } from "react-redux";
import { APIStore, createStoreWithState, createStoreWithoutState } from "./store";
let ApiExplorer: React.FC<ApiExplorerProps> = () => <div />;
if (ExecutionEnvironment.canUseDOM) {
// @ts-expect-error - Dynamic import
ApiExplorer = await import("@theme/ApiExplorer").then((mod) => mod.default);
}
function base64ToUint8Array(base64: string) {
const binary = atob(base64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function decodeAPI(encodedAPI: string): ApiItemType | null {
try {
return JSON.parse(
ungzip(base64ToUint8Array(encodedAPI), {
to: "string",
}),
);
} catch (_error) {
return null;
}
}
interface APIItemSchemeProps {
content: PropDocContent;
}
const APIItemScheme: React.FC<APIItemSchemeProps> = (props) => {
const MDXComponent = props.content;
const docHtmlClassName = `docs-doc-id-${props.content.metadata.id}`;
const { frontMatter } = MDXComponent;
const { sample } = frontMatter;
return (
<DocProvider content={props.content}>
<HtmlClassNameProvider className={docHtmlClassName}>
<DocItemMetadata />
<DocItemLayout>
<div className={clsx("row", "theme-api-markdown")}>
<div className="col col--7 openapi-left-panel__container schema">
<MDXComponent />
</div>
<div className="col col--5 openapi-right-panel__container">
{sample ? (
<CodeBlock language="json" title={`${frontMatter.title}`}>
{JSON.stringify(sample, null, 2)}
</CodeBlock>
) : null}
</div>
</div>
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
};
interface APIItemAPIProps {
content: PropDocContent;
api: ApiItemType;
}
const APIItemAPI: React.FC<APIItemAPIProps> = ({ content: MDXComponent, api }) => {
const docHtmlClassName = `docs-doc-id-${MDXComponent.metadata.id}`;
const frontMatter = MDXComponent.frontMatter;
const { siteConfig } = useDocusaurusContext();
const themeConfig = siteConfig.themeConfig as ThemeConfig;
const options = themeConfig.api;
const isBrowser = useIsBrowser();
// Regex for 2XX status
const statusRegex = new RegExp("(20[0-9]|2[1-9][0-9])");
let store: APIStore;
const persistanceMiddleware = createPersistanceMiddleware(options);
// Init store for SSR
if (!isBrowser) {
store = createStoreWithoutState({}, [persistanceMiddleware]);
} else {
// Init store for CSR to hydrate components
// Create list of only 2XX response content types to create request samples from
const acceptArrayInit: string[][] = [];
for (const [code, content] of Object.entries(api.responses ?? [])) {
if (statusRegex.test(code)) {
acceptArrayInit.push(Object.keys(content.content ?? {}));
}
}
const acceptArray = acceptArrayInit.flat();
const content = api.requestBody?.content ?? {};
const contentTypeArray = Object.keys(content);
const servers = api.servers ?? [];
const params = {
path: [] as ParameterObject[],
query: [] as ParameterObject[],
header: [] as ParameterObject[],
cookie: [] as ParameterObject[],
};
api.parameters?.forEach((param: { in: "path" | "query" | "header" | "cookie" }) => {
const paramType = param.in;
const paramsArray: ParameterObject[] = params[paramType];
paramsArray.push(param as ParameterObject);
});
const auth = createAuth({
security: api.security,
securitySchemes: api.securitySchemes,
options,
});
const server = window?.sessionStorage.getItem("server");
const serverObject = (JSON.parse(server!) as ServerObject) ?? {};
store = createStoreWithState(
{
accept: {
value: acceptArray[0],
options: acceptArray,
},
contentType: {
value: contentTypeArray[0],
options: contentTypeArray,
},
server: {
value: serverObject.url ? serverObject : undefined,
options: servers,
},
response: { value: undefined },
body: { type: "empty" },
params,
auth,
},
[persistanceMiddleware],
);
}
return (
<DocProvider content={MDXComponent}>
<HtmlClassNameProvider className={docHtmlClassName}>
<DocItemMetadata />
<DocItemLayout>
<Provider store={store}>
<div className={clsx("row", "theme-api-markdown")}>
<div className="col col--7 openapi-left-panel__container">
<MDXComponent />
</div>
<div className="col col--5 openapi-right-panel__container">
<BrowserOnly fallback={<SkeletonLoader size="lg" />}>
{() => {
return (
<ApiExplorer
item={api}
infoPath={frontMatter.info_path}
/>
);
}}
</BrowserOnly>
</div>
</div>
</Provider>
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
};
interface APIItemProps {
content: PropDocContent;
}
const ApiItem: React.FC<APIItemProps> = ({ content: MDXComponent }) => {
const frontMatter = MDXComponent.frontMatter;
if (frontMatter.schema) {
return <APIItemScheme content={MDXComponent} />;
}
if (!MDXComponent.api) {
// Non-API docs
return (
<DocProvider content={MDXComponent}>
<HtmlClassNameProvider className={`docs-doc-id-${MDXComponent.metadata.id}`}>
<DocItemMetadata />
<DocItemLayout>
<div className="row">
<div className="col col--12 markdown">
<MDXComponent />
</div>
</div>
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
}
return (
<BrowserOnly fallback={<SkeletonLoader size="lg" />}>
{() => {
const api = decodeAPI(MDXComponent.api!);
if (!api) {
console.error("Failed to decode API", frontMatter);
throw new Error("Failed to decode API");
}
return <APIItemAPI content={MDXComponent} api={api} />;
}}
</BrowserOnly>
);
};
export default ApiItem;

View File

@ -1,44 +0,0 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { Middleware } from "@reduxjs/toolkit";
import accept from "@theme/ApiExplorer/Accept/slice";
import auth from "@theme/ApiExplorer/Authorization/slice";
import body from "@theme/ApiExplorer/Body/slice";
import contentType from "@theme/ApiExplorer/ContentType/slice";
import params from "@theme/ApiExplorer/ParamOptions/slice";
import response from "@theme/ApiExplorer/Response/slice";
import server from "@theme/ApiExplorer/Server/slice";
const rootReducer = combineReducers({
accept,
contentType,
response,
server,
body,
params,
auth,
});
export type RootState = ReturnType<typeof rootReducer>;
export function createStoreWithState(preloadedState: RootState, middlewares: Middleware[]) {
return configureStore({
reducer: rootReducer,
preloadedState,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(...middlewares),
});
}
export type APIStore = ReturnType<typeof createStoreWithState>;
export function createStoreWithoutState(
preloadedState: Partial<RootState>,
middlewares: Middleware[],
) {
return configureStore({
reducer: rootReducer,
preloadedState,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(...middlewares),
});
}
export type AppDispatch = ReturnType<typeof createStoreWithState>["dispatch"];

View File

@ -1,70 +0,0 @@
import Link from "@docusaurus/Link";
import isInternalUrl from "@docusaurus/isInternalUrl";
import { isActiveSidebarItem } from "@docusaurus/plugin-content-docs/client";
import { ThemeClassNames } from "@docusaurus/theme-common";
import type { Props } from "@theme/DocSidebarItem/Link";
import IconExternalLink from "@theme/Icon/ExternalLink";
import clsx from "clsx";
import React from "react";
import "./styles.css";
const docsURL = new URL(process.env.DOCS_URL || "https://docs.goauthentik.io");
function isInternalUrlOrDocsUrl(url: string) {
if (isInternalUrl(url)) return true;
const inputURL = new URL(url);
return inputURL.origin === docsURL.origin;
}
const DocSidebarItemLink: React.FC<Props> = ({
item,
onItemClick,
activePath,
level,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
index,
...props
}) => {
const { href, label, className, autoAddBaseUrl } = item;
const isActive = isActiveSidebarItem(item, activePath);
const internalLink = isInternalUrlOrDocsUrl(href);
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
"menu__list-item",
className,
)}
key={label}
>
<Link
className={clsx("menu__link", {
"menu__link--external": !internalLink,
"menu__link--active": isActive,
})}
autoAddBaseUrl={autoAddBaseUrl}
aria-current={isActive ? "page" : undefined}
to={href}
{...(internalLink && {
onClick: onItemClick ? () => onItemClick(item) : undefined,
})}
{...props}
>
{item.className?.includes("api-method") ? (
<div className="badge-container">
<span role="img" className="badge method" />
</div>
) : null}
{label}
{!internalLink && <IconExternalLink />}
</Link>
</li>
);
};
export default DocSidebarItemLink;

View File

@ -1,127 +0,0 @@
.theme-layout-main {
--doc-sidebar-width: 400px;
}
.navbar-sidebar__upwards {
.menu__link {
font-size: var(--ifm-h6-font-size);
font-weight: var(--ifm-font-weight-bold);
color: var(--ifm-color-info-light);
padding-block: calc(var(--ifm-spacing-vertical) / 1.5);
}
}
.theme-doc-sidebar-item-category.api-reference {
> .menu__list-item-collapsible {
font-weight: 900;
font-size: var(--ifm-h3-font-size);
}
.menu__list {
padding-left: 0;
}
.theme-doc-sidebar-item-category-level-2 .menu__list-item-collapsible {
font-size: var(--ifm-h4-font-size);
font-weight: bold;
text-transform: capitalize;
}
}
.menu__link.menu__link--external {
align-items: center;
}
.menu__list-item.api-method {
.badge-container {
flex: 0 0 auto;
display: flex;
width: 100%;
justify-content: end;
}
> .menu__link {
--menu-border-width: 2px;
color: var(--menu-item-contrast-foreground, red);
background-color: var(--menu-item-background-color, transparent);
flex-flow: column;
font-family: var(--ifm-font-family-monospace);
font-weight: 600;
gap: 0.25em;
padding-inline-end: 0.25em;
word-break: break-all;
align-items: start;
border-radius: 0;
margin-inline-end: calc(var(--ifm-menu-link-padding-horizontal) / 2);
font-size: var(--ifm-h6-font-size);
letter-spacing: 0.015em;
text-rendering: optimizelegibility;
position: relative;
box-shadow: var(--ifm-global-shadow-lw);
&::before {
position: absolute;
left: 0;
top: 0;
width: var(--menu-border-width);
height: 100%;
display: block;
z-index: 1;
background-color: var(--ifm-badge-color, var(--ifm-color-primary));
content: "";
transition: width 0.2s var(--ifm-transition-timing-default);
}
&:hover,
&.menu__link--active {
--menu-border-width: 6px;
}
}
&.get {
--method-label: "GET";
--menu-item-contrast-foreground: var(--ifm-color-content);
--menu-item-background-color: var(--ifm-card-background-color);
--ifm-badge-color: var(--ifm-color-primary-light);
}
&.post {
--method-label: "POST";
--menu-item-contrast-foreground: var(--ifm-color-success-contrast-foreground);
--menu-item-background-color: var(--ifm-color-success-contrast-background);
--ifm-badge-color: var(--ifm-color-success-lightest);
}
&.put {
--method-label: "PUT";
--menu-item-contrast-foreground: var(--ifm-color-info-contrast-foreground);
--menu-item-background-color: var(--ifm-color-info-contrast-background);
--ifm-badge-color: var(--ifm-color-info-lightest);
}
&.patch {
--method-label: "PATCH";
--menu-item-contrast-foreground: var(--ifm-color-warning-contrast-foreground);
--menu-item-background-color: var(--ifm-color-warning-contrast-background);
--ifm-badge-color: var(--ifm-color-warning-lightest);
}
&.delete {
--method-label: "DELETE";
--menu-item-contrast-foreground: var(--ifm-color-danger-contrast-foreground);
--menu-item-background-color: var(--ifm-color-danger-contrast-background);
--ifm-badge-color: var(--ifm-color-danger-lightest);
}
}
.badge.method {
position: relative;
flex: 0 0 auto;
user-select: none;
&::before {
content: var(--method-label, "METHOD");
display: block;
}
}

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../docusaurus-theme"
}
]
}

View File

@ -1,37 +0,0 @@
/// <reference types="docusaurus-theme-openapi-docs" />
/// <reference types="docusaurus-plugin-openapi-docs" />
declare module "@docusaurus/plugin-content-docs/src/sidebars/types" {
export * from "@docusaurus/plugin-content-docs/src/sidebars/types.ts";
}
declare module "@theme/RequestSchema";
declare module "@theme/ParamsDetails";
declare module "@theme/StatusCodes";
declare module "@theme/OperationTabs";
declare module "@theme/SkeletonLoader" {
import { FC } from "react";
const SkeletonLoader: FC<{ size: "sm" | "md" | "lg" }>;
export default SkeletonLoader;
}
declare module "@theme/APIExplorer" {
import { FC } from "react";
export interface ApiExplorerProps {
item: unknown;
infoPath: unknown;
}
const ApiExplorer: FC<ApiExplorerProps>;
export default ApiExplorer;
}
declare module "@theme/ApiExplorer/persistanceMiddleware" {
import { Middleware } from "@reduxjs/toolkit";
import type { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types";
export const createPersistanceMiddleware: (options: ThemeConfig["api"]) => Middleware;
}

View File

@ -1,34 +0,0 @@
/**
* @file Supplemental type definitions for Docusaurus.
*
* @remarks
*
* Docusaurus uses an unconventional module resolution strategy, which can lead to
* issues when using TypeScript.
*
* The types in this file are intended to expose less visible types to TypeScript's
* project references, allowing for better type checking and autocompletion.
*/
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="@docusaurus/plugin-content-docs" />
/// <reference types="@docusaurus/theme-classic" />
import type { PropDocContent as BasePropDocContent } from "@docusaurus/plugin-content-docs";
import type { DocContextValue as BaseDocContextValue } from "@docusaurus/plugin-content-docs/client";
declare global {
export interface APIDocFrontMatter {
readonly info_path?: string;
readonly api?: string;
readonly schema?: boolean;
readonly sample?: unknown;
}
export interface PropDocContent extends BasePropDocContent {
readonly api?: string;
frontMatter: APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
export interface DocContextValue extends BaseDocContextValue {
frontMatter: APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
}

View File

@ -1,97 +0,0 @@
import {
createVersionURL,
isPrerelease,
parseHostnameSemVer,
} from "#components/VersionPicker/utils.ts";
import clsx from "clsx";
import React, { memo } from "react";
import "./styles.css";
export interface VersionDropdownProps {
/**
* The hostname of the client.
*/
hostname: string | null;
/**
* The origin of the prerelease documentation.
*
* @format url
*/
prereleaseOrigin: string;
/**
* The available versions of the documentation.
*
* @format semver
*/
releases: string[];
}
/**
* A dropdown that shows the available versions of the documentation.
*/
export const VersionDropdown = memo<VersionDropdownProps>(
({ hostname, prereleaseOrigin, releases }) => {
const prerelease = isPrerelease(hostname);
const parsedSemVer = !prerelease ? parseHostnameSemVer(hostname) : null;
const currentLabel = parsedSemVer || "Pre-Release";
const endIndex = parsedSemVer ? releases.indexOf(parsedSemVer) : -1;
const visibleReleases = releases.slice(0, endIndex === -1 ? 3 : endIndex + 3);
return (
<li className="navbar__item dropdown dropdown--hoverable dropdown--right ak-version-selector">
<div
aria-haspopup="true"
aria-expanded="false"
role="button"
className="navbar__link menu__link"
>
Version: {currentLabel}
</div>
<ul className="dropdown__menu menu__list-item--collapsed">
{!prerelease ? (
<li>
<a
href={prereleaseOrigin}
target="_blank"
rel="noopener noreferrer"
className="dropdown__link menu__link"
>
Pre-Release
</a>
</li>
) : null}
{visibleReleases.map((semVer, idx) => {
const label = semVer;
// TODO: Flesh this out after we settle on versioning strategy.
// if (idx === 0) {
// label += " (Current Release)";
// }
return (
<li key={idx}>
<a
href={createVersionURL(semVer)}
target="_blank"
rel="noopener noreferrer"
className={clsx("dropdown__link menu__link", {
"menu__link--active": semVer === currentLabel,
})}
>
{label}
</a>
</li>
);
})}
</ul>
</li>
);
},
);

View File

@ -1,76 +0,0 @@
import { VersionDropdown } from "#components/VersionPicker/VersionDropdown.tsx";
import { LocalhostAliases, ProductionURL, useHostname } from "#components/VersionPicker/utils.ts";
import { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import useIsBrowser from "@docusaurus/useIsBrowser";
import React, { useEffect, useMemo, useState } from "react";
export interface VersionPickerLoaderProps {
pluginData: AKReleasesPluginData;
}
/**
* A data-fetching component that loads available versions of the documentation.
*
* @see {@linkcode VersionPicker} for the component.
* @see {@linkcode AKReleasesPluginData} for the plugin data.
* @client
*/
export const VersionPickerLoader: React.FC<VersionPickerLoaderProps> = ({ pluginData }) => {
const [releases, setReleases] = useState(pluginData.releases);
const browser = useIsBrowser();
const hostname = useHostname();
const prereleaseOrigin = useMemo(() => {
if (browser && LocalhostAliases.has(window.location.hostname)) {
return window.location.origin;
}
return ProductionURL.href;
}, [browser]);
useEffect(() => {
if (!browser || !prereleaseOrigin) return;
const controller = new AbortController();
const updateURL = new URL(pluginData.publicPath, prereleaseOrigin);
fetch(updateURL, {
signal: controller.signal,
})
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch new releases: ${response.status}`);
}
return response.json();
})
.then((data: unknown) => {
// We're extra cautious here to be ready if the API shape ever changes.
if (!data) throw new Error("Failed to parse releases");
if (!Array.isArray(data)) throw new Error("Releases must be an array");
if (!data.every((item) => typeof item === "string"))
throw new Error("Releases must be an array of strings");
setReleases(data);
})
.catch((error) => {
console.warn(`Failed to fetch new releases: ${error}`);
});
// eslint-disable-next-line consistent-return
return () => controller.abort("unmount");
}, [browser, pluginData.publicPath, prereleaseOrigin]);
return (
<VersionDropdown
hostname={hostname}
prereleaseOrigin={prereleaseOrigin}
releases={releases}
/>
);
};

View File

@ -1,32 +0,0 @@
import { VersionDropdown } from "#components/VersionPicker/VersionDropdown.tsx";
import { useHostname, usePrereleaseOrigin } from "#components/VersionPicker/utils.ts";
import { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import { usePluginData } from "@docusaurus/useGlobalData";
/**
* A component that shows the available versions of the documentation.
*
* @see {@linkcode VersionPickerLoader} for the data-fetching component.
*/
export const VersionPicker: React.FC = () => {
const hostname = useHostname();
const prereleaseOrigin = usePrereleaseOrigin();
const pluginData = usePluginData("ak-releases-plugin", undefined) as
| AKReleasesPluginData
| undefined;
if (!pluginData?.releases.length) return null;
// return <VersionPickerLoader pluginData={pluginData} />;
return (
<VersionDropdown
hostname={hostname}
prereleaseOrigin={prereleaseOrigin}
releases={pluginData.releases}
/>
);
};

View File

@ -1,33 +0,0 @@
.theme-doc-sidebar-menu {
--ak-version-selector-padding: calc(var(--ifm-spacing-vertical) / 2);
.dropdown.ak-version-selector {
width: calc(100% - (var(--ifm-spacing-horizontal) / 2));
border-block-end: var(--ifm-hr-height) solid var(--ifm-color-emphasis-200);
padding-block-start: calc(var(--ak-version-selector-padding) / 2);
padding-block-end: var(--ak-version-selector-padding);
margin-block-end: var(--ak-version-selector-padding);
&:has(+ .navbar-sidebar__upwards) {
margin-block-end: 0;
}
.navbar__link.menu__link {
display: flex;
width: 100%;
justify-content: space-between;
font-weight: var(--ifm-font-weight-semibold);
&::after {
color: var(--ifm-color-emphasis-400);
filter: var(--ifm-menu-link-sublist-icon-filter);
}
}
.dropdown__menu {
background: var(--ifm-dropdown-background-color);
box-shadow: var(--ifm-global-shadow-lw);
border: 1px solid var(--ifm-color-emphasis-200);
}
}
}

View File

@ -1,81 +0,0 @@
import useIsBrowser from "@docusaurus/useIsBrowser";
import { useMemo } from "react";
import { coerce } from "semver";
export const ProductionURL = new URL("https://docs.goauthentik.io");
export const LocalhostAliases: ReadonlySet<string> = new Set(["localhost", "127.0.0.1"]);
/**
* Given a semver, create the URL for the version.
*/
export function createVersionURL(semver: string): string {
const subdomain = `version-${semver.replace(".", "-")}`;
return `https://${subdomain}.goauthentik.io`;
}
/**
* Predicate to determine if a hostname appears to be a prerelease origin.
*/
export function isPrerelease(hostname: string | null): boolean {
if (!hostname) return false;
if (hostname === ProductionURL.hostname) return true;
if (hostname.endsWith(".netlify.app")) return true;
if (LocalhostAliases.has(hostname)) return true;
return false;
}
/**
* Given a hostname, parse the semver from the subdomain.
*/
export function parseHostnameSemVer(hostname: string | null): string | null {
if (!hostname) return null;
const [, possibleSemVer] = hostname.match(/version-(.+)\.goauthentik\.io/) || [];
if (!possibleSemVer) return null;
const formattedSemVer = possibleSemVer.replace("-", ".");
if (!coerce(formattedSemVer)) return null;
return formattedSemVer;
}
export function useHostname() {
const browser = useIsBrowser();
const hostname = useMemo(() => {
if (!browser) return null;
const searchParams = new URLSearchParams(window.location.search);
// Query parameter used for debugging.
// Note that this doesn't synchronize with Docusaurus's router state.
const subdomain = searchParams.get("version");
if (subdomain) return subdomain;
return window.location.hostname;
}, [browser]);
return hostname;
}
export function usePrereleaseOrigin() {
const browser = useIsBrowser();
const prereleaseOrigin = useMemo(() => {
if (browser && LocalhostAliases.has(window.location.hostname)) {
return window.location.origin;
}
return ProductionURL.href;
}, [browser]);
return prereleaseOrigin;
}

View File

@ -1,85 +0,0 @@
/**
* @file Docusaurus config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
* @import { BuildUrlValues } from "remark-github";
*/
import {
remarkEnterpriseDirective,
remarkPreviewDirective,
remarkSupportDirective,
remarkVersionDirective,
} from "#remark";
import remarkNPM2Yarn from "@docusaurus/remark-plugin-npm2yarn";
import remarkDirective from "remark-directive";
import remarkGithub, { defaultBuildUrl } from "remark-github";
//#region Common configuration
/**
* @satisfies {DocsPluginOptions}
*/
export const CommonDocsPluginOptions = {
id: "docs",
routeBasePath: "/",
path: "docs",
sidebarPath: "./docs/sidebar.mjs",
showLastUpdateTime: false,
editUrl: "https://github.com/goauthentik/authentik/edit/main/docs/",
//#region Docs Plugins
beforeDefaultRemarkPlugins: [
remarkDirective,
remarkVersionDirective,
remarkEnterpriseDirective,
remarkPreviewDirective,
remarkSupportDirective,
],
remarkPlugins: [
[remarkNPM2Yarn, { sync: true }],
[
remarkGithub,
{
repository: "goauthentik/authentik",
/**
* @param {BuildUrlValues} values
*/
buildUrl: (values) => {
// Only replace issues and PR links
return values.type === "issue" || values.type === "mention"
? defaultBuildUrl(values)
: false;
},
},
],
],
};
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
export const CommonConfig = {
themes: ["@docusaurus/theme-mermaid"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
algolia: {
appId: "36ROD0O0FV",
apiKey: "727db511300ca9aec5425645bbbddfb5",
indexName: "goauthentik",
},
}),
plugins: [
[
"@docusaurus/plugin-google-gtag",
{
trackingID: ["G-9MVR9WZFZH"],
anonymizeIP: true,
},
],
],
};

View File

@ -1,3 +0,0 @@
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
export default createESLintPackageConfig();

View File

@ -1,21 +0,0 @@
/**
* @file Docusaurus theme plugin.
* @import { Plugin } from "@docusaurus/types";
*/
/**
* @returns {Plugin<void>}
*/
export default function docusaurusThemeAuthentik() {
return {
name: "docusaurus-theme-authentik",
getThemePath() {
return "./theme";
},
getTypeScriptThemePath() {
return "./theme";
},
};
}

View File

@ -1,23 +0,0 @@
{
"name": "@goauthentik/docusaurus-theme",
"version": "0.0.0",
"license": "MIT",
"private": true,
"type": "module",
"exports": {
"./package.json": "./package.json",
".": "./index.js",
"./config": "./config.js",
"./remark": "./remark/index.mjs",
"./components/*": "./components/*",
"./releases/plugin": "./releases/plugin.mjs",
"./releases/utils": "./releases/utils.mjs"
},
"imports": {
"#remark": "./remark/index.mjs",
"#remark/*": "./remark/*",
"#components/*": "./components/*",
"#hooks/*": "./hooks/*",
"#theme/*": "./theme/*"
}
}

View File

@ -1,65 +0,0 @@
/* eslint-disable no-console */
/**
* @file Docusaurus releases plugin.
*
* @import { LoadContext, Plugin } from "@docusaurus/types"
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { collectReleaseFiles } from "./utils.mjs";
const PLUGIN_NAME = "ak-releases-plugin";
const RELEASES_FILENAME = "releases.gen.json";
/**
* @typedef {object} ReleasesPluginOptions
* @property {string} docsDirectory The path to the documentation directory.
*/
/**
* @typedef {object} AKReleasesPluginData
* @property {string} publicPath The URL to the plugin's public directory.
* @property {string[]} releases The available versions of the documentation.
*/
/**
* @param {LoadContext} loadContext
* @param {ReleasesPluginOptions} options
* @returns {Promise<Plugin<AKReleasesPluginData>>}
*/
async function akReleasesPlugin(loadContext, { docsDirectory }) {
return {
name: PLUGIN_NAME,
async loadContent() {
console.log(`🚀 ${PLUGIN_NAME} loaded`);
const releases = collectReleaseFiles(docsDirectory).map((release) => release.name);
const outputPath = path.join(loadContext.siteDir, "static", RELEASES_FILENAME);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, JSON.stringify(releases, null, 2), "utf-8");
console.log(`${RELEASES_FILENAME} generated`);
/**
* @type {AKReleasesPluginData}
*/
const content = {
releases,
publicPath: path.join("/", RELEASES_FILENAME),
};
return content;
},
contentLoaded({ content, actions }) {
const { setGlobalData } = actions;
setGlobalData(content);
},
};
}
export default akReleasesPlugin;

View File

@ -1,69 +0,0 @@
/**
* @file Docusaurus release utils.
*
* @import { SidebarItemConfig } from "@docusaurus/plugin-content-docs/src/sidebars/types.js"
*/
import FastGlob from "fast-glob";
import * as path from "node:path";
import { coerce } from "semver";
/**
*
* @param {string} releasesParentDirectory
* @returns {FastGlob.Entry[]}
*/
export function collectReleaseFiles(releasesParentDirectory) {
const releaseFiles = FastGlob.sync("releases/**/v*.{md,mdx}", {
cwd: releasesParentDirectory,
onlyFiles: true,
objectMode: true,
})
.map((fileEntry) => {
return {
...fileEntry,
path: fileEntry.path.replace(/\.mdx?$/, ""),
name: fileEntry.name.replace(/^v/, "").replace(/\.mdx?$/, ""),
};
})
.sort((a, b) => {
const aSemVer = coerce(a.name);
const bSemVer = coerce(b.name);
if (aSemVer && bSemVer) {
return bSemVer.compare(aSemVer);
}
return b.name.localeCompare(a.name);
});
return releaseFiles;
}
export const SUPPORTED_RELEASE_COUNT = 3;
/**
*
* @param {FastGlob.Entry[]} releaseFiles
*/
export function createReleaseSidebarEntries(releaseFiles) {
/**
* @type {SidebarItemConfig[]}
*/
let sidebarEntries = releaseFiles.map((fileEntry) => {
return path.join(fileEntry.path);
});
if (releaseFiles.length > SUPPORTED_RELEASE_COUNT) {
// Then we add the rest of the releases as a category.
sidebarEntries = [
...sidebarEntries.slice(0, SUPPORTED_RELEASE_COUNT),
{
type: "category",
label: "Previous versions",
items: sidebarEntries.slice(SUPPORTED_RELEASE_COUNT),
},
];
}
return sidebarEntries;
}

View File

@ -1,5 +0,0 @@
export * from "./enterprise-directive.mjs";
export * from "./link-rewrite-directive.mjs";
export * from "./preview-directive.mjs";
export * from "./support-directive.mjs";
export * from "./version-directive.mjs";

View File

@ -1,35 +0,0 @@
/**
* @import { Root } from "mdast";
*/
import { SKIP, visit } from "unist-util-visit";
/**
* @typedef {[pattern: string | RegExp, replacement: string]} Rewrite
*/
/**
* Remark plugin to transform relative links to docs to absolute URLs
* @param {Iterable<[string, string]>} rewrites Map of urls to rewrite where the key is the prefix to check for and the value is the domain to add
*/
export function remarkLinkRewrite(rewrites) {
const map = new Map(rewrites);
return () => {
/**
* @param {Root} tree The MDAST tree to transform.
*/
return (tree) => {
visit(tree, "link", (node) => {
for (const [pattern, replacement] of map) {
if (!node.url.startsWith(pattern)) continue;
node.url = node.url.replace(pattern, replacement);
}
return SKIP;
});
};
};
}
export default remarkLinkRewrite;

View File

@ -1,26 +0,0 @@
/// <reference types="@docusaurus/plugin-content-docs" />
import { VersionPicker } from "#components/VersionPicker/index.tsx";
import {
DocSidebarItemsExpandedStateProvider,
useVisibleSidebarItems,
} from "@docusaurus/plugin-content-docs/client";
import DocSidebarItem from "@theme/DocSidebarItem";
import type { Props as DocSidebarItemsProps } from "@theme/DocSidebarItems";
import { memo } from "react";
const DocSidebarItems = ({ items, ...props }: DocSidebarItemsProps): JSX.Element => {
const visibleItems = useVisibleSidebarItems(items, props.activePath);
const includeVersionPicker = props.level === 1 && !props.activePath.startsWith("/integrations");
return (
<DocSidebarItemsExpandedStateProvider>
{includeVersionPicker ? <VersionPicker /> : null}
{visibleItems.map((item, index) => (
<DocSidebarItem key={index} item={item} index={index} {...props} />
))}
</DocSidebarItemsExpandedStateProvider>
);
};
export default memo(DocSidebarItems);

View File

@ -1,3 +0,0 @@
{
"extends": "../tsconfig.base.json"
}

View File

@ -1,44 +0,0 @@
/**
* @file Supplemental type definitions for Docusaurus.
*
* @remarks
*
* Docusaurus uses an unconventional module resolution strategy, which can lead to
* issues when using TypeScript.
*
* The types in this file are intended to expose less visible types to TypeScript's
* project references, allowing for better type checking and autocompletion.
*/
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="@docusaurus/plugin-content-docs" />
/// <reference types="@docusaurus/theme-classic" />
import type { PropDocContent as BasePropDocContent } from "@docusaurus/plugin-content-docs";
import type { DocContextValue as BaseDocContextValue } from "@docusaurus/plugin-content-docs/client";
declare global {
/**
* @monkeypatch
*/
export interface DocFrontMatter {
support_level?: string;
authentik_version?: string;
authentik_preview: boolean;
authentik_enterprise: boolean;
}
export interface APIDocFrontMatter {
readonly info_path?: string;
readonly api?: string;
readonly schema?: boolean;
readonly sample?: unknown;
}
export interface PropDocContent extends BasePropDocContent {
readonly api?: string;
frontMatter: APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
export interface DocContextValue extends BaseDocContextValue {
frontMatter: DocFrontMatter & APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
}

View File

@ -1,11 +0,0 @@
import { DefaultIgnorePatterns, createESLintPackageConfig } from "@goauthentik/eslint-config";
export default createESLintPackageConfig({
ignorePatterns: [
// ---
...DefaultIgnorePatterns,
"**/.docusaurus/",
"**/build",
"**/reference",
],
});

View File

@ -1,7 +0,0 @@
.theme-doc-sidebar-item-category-level-1 .menu__list-item-collapsible {
border-top: 0.5px solid;
border-top-color: var(--ifm-category-color, var(--ifm-menu-color-background-active));
border-radius: 0;
font-weight: 600;
padding-block: 0.25em;
}

View File

@ -1 +0,0 @@
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);

View File

@ -1,87 +0,0 @@
/**
* @file Docusaurus Integrations config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
*/
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { CommonConfig, CommonDocsPluginOptions } from "@goauthentik/docusaurus-theme/config";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
import { GlobExcludeDefault } from "@docusaurus/utils";
import { deepmerge } from "deepmerge-ts";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
const __dirname = fileURLToPath(new URL(".", import.meta.url));
//#region Configuration
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
const config = {
staticDirectories: [
// ---
resolve(__dirname, "..", "static"),
"static",
],
themes: ["@goauthentik/docusaurus-theme"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
navbarReplacements: {
INTEGRATIONS_URL: "/",
},
algolia: {
externalUrlRegex: /^(?:https?:\/\/)(integrations|api).?(goauthentik.io)/.source,
},
}),
plugins: [
[
"@docusaurus/theme-classic",
{
customCss: [
"./custom.css",
require.resolve("@goauthentik/docusaurus-config/css/index.css"),
],
},
],
//#region Documentation
[
"@docusaurus/plugin-content-docs",
deepmerge(
CommonDocsPluginOptions,
/** @type {DocsPluginOptions} */ ({
id: "docs",
routeBasePath: "/",
path: ".",
exclude: [...GlobExcludeDefault],
include: ["**/*.mdx", "**/*.md"],
sidebarPath: "./sidebar.mjs",
showLastUpdateTime: false,
editUrl:
"https://github.com/goauthentik/authentik/edit/main/docs/topics/integrations/",
//#region Docs Plugins
beforeDefaultRemarkPlugins: [
remarkLinkRewrite([
// ---
["/api", "https://api.goauthentik.io"],
["/docs", "https://docs.goauthentik.io"],
]),
],
}),
),
],
],
};
export default /** @type {Config} */ (deepmerge(CommonConfig, createDocusaurusConfig(config)));

View File

@ -1,10 +0,0 @@
import { DefaultIgnorePatterns, createESLintPackageConfig } from "@goauthentik/eslint-config";
export default createESLintPackageConfig({
ignorePatterns: [
// ---
...DefaultIgnorePatterns,
".docusaurus/",
"./build",
],
});

View File

@ -1,30 +0,0 @@
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [".docusaurus", ".cache", 'node_modules/.cache']
[[plugins]]
package = "netlify-plugin-debug-cache"
[build]
base = "docs"
package = "integrations"
command = "npm run build -w integrations"
publish = "integrations/build"
[dev]
command = "npm start"
targetPort = 3000
publish = "integrations/build"
[context.production.environment]
NODE_ENV = "production"
[context.dev.environment]
NODE_ENV = "development"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"

View File

@ -1,22 +0,0 @@
{
"name": "@goauthentik/integration-docs",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:types build:docusaurus",
"build:docusaurus": "docusaurus build",
"build:types": "tsc -b .",
"deploy": "docusaurus deploy",
"docusaurus": "docusaurus",
"serve": "docusaurus serve",
"start": "docusaurus start",
"test": "node --test"
},
"dependencies": {
"@goauthentik/docusaurus-theme": "*"
},
"engines": {
"node": ">=24"
}
}

View File

@ -1,49 +0,0 @@
/**
* @file Sidebar configuration for the authentik integrations.
*
* @import { SidebarItemConfig } from "@docusaurus/plugin-content-docs/src/sidebars/types.js"
*/
/**
* @type {ReadonlyArray<readonly [string, string]>}
*/
const categories = [
["chat-communication-collaboration", "Chat, Communication & Collaboration"],
["device-management", "Device Management"],
["cloud-providers", "Cloud Providers"],
["dashboards", "Dashboards"],
["development", "Development"],
["documentation", "Documentation"],
["hypervisors-orchestrators", "Hypervisors / Orchestrators"],
["infrastructure", "Infrastructure"],
["networking", "Networking"],
["media", "Media"],
["miscellaneous", "Miscellaneous"],
["monitoring", "Monitoring"],
["platforms", "Platforms"],
["security", "Security"],
];
export default /** @type {SidebarItemConfig} */
({
integrations: [
{
type: "doc",
id: "index",
},
{
type: "doc",
id: "applications",
},
...categories.map(([dirName, label]) => ({
type: "category",
label,
items: [
{
type: "autogenerated",
dirName,
},
],
})),
],
});

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../docusaurus-theme"
}
]
}

View File

@ -1,13 +0,0 @@
/**
* @file Supplemental type definitions for Docusaurus.
*
* @remarks
*
* Docusaurus uses an unconventional module resolution strategy, which can lead to
* issues when using TypeScript.
*
* The types in this file are intended to expose less visible types to TypeScript's
* project references, allowing for better type checking and autocompletion.
*/
/// <reference types="@docusaurus/plugin-content-docs" />
/// <reference types="@docusaurus/theme-classic" />

View File

@ -1,101 +0,0 @@
{
"name": "@goauthentik/docs",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:types build:docusaurus",
"build:docusaurus": "npm run build -w topics",
"build:types": "tsc -b",
"docusaurus": "docusaurus",
"lint": "eslint --fix .",
"lint-check": "eslint --max-warnings 0 .",
"prettier": "prettier --write .",
"prettier-check": "prettier --check .",
"start": "npm start -w topics"
},
"dependencies": {
"@docusaurus/core": "^3.8.1",
"@docusaurus/faster": "^3.8.1",
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/plugin-client-redirects": "^3.8.1",
"@docusaurus/plugin-content-docs": "^3.8.1",
"@docusaurus/preset-classic": "^3.8.1",
"@docusaurus/remark-plugin-npm2yarn": "^3.8.1",
"@docusaurus/theme-common": "^3.8.1",
"@docusaurus/theme-mermaid": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@docusaurus/types": "^3.8.1",
"@eslint/js": "^9.29.0",
"@goauthentik/docusaurus-config": "^2.1.1",
"@goauthentik/docusaurus-theme": "*",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/prettier-config": "^2.0.1",
"@goauthentik/tsconfig": "^1.0.4",
"@mdx-js/react": "^3.1.0",
"@reduxjs/toolkit": "^1.7.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/lodash": "^4.17.18",
"@types/node": "^24.0.3",
"@types/pako": "^2.0.3",
"@types/pidusage": "^2.0.5",
"@types/postman-collection": "^3.5.11",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"docusaurus-plugin-openapi-docs": "^4.4.0",
"docusaurus-theme-openapi-docs": "^4.4.0",
"eslint": "^9.29.0",
"fast-glob": "^3.3.3",
"netlify-cli": "^22.2.1",
"netlify-plugin-cache": "^1.0.3",
"npm-run-all": "^4.1.5",
"pako": "^2.1.0",
"pidtree": "^0.6.0",
"pidusage": "^4.0.1",
"postcss": "^8.5.6",
"prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.15",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-before-after-slider-component": "^1.1.8",
"react-dom": "^18.3.1",
"react-redux": "^7.2.0",
"remark-directive": "^4.0.0",
"remark-github": "^12.0.0",
"semver": "^7.7.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.35.0"
},
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.3.15",
"@rspack/binding-linux-arm64-gnu": "1.3.15",
"@rspack/binding-linux-x64-gnu": "1.3.15",
"@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",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"
},
"engines": {
"node": ">=24"
},
"workspaces": [
"api",
"docusaurus-theme",
"integrations",
"topics"
],
"prettier": "@goauthentik/prettier-config",
"overrides": {
"@rspack/core": "1.4.0-rc.0"
}
}

View File

@ -1,3 +0,0 @@
### Global customization
See [Brand Settings](../../../sys-mgmt/brands.md#branding-settings)

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

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

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

@ -1 +0,0 @@
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);

View File

@ -1,92 +0,0 @@
/**
* @file Docusaurus Documentation config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
* @import { ReleasesPluginOptions } from "@goauthentik/docusaurus-theme/releases/plugin"
*/
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { CommonConfig, CommonDocsPluginOptions } from "@goauthentik/docusaurus-theme/config";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
import { GlobExcludeDefault } from "@docusaurus/utils";
import { deepmerge } from "deepmerge-ts";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const require = createRequire(import.meta.url);
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
const config = {
staticDirectories: [
// ---
resolve(__dirname, "..", "static"),
"static",
],
themes: ["@goauthentik/docusaurus-theme"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
navbarReplacements: {
DOCS_URL: "/",
},
algolia: {
externalUrlRegex: /^(?:https?:\/\/)(integrations|api).?(goauthentik.io)/.source,
},
}),
plugins: [
[
"@docusaurus/theme-classic",
{
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
},
],
[
"@goauthentik/docusaurus-theme/releases/plugin",
/** @type {ReleasesPluginOptions} */ ({
docsDirectory: __dirname,
}),
],
//#region Documentation
[
"@docusaurus/plugin-content-docs",
deepmerge(
CommonDocsPluginOptions,
/** @type {DocsPluginOptions} */ ({
id: "docs",
routeBasePath: "/",
path: ".",
exclude: [...GlobExcludeDefault],
include: ["**/*.mdx", "**/*.md"],
sidebarPath: "./sidebar.mjs",
showLastUpdateTime: false,
editUrl: "https://github.com/goauthentik/authentik/edit/main/docs/",
//#region Docs Plugins
beforeDefaultRemarkPlugins: [
remarkLinkRewrite([
// ---
["/docs", ""],
["/api", "https://api.goauthentik.io"],
["/integrations", "https://integrations.goauthentik.io"],
]),
],
}),
),
],
],
};
export default /** @type {Config} */ (deepmerge(CommonConfig, createDocusaurusConfig(config)));

View File

@ -1,46 +0,0 @@
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [".docusaurus", ".cache", 'node_modules/.cache']
[[plugins]]
package = "netlify-plugin-debug-cache"
[build]
base = "docs"
package = "topics"
command = "npm run build -w topics"
publish = "topics/build"
[dev]
command = "npm start"
targetPort = 3000
publish = "topics/build"
[context.production.environment]
NODE_ENV = "production"
[context.dev.environment]
NODE_ENV = "development"
# Migration from docs to separate directory
[[redirects]]
from = "/docs/integrations/*"
to = "https://integrations.goauthentik.io/:splat"
status = 302
[[redirects]]
from = "/integrations/*"
to = "https://integrations.goauthentik.io/:splat"
status = 302
[[redirects]]
from = "/docs/*"
to = "/:splat"
status = 302
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"

View File

@ -1,22 +0,0 @@
{
"name": "@goauthentik/docs-topics",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:types build:docusaurus",
"build:docusaurus": "docusaurus build",
"build:types": "tsc -b .",
"deploy": "docusaurus deploy",
"docusaurus": "docusaurus",
"serve": "docusaurus serve",
"start": "docusaurus start",
"test": "node --test"
},
"dependencies": {
"@goauthentik/docusaurus-theme": "*"
},
"engines": {
"node": ">=24"
}
}

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../docusaurus-theme"
}
]
}

View File

@ -1,27 +0,0 @@
// @file TSConfig used by the docs package during development.
//
// @remarks
// While this configuration will influence the IDE experience,
// Docusaurus will instead use an internal configuration to build the site.
//
// @see https://docusaurus.io/docs/typescript-support
{
"extends": "./tsconfig.base.json",
"files": [],
"references": [
{
"path": "./docusaurus-theme"
},
{
"path": "./api"
},
{
"path": "./integrations"
},
{
"path": "./topics"
}
]
}

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
}

Some files were not shown because too many files have changed in this diff Show More