Compare commits
60 Commits
docusaurus
...
eap-but-ac
Author | SHA1 | Date | |
---|---|---|---|
06848be14b | |||
4bae3bbe60 | |||
e33f839d7f | |||
f5eb827d14 | |||
9045f5ba73 | |||
7b97e92094 | |||
3027cdcc4b | |||
67f627a925 | |||
f1101e0c01 | |||
fb01a117ad | |||
fad18db70b | |||
e0c837257c | |||
2a567ccc85 | |||
e36373ceab | |||
d8a625be03 | |||
4d944f7444 | |||
c49274042b | |||
10fc15ffe0 | |||
7c996d9d9d | |||
5d25f68b71 | |||
8da54d5811 | |||
4571f5e644 | |||
ee234ea3aa | |||
82c177b7eb | |||
1155ccb3e8 | |||
1575b96262 | |||
19bb77638a | |||
d6cf129eaa | |||
b6686cff14 | |||
8cf8f1e199 | |||
50c50c4109 | |||
51f4a8d83d | |||
3ada3a7e0e | |||
fa06c9fe4e | |||
2a024238fe | |||
91c87b7c3c | |||
318443f270 | |||
ac88784089 | |||
855afa7b9f | |||
240abfef41 | |||
03075f1890 | |||
5bc0ed6e11 | |||
8f4cfc28c7 | |||
6d77eaaab7 | |||
9cee59537c | |||
fc5c0e2789 | |||
573446689f | |||
fd4bfe604d | |||
06e76a5b37 | |||
3c228bf5c3 | |||
8a80f07db2 | |||
ae59a3e576 | |||
df21e678d6 | |||
a71532b3e3 | |||
d7cb0b3ea1 | |||
ba8f137885 | |||
958ff66070 | |||
ad57c66a32 | |||
2bba0ddd74 | |||
767c0a8e45 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -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:
|
||||
|
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@ -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`)
|
||||
|
83
.github/workflows/ci-api-docs.yml
vendored
83
.github/workflows/ci-api-docs.yml
vendored
@ -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 }}
|
4
.github/workflows/ci-outpost.yml
vendored
4
.github/workflows/ci-outpost.yml
vendored
@ -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
|
||||
|
@ -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:
|
2
.github/workflows/release-publish.yml
vendored
2
.github/workflows/release-publish.yml
vendored
@ -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: .
|
||||
|
@ -10,7 +10,7 @@ coverage
|
||||
dist
|
||||
out
|
||||
.docusaurus
|
||||
docs/api/reference
|
||||
website/docs/developer-docs/api/**/*
|
||||
|
||||
## Environment
|
||||
*.env
|
||||
|
44
.vscode/tasks.json
vendored
44
.vscode/tasks.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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 && \
|
||||
|
2
LICENSE
2
LICENSE
@ -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.
|
||||
|
26
Makefile
26
Makefile
@ -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
|
||||
|
@ -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
|
||||
|
@ -44,6 +44,7 @@ class RadiusProviderSerializer(ProviderSerializer):
|
||||
"shared_secret",
|
||||
"outpost_set",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
@ -79,6 +80,7 @@ class RadiusOutpostConfigSerializer(ModelSerializer):
|
||||
"client_networks",
|
||||
"shared_secret",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-16 13:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0004_alter_certificatekeypair_name"),
|
||||
("authentik_providers_radius", "0004_alter_radiusproviderpropertymapping_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="radiusprovider",
|
||||
name="certificate",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
]
|
@ -1,11 +1,14 @@
|
||||
"""Radius Provider"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import PropertyMapping, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
@ -38,6 +41,10 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
),
|
||||
)
|
||||
|
||||
certificate = models.ForeignKey(
|
||||
CertificateKeyPair, on_delete=models.CASCADE, default=None, null=True
|
||||
)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> str | None:
|
||||
"""Radius never has a launch URL"""
|
||||
@ -57,6 +64,12 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
|
||||
return RadiusProviderSerializer
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
required_models = [self, "authentik_stages_mtls.pass_outpost_certificate"]
|
||||
if self.certificate is not None:
|
||||
required_models.append(self.certificate)
|
||||
return required_models
|
||||
|
||||
def __str__(self):
|
||||
return f"Radius Provider {self.name}"
|
||||
|
||||
|
@ -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": []
|
||||
|
@ -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
|
@ -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
|
@ -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.
|
@ -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 />
|
@ -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.
|
@ -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.
|
||||
:::
|
@ -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
|
||||
```
|
@ -1 +0,0 @@
|
||||
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);
|
@ -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);
|
@ -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);
|
||||
}
|
@ -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.
|
@ -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"
|
@ -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": "*"
|
||||
}
|
||||
}
|
@ -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;
|
@ -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}}}
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
.docItemContainer header + *,
|
||||
.docItemContainer article > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 997px) {
|
||||
.docItemCol {
|
||||
max-width: 75% !important;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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"];
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"references": [
|
||||
{
|
||||
"path": "../docusaurus-theme"
|
||||
}
|
||||
]
|
||||
}
|
37
docs/api/types/api-plugin.d.ts
vendored
37
docs/api/types/api-plugin.d.ts
vendored
@ -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;
|
||||
}
|
34
docs/api/types/globals.d.ts
vendored
34
docs/api/types/globals.d.ts
vendored
@ -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"];
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
},
|
||||
);
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||
|
||||
export default createESLintPackageConfig();
|
@ -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";
|
||||
},
|
||||
};
|
||||
}
|
@ -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/*"
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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";
|
@ -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;
|
@ -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);
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json"
|
||||
}
|
44
docs/docusaurus-theme/types/globals.d.ts
vendored
44
docs/docusaurus-theme/types/globals.d.ts
vendored
@ -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"];
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { DefaultIgnorePatterns, createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||
|
||||
export default createESLintPackageConfig({
|
||||
ignorePatterns: [
|
||||
// ---
|
||||
...DefaultIgnorePatterns,
|
||||
"**/.docusaurus/",
|
||||
"**/build",
|
||||
"**/reference",
|
||||
],
|
||||
});
|
@ -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;
|
||||
}
|
@ -1 +0,0 @@
|
||||
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);
|
@ -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)));
|
@ -1,10 +0,0 @@
|
||||
import { DefaultIgnorePatterns, createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||
|
||||
export default createESLintPackageConfig({
|
||||
ignorePatterns: [
|
||||
// ---
|
||||
...DefaultIgnorePatterns,
|
||||
".docusaurus/",
|
||||
"./build",
|
||||
],
|
||||
});
|
@ -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"
|
@ -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"
|
||||
}
|
||||
}
|
@ -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,
|
||||
},
|
||||
],
|
||||
})),
|
||||
],
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"references": [
|
||||
{
|
||||
"path": "../docusaurus-theme"
|
||||
}
|
||||
]
|
||||
}
|
13
docs/integrations/types/globals.d.ts
vendored
13
docs/integrations/types/globals.d.ts
vendored
@ -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" />
|
@ -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"
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
### Global customization
|
||||
|
||||
See [Brand Settings](../../../sys-mgmt/brands.md#branding-settings)
|
@ -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 />
|
@ -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 />
|
@ -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 />
|
@ -1 +0,0 @@
|
||||
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);
|
@ -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)));
|
@ -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"
|
@ -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"
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"references": [
|
||||
{
|
||||
"path": "../docusaurus-theme"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -34,9 +34,10 @@ var (
|
||||
type SolverFunction func(*api.ChallengeTypes, api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error)
|
||||
|
||||
type FlowExecutor struct {
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
InteractiveSolver SolverFunction
|
||||
|
||||
solvers map[StageComponent]SolverFunction
|
||||
|
||||
@ -94,6 +95,10 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
|
||||
return fe
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) AddHeader(name string, value string) {
|
||||
fe.api.GetConfig().AddDefaultHeader(name, value)
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := fe.transport.RoundTrip(req)
|
||||
if res != nil {
|
||||
@ -110,7 +115,7 @@ func (fe *FlowExecutor) ApiClient() *api.APIClient {
|
||||
return fe.api
|
||||
}
|
||||
|
||||
type challengeCommon interface {
|
||||
type ChallengeCommon interface {
|
||||
GetComponent() string
|
||||
GetResponseErrors() map[string][]api.ErrorDetail
|
||||
}
|
||||
@ -165,7 +170,7 @@ func (fe *FlowExecutor) getInitialChallenge() (*api.ChallengeTypes, error) {
|
||||
if i == nil {
|
||||
return nil, errors.New("response instance was null")
|
||||
}
|
||||
ch := i.(challengeCommon)
|
||||
ch := i.(ChallengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got challenge")
|
||||
gcsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
gcsp.Finish()
|
||||
@ -184,7 +189,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response request instance was null")
|
||||
}
|
||||
ch := i.(challengeCommon)
|
||||
ch := i.(ChallengeCommon)
|
||||
|
||||
// Check for any validation errors that we might've gotten
|
||||
if len(ch.GetResponseErrors()) > 0 {
|
||||
@ -201,11 +206,17 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
case string(StageRedirect):
|
||||
return true, nil
|
||||
default:
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
var err error
|
||||
var rr api.FlowChallengeResponseRequest
|
||||
if fe.InteractiveSolver != nil {
|
||||
rr, err = fe.InteractiveSolver(challenge, responseReq)
|
||||
} else {
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
}
|
||||
rr, err = solver(challenge, responseReq)
|
||||
}
|
||||
rr, err := solver(challenge, responseReq)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -220,7 +231,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response instance was null")
|
||||
}
|
||||
ch = i.(challengeCommon)
|
||||
ch = i.(ChallengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got response")
|
||||
scsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
scsp.Finish()
|
||||
|
@ -8,6 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
var a challengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
var a ChallengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
assert.NotNil(t, a)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func parseCIDRs(raw string) []*net.IPNet {
|
||||
@ -41,26 +42,28 @@ func (rs *RadiusServer) Refresh() error {
|
||||
if len(apiProviders) < 1 {
|
||||
return errors.New("no radius provider defined")
|
||||
}
|
||||
providers := make([]*ProviderInstance, len(apiProviders))
|
||||
for idx, provider := range apiProviders {
|
||||
providers := make(map[int32]*ProviderInstance)
|
||||
for _, provider := range apiProviders {
|
||||
existing, ok := rs.providers[provider.Pk]
|
||||
state := map[string]*protocol.State{}
|
||||
if ok {
|
||||
state = existing.eapState
|
||||
}
|
||||
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
|
||||
providers[idx] = &ProviderInstance{
|
||||
providers[provider.Pk] = &ProviderInstance{
|
||||
SharedSecret: []byte(provider.GetSharedSecret()),
|
||||
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
|
||||
MFASupport: provider.GetMfaSupport(),
|
||||
appSlug: provider.ApplicationSlug,
|
||||
flowSlug: provider.AuthFlowSlug,
|
||||
certId: provider.GetCertificate(),
|
||||
providerId: provider.Pk,
|
||||
s: rs,
|
||||
log: logger,
|
||||
eapState: state,
|
||||
}
|
||||
}
|
||||
rs.providers = providers
|
||||
rs.log.Info("Update providers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) StartRadiusServer() error {
|
||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||
return rs.s.ListenAndServe()
|
||||
}
|
||||
|
44
internal/outpost/radius/eap/README.md
Normal file
44
internal/outpost/radius/eap/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
# EAP protocol implementation
|
||||
|
||||
Install `eapol_test` (`sudo apt install eapoltest`)
|
||||
|
||||
Both PEAP and EAP-TLS require a minimal PKI setup. A CA, a certificate for the server and for EAP-TLS a client certificate need to be provided.
|
||||
|
||||
Save either of the config files below and run eapoltest like so:
|
||||
|
||||
```
|
||||
# peap.conf is the config file under the PEAP testing section
|
||||
# foo is the shared RADIUS secret
|
||||
# 1.2.3.4 is the IP of the RADIUS server
|
||||
eapol_test -c peap.conf -s foo -a 1.2.3.4
|
||||
```
|
||||
|
||||
### PEAP testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=PEAP
|
||||
identity="foo"
|
||||
password="bar"
|
||||
ca_cert="ca.pem"
|
||||
phase2="auth=MSCHAPV2"
|
||||
}
|
||||
```
|
||||
|
||||
### EAP-TLS testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=TLS
|
||||
identity="foo"
|
||||
ca_cert="ca.pem"
|
||||
client_cert="cert_client.pem"
|
||||
private_key="cert_client.key"
|
||||
eapol_flags=3
|
||||
eap_workaround=0
|
||||
}
|
||||
```
|
55
internal/outpost/radius/eap/context.go
Normal file
55
internal/outpost/radius/eap/context.go
Normal file
@ -0,0 +1,55 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
req *radius.Request
|
||||
rootPayload protocol.Payload
|
||||
typeState map[protocol.Type]any
|
||||
log *log.Entry
|
||||
settings interface{}
|
||||
parent *context
|
||||
endStatus protocol.Status
|
||||
handleInner func(protocol.Payload, protocol.StateManager, protocol.Context) (protocol.Payload, error)
|
||||
}
|
||||
|
||||
func (ctx *context) RootPayload() protocol.Payload { return ctx.rootPayload }
|
||||
func (ctx *context) Packet() *radius.Request { return ctx.req }
|
||||
func (ctx *context) ProtocolSettings() any { return ctx.settings }
|
||||
func (ctx *context) GetProtocolState(p protocol.Type) any { return ctx.typeState[p] }
|
||||
func (ctx *context) SetProtocolState(p protocol.Type, st any) { ctx.typeState[p] = st }
|
||||
func (ctx *context) IsProtocolStart(p protocol.Type) bool { return ctx.typeState[p] == nil }
|
||||
func (ctx *context) Log() *log.Entry { return ctx.log }
|
||||
func (ctx *context) HandleInnerEAP(p protocol.Payload, st protocol.StateManager) (protocol.Payload, error) {
|
||||
return ctx.handleInner(p, st, ctx)
|
||||
}
|
||||
func (ctx *context) Inner(p protocol.Payload, t protocol.Type) protocol.Context {
|
||||
nctx := &context{
|
||||
req: ctx.req,
|
||||
rootPayload: ctx.rootPayload,
|
||||
typeState: ctx.typeState,
|
||||
log: ctx.log.WithField("type", fmt.Sprintf("%T", p)).WithField("code", t),
|
||||
settings: ctx.settings,
|
||||
parent: ctx,
|
||||
handleInner: ctx.handleInner,
|
||||
}
|
||||
nctx.log.Debug("Creating inner context")
|
||||
return nctx
|
||||
}
|
||||
func (ctx *context) EndInnerProtocol(st protocol.Status) {
|
||||
ctx.log.Info("Ending protocol")
|
||||
if ctx.parent != nil {
|
||||
ctx.parent.EndInnerProtocol(st)
|
||||
return
|
||||
}
|
||||
if ctx.endStatus != protocol.StatusUnknown {
|
||||
return
|
||||
}
|
||||
ctx.endStatus = st
|
||||
}
|
13
internal/outpost/radius/eap/debug/debug.go
Normal file
13
internal/outpost/radius/eap/debug/debug.go
Normal file
@ -0,0 +1,13 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FormatBytes(d []byte) string {
|
||||
b := d
|
||||
if len(b) > 32 {
|
||||
b = b[:32]
|
||||
}
|
||||
return fmt.Sprintf("% x", b)
|
||||
}
|
182
internal/outpost/radius/eap/handler.go
Normal file
182
internal/outpost/radius/eap/handler.go
Normal file
@ -0,0 +1,182 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2865"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
func sendErrorResponse(w radius.ResponseWriter, r *radius.Request) {
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
err := w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) HandleRadiusPacket(w radius.ResponseWriter, r *radius.Request) {
|
||||
p.r = r
|
||||
rst := rfc2865.State_GetString(r.Packet)
|
||||
if rst == "" {
|
||||
rst = base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(12))
|
||||
}
|
||||
p.state = rst
|
||||
|
||||
rp := &Packet{r: r}
|
||||
rep, err := p.handleEAP(p.eap, p.stm, nil)
|
||||
rp.eap = rep
|
||||
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
if err == nil {
|
||||
switch rp.eap.Code {
|
||||
case protocol.CodeRequest:
|
||||
rres.Code = radius.CodeAccessChallenge
|
||||
case protocol.CodeFailure:
|
||||
rres.Code = radius.CodeAccessReject
|
||||
case protocol.CodeSuccess:
|
||||
rres.Code = radius.CodeAccessAccept
|
||||
}
|
||||
} else {
|
||||
rres.Code = radius.CodeAccessReject
|
||||
log.WithError(err).Debug("Rejecting request")
|
||||
}
|
||||
for _, mod := range p.responseModifiers {
|
||||
err := mod.ModifyRADIUSResponse(rres, r.Packet)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Root-EAP: failed to modify response packet")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rfc2865.State_SetString(rres, p.state)
|
||||
eapEncoded, err := rp.Encode()
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to encode response")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
log.WithField("length", len(eapEncoded)).WithField("type", fmt.Sprintf("%T", rp.eap.Payload)).Debug("Root-EAP: encapsulated challenge")
|
||||
rfc2869.EAPMessage_Set(rres, eapEncoded)
|
||||
err = p.setMessageAuthenticator(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send message authenticator")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
err = w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) handleEAP(pp protocol.Payload, stm protocol.StateManager, parentContext *context) (*eap.Payload, error) {
|
||||
st := stm.GetEAPState(p.state)
|
||||
if st == nil {
|
||||
log.Debug("Root-EAP: blank state")
|
||||
st = protocol.BlankState(stm.GetEAPSettings())
|
||||
}
|
||||
|
||||
nextChallengeToOffer, err := st.GetNextProtocol()
|
||||
if err != nil {
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeFailure,
|
||||
ID: p.eap.ID,
|
||||
}, err
|
||||
}
|
||||
|
||||
next := func() (*eap.Payload, error) {
|
||||
st.ProtocolIndex += 1
|
||||
st.TypeState = map[protocol.Type]any{}
|
||||
stm.SetEAPState(p.state, st)
|
||||
return p.handleEAP(pp, stm, nil)
|
||||
}
|
||||
|
||||
if n, ok := pp.(*eap.Payload).Payload.(*legacy_nak.Payload); ok {
|
||||
log.WithField("desired", n.DesiredType).Debug("Root-EAP: received NAK, trying next protocol")
|
||||
pp.(*eap.Payload).Payload = nil
|
||||
return next()
|
||||
}
|
||||
|
||||
np, t, _ := eap.EmptyPayload(stm.GetEAPSettings(), nextChallengeToOffer)
|
||||
|
||||
var ctx *context
|
||||
if parentContext != nil {
|
||||
ctx = parentContext.Inner(np, t).(*context)
|
||||
ctx.settings = stm.GetEAPSettings().ProtocolSettings[np.Type()]
|
||||
} else {
|
||||
ctx = &context{
|
||||
req: p.r,
|
||||
rootPayload: p.eap,
|
||||
typeState: st.TypeState,
|
||||
log: log.WithField("type", fmt.Sprintf("%T", np)).WithField("code", t),
|
||||
settings: stm.GetEAPSettings().ProtocolSettings[t],
|
||||
}
|
||||
ctx.handleInner = func(pp protocol.Payload, sm protocol.StateManager, ctx protocol.Context) (protocol.Payload, error) {
|
||||
// cctx := ctx.Inner(np, np.Type(), nil).(*context)
|
||||
return p.handleEAP(pp, sm, ctx.(*context))
|
||||
}
|
||||
}
|
||||
if !np.Offerable() {
|
||||
ctx.Log().Debug("Root-EAP: protocol not offerable, skipping")
|
||||
return next()
|
||||
}
|
||||
ctx.Log().Debug("Root-EAP: Passing to protocol")
|
||||
|
||||
res := &eap.Payload{
|
||||
Code: protocol.CodeRequest,
|
||||
ID: p.eap.ID + 1,
|
||||
MsgType: t,
|
||||
}
|
||||
var payload any
|
||||
if reflect.TypeOf(pp.(*eap.Payload).Payload) == reflect.TypeOf(np) {
|
||||
np.Decode(pp.(*eap.Payload).RawPayload)
|
||||
}
|
||||
payload = np.Handle(ctx)
|
||||
if payload != nil {
|
||||
res.Payload = payload.(protocol.Payload)
|
||||
}
|
||||
|
||||
stm.SetEAPState(p.state, st)
|
||||
|
||||
if rm, ok := np.(protocol.ResponseModifier); ok {
|
||||
ctx.log.Debug("Root-EAP: Registered response modifier")
|
||||
p.responseModifiers = append(p.responseModifiers, rm)
|
||||
}
|
||||
|
||||
switch ctx.endStatus {
|
||||
case protocol.StatusSuccess:
|
||||
res.Code = protocol.CodeSuccess
|
||||
res.ID -= 1
|
||||
case protocol.StatusError:
|
||||
res.Code = protocol.CodeFailure
|
||||
res.ID -= 1
|
||||
case protocol.StatusNextProtocol:
|
||||
ctx.log.Debug("Root-EAP: Protocol ended, starting next protocol")
|
||||
return next()
|
||||
case protocol.StatusUnknown:
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *Packet) setMessageAuthenticator(rp *radius.Packet) error {
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
|
||||
hash := hmac.New(md5.New, rp.Secret)
|
||||
encode, err := rp.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash.Write(encode)
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
|
||||
return nil
|
||||
}
|
34
internal/outpost/radius/eap/packet.go
Normal file
34
internal/outpost/radius/eap/packet.go
Normal file
@ -0,0 +1,34 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Packet struct {
|
||||
r *radius.Request
|
||||
eap *eap.Payload
|
||||
stm protocol.StateManager
|
||||
state string
|
||||
responseModifiers []protocol.ResponseModifier
|
||||
}
|
||||
|
||||
func Decode(stm protocol.StateManager, raw []byte) (*Packet, error) {
|
||||
packet := &Packet{
|
||||
eap: &eap.Payload{
|
||||
Settings: stm.GetEAPSettings(),
|
||||
},
|
||||
stm: stm,
|
||||
responseModifiers: []protocol.ResponseModifier{},
|
||||
}
|
||||
err := packet.eap.Decode(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func (p *Packet) Encode() ([]byte, error) {
|
||||
return p.eap.Encode()
|
||||
}
|
32
internal/outpost/radius/eap/protocol/context.go
Normal file
32
internal/outpost/radius/eap/protocol/context.go
Normal file
@ -0,0 +1,32 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusUnknown Status = iota
|
||||
StatusSuccess
|
||||
StatusError
|
||||
StatusNextProtocol
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
Packet() *radius.Request
|
||||
RootPayload() Payload
|
||||
|
||||
ProtocolSettings() interface{}
|
||||
|
||||
GetProtocolState(p Type) interface{}
|
||||
SetProtocolState(p Type, s interface{})
|
||||
IsProtocolStart(p Type) bool
|
||||
|
||||
HandleInnerEAP(Payload, StateManager) (Payload, error)
|
||||
Inner(Payload, Type) Context
|
||||
EndInnerProtocol(Status)
|
||||
|
||||
Log() *log.Entry
|
||||
}
|
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
@ -0,0 +1,23 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func EmptyPayload(settings protocol.Settings, t protocol.Type) (protocol.Payload, protocol.Type, error) {
|
||||
for _, cons := range settings.Protocols {
|
||||
np := cons()
|
||||
if np.Type() == t {
|
||||
return np, np.Type(), nil
|
||||
}
|
||||
// If the protocol has an inner protocol, return the original type but the code for the inner protocol
|
||||
if i, ok := np.(protocol.Inner); ok {
|
||||
if ii := i.HasInner(); ii != nil {
|
||||
return np, ii.Type(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, protocol.Type(0), fmt.Errorf("unsupported EAP type %d", t)
|
||||
}
|
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
@ -0,0 +1,96 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeEAP protocol.Type = 0
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Code protocol.Code
|
||||
ID uint8
|
||||
Length uint16
|
||||
MsgType protocol.Type
|
||||
Payload protocol.Payload
|
||||
RawPayload []byte
|
||||
|
||||
Settings protocol.Settings
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeEAP
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Code = protocol.Code(raw[0])
|
||||
p.ID = raw[1]
|
||||
p.Length = binary.BigEndian.Uint16(raw[2:])
|
||||
if p.Length != uint16(len(raw)) {
|
||||
return fmt.Errorf("mismatched packet length; got %d, expected %d", p.Length, uint16(len(raw)))
|
||||
}
|
||||
if len(raw) > 4 && (p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse) {
|
||||
p.MsgType = protocol.Type(raw[4])
|
||||
}
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Trace("EAP: decode raw")
|
||||
p.RawPayload = raw[5:]
|
||||
if p.Payload == nil {
|
||||
pp, _, err := EmptyPayload(p.Settings, p.MsgType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Payload = pp
|
||||
}
|
||||
err := p.Payload.Decode(raw[5:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
buff := make([]byte, 4)
|
||||
buff[0] = uint8(p.Code)
|
||||
buff[1] = uint8(p.ID)
|
||||
|
||||
if p.Payload != nil {
|
||||
payloadBuffer, err := p.Payload.Encode()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
if p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse {
|
||||
buff = append(buff, uint8(p.MsgType))
|
||||
}
|
||||
buff = append(buff, payloadBuffer...)
|
||||
}
|
||||
binary.BigEndian.PutUint16(buff[2:], uint16(len(buff)))
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
ctx.Log().Debug("EAP: Handle")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<EAP Packet Code=%d, ID=%d, Type=%d, Length=%d, Payload=%T>",
|
||||
p.Code,
|
||||
p.ID,
|
||||
p.MsgType,
|
||||
p.Length,
|
||||
p.Payload,
|
||||
)
|
||||
}
|
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
@ -0,0 +1,5 @@
|
||||
package eap
|
||||
|
||||
type State struct {
|
||||
PacketID uint8
|
||||
}
|
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
@ -0,0 +1,61 @@
|
||||
package gtc
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeGTC protocol.Type = 6
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Challenge []byte
|
||||
|
||||
st *State
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeGTC
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.raw = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return p.Challenge, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeGTC, p.st)
|
||||
}()
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
if ctx.IsProtocolStart(TypeGTC) {
|
||||
g, v := settings.ChallengeHandler(ctx)
|
||||
p.st = &State{
|
||||
getChallenge: g,
|
||||
validateResponse: v,
|
||||
}
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeGTC).(*State)
|
||||
p.st.validateResponse(p.raw)
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return "<GTC Packet>"
|
||||
}
|
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
@ -0,0 +1,10 @@
|
||||
package gtc
|
||||
|
||||
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
|
||||
type GetChallenge func() []byte
|
||||
type ValidateResponse func(answer []byte)
|
||||
|
||||
type Settings struct {
|
||||
ChallengeHandler func(ctx protocol.Context) (GetChallenge, ValidateResponse)
|
||||
}
|
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
@ -0,0 +1,6 @@
|
||||
package gtc
|
||||
|
||||
type State struct {
|
||||
getChallenge GetChallenge
|
||||
validateResponse ValidateResponse
|
||||
}
|
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
@ -0,0 +1,48 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeIdentity protocol.Type = 1
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Identity string
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeIdentity
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Identity = string(raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeIdentity) {
|
||||
ctx.EndInnerProtocol(protocol.StatusNextProtocol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Identity Packet Identity=%s>",
|
||||
p.Identity,
|
||||
)
|
||||
}
|
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
@ -0,0 +1,48 @@
|
||||
package legacy_nak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeLegacyNAK protocol.Type = 3
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
DesiredType protocol.Type
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeLegacyNAK
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.DesiredType = protocol.Type(raw[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{byte(p.DesiredType)}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeLegacyNAK) {
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Legacy NAK Packet DesiredType=%d>",
|
||||
p.DesiredType,
|
||||
)
|
||||
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
@ -0,0 +1,23 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Challenge []byte
|
||||
NTResponse []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func ParseResponse(raw []byte) (*Response, error) {
|
||||
res := &Response{}
|
||||
res.Challenge = raw[:challengeValueSize]
|
||||
if !bytes.Equal(raw[challengeValueSize:challengeValueSize+responseReservedSize], make([]byte, 8)) {
|
||||
return nil, errors.New("MSCHAPv2: Reserved bytes not empty?")
|
||||
}
|
||||
res.NTResponse = raw[challengeValueSize+responseReservedSize : challengeValueSize+responseReservedSize+responseNTResponseSize]
|
||||
res.Flags = (raw[challengeValueSize+responseReservedSize+responseNTResponseSize])
|
||||
return res, nil
|
||||
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
@ -0,0 +1,23 @@
|
||||
package mschapv2
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
type SuccessRequest struct {
|
||||
*Payload
|
||||
Authenticator []byte
|
||||
}
|
||||
|
||||
// A success request is encoded slightly differently, it doesn't have a challenge and as such
|
||||
// doesn't need to encode the length of it
|
||||
func (sr *SuccessRequest) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(sr.OpCode),
|
||||
sr.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
encoded = append(encoded, sr.Authenticator...)
|
||||
sr.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], sr.MSLength)
|
||||
return encoded, nil
|
||||
}
|
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
@ -0,0 +1,196 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/vendors/microsoft"
|
||||
)
|
||||
|
||||
const TypeMSCHAPv2 protocol.Type = 26
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
const (
|
||||
challengeValueSize = 16
|
||||
responseValueSize = 49
|
||||
responseReservedSize = 8
|
||||
responseNTResponseSize = 24
|
||||
)
|
||||
|
||||
type OpCode uint8
|
||||
|
||||
const (
|
||||
OpChallenge OpCode = 1
|
||||
OpResponse OpCode = 2
|
||||
OpSuccess OpCode = 3
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
OpCode OpCode
|
||||
MSCHAPv2ID uint8
|
||||
MSLength uint16
|
||||
ValueSize uint8
|
||||
|
||||
Challenge []byte
|
||||
Response []byte
|
||||
|
||||
Name []byte
|
||||
|
||||
st *State
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeMSCHAPv2
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debugf("MSCHAPv2: decode raw")
|
||||
p.OpCode = OpCode(raw[0])
|
||||
if p.OpCode == OpSuccess {
|
||||
return nil
|
||||
}
|
||||
// TODO: Validate against root EAP packet
|
||||
p.MSCHAPv2ID = raw[1]
|
||||
p.MSLength = binary.BigEndian.Uint16(raw[2:])
|
||||
|
||||
p.ValueSize = raw[4]
|
||||
if p.ValueSize != responseValueSize {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect value size: %d", p.ValueSize)
|
||||
}
|
||||
p.Response = raw[5 : p.ValueSize+5]
|
||||
p.Name = raw[5+p.ValueSize:]
|
||||
if int(p.MSLength) != len(raw) {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect MS-Length: %d, should be %d", p.MSLength, len(raw))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(p.OpCode),
|
||||
p.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
byte(len(p.Challenge)),
|
||||
}
|
||||
encoded = append(encoded, p.Challenge...)
|
||||
encoded = append(encoded, p.Name...)
|
||||
p.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], p.MSLength)
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeMSCHAPv2, p.st)
|
||||
}()
|
||||
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
|
||||
if ctx.IsProtocolStart(TypeMSCHAPv2) {
|
||||
ctx.Log().Debug("MSCHAPv2: Empty state, starting")
|
||||
p.st = &State{
|
||||
Challenge: securecookie.GenerateRandomKey(challengeValueSize),
|
||||
}
|
||||
return &Payload{
|
||||
OpCode: OpChallenge,
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
Challenge: p.st.Challenge,
|
||||
Name: []byte("authentik"),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeMSCHAPv2).(*State)
|
||||
|
||||
response := &Payload{
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
}
|
||||
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
|
||||
ctx.Log().Debugf("MSCHAPv2: OpCode: %d", p.OpCode)
|
||||
if p.OpCode == OpResponse {
|
||||
res, err := ParseResponse(p.Response)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to parse response")
|
||||
return nil
|
||||
}
|
||||
p.st.PeerChallenge = res.Challenge
|
||||
auth, err := settings.AuthenticateRequest(AuthRequest{
|
||||
Challenge: p.st.Challenge,
|
||||
PeerChallenge: p.st.PeerChallenge,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to check password")
|
||||
return nil
|
||||
}
|
||||
if !bytes.Equal(auth.NTResponse, res.NTResponse) {
|
||||
ctx.Log().Warning("MSCHAPv2: NT response mismatch")
|
||||
return nil
|
||||
}
|
||||
ctx.Log().Info("MSCHAPv2: Successfully checked password")
|
||||
p.st.AuthResponse = auth
|
||||
succ := &SuccessRequest{
|
||||
Payload: &Payload{
|
||||
OpCode: OpSuccess,
|
||||
},
|
||||
Authenticator: []byte(auth.AuthenticatorResponse),
|
||||
}
|
||||
return succ
|
||||
} else if p.OpCode == OpSuccess && p.st.AuthResponse != nil {
|
||||
ep := &peap.ExtensionPayload{
|
||||
AVPs: []peap.ExtensionAVP{
|
||||
{
|
||||
Mandatory: true,
|
||||
Type: peap.AVPAckResult,
|
||||
Value: []byte{0, 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
p.st.IsProtocolEnded = true
|
||||
return ep
|
||||
} else if p.st.IsProtocolEnded {
|
||||
ctx.EndInnerProtocol(protocol.StatusSuccess)
|
||||
return &Payload{}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||
if p.st == nil || p.st.AuthResponse == nil {
|
||||
return nil
|
||||
}
|
||||
if r.Code != radius.CodeAccessAccept {
|
||||
return nil
|
||||
}
|
||||
log.Debug("MSCHAPv2: Radius modifier")
|
||||
if len(microsoft.MSMPPERecvKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPERecvKey_Set(r, p.st.AuthResponse.RecvKey)
|
||||
}
|
||||
if len(microsoft.MSMPPESendKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPESendKey_Set(r, p.st.AuthResponse.SendKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<MSCHAPv2 Packet OpCode=%d, MSCHAPv2ID=%d>",
|
||||
p.OpCode,
|
||||
p.MSCHAPv2ID,
|
||||
)
|
||||
}
|
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
@ -0,0 +1,50 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"layeh.com/radius/rfc2759"
|
||||
"layeh.com/radius/rfc3079"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
AuthenticateRequest func(req AuthRequest) (*AuthResponse, error)
|
||||
}
|
||||
|
||||
type AuthRequest struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
NTResponse []byte
|
||||
RecvKey []byte
|
||||
SendKey []byte
|
||||
AuthenticatorResponse string
|
||||
}
|
||||
|
||||
func DebugStaticCredentials(user, password []byte) func(req AuthRequest) (*AuthResponse, error) {
|
||||
return func(req AuthRequest) (*AuthResponse, error) {
|
||||
res := &AuthResponse{}
|
||||
ntResponse, err := rfc2759.GenerateNTResponse(req.Challenge, req.PeerChallenge, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.NTResponse = ntResponse
|
||||
|
||||
res.RecvKey, err = rfc3079.MakeKey(ntResponse, password, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.SendKey, err = rfc3079.MakeKey(ntResponse, password, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.AuthenticatorResponse, err = rfc2759.GenerateAuthenticatorResponse(req.Challenge, req.PeerChallenge, ntResponse, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
}
|
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
@ -0,0 +1,8 @@
|
||||
package mschapv2
|
||||
|
||||
type State struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
IsProtocolEnded bool
|
||||
AuthResponse *AuthResponse
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user