Compare commits
34 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
e095e9f694 | |||
10e311534f | |||
46fdb45273 | |||
6d4125cb90 | |||
bc83176962 | |||
0fa8432b72 | |||
bb9a524b53 | |||
d31c05625b | |||
399223b770 | |||
19197d3f9b | |||
1cd000dfe2 | |||
00ae97944a | |||
9f3ccfb7c7 | |||
9ed9c39ac8 | |||
30b6eeee9f | |||
afe2621783 | |||
8b12c6a01a | |||
f63adfed96 | |||
9c8fec21cf | |||
4776d2bcc5 | |||
a15a040362 | |||
fcd6dc1d60 | |||
acc3b59869 | |||
d9d5ac10e6 | |||
750669dcab | |||
88a3eed67e | |||
6c214fffc4 | |||
70100fc105 | |||
3c1163fabd | |||
539e8242ff | |||
2648333590 | |||
fe828ef993 | |||
29a6530742 | |||
a6b9274c4f |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2023.10.4
|
||||
current_version = 2023.10.7
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
|
5
.github/actions/setup/action.yml
vendored
5
.github/actions/setup/action.yml
vendored
@ -2,7 +2,7 @@ name: "Setup authentik testing environment"
|
||||
description: "Setup authentik testing environment"
|
||||
|
||||
inputs:
|
||||
postgresql_tag:
|
||||
postgresql_version:
|
||||
description: "Optional postgresql image tag"
|
||||
default: "12"
|
||||
|
||||
@ -33,9 +33,8 @@ runs:
|
||||
- name: Setup dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_tag }}
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker-compose -f .github/actions/setup/docker-compose.yml up -d
|
||||
poetry env use python3.11
|
||||
poetry install
|
||||
cd web && npm ci
|
||||
- name: Generate config
|
||||
|
23
.github/workflows/ci-main.yml
vendored
23
.github/workflows/ci-main.yml
vendored
@ -48,25 +48,38 @@ jobs:
|
||||
- name: run migrations
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
test-migrations-from-stable:
|
||||
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }}
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
psql:
|
||||
- 12-alpine
|
||||
- 15-alpine
|
||||
- 16-alpine
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
- name: checkout stable
|
||||
run: |
|
||||
# Delete all poetry envs
|
||||
rm -rf /home/runner/.cache/pypoetry
|
||||
# Copy current, latest config to local
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
cp -R .github ..
|
||||
cp -R scripts ..
|
||||
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
git checkout version/$(python -c "from authentik import __version__; print(__version__)")
|
||||
rm -rf .github/ scripts/
|
||||
mv ../.github ../scripts .
|
||||
- name: Setup authentik env (ensure stable deps are installed)
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
- name: run migrations to stable
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
- name: checkout current code
|
||||
@ -76,9 +89,13 @@ jobs:
|
||||
git reset --hard HEAD
|
||||
git clean -d -fx .
|
||||
git checkout $GITHUB_SHA
|
||||
# Delete previous poetry env
|
||||
rm -rf $(poetry env info --path)
|
||||
poetry install
|
||||
- name: Setup authentik env (ensure latest deps are installed)
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
- name: migrate to latest
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
test-unittest:
|
||||
@ -97,7 +114,7 @@ jobs:
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
postgresql_tag: ${{ matrix.psql }}
|
||||
postgresql_version: ${{ matrix.psql }}
|
||||
- name: run unittest
|
||||
run: |
|
||||
poetry run make test
|
||||
|
14
Dockerfile
14
Dockerfile
@ -1,3 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build website
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as website-builder
|
||||
|
||||
@ -7,7 +9,7 @@ WORKDIR /work/website
|
||||
|
||||
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
|
||||
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
|
||||
npm ci --include=dev
|
||||
|
||||
COPY ./website /work/website/
|
||||
@ -25,7 +27,7 @@ WORKDIR /work/web
|
||||
|
||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
||||
npm ci --include=dev
|
||||
|
||||
COPY ./web /work/web/
|
||||
@ -62,8 +64,8 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/authentik ./cmd/server
|
||||
|
||||
# Stage 4: MaxMind GeoIP
|
||||
@ -89,7 +91,9 @@ ENV VENV_PATH="/ak-root/venv" \
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
PATH="/ak-root/venv/bin:$PATH"
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt \
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
apt-get update && \
|
||||
# Required for installing pip packages
|
||||
apt-get install -y --no-install-recommends build-essential pkg-config libxmlsec1-dev zlib1g-dev libpq-dev
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2023.10.4"
|
||||
__version__ = "2023.10.7"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -21,7 +21,9 @@ _other_urls = []
|
||||
for _authentik_app in get_apps():
|
||||
try:
|
||||
api_urls = import_module(f"{_authentik_app.name}.urls")
|
||||
except (ModuleNotFoundError, ImportError) as exc:
|
||||
except ModuleNotFoundError:
|
||||
continue
|
||||
except ImportError as exc:
|
||||
LOGGER.warning("Could not import app's URLs", app_name=_authentik_app.name, exc=exc)
|
||||
continue
|
||||
if not hasattr(api_urls, "api_urlpatterns"):
|
||||
|
@ -40,7 +40,7 @@ class ManagedAppConfig(AppConfig):
|
||||
meth()
|
||||
self._logger.debug("Successfully reconciled", name=name)
|
||||
except (DatabaseError, ProgrammingError, InternalError) as exc:
|
||||
self._logger.debug("Failed to run reconcile", name=name, exc=exc)
|
||||
self._logger.warning("Failed to run reconcile", name=name, exc=exc)
|
||||
|
||||
|
||||
class AuthentikBlueprintsConfig(ManagedAppConfig):
|
||||
|
@ -75,13 +75,13 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
||||
return
|
||||
if event.is_directory:
|
||||
return
|
||||
root = Path(CONFIG.get("blueprints_dir")).absolute()
|
||||
path = Path(event.src_path).absolute()
|
||||
rel_path = str(path.relative_to(root))
|
||||
if isinstance(event, FileCreatedEvent):
|
||||
LOGGER.debug("new blueprint file created, starting discovery")
|
||||
blueprints_discovery.delay()
|
||||
LOGGER.debug("new blueprint file created, starting discovery", path=rel_path)
|
||||
blueprints_discovery.delay(rel_path)
|
||||
if isinstance(event, FileModifiedEvent):
|
||||
path = Path(event.src_path)
|
||||
root = Path(CONFIG.get("blueprints_dir")).absolute()
|
||||
rel_path = str(path.relative_to(root))
|
||||
for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
|
||||
LOGGER.debug("modified blueprint file, starting apply", instance=instance)
|
||||
apply_blueprint.delay(instance.pk.hex)
|
||||
@ -98,39 +98,32 @@ def blueprints_find_dict():
|
||||
return blueprints
|
||||
|
||||
|
||||
def blueprints_find():
|
||||
def blueprints_find() -> list[BlueprintFile]:
|
||||
"""Find blueprints and return valid ones"""
|
||||
blueprints = []
|
||||
root = Path(CONFIG.get("blueprints_dir"))
|
||||
for path in root.rglob("**/*.yaml"):
|
||||
rel_path = path.relative_to(root)
|
||||
# Check if any part in the path starts with a dot and assume a hidden file
|
||||
if any(part for part in path.parts if part.startswith(".")):
|
||||
continue
|
||||
LOGGER.debug("found blueprint", path=str(path))
|
||||
with open(path, "r", encoding="utf-8") as blueprint_file:
|
||||
try:
|
||||
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
|
||||
except YAMLError as exc:
|
||||
raw_blueprint = None
|
||||
LOGGER.warning("failed to parse blueprint", exc=exc, path=str(path))
|
||||
LOGGER.warning("failed to parse blueprint", exc=exc, path=str(rel_path))
|
||||
if not raw_blueprint:
|
||||
continue
|
||||
metadata = raw_blueprint.get("metadata", None)
|
||||
version = raw_blueprint.get("version", 1)
|
||||
if version != 1:
|
||||
LOGGER.warning("invalid blueprint version", version=version, path=str(path))
|
||||
LOGGER.warning("invalid blueprint version", version=version, path=str(rel_path))
|
||||
continue
|
||||
file_hash = sha512(path.read_bytes()).hexdigest()
|
||||
blueprint = BlueprintFile(
|
||||
str(path.relative_to(root)), version, file_hash, int(path.stat().st_mtime)
|
||||
)
|
||||
blueprint = BlueprintFile(str(rel_path), version, file_hash, int(path.stat().st_mtime))
|
||||
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
|
||||
blueprints.append(blueprint)
|
||||
LOGGER.debug(
|
||||
"parsed & loaded blueprint",
|
||||
hash=file_hash,
|
||||
path=str(path),
|
||||
)
|
||||
return blueprints
|
||||
|
||||
|
||||
@ -138,10 +131,12 @@ def blueprints_find():
|
||||
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
|
||||
)
|
||||
@prefill_task
|
||||
def blueprints_discovery(self: MonitoredTask):
|
||||
def blueprints_discovery(self: MonitoredTask, path: Optional[str] = None):
|
||||
"""Find blueprints and check if they need to be created in the database"""
|
||||
count = 0
|
||||
for blueprint in blueprints_find():
|
||||
if path and blueprint.path != path:
|
||||
continue
|
||||
check_blueprint_v1_file(blueprint)
|
||||
count += 1
|
||||
self.set_status(
|
||||
@ -171,7 +166,11 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
|
||||
metadata={},
|
||||
)
|
||||
instance.save()
|
||||
LOGGER.info(
|
||||
"Creating new blueprint instance from file", instance=instance, path=instance.path
|
||||
)
|
||||
if instance.last_applied_hash != blueprint.hash:
|
||||
LOGGER.info("Applying blueprint due to changed file", instance=instance, path=instance.path)
|
||||
apply_blueprint.delay(str(instance.pk))
|
||||
|
||||
|
||||
|
@ -38,7 +38,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
|
||||
managed = ReadOnlyField()
|
||||
component = SerializerMethodField()
|
||||
icon = ReadOnlyField(source="get_icon")
|
||||
icon = ReadOnlyField(source="icon_url")
|
||||
|
||||
def get_component(self, obj: Source) -> str:
|
||||
"""Get object component so that we know how to edit the object"""
|
||||
|
@ -233,9 +233,9 @@ class UserSelfSerializer(ModelSerializer):
|
||||
def get_system_permissions(self, user: User) -> list[str]:
|
||||
"""Get all system permissions assigned to the user"""
|
||||
return list(
|
||||
user.user_permissions.filter(
|
||||
content_type__app_label="authentik_rbac", content_type__model="systempermission"
|
||||
).values_list("codename", flat=True)
|
||||
x.split(".", maxsplit=1)[1]
|
||||
for x in user.get_all_permissions()
|
||||
if x.startswith("authentik_rbac")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -44,6 +44,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||
if request:
|
||||
req.http_request = request
|
||||
self._context["request"] = req
|
||||
req.context.update(**kwargs)
|
||||
self._context.update(**kwargs)
|
||||
self.dry_run = dry_run
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||
<script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
|
||||
<script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
|
||||
|
@ -6,6 +6,7 @@
|
||||
{% block head_before %}
|
||||
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||
{% include "base/header_js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -217,6 +217,7 @@ class Event(SerializerModel, ExpiringModel):
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
||||
}
|
||||
# Special case for events created during flow execution
|
||||
# since they keep the http query within a wrapped query
|
||||
|
@ -53,7 +53,15 @@ class TestEvents(TestCase):
|
||||
"""Test plain from_http"""
|
||||
event = Event.new("unittest").from_http(self.factory.get("/"))
|
||||
self.assertEqual(
|
||||
event.context, {"http_request": {"args": {}, "method": "GET", "path": "/"}}
|
||||
event.context,
|
||||
{
|
||||
"http_request": {
|
||||
"args": {},
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"user_agent": "",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def test_from_http_clean_querystring(self):
|
||||
@ -67,6 +75,7 @@ class TestEvents(TestCase):
|
||||
"args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"user_agent": "",
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -83,6 +92,7 @@ class TestEvents(TestCase):
|
||||
"args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"user_agent": "",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -5,12 +5,13 @@ from dataclasses import asdict, is_dataclass
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from types import GeneratorType
|
||||
from types import GeneratorType, NoneType
|
||||
from typing import Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.db.models.base import Model
|
||||
from django.http.request import HttpRequest
|
||||
@ -159,7 +160,14 @@ def sanitize_item(value: Any) -> Any:
|
||||
"name": value.__name__,
|
||||
"module": value.__module__,
|
||||
}
|
||||
return value
|
||||
# List taken from the stdlib's JSON encoder (_make_iterencode, encoder.py:415)
|
||||
if isinstance(value, (bool, int, float, NoneType, list, tuple, dict)):
|
||||
return value
|
||||
try:
|
||||
return DjangoJSONEncoder().default(value)
|
||||
except TypeError:
|
||||
return str(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
|
||||
|
@ -167,7 +167,11 @@ class ChallengeStageView(StageView):
|
||||
stage_type=self.__class__.__name__, method="get_challenge"
|
||||
).time(),
|
||||
):
|
||||
challenge = self.get_challenge(*args, **kwargs)
|
||||
try:
|
||||
challenge = self.get_challenge(*args, **kwargs)
|
||||
except StageInvalidException as exc:
|
||||
self.logger.debug("Got StageInvalidException", exc=exc)
|
||||
return self.executor.stage_invalid()
|
||||
with Hub.current.start_span(
|
||||
op="authentik.flow.stage._get_challenge",
|
||||
description=self.__class__.__name__,
|
||||
|
@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer, is_dict
|
||||
from authentik.core.models import Provider
|
||||
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
|
||||
from authentik.outposts.models import (
|
||||
Outpost,
|
||||
OutpostConfig,
|
||||
@ -47,6 +47,16 @@ class OutpostSerializer(ModelSerializer):
|
||||
source="service_connection", read_only=True
|
||||
)
|
||||
|
||||
def validate_name(self, name: str) -> str:
|
||||
"""Validate name (especially for embedded outpost)"""
|
||||
if not self.instance:
|
||||
return name
|
||||
if self.instance.managed == MANAGED_OUTPOST and name != MANAGED_OUTPOST_NAME:
|
||||
raise ValidationError("Embedded outpost's name cannot be changed")
|
||||
if self.instance.name == MANAGED_OUTPOST_NAME:
|
||||
self.instance.managed = MANAGED_OUTPOST
|
||||
return name
|
||||
|
||||
def validate_providers(self, providers: list[Provider]) -> list[Provider]:
|
||||
"""Check that all providers match the type of the outpost"""
|
||||
type_map = {
|
||||
|
@ -15,6 +15,7 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
|
||||
["outpost", "uid", "version"],
|
||||
)
|
||||
MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
|
||||
MANAGED_OUTPOST_NAME = "authentik Embedded Outpost"
|
||||
|
||||
|
||||
class AuthentikOutpostConfig(ManagedAppConfig):
|
||||
@ -35,14 +36,17 @@ class AuthentikOutpostConfig(ManagedAppConfig):
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostConfig,
|
||||
OutpostType,
|
||||
)
|
||||
|
||||
if outpost := Outpost.objects.filter(name=MANAGED_OUTPOST_NAME, managed="").first():
|
||||
outpost.managed = MANAGED_OUTPOST
|
||||
outpost.save()
|
||||
return
|
||||
outpost, updated = Outpost.objects.update_or_create(
|
||||
defaults={
|
||||
"name": "authentik Embedded Outpost",
|
||||
"type": OutpostType.PROXY,
|
||||
"name": MANAGED_OUTPOST_NAME,
|
||||
},
|
||||
managed=MANAGED_OUTPOST,
|
||||
)
|
||||
@ -51,10 +55,4 @@ class AuthentikOutpostConfig(ManagedAppConfig):
|
||||
outpost.service_connection = KubernetesServiceConnection.objects.first()
|
||||
elif DockerServiceConnection.objects.exists():
|
||||
outpost.service_connection = DockerServiceConnection.objects.first()
|
||||
outpost.config = OutpostConfig(
|
||||
kubernetes_disabled_components=[
|
||||
"deployment",
|
||||
"secret",
|
||||
]
|
||||
)
|
||||
outpost.save()
|
||||
|
@ -43,6 +43,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
self.api = AppsV1Api(controller.client)
|
||||
self.outpost = self.controller.outpost
|
||||
|
||||
@property
|
||||
def noop(self) -> bool:
|
||||
return self.is_embedded
|
||||
|
||||
@staticmethod
|
||||
def reconciler_name() -> str:
|
||||
return "deployment"
|
||||
|
@ -24,6 +24,10 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
||||
super().__init__(controller)
|
||||
self.api = CoreV1Api(controller.client)
|
||||
|
||||
@property
|
||||
def noop(self) -> bool:
|
||||
return self.is_embedded
|
||||
|
||||
@staticmethod
|
||||
def reconciler_name() -> str:
|
||||
return "secret"
|
||||
|
@ -77,7 +77,10 @@ class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusSe
|
||||
|
||||
@property
|
||||
def noop(self) -> bool:
|
||||
return (not self._crd_exists()) or (self.is_embedded)
|
||||
if not self._crd_exists():
|
||||
self.logger.debug("CRD doesn't exist")
|
||||
return True
|
||||
return self.is_embedded
|
||||
|
||||
def _crd_exists(self) -> bool:
|
||||
"""Check if the Prometheus ServiceMonitor exists"""
|
||||
|
@ -2,11 +2,13 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.api.outposts import OutpostSerializer
|
||||
from authentik.outposts.models import OutpostType, default_outpost_config
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.models import Outpost, OutpostType, default_outpost_config
|
||||
from authentik.providers.ldap.models import LDAPProvider
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
|
||||
@ -22,7 +24,36 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_outpost_validaton(self):
|
||||
@reconcile_app("authentik_outposts")
|
||||
def test_managed_name_change(self):
|
||||
"""Test name change for embedded outpost"""
|
||||
embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first()
|
||||
self.assertIsNotNone(embedded_outpost)
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}),
|
||||
{"name": "foo"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content, {"name": ["Embedded outpost's name cannot be changed"]}
|
||||
)
|
||||
|
||||
@reconcile_app("authentik_outposts")
|
||||
def test_managed_without_managed(self):
|
||||
"""Test name change for embedded outpost"""
|
||||
embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first()
|
||||
self.assertIsNotNone(embedded_outpost)
|
||||
embedded_outpost.managed = ""
|
||||
embedded_outpost.save()
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}),
|
||||
{"name": "foo"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
embedded_outpost.refresh_from_db()
|
||||
self.assertEqual(embedded_outpost.managed, MANAGED_OUTPOST)
|
||||
|
||||
def test_outpost_validation(self):
|
||||
"""Test Outpost validation"""
|
||||
valid = OutpostSerializer(
|
||||
data={
|
||||
|
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.0 on 2023-12-22 23:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0016_alter_refreshtoken_token"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="accesstoken",
|
||||
name="session_id",
|
||||
field=models.CharField(blank=True, default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authorizationcode",
|
||||
name="session_id",
|
||||
field=models.CharField(blank=True, default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="refreshtoken",
|
||||
name="session_id",
|
||||
field=models.CharField(blank=True, default=""),
|
||||
),
|
||||
]
|
@ -296,6 +296,7 @@ class BaseGrantModel(models.Model):
|
||||
revoked = models.BooleanField(default=False)
|
||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||
auth_time = models.DateTimeField(verbose_name="Authentication time")
|
||||
session_id = models.CharField(default="", blank=True)
|
||||
|
||||
@property
|
||||
def scope(self) -> list[str]:
|
||||
|
@ -85,6 +85,25 @@ class TestAuthorize(OAuthTestCase):
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_blocked_redirect_uri(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="data:local.invalid",
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "data:localhost",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_invalid_redirect_uri_empty(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
|
@ -22,8 +22,9 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
self.factory = RequestFactory()
|
||||
self.app = Application.objects.create(name=generate_id(), slug="test")
|
||||
|
||||
def test_pkce_missing_in_token(self):
|
||||
"""Test full with pkce"""
|
||||
def test_pkce_missing_in_authorize(self):
|
||||
"""Test PKCE with code_challenge in authorize request
|
||||
and missing verifier in token request"""
|
||||
flow = create_test_flow()
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
@ -74,7 +75,77 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{"error": "invalid_request", "error_description": "The request is otherwise malformed"},
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": (
|
||||
"The provided authorization grant or refresh token is invalid, expired, "
|
||||
"revoked, does not match the redirection URI used in the authorization "
|
||||
"request, or was issued to another client"
|
||||
),
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_pkce_missing_in_token(self):
|
||||
"""Test PKCE with missing code_challenge in authorization request but verifier
|
||||
set in token request"""
|
||||
flow = create_test_flow()
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"redirect_uri": "foo://localhost",
|
||||
# "code_challenge": challenge,
|
||||
# "code_challenge_method": "S256",
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
{
|
||||
"component": "xak-flow-redirect",
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": f"foo://localhost?code={code.code}&state={state}",
|
||||
},
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"code_verifier": generate_id(),
|
||||
"redirect_uri": "foo://localhost",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": (
|
||||
"The provided authorization grant or refresh token is invalid, expired, "
|
||||
"revoked, does not match the redirection URI used in the authorization "
|
||||
"request, or was issued to another client"
|
||||
),
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""authentik OAuth2 Authorization views"""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from hashlib import sha256
|
||||
from json import dumps
|
||||
from re import error as RegexError
|
||||
from re import fullmatch
|
||||
@ -74,6 +75,7 @@ PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params"
|
||||
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
|
||||
|
||||
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
|
||||
FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@ -174,6 +176,10 @@ class OAuthAuthorizationParams:
|
||||
self.check_scope()
|
||||
self.check_nonce()
|
||||
self.check_code_challenge()
|
||||
if self.request:
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "request_not_supported", self.grant_type, self.state
|
||||
)
|
||||
|
||||
def check_redirect_uri(self):
|
||||
"""Redirect URI validation."""
|
||||
@ -211,10 +217,9 @@ class OAuthAuthorizationParams:
|
||||
expected=allowed_redirect_urls,
|
||||
)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
if self.request:
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "request_not_supported", self.grant_type, self.state
|
||||
)
|
||||
# Check against forbidden schemes
|
||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
|
||||
def check_scope(self):
|
||||
"""Ensure openid scope is set in Hybrid flows, or when requesting an id_token"""
|
||||
@ -282,6 +287,7 @@ class OAuthAuthorizationParams:
|
||||
expires=now + timedelta_from_string(self.provider.access_code_validity),
|
||||
scope=self.scope,
|
||||
nonce=self.nonce,
|
||||
session_id=sha256(request.session.session_key.encode("ascii")).hexdigest(),
|
||||
)
|
||||
|
||||
if self.code_challenge and self.code_challenge_method:
|
||||
@ -569,6 +575,7 @@ class OAuthFulfillmentStage(StageView):
|
||||
expires=access_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=auth_event.created if auth_event else now,
|
||||
session_id=sha256(self.request.session.session_key.encode("ascii")).hexdigest(),
|
||||
)
|
||||
|
||||
id_token = IDToken.new(self.provider, token, self.request)
|
||||
|
@ -6,6 +6,7 @@ from hashlib import sha256
|
||||
from re import error as RegexError
|
||||
from re import fullmatch
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils import timezone
|
||||
@ -54,6 +55,7 @@ from authentik.providers.oauth2.models import (
|
||||
RefreshToken,
|
||||
)
|
||||
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
|
||||
from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
@ -205,6 +207,10 @@ class TokenParams:
|
||||
).from_http(request)
|
||||
raise TokenError("invalid_client")
|
||||
|
||||
# Check against forbidden schemes
|
||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||
raise TokenError("invalid_request")
|
||||
|
||||
self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
|
||||
if not self.authorization_code:
|
||||
LOGGER.warning("Code does not exist", code=raw_code)
|
||||
@ -225,7 +231,7 @@ class TokenParams:
|
||||
if self.authorization_code.code_challenge:
|
||||
# Authorization code had PKCE but we didn't get one
|
||||
if not self.code_verifier:
|
||||
raise TokenError("invalid_request")
|
||||
raise TokenError("invalid_grant")
|
||||
if self.authorization_code.code_challenge_method == PKCE_METHOD_S256:
|
||||
new_code_challenge = (
|
||||
urlsafe_b64encode(sha256(self.code_verifier.encode("ascii")).digest())
|
||||
@ -238,6 +244,10 @@ class TokenParams:
|
||||
if new_code_challenge != self.authorization_code.code_challenge:
|
||||
LOGGER.warning("Code challenge not matching")
|
||||
raise TokenError("invalid_grant")
|
||||
# Token request had a code_verifier but code did not have a code challenge
|
||||
# Prevent downgrade
|
||||
if not self.authorization_code.code_challenge and self.code_verifier:
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
def __post_init_refresh(self, raw_token: str, request: HttpRequest):
|
||||
if not raw_token:
|
||||
@ -487,6 +497,7 @@ class TokenView(View):
|
||||
# Keep same scopes as previous token
|
||||
scope=self.params.authorization_code.scope,
|
||||
auth_time=self.params.authorization_code.auth_time,
|
||||
session_id=self.params.authorization_code.session_id,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
@ -502,6 +513,7 @@ class TokenView(View):
|
||||
expires=refresh_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=self.params.authorization_code.auth_time,
|
||||
session_id=self.params.authorization_code.session_id,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
@ -539,6 +551,7 @@ class TokenView(View):
|
||||
# Keep same scopes as previous token
|
||||
scope=self.params.refresh_token.scope,
|
||||
auth_time=self.params.refresh_token.auth_time,
|
||||
session_id=self.params.refresh_token.session_id,
|
||||
)
|
||||
access_token.id_token = IDToken.new(
|
||||
self.provider,
|
||||
@ -554,6 +567,7 @@ class TokenView(View):
|
||||
expires=refresh_token_expiry,
|
||||
provider=self.provider,
|
||||
auth_time=self.params.refresh_token.auth_time,
|
||||
session_id=self.params.refresh_token.session_id,
|
||||
)
|
||||
id_token = IDToken.new(
|
||||
self.provider,
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""proxy provider tasks"""
|
||||
from hashlib import sha256
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||
@ -23,6 +25,7 @@ def proxy_set_defaults():
|
||||
def proxy_on_logout(session_id: str):
|
||||
"""Update outpost instances connected to a single outpost"""
|
||||
layer = get_channel_layer()
|
||||
hashed_session_id = sha256(session_id.encode("ascii")).hexdigest()
|
||||
for outpost in Outpost.objects.filter(type=OutpostType.PROXY):
|
||||
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
|
||||
async_to_sync(layer.group_send)(
|
||||
@ -30,6 +33,6 @@ def proxy_on_logout(session_id: str):
|
||||
{
|
||||
"type": "event.provider.specific",
|
||||
"sub_type": "logout",
|
||||
"session_id": session_id,
|
||||
"session_id": hashed_session_id,
|
||||
},
|
||||
)
|
||||
|
@ -93,7 +93,7 @@ class SCIMMembershipTests(TestCase):
|
||||
"emails": [],
|
||||
"active": True,
|
||||
"externalId": user.uid,
|
||||
"name": {"familyName": "", "formatted": "", "givenName": ""},
|
||||
"name": {"familyName": " ", "formatted": " ", "givenName": ""},
|
||||
"displayName": "",
|
||||
"userName": user.username,
|
||||
},
|
||||
@ -184,7 +184,7 @@ class SCIMMembershipTests(TestCase):
|
||||
"displayName": "",
|
||||
"emails": [],
|
||||
"externalId": user.uid,
|
||||
"name": {"familyName": "", "formatted": "", "givenName": ""},
|
||||
"name": {"familyName": " ", "formatted": " ", "givenName": ""},
|
||||
"userName": user.username,
|
||||
},
|
||||
)
|
||||
|
@ -57,7 +57,7 @@ class SCIMUserTests(TestCase):
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 2)
|
||||
@ -77,11 +77,11 @@ class SCIMUserTests(TestCase):
|
||||
],
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": "",
|
||||
"formatted": uid,
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"displayName": uid,
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
@ -110,7 +110,7 @@ class SCIMUserTests(TestCase):
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 2)
|
||||
@ -131,11 +131,11 @@ class SCIMUserTests(TestCase):
|
||||
"value": f"{uid}@goauthentik.io",
|
||||
}
|
||||
],
|
||||
"displayName": uid,
|
||||
"displayName": f"{uid} {uid}",
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": "",
|
||||
"formatted": uid,
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"userName": uid,
|
||||
@ -166,7 +166,7 @@ class SCIMUserTests(TestCase):
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 2)
|
||||
@ -186,11 +186,11 @@ class SCIMUserTests(TestCase):
|
||||
],
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": "",
|
||||
"formatted": uid,
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"displayName": uid,
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
@ -230,7 +230,7 @@ class SCIMUserTests(TestCase):
|
||||
)
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
|
||||
@ -254,11 +254,11 @@ class SCIMUserTests(TestCase):
|
||||
],
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": "",
|
||||
"formatted": uid,
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"displayName": uid,
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
|
@ -24,7 +24,10 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
|
||||
|
||||
def get_app_label_verbose(self, instance: GroupObjectPermission) -> str:
|
||||
"""Get app label from permission's model"""
|
||||
return apps.get_app_config(instance.content_type.app_label).verbose_name
|
||||
try:
|
||||
return apps.get_app_config(instance.content_type.app_label).verbose_name
|
||||
except LookupError:
|
||||
return instance.content_type.app_label
|
||||
|
||||
def get_model_verbose(self, instance: GroupObjectPermission) -> str:
|
||||
"""Get model label from permission's model"""
|
||||
|
@ -24,7 +24,10 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
|
||||
|
||||
def get_app_label_verbose(self, instance: UserObjectPermission) -> str:
|
||||
"""Get app label from permission's model"""
|
||||
return apps.get_app_config(instance.content_type.app_label).verbose_name
|
||||
try:
|
||||
return apps.get_app_config(instance.content_type.app_label).verbose_name
|
||||
except LookupError:
|
||||
return instance.content_type.app_label
|
||||
|
||||
def get_model_verbose(self, instance: UserObjectPermission) -> str:
|
||||
"""Get model label from permission's model"""
|
||||
|
@ -56,6 +56,7 @@ class OAuthSourceSerializer(SourceSerializer):
|
||||
"""Get source's type configuration"""
|
||||
return SourceTypeSerializer(instance.source_type).data
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
session = get_http_session()
|
||||
source_type = registry.find_type(attrs["provider_type"])
|
||||
@ -73,9 +74,17 @@ class OAuthSourceSerializer(SourceSerializer):
|
||||
config = well_known_config.json()
|
||||
if "issuer" not in config:
|
||||
raise ValidationError({"oidc_well_known_url": "Invalid well-known configuration"})
|
||||
attrs["authorization_url"] = config.get("authorization_endpoint", "")
|
||||
attrs["access_token_url"] = config.get("token_endpoint", "")
|
||||
attrs["profile_url"] = config.get("userinfo_endpoint", "")
|
||||
field_map = {
|
||||
# authentik field to oidc field
|
||||
"authorization_url": "authorization_endpoint",
|
||||
"access_token_url": "token_endpoint",
|
||||
"profile_url": "userinfo_endpoint",
|
||||
}
|
||||
for ak_key, oidc_key in field_map.items():
|
||||
# Don't overwrite user-set values
|
||||
if ak_key in attrs and attrs[ak_key]:
|
||||
continue
|
||||
attrs[ak_key] = config.get(oidc_key, "")
|
||||
inferred_oidc_jwks_url = config.get("jwks_uri", "")
|
||||
|
||||
# Prefer user-entered URL to inferred URL to default URL
|
||||
|
@ -44,3 +44,7 @@ class TestTypeAzureAD(TestCase):
|
||||
self.assertEqual(ak_context["username"], AAD_USER["userPrincipalName"])
|
||||
self.assertEqual(ak_context["email"], AAD_USER["mail"])
|
||||
self.assertEqual(ak_context["name"], AAD_USER["displayName"])
|
||||
|
||||
def test_user_id(self):
|
||||
"""Test azure AD user ID"""
|
||||
self.assertEqual(AzureADOAuthCallback().get_user_id(AAD_USER), AAD_USER["id"])
|
||||
|
@ -69,9 +69,6 @@ class TestOAuthSource(TestCase):
|
||||
"provider_type": "openidconnect",
|
||||
"consumer_key": "foo",
|
||||
"consumer_secret": "foo",
|
||||
"authorization_url": "http://foo",
|
||||
"access_token_url": "http://foo",
|
||||
"profile_url": "http://foo",
|
||||
"oidc_well_known_url": url,
|
||||
"oidc_jwks_url": "",
|
||||
},
|
||||
|
@ -4,8 +4,8 @@ from typing import Any
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -20,11 +20,16 @@ class AzureADOAuthRedirect(OAuthRedirect):
|
||||
}
|
||||
|
||||
|
||||
class AzureADOAuthCallback(OAuthCallback):
|
||||
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
|
||||
"""AzureAD OAuth2 Callback"""
|
||||
|
||||
client_class = UserprofileHeaderAuthClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
# Default try to get `id` for the Graph API endpoint
|
||||
# fallback to OpenID logic in case the profile URL was changed
|
||||
return info.get("id", super().get_user_id(info))
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
info: dict[str, Any],
|
||||
|
@ -23,7 +23,7 @@ class OpenIDConnectOAuth2Callback(OAuthCallback):
|
||||
client_class = UserprofileHeaderAuthClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
return info.get("sub", "")
|
||||
return info.get("sub", None)
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
|
@ -3,8 +3,8 @@ from typing import Any
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ class OktaOAuthRedirect(OAuthRedirect):
|
||||
}
|
||||
|
||||
|
||||
class OktaOAuth2Callback(OAuthCallback):
|
||||
class OktaOAuth2Callback(OpenIDConnectOAuth2Callback):
|
||||
"""Okta OAuth2 Callback"""
|
||||
|
||||
# Okta has the same quirk as azure and throws an error if the access token
|
||||
@ -25,9 +25,6 @@ class OktaOAuth2Callback(OAuthCallback):
|
||||
# see https://github.com/goauthentik/authentik/issues/1910
|
||||
client_class = UserprofileHeaderAuthClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
return info.get("sub", "")
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
info: dict[str, Any],
|
||||
|
@ -3,8 +3,8 @@ from json import dumps
|
||||
from typing import Any, Optional
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
|
||||
@ -27,14 +27,11 @@ class TwitchOAuthRedirect(OAuthRedirect):
|
||||
}
|
||||
|
||||
|
||||
class TwitchOAuth2Callback(OAuthCallback):
|
||||
class TwitchOAuth2Callback(OpenIDConnectOAuth2Callback):
|
||||
"""Twitch OAuth2 Callback"""
|
||||
|
||||
client_class = TwitchClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
return info.get("sub", "")
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
info: dict[str, Any],
|
||||
|
@ -69,7 +69,6 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
||||
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||
hashed_number = hash_phone_number(phone_number)
|
||||
query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
|
||||
print(SMSDevice.objects.filter(query, stage=stage.pk))
|
||||
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
|
||||
raise ValidationError(_("Invalid phone number"))
|
||||
# No code yet, but we have a phone number, so send a verification message
|
||||
|
@ -199,11 +199,9 @@ class AuthenticatorSMSStageTests(FlowTestCase):
|
||||
sms_send_mock,
|
||||
),
|
||||
):
|
||||
print(self.client.session[SESSION_KEY_PLAN])
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
print(response.content.decode())
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
|
@ -38,4 +38,11 @@ class Migration(migrations.Migration):
|
||||
name="statictoken",
|
||||
options={"verbose_name": "Static Token", "verbose_name_plural": "Static Tokens"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="authenticatorstaticstage",
|
||||
options={
|
||||
"verbose_name": "Static Authenticator Setup Stage",
|
||||
"verbose_name_plural": "Static Authenticator Setup Stages",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -46,11 +46,11 @@ class AuthenticatorStaticStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Static Authenticator Stage {self.name}"
|
||||
return f"Static Authenticator Setup Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Static Authenticator Stage")
|
||||
verbose_name_plural = _("Static Authenticator Stages")
|
||||
verbose_name = _("Static Authenticator Setup Stage")
|
||||
verbose_name_plural = _("Static Authenticator Setup Stages")
|
||||
|
||||
|
||||
class StaticDevice(SerializerModel, ThrottlingMixin, Device):
|
||||
|
@ -300,8 +300,10 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
serializer = SelectableStageSerializer(
|
||||
data={
|
||||
"pk": stage.pk,
|
||||
"name": stage.name,
|
||||
"verbose_name": str(stage._meta.verbose_name),
|
||||
"name": stage.friendly_name or stage.name,
|
||||
"verbose_name": str(stage._meta.verbose_name)
|
||||
.replace("Setup Stage", "")
|
||||
.strip(),
|
||||
"meta_model_name": f"{stage._meta.app_label}.{stage._meta.model_name}",
|
||||
}
|
||||
)
|
||||
|
@ -184,6 +184,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||
"args": {},
|
||||
"method": "GET",
|
||||
"path": f"/api/v3/flows/executor/{flow.slug}/",
|
||||
"user_agent": "",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
|
||||
@ -26,19 +27,22 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
||||
|
||||
def test_not_configured_action(self):
|
||||
"""Test not_configured_action"""
|
||||
conf_stage = IdentificationStage.objects.create(
|
||||
ident_stage = IdentificationStage.objects.create(
|
||||
name=generate_id(),
|
||||
user_fields=[
|
||||
UserFields.USERNAME,
|
||||
],
|
||||
)
|
||||
conf_stage = AuthenticatorStaticStage.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
stage = AuthenticatorValidateStage.objects.create(
|
||||
name=generate_id(),
|
||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||
)
|
||||
stage.configuration_stages.set([conf_stage])
|
||||
flow = create_test_flow()
|
||||
FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
|
||||
|
||||
response = self.client.get(
|
||||
@ -57,27 +61,22 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
flow,
|
||||
component="ak-stage-identification",
|
||||
password_fields=False,
|
||||
primary_action="Continue",
|
||||
user_fields=["username"],
|
||||
sources=[],
|
||||
show_source_labels=False,
|
||||
component="ak-stage-authenticator-static",
|
||||
)
|
||||
|
||||
def test_not_configured_action_multiple(self):
|
||||
"""Test not_configured_action"""
|
||||
conf_stage = IdentificationStage.objects.create(
|
||||
ident_stage = IdentificationStage.objects.create(
|
||||
name=generate_id(),
|
||||
user_fields=[
|
||||
UserFields.USERNAME,
|
||||
],
|
||||
)
|
||||
conf_stage2 = IdentificationStage.objects.create(
|
||||
conf_stage = AuthenticatorStaticStage.objects.create(
|
||||
name=generate_id(),
|
||||
)
|
||||
conf_stage2 = AuthenticatorStaticStage.objects.create(
|
||||
name=generate_id(),
|
||||
user_fields=[
|
||||
UserFields.USERNAME,
|
||||
],
|
||||
)
|
||||
stage = AuthenticatorValidateStage.objects.create(
|
||||
name=generate_id(),
|
||||
@ -85,7 +84,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
||||
)
|
||||
stage.configuration_stages.set([conf_stage, conf_stage2])
|
||||
flow = create_test_flow()
|
||||
FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
|
||||
|
||||
# Get initial identification stage
|
||||
@ -118,12 +117,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
flow,
|
||||
component="ak-stage-identification",
|
||||
password_fields=False,
|
||||
primary_action="Continue",
|
||||
user_fields=["username"],
|
||||
sources=[],
|
||||
show_source_labels=False,
|
||||
component="ak-stage-authenticator-static",
|
||||
)
|
||||
|
||||
def test_stage_validation(self):
|
||||
|
@ -5,6 +5,7 @@ from uuid import uuid4
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
@ -12,11 +13,14 @@ from django.utils.translation import gettext as _
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.models import FlowDesignation, FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
@ -59,7 +63,6 @@ class EmailStageView(ChallengeStageView):
|
||||
query_params = QueryDict(self.request.GET.get(QS_QUERY), mutable=True)
|
||||
query_params.pop(QS_KEY_TOKEN, None)
|
||||
query_params.update(kwargs)
|
||||
print(query_params)
|
||||
full_url = base_url
|
||||
if len(query_params) > 0:
|
||||
full_url = f"{full_url}?{query_params.urlencode()}"
|
||||
@ -104,18 +107,27 @@ class EmailStageView(ChallengeStageView):
|
||||
current_stage: EmailStage = self.executor.current_stage
|
||||
token = self.get_token()
|
||||
# Send mail to user
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(current_stage.subject),
|
||||
to=[email],
|
||||
language=pending_user.locale(self.request),
|
||||
template_name=current_stage.template,
|
||||
template_context={
|
||||
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
|
||||
"user": pending_user,
|
||||
"expires": token.expires,
|
||||
},
|
||||
)
|
||||
send_mails(current_stage, message)
|
||||
try:
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(current_stage.subject),
|
||||
to=[email],
|
||||
language=pending_user.locale(self.request),
|
||||
template_name=current_stage.template,
|
||||
template_context={
|
||||
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
|
||||
"user": pending_user,
|
||||
"expires": token.expires,
|
||||
},
|
||||
)
|
||||
send_mails(current_stage, message)
|
||||
except TemplateSyntaxError as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=_("Exception occurred while rendering E-mail template"),
|
||||
error=exception_to_string(exc),
|
||||
template=current_stage.template,
|
||||
).from_http(self.request)
|
||||
raise StageInvalidException from exc
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
# Check if the user came back from the email link to verify
|
||||
@ -136,7 +148,11 @@ class EmailStageView(ChallengeStageView):
|
||||
return self.executor.stage_invalid()
|
||||
# Check if we've already sent the initial e-mail
|
||||
if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:
|
||||
self.send_email()
|
||||
try:
|
||||
self.send_email()
|
||||
except StageInvalidException as exc:
|
||||
self.logger.debug("Got StageInvalidException", exc=exc)
|
||||
return self.executor.stage_invalid()
|
||||
self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
@ -4,11 +4,20 @@ from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp, mkstemp
|
||||
from typing import Any
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.stages.email.models import get_template_choices
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.email.models import EmailStage, get_template_choices
|
||||
|
||||
|
||||
def get_templates_setting(temp_dir: str) -> dict[str, Any]:
|
||||
@ -18,11 +27,18 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
|
||||
return templates_setting
|
||||
|
||||
|
||||
class TestEmailStageTemplates(TestCase):
|
||||
class TestEmailStageTemplates(FlowTestCase):
|
||||
"""Email tests"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.dir = mkdtemp()
|
||||
self.dir = Path(mkdtemp())
|
||||
self.user = create_test_admin_user()
|
||||
|
||||
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
self.stage = EmailStage.objects.create(
|
||||
name="email",
|
||||
)
|
||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
rmtree(self.dir)
|
||||
@ -38,3 +54,37 @@ class TestEmailStageTemplates(TestCase):
|
||||
self.assertEqual(len(choices), 3)
|
||||
unlink(file)
|
||||
unlink(file2)
|
||||
|
||||
def test_custom_template_invalid_syntax(self):
|
||||
"""Test with custom template"""
|
||||
with open(self.dir / Path("invalid.html"), "w+", encoding="utf-8") as _invalid:
|
||||
_invalid.write("{% blocktranslate %}")
|
||||
with self.settings(TEMPLATES=get_templates_setting(self.dir)):
|
||||
self.stage.template = "invalid.html"
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
error_message="Unknown error",
|
||||
)
|
||||
events = Event.objects.filter(action=EventAction.CONFIGURATION_ERROR)
|
||||
self.assertEqual(len(events), 1)
|
||||
event = events.first()
|
||||
self.assertEqual(
|
||||
event.context["message"], "Exception occurred while rendering E-mail template"
|
||||
)
|
||||
self.assertEqual(event.context["template"], "invalid.html")
|
||||
|
@ -14,6 +14,7 @@ entries:
|
||||
- attrs:
|
||||
configure_flow: !KeyOf flow
|
||||
token_count: 6
|
||||
friendly_name: Static tokens
|
||||
identifiers:
|
||||
name: default-authenticator-static-setup
|
||||
id: default-authenticator-static-setup
|
||||
|
@ -14,6 +14,7 @@ entries:
|
||||
- attrs:
|
||||
configure_flow: !KeyOf flow
|
||||
digits: 6
|
||||
friendly_name: TOTP Device
|
||||
identifiers:
|
||||
name: default-authenticator-totp-setup
|
||||
id: default-authenticator-totp-setup
|
||||
|
@ -13,6 +13,7 @@ entries:
|
||||
id: flow
|
||||
- attrs:
|
||||
configure_flow: !KeyOf flow
|
||||
friendly_name: WebAuthn device
|
||||
identifiers:
|
||||
name: default-authenticator-webauthn-setup
|
||||
id: default-authenticator-webauthn-setup
|
||||
|
@ -14,8 +14,11 @@ entries:
|
||||
expression: |
|
||||
# This mapping is used by the authentik proxy. It passes extra user attributes,
|
||||
# which are used for example for the HTTP-Basic Authentication mapping.
|
||||
session_id = None
|
||||
if "token" in request.context:
|
||||
session_id = request.context.get("token").session_id
|
||||
return {
|
||||
"sid": request.http_request.session.session_key,
|
||||
"sid": session_id,
|
||||
"ak_proxy": {
|
||||
"user_attributes": request.user.group_attributes(request),
|
||||
"is_superuser": request.user.is_superuser,
|
||||
|
@ -11,13 +11,15 @@ entries:
|
||||
name: "authentik default SCIM Mapping: User"
|
||||
expression: |
|
||||
# Some implementations require givenName and familyName to be set
|
||||
givenName, familyName = request.user.name, ""
|
||||
givenName, familyName = request.user.name, " "
|
||||
formatted = request.user.name + " "
|
||||
# This default sets givenName to the name before the first space
|
||||
# and the remainder as family name
|
||||
# if the user's name has no space the givenName is the entire name
|
||||
# (this might cause issues with some SCIM implementations)
|
||||
if " " in request.user.name:
|
||||
givenName, _, familyName = request.user.name.partition(" ")
|
||||
formatted = request.user.name
|
||||
|
||||
# photos supports URLs to images, however authentik might return data URIs
|
||||
avatar = request.user.avatar
|
||||
@ -39,7 +41,7 @@ entries:
|
||||
return {
|
||||
"userName": request.user.username,
|
||||
"name": {
|
||||
"formatted": request.user.name,
|
||||
"formatted": formatted,
|
||||
"givenName": givenName,
|
||||
"familyName": familyName,
|
||||
},
|
||||
|
@ -32,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -53,7 +53,7 @@ services:
|
||||
- postgresql
|
||||
- redis
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.4}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
2
go.mod
2
go.mod
@ -4,7 +4,6 @@ go 1.21
|
||||
|
||||
require (
|
||||
beryju.io/ldap v0.1.0
|
||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/getsentry/sentry-go v0.25.0
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
@ -24,6 +23,7 @@ require (
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/redis/go-redis/v9 v9.2.1
|
||||
github.com/sethvargo/go-envconfig v1.0.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
|
8
go.sum
8
go.sum
@ -37,8 +37,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb h1:w9IDEB7P1VzNcBpOG7kMpFkZp2DkyJIUt0gDx5MBhRU=
|
||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
@ -198,8 +196,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
@ -303,6 +301,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sethvargo/go-envconfig v1.0.0 h1:1C66wzy4QrROf5ew4KdVw942CQDa55qmlYmw9FZxZdU=
|
||||
github.com/sethvargo/go-envconfig v1.0.0/go.mod h1:Lzc75ghUn5ucmcRGIdGQ33DKJrcjk4kihFYgSTBmjIc=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
|
@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -10,10 +11,11 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
env "github.com/Netflix/go-env"
|
||||
env "github.com/sethvargo/go-envconfig"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/authentik/lib"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"goauthentik.io/authentik/lib"
|
||||
)
|
||||
|
||||
var cfg *Config
|
||||
@ -113,7 +115,8 @@ func (c *Config) LoadConfigFromFile(path string) error {
|
||||
}
|
||||
|
||||
func (c *Config) fromEnv() error {
|
||||
_, err := env.UnmarshalFromEnviron(c)
|
||||
ctx := context.Background()
|
||||
err := env.Process(ctx, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load environment variables: %w", err)
|
||||
}
|
||||
|
@ -3,17 +3,17 @@ package config
|
||||
type Config struct {
|
||||
// Core specific config
|
||||
Paths PathsConfig `yaml:"paths"`
|
||||
LogLevel string `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL"`
|
||||
ErrorReporting ErrorReportingConfig `yaml:"error_reporting"`
|
||||
Redis RedisConfig `yaml:"redis"`
|
||||
Outposts OutpostConfig `yaml:"outposts"`
|
||||
LogLevel string `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL, overwrite"`
|
||||
ErrorReporting ErrorReportingConfig `yaml:"error_reporting" env:", prefix=AUTHENTIK_ERROR_REPORTING__"`
|
||||
Redis RedisConfig `yaml:"redis" env:", prefix=AUTHENTIK_REDIS__"`
|
||||
Outposts OutpostConfig `yaml:"outposts" env:", prefix=AUTHENTIK_OUTPOSTS__"`
|
||||
|
||||
// Config for core and embedded outpost
|
||||
SecretKey string `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY"`
|
||||
SecretKey string `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY, overwrite"`
|
||||
|
||||
// Config for both core and outposts
|
||||
Debug bool `yaml:"debug" env:"AUTHENTIK_DEBUG"`
|
||||
Listen ListenConfig `yaml:"listen"`
|
||||
Debug bool `yaml:"debug" env:"AUTHENTIK_DEBUG, overwrite"`
|
||||
Listen ListenConfig `yaml:"listen" env:", prefix=AUTHENTIK_LISTEN__"`
|
||||
|
||||
// Outpost specific config
|
||||
// These are only relevant for proxy/ldap outposts, and cannot be set via YAML
|
||||
@ -25,27 +25,28 @@ type Config struct {
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `yaml:"host" env:"AUTHENTIK_REDIS__HOST"`
|
||||
Port int `yaml:"port" env:"AUTHENTIK_REDIS__PORT"`
|
||||
Password string `yaml:"password" env:"AUTHENTIK_REDIS__PASSWORD"`
|
||||
TLS bool `yaml:"tls" env:"AUTHENTIK_REDIS__TLS"`
|
||||
TLSReqs string `yaml:"tls_reqs" env:"AUTHENTIK_REDIS__TLS_REQS"`
|
||||
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__DB"`
|
||||
CacheTimeout int `yaml:"cache_timeout" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT"`
|
||||
CacheTimeoutFlows int `yaml:"cache_timeout_flows" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS"`
|
||||
CacheTimeoutPolicies int `yaml:"cache_timeout_policies" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_POLICIES"`
|
||||
CacheTimeoutReputation int `yaml:"cache_timeout_reputation" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_REPUTATION"`
|
||||
Host string `yaml:"host" env:"HOST, overwrite"`
|
||||
Port int `yaml:"port" env:"PORT, overwrite"`
|
||||
DB int `yaml:"db" env:"DB, overwrite"`
|
||||
Username string `yaml:"username" env:"USERNAME, overwrite"`
|
||||
Password string `yaml:"password" env:"PASSWORD, overwrite"`
|
||||
TLS bool `yaml:"tls" env:"TLS, overwrite"`
|
||||
TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
|
||||
CacheTimeout int `yaml:"cache_timeout" env:"CACHE_TIMEOUT, overwrite"`
|
||||
CacheTimeoutFlows int `yaml:"cache_timeout_flows" env:"CACHE_TIMEOUT_FLOWS, overwrite"`
|
||||
CacheTimeoutPolicies int `yaml:"cache_timeout_policies" env:"CACHE_TIMEOUT_POLICIES, overwrite"`
|
||||
CacheTimeoutReputation int `yaml:"cache_timeout_reputation" env:"CACHE_TIMEOUT_REPUTATION, overwrite"`
|
||||
}
|
||||
|
||||
type ListenConfig struct {
|
||||
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
|
||||
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
|
||||
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
|
||||
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
|
||||
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
|
||||
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
|
||||
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
|
||||
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
|
||||
HTTP string `yaml:"listen_http" env:"HTTP, overwrite"`
|
||||
HTTPS string `yaml:"listen_https" env:"HTTPS, overwrite"`
|
||||
LDAP string `yaml:"listen_ldap" env:"LDAP, overwrite"`
|
||||
LDAPS string `yaml:"listen_ldaps" env:"LDAPS, overwrite"`
|
||||
Radius string `yaml:"listen_radius" env:"RADIUS, overwrite"`
|
||||
Metrics string `yaml:"listen_metrics" env:"METRICS, overwrite"`
|
||||
Debug string `yaml:"listen_debug" env:"DEBUG, overwrite"`
|
||||
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"TRUSTED_PROXY_CIDRS, overwrite"`
|
||||
}
|
||||
|
||||
type PathsConfig struct {
|
||||
@ -53,15 +54,15 @@ type PathsConfig struct {
|
||||
}
|
||||
|
||||
type ErrorReportingConfig struct {
|
||||
Enabled bool `yaml:"enabled" env:"AUTHENTIK_ERROR_REPORTING__ENABLED"`
|
||||
SentryDSN string `yaml:"sentry_dsn" env:"AUTHENTIK_ERROR_REPORTING__SENTRY_DSN"`
|
||||
Environment string `yaml:"environment" env:"AUTHENTIK_ERROR_REPORTING__ENVIRONMENT"`
|
||||
SendPII bool `yaml:"send_pii" env:"AUTHENTIK_ERROR_REPORTING__SEND_PII"`
|
||||
SampleRate float64 `yaml:"sample_rate" env:"AUTHENTIK_ERROR_REPORTING__SAMPLE_RATE"`
|
||||
Enabled bool `yaml:"enabled" env:"ENABLED, overwrite"`
|
||||
SentryDSN string `yaml:"sentry_dsn" env:"SENTRY_DSN, overwrite"`
|
||||
Environment string `yaml:"environment" env:"ENVIRONMENT, overwrite"`
|
||||
SendPII bool `yaml:"send_pii" env:"SEND_PII, overwrite"`
|
||||
SampleRate float64 `yaml:"sample_rate" env:"SAMPLE_RATE, overwrite"`
|
||||
}
|
||||
|
||||
type OutpostConfig struct {
|
||||
ContainerImageBase string `yaml:"container_image_base" env:"AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE"`
|
||||
Discover bool `yaml:"discover" env:"AUTHENTIK_OUTPOSTS__DISCOVER"`
|
||||
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST"`
|
||||
ContainerImageBase string `yaml:"container_image_base" env:"CONTAINER_IMAGE_BASE, overwrite"`
|
||||
Discover bool `yaml:"discover" env:"DISCOVER, overwrite"`
|
||||
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"DISABLE_EMBEDDED_OUTPOST, overwrite"`
|
||||
}
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.10.4"
|
||||
const VERSION = "2023.10.7"
|
||||
|
@ -31,16 +31,11 @@ func (a *Application) redeemCallback(savedState string, u *url.URL, c context.Co
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract the ID Token from OAuth2 token.
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing id_token")
|
||||
}
|
||||
|
||||
a.log.WithField("id_token", rawIDToken).Trace("id_token")
|
||||
jwt := oauth2Token.AccessToken
|
||||
a.log.WithField("jwt", jwt).Trace("access_token")
|
||||
|
||||
// Parse and verify ID Token payload.
|
||||
idToken, err := a.tokenVerifier.Verify(ctx, rawIDToken)
|
||||
idToken, err := a.tokenVerifier.Verify(ctx, jwt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -53,6 +48,6 @@ func (a *Application) redeemCallback(savedState string, u *url.URL, c context.Co
|
||||
if claims.Proxy == nil {
|
||||
claims.Proxy = &ProxyClaims{}
|
||||
}
|
||||
claims.RawToken = rawIDToken
|
||||
claims.RawToken = jwt
|
||||
return claims, nil
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||
// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7
|
||||
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
|
||||
// securecookie: the value is too long
|
||||
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
|
||||
// when using OpenID Connect, since this can contain a large amount of extra information in the id_token
|
||||
|
||||
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
|
||||
cs.MaxLength(math.MaxInt)
|
||||
|
@ -36,6 +36,7 @@ func (ps *ProxyServer) handleWSMessage(ctx context.Context, args map[string]inte
|
||||
switch msg.SubType {
|
||||
case WSProviderSubTypeLogout:
|
||||
for _, p := range ps.apps {
|
||||
ps.log.WithField("provider", p.Host).Debug("Logging out")
|
||||
err := p.Logout(ctx, func(c application.Claims) bool {
|
||||
return c.Sid == msg.SessionID
|
||||
})
|
||||
|
@ -1,3 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
|
||||
|
||||
@ -18,8 +20,8 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/ldap ./cmd/ldap
|
||||
|
||||
# Stage 2: Run
|
||||
|
@ -1,3 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build website
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as web-builder
|
||||
|
||||
@ -34,8 +36,8 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/proxy ./cmd/proxy
|
||||
|
||||
# Stage 3: Run
|
||||
|
@ -97,7 +97,7 @@ const-rgx = "[a-zA-Z0-9_]{1,40}$"
|
||||
|
||||
ignored-modules = ["binascii", "socket", "zlib"]
|
||||
generated-members = ["xmlsec.constants.*", "xmlsec.tree.*", "xmlsec.template.*"]
|
||||
ignore = "migrations"
|
||||
ignore = ["migrations", "tests"]
|
||||
max-attributes = 12
|
||||
max-branches = 20
|
||||
|
||||
@ -113,7 +113,7 @@ filterwarnings = [
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.10.4"
|
||||
version = "2023.10.7"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
|
||||
|
||||
@ -18,8 +20,8 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
GOARM="${TARGETVARIANT#v}" go build -o /go/radius ./cmd/radius
|
||||
|
||||
# Stage 2: Run
|
||||
|
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2023.10.4
|
||||
version: 2023.10.7
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
@ -36,8 +36,8 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
interval=5 * 1_000 * 1_000_000,
|
||||
start_period=1 * 1_000 * 1_000_000,
|
||||
),
|
||||
"environment": {
|
||||
"GF_AUTH_GITHUB_ENABLED": "true",
|
||||
|
@ -42,8 +42,8 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
interval=5 * 1_000 * 1_000_000,
|
||||
start_period=1 * 1_000 * 1_000_000,
|
||||
),
|
||||
"environment": {
|
||||
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
||||
|
@ -113,8 +113,8 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
"command": "dex serve /config.yml",
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
interval=5 * 1_000 * 1_000_000,
|
||||
start_period=1 * 1_000 * 1_000_000,
|
||||
),
|
||||
"volumes": {str(Path(CONFIG_PATH).absolute()): {"bind": "/config.yml", "mode": "ro"}},
|
||||
}
|
||||
|
@ -83,8 +83,8 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "curl", "http://localhost:8080"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
interval=5 * 1_000 * 1_000_000,
|
||||
start_period=1 * 1_000 * 1_000_000,
|
||||
),
|
||||
"environment": {
|
||||
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
|
||||
|
@ -34,8 +34,8 @@ class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
privileged=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "docker", "info"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=5 * 100 * 1000000,
|
||||
interval=5 * 1_000 * 1_000_000,
|
||||
start_period=5 * 1_000 * 1_000_000,
|
||||
),
|
||||
environment={"DOCKER_TLS_CERTDIR": "/ssl"},
|
||||
volumes={
|
||||
|
@ -34,8 +34,8 @@ class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
privileged=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "docker", "info"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=5 * 100 * 1000000,
|
||||
interval=5 * 1_000 * 1_000_000,
|
||||
start_period=5 * 1_000 * 1_000_000,
|
||||
),
|
||||
environment={"DOCKER_TLS_CERTDIR": "/ssl"},
|
||||
volumes={
|
||||
|
@ -184,28 +184,31 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
</p>
|
||||
</ak-form-element-horizontal> `
|
||||
: html``}
|
||||
${this.providerType.slug === ProviderTypeEnum.Openidconnect
|
||||
? html`
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("OIDC Well-known URL")}
|
||||
name="oidcWellKnownUrl"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.oidcWellKnownUrl,
|
||||
this.providerType.oidcWellKnownUrl,
|
||||
"",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"OIDC well-known configuration URL. Can be used to automatically configure the URLs above.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
${this.providerType.slug === ProviderTypeEnum.Openidconnect ||
|
||||
this.providerType.oidcWellKnownUrl !== ""
|
||||
? html`<ak-form-element-horizontal
|
||||
label=${msg("OIDC Well-known URL")}
|
||||
name="oidcWellKnownUrl"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.oidcWellKnownUrl,
|
||||
this.providerType.oidcWellKnownUrl,
|
||||
"",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"OIDC well-known configuration URL. Can be used to automatically configure the URLs above.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`
|
||||
: html``}
|
||||
${this.providerType.slug === ProviderTypeEnum.Openidconnect ||
|
||||
this.providerType.oidcJwksUrl !== ""
|
||||
? html`<ak-form-element-horizontal
|
||||
label=${msg("OIDC JWKS URL")}
|
||||
name="oidcJwksUrl"
|
||||
>
|
||||
@ -224,7 +227,6 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("OIDC JWKS")} name="oidcJwks">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.JavaScript}
|
||||
@ -232,8 +234,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">${msg("Raw JWKS data.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
`
|
||||
</ak-form-element-horizontal>`
|
||||
: html``}
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
|
@ -62,20 +62,24 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
multipleEnrollmentFlows = false;
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Invitation>> {
|
||||
// Check if any invitation stages exist
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({
|
||||
noFlows: false,
|
||||
});
|
||||
this.invitationStageExists = stages.pagination.count > 0;
|
||||
this.expandable = this.invitationStageExists;
|
||||
stages.results.forEach((stage) => {
|
||||
const enrollmentFlows = (stage.flowSet || []).filter(
|
||||
(flow) => flow.designation === FlowDesignationEnum.Enrollment,
|
||||
);
|
||||
if (enrollmentFlows.length > 1) {
|
||||
this.multipleEnrollmentFlows = true;
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Check if any invitation stages exist
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({
|
||||
noFlows: false,
|
||||
});
|
||||
this.invitationStageExists = stages.pagination.count > 0;
|
||||
this.expandable = this.invitationStageExists;
|
||||
stages.results.forEach((stage) => {
|
||||
const enrollmentFlows = (stage.flowSet || []).filter(
|
||||
(flow) => flow.designation === FlowDesignationEnum.Enrollment,
|
||||
);
|
||||
if (enrollmentFlows.length > 1) {
|
||||
this.multipleEnrollmentFlows = true;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// assuming we can't fetch stages, ignore the error
|
||||
}
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsList({
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2023.10.4";
|
||||
export const VERSION = "2023.10.7";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -257,7 +257,8 @@ select[multiple] option:checked {
|
||||
.pf-c-login__main-header-desc {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-login__main-footer-links-item img {
|
||||
.pf-c-login__main-footer-links-item img,
|
||||
.pf-c-login__main-footer-links-item .fas {
|
||||
filter: invert(1);
|
||||
}
|
||||
.pf-c-login__main-footer-band {
|
||||
@ -310,6 +311,12 @@ select[multiple] option:checked {
|
||||
--pf-c-wizard__nav-link--before--BackgroundColor: transparent;
|
||||
}
|
||||
/* tree view */
|
||||
.pf-c-tree-view__node {
|
||||
--pf-c-tree-view__node--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-tree-view__node-toggle {
|
||||
--pf-c-tree-view__node-toggle--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-tree-view__node:focus {
|
||||
--pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
|
@ -79,6 +79,7 @@ export class PageHeader extends AKElement {
|
||||
}
|
||||
.pf-c-page__main-section {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
@ -89,6 +89,9 @@ export class AuthenticatorValidateStage
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:host([theme="dark"]) .authenticator-button {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
i {
|
||||
font-size: 1.5rem;
|
||||
padding: 1rem 0;
|
||||
|
@ -96,7 +96,7 @@ export class LibraryApplication extends AKElement {
|
||||
this.application.metaPublisher !== "" ||
|
||||
this.application.metaDescription !== "";
|
||||
|
||||
const classes = { "pf-m-selectable pf-m-selected": this.selected };
|
||||
const classes = { "pf-m-selectable": this.selected, "pf-m-selected": this.selected };
|
||||
const styles = this.background ? { background: this.background } : {};
|
||||
|
||||
return html` <div
|
||||
|
@ -38,7 +38,9 @@ export class LibraryPageApplicationList extends AKElement {
|
||||
];
|
||||
|
||||
@property({ attribute: false })
|
||||
apps: Application[] = [];
|
||||
set apps(value: Application[]) {
|
||||
this.fuse.setCollection(value);
|
||||
}
|
||||
|
||||
@property()
|
||||
query = getURLParam<string | undefined>("search", undefined);
|
||||
@ -63,7 +65,7 @@ export class LibraryPageApplicationList extends AKElement {
|
||||
shouldSort: true,
|
||||
ignoreFieldNorm: true,
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.5,
|
||||
threshold: 0.3,
|
||||
});
|
||||
}
|
||||
|
||||
@ -77,7 +79,6 @@ export class LibraryPageApplicationList extends AKElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fuse.setCollection(this.apps);
|
||||
if (!this.query) {
|
||||
return;
|
||||
}
|
||||
|
@ -82,9 +82,9 @@ export class UserInterface extends Interface {
|
||||
:host([theme="dark"]) .pf-c-page__header {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
.pf-c-page__header-tools-item .fas,
|
||||
.pf-c-notification-badge__count,
|
||||
.pf-c-page__header-tools-group .pf-c-button {
|
||||
:host([theme="light"]) .pf-c-page__header-tools-item .fas,
|
||||
:host([theme="light"]) .pf-c-notification-badge__count,
|
||||
:host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button {
|
||||
color: var(--ak-global--Color--100) !important;
|
||||
}
|
||||
.pf-c-page {
|
||||
@ -183,7 +183,7 @@ export class UserInterface extends Interface {
|
||||
<ak-enterprise-status interface="user"></ak-enterprise-status>
|
||||
<div class="pf-c-page">
|
||||
<div class="background-wrapper" style="${this.uiConfig.theme.background}">
|
||||
${this.uiConfig.theme.background === ""
|
||||
${(this.uiConfig.theme.background || "") === ""
|
||||
? html`<div class="background-default-slant"></div>`
|
||||
: html``}
|
||||
</div>
|
||||
|
@ -6037,6 +6037,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6318,6 +6318,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5952,6 +5952,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -2561,31 +2561,6 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target>
|
||||
<source>If the password's score is less than or equal this value, the policy will fail.</source>
|
||||
<target>Si le score du mot de passe est inférieur ou égal à cette valeur, la politique échoue.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s1bfe7505059d164f">
|
||||
<source>0: Too guessable: risky password. (guesses < 10^3)</source>
|
||||
<target>0: Trop prévisible: mot de passe risqué. (essais < 10^3)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s423d1f2477998d0b">
|
||||
<source>1: Very guessable: protection from throttled online attacks. (guesses < 10^6)</source>
|
||||
<target>1: Très prévisible: protection contre les attaques en ligne limitées. (essais < 10^6)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s33849cc046eb901d">
|
||||
<source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)</source>
|
||||
<target>2: Quelque peu prévisible: protection contre les attaques en ligne non limitées. (essais < 10^8)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s578dcce295718e1b">
|
||||
<source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)</source>
|
||||
<target>3: Sûrement imprévisible: protection modérée contre les attaques de hash-lent hors ligne. (essais < 10^10)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7a46de49f4eba5d7">
|
||||
<source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)</source>
|
||||
<target>4: Très imprévisible: forte protection control les attaques de hash-lent hors ligne. (essais >= 10^10)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sd6cd7ce2310a73a4">
|
||||
<source>Checks the value from the policy request against several rules, mostly used to ensure password strength.</source>
|
||||
@ -7597,6 +7572,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<source>Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello).</source>
|
||||
<target>Étape de configuration d'un authentificateur WebAuthn (Yubikey, FaceID/Windows Hello).</target>
|
||||
</trans-unit>
|
||||
<<<<<<< HEAD
|
||||
<trans-unit id="s1cffe58249b04669">
|
||||
<source>Internal application name used in URLs.</source>
|
||||
<target>Nom de l'application interne utilisé dans les URLs.</target>
|
||||
@ -7641,14 +7617,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<source>Your application has been saved</source>
|
||||
<target>L'application a été sauvegardée</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf60f1e5b76897c93">
|
||||
<source>In the Application:</source>
|
||||
<target>Dans l'application :</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7ce65cf482b7bff0">
|
||||
<source>In the Provider:</source>
|
||||
<target>Dans le fournisseur :</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s67d858051b34c38b">
|
||||
<source>Method's display Name.</source>
|
||||
<target>Nom d'affichage de la méthode.</target>
|
||||
@ -7923,17 +7891,50 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<source><No name set></source>
|
||||
<target><No name set></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc9a6ad1af30572c">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth</source>
|
||||
<target>Pour nginx auth_request ou traefik forwardAuth</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc31264ef7ff86ef">
|
||||
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
|
||||
<target>Pour nginx auth_request ou traefik forwardAuth par domaine racine</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc615309d10a9228c">
|
||||
<source>RBAC is in preview.</source>
|
||||
<target>RBAC est en aperçu,</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
<target>Type d'utilisateur pour les utilisateurs nouvellement créés.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
<target>Également appelé Client ID.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
<target>Également appelé Client Secret.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf60f1e5b76897c93">
|
||||
<source>In the Application:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7ce65cf482b7bff0">
|
||||
<source>In the Provider:</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1bfe7505059d164f">
|
||||
<source>0: Too guessable: risky password. (guesses < 10^3)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s423d1f2477998d0b">
|
||||
<source>1: Very guessable: protection from throttled online attacks. (guesses < 10^6)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s33849cc046eb901d">
|
||||
<source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s578dcce295718e1b">
|
||||
<source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7a46de49f4eba5d7">
|
||||
<source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -6160,6 +6160,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -7848,4 +7848,10 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
@ -5945,6 +5945,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -2562,6 +2562,31 @@
|
||||
<source>If the password's score is less than or equal this value, the policy will fail.</source>
|
||||
<target>如果密码分数小于等于此值,则策略失败。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s1bfe7505059d164f">
|
||||
<source>0: Too guessable: risky password. (guesses < 10^3)</source>
|
||||
<target>0:过于易猜测:密码有风险。(猜测次数 < 10^3)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s423d1f2477998d0b">
|
||||
<source>1: Very guessable: protection from throttled online attacks. (guesses < 10^6)</source>
|
||||
<target>1:非常易猜测:可以防范受限的在线攻击。(猜测次数 < 10^6)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s33849cc046eb901d">
|
||||
<source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)</source>
|
||||
<target>2:有些易猜测:可以防范不受限的在线攻击。(猜测次数 < 10^8)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s578dcce295718e1b">
|
||||
<source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)</source>
|
||||
<target>3:难以猜测:适度防范离线慢速哈希场景。(猜测次数 < 10^10)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7a46de49f4eba5d7">
|
||||
<source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)</source>
|
||||
<target>4:非常难以猜测:高度防范离线慢速哈希场景。(猜测次数 >= 10^10)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sd6cd7ce2310a73a4">
|
||||
<source>Checks the value from the policy request against several rules, mostly used to ensure password strength.</source>
|
||||
@ -7917,77 +7942,11 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>User type used for newly created users.</source>
|
||||
<target>新创建用户使用的用户类型。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4a34a6be4c68ec87">
|
||||
<source>Users created</source>
|
||||
<target>已创建用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s275c956687e2e656">
|
||||
<source>Failed logins</source>
|
||||
<target>失败登录</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
<target>也称为客户端 ID。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
<target>也称为客户端密钥。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4476e9c50cfd13f4">
|
||||
<source>Global status</source>
|
||||
<target>全局状态</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd21a971eea208533">
|
||||
<source>Vendor</source>
|
||||
<target>供应商</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sadadfe9dfa06d7dd">
|
||||
<source>No sync status.</source>
|
||||
<target>无同步状态。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2b1c81130a65a55b">
|
||||
<source>Sync currently running.</source>
|
||||
<target>当前正在同步。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf36170f71cea38c2">
|
||||
<source>Connectivity</source>
|
||||
<target>连接性</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd94e99af8b41ff54">
|
||||
<source>0: Too guessable: risky password. (guesses &lt; 10^3)</source>
|
||||
<target>0:过于易猜测:密码有风险。(猜测次数 &lt; 10^3)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc926385d1a624c3a">
|
||||
<source>1: Very guessable: protection from throttled online attacks. (guesses &lt; 10^6)</source>
|
||||
<target>1:非常易猜测:可以防范受限的在线攻击。(猜测次数 &lt; 10^6)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8aae61c41319602c">
|
||||
<source>2: Somewhat guessable: protection from unthrottled online attacks. (guesses &lt; 10^8)</source>
|
||||
<target>2:有些易猜测:可以防范不受限的在线攻击。(猜测次数 &lt; 10^8)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc1f4b57e722a89d6">
|
||||
<source>3: Safely unguessable: moderate protection from offline slow-hash scenario. (guesses &lt; 10^10)</source>
|
||||
<target>3:难以猜测:适度防范离线慢速哈希场景。(猜测次数 &lt; 10^10)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd47f3d3c9741343d">
|
||||
<source>4: Very unguessable: strong protection from offline slow-hash scenario. (guesses &gt;= 10^10)</source>
|
||||
<target>4:非常难以猜测:高度防范离线慢速哈希场景。(猜测次数 &gt;= 10^10)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d2a8b86a4f5a810">
|
||||
<source>Successfully created user and added to group <x id="0" equiv-text="${this.group.name}"/></source>
|
||||
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s824e0943a7104668">
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s62e7f6ed7d9cb3ca">
|
||||
<source>Pretend user exists</source>
|
||||
<target>假作用户存在</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s52bdc80690a9a8dc">
|
||||
<source>When enabled, the stage will always accept the given user identifier and continue.</source>
|
||||
<target>启用时,此阶段总是会接受指定的用户 ID 并继续。</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5993,6 +5993,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -5992,6 +5992,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32babfed740fd3c1">
|
||||
<source>User type used for newly created users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb35c08e3a541188f">
|
||||
<source>Also known as Client ID.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd46fd9b647cfea10">
|
||||
<source>Also known as Client Secret.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -86,7 +86,7 @@ To check if your config has been applied correctly, you can run the following co
|
||||
`AUTHENTIK_REDIS__CACHE_TIMEOUT_REPUTATION` only applies to the cache expiry, see [`AUTHENTIK_REPUTATION__EXPIRY`](#authentik_reputation__expiry) to control how long reputation is persisted for.
|
||||
:::
|
||||
|
||||
## Listen Setting
|
||||
## Listen Settings
|
||||
|
||||
- `AUTHENTIK_LISTEN__HTTP`: Listening address:port (e.g. `0.0.0.0:9000`) for HTTP (Applies to Server and Proxy outpost)
|
||||
- `AUTHENTIK_LISTEN__HTTPS`: Listening address:port (e.g. `0.0.0.0:9443`) for HTTPS (Applies to Server and Proxy outpost)
|
||||
@ -94,7 +94,7 @@ To check if your config has been applied correctly, you can run the following co
|
||||
- `AUTHENTIK_LISTEN__LDAPS`: Listening address:port (e.g. `0.0.0.0:6636`) for LDAPS (Applies to LDAP outpost)
|
||||
- `AUTHENTIK_LISTEN__METRICS`: Listening address:port (e.g. `0.0.0.0:9300`) for Prometheus metrics (Applies to All)
|
||||
- `AUTHENTIK_LISTEN__DEBUG`: Listening address:port (e.g. `0.0.0.0:9900`) for Go Debugging metrics (Applies to All)
|
||||
- `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS`: List of CIDRs that proxy headers should be accepted from (Applies to Server)
|
||||
- `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS`: List of comma-separated CIDRs that proxy headers should be accepted from (Applies to Server)
|
||||
|
||||
Defaults to `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fe80::/10`, `::1/128`.
|
||||
|
||||
|
@ -137,6 +137,62 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2023.10
|
||||
- web/admin: fix @change handler for ak-radio elements (#7348)
|
||||
- web/admin: fix role form reacting to enter (#7330)
|
||||
|
||||
## Fixed in 2023.10.3
|
||||
|
||||
- ci: explicitly give write permissions to packages (cherry-pick #7428) (#7430)
|
||||
- core: fix worker beat toggle inverted (cherry-pick #7508) (#7509)
|
||||
- events: fix gdpr compliance always running (cherry-pick #7491) (#7505)
|
||||
- providers/oauth2: set auth_via for token and other endpoints (cherry-pick #7417) (#7427)
|
||||
- providers/proxy: fix closed redis client (cherry-pick #7385) (#7429)
|
||||
- root: Improve multi arch Docker image build speed (cherry-pick #7355) (#7426)
|
||||
- sources/oauth: fix patreon (cherry-pick #7454) (#7456)
|
||||
- stages/email: fix duplicate querystring encoding (cherry-pick #7386) (#7425)
|
||||
- web: bugfix: broken backchannel selector (cherry-pick #7480) (#7507)
|
||||
- web/admin: fix html error on oauth2 provider page (cherry-pick #7384) (#7424)
|
||||
- web/flows: attempt to fix bitwareden android compatibility (cherry-pick #7455) (#7457)
|
||||
|
||||
## Fixed in 2023.10.4
|
||||
|
||||
- ci: fix permissions for release pipeline to publish binaries (cherry-pick #7512) (#7621)
|
||||
- core: bump golang from 1.21.3-bookworm to 1.21.4-bookworm (cherry-pick #7483) (#7622)
|
||||
- events: don't update internal service accounts unless needed (cherry-pick #7611) (#7640)
|
||||
- events: fix missing model\_\* events when not directly authenticated (cherry-pick #7588) (#7597)
|
||||
- events: sanitize functions (cherry-pick #7587) (#7589)
|
||||
- providers/proxy: Fix duplicate cookies when using file system store. (cherry-pick #7541) (#7544)
|
||||
- providers/scim: fix missing schemas attribute for User and Group (cherry-pick #7477) (#7596)
|
||||
- root: specify node and python versions in respective config files, deduplicate in CI (#7620)
|
||||
- security: fix [CVE-2023-48228](../../security/CVE-2023-48228.md), Reported by [@Sapd](https://github.com/Sapd) (#7666)
|
||||
- stages/email: use uuid for email confirmation token instead of username (cherry-pick #7581) (#7584)
|
||||
- web/admin: fix admins not able to delete MFA devices (#7660)
|
||||
|
||||
## Fixed in 2023.10.5
|
||||
|
||||
- blueprints: improve file change handler (cherry-pick #7813) (#7934)
|
||||
- events: add better fallback for sanitize_item to ensure everything can be saved as JSON (cherry-pick #7694) (#7937)
|
||||
- events: fix lint (#7700)
|
||||
- events: include user agent in events (cherry-pick #7693) (#7938)
|
||||
- providers/scim: change familyName default (cherry-pick #7904) (#7930)
|
||||
- root: don't show warning when app has no URLs to import (cherry-pick #7765) (#7935)
|
||||
- root: Fix cache related image build issues (cherry-pick #7831) (#7932)
|
||||
- stages/email: improve error handling for incorrect template syntax (cherry-pick #7758) (#7936)
|
||||
- tests: fix flaky tests (cherry-pick #7676) (#7939)
|
||||
- web: dark/light theme fixes (#7872)
|
||||
- web: fix overflow glitch on ak-page-header (cherry-pick #7883) (#7931)
|
||||
- web/admin: always show oidc well-known URL fields when they're set (#7560)
|
||||
- web/user: fix search not updating app (cherry-pick #7825) (#7933)
|
||||
|
||||
## Fixed in 2023.10.6
|
||||
|
||||
- core: fix PropertyMapping context not being available in request context
|
||||
- outposts: disable deployment and secret reconciler for embedded outpost in code instead of in config (cherry-pick #8021) (#8024)
|
||||
- outposts: fix Outpost reconcile not re-assigning managed attribute (cherry-pick #8014) (#8020)
|
||||
- providers/oauth2: fix [CVE-2024-21637](../../security/CVE-2024-21637.md), Reported by [@lauritzh](https://github.com/lauritzh) (#8104)
|
||||
- providers/oauth2: remember session_id from initial token (cherry-pick #7976) (#7977)
|
||||
- providers/proxy: use access token (cherry-pick #8022) (#8023)
|
||||
- rbac: fix error when looking up permissions for now uninstalled apps (cherry-pick #8068) (#8070)
|
||||
- sources/oauth: fix missing get_user_id for OIDC-like sources (Azure AD) (#7970)
|
||||
- web/flows: fix device picker incorrect foreground color (cherry-pick #8067) (#8069)
|
||||
|
||||
## API Changes
|
||||
|
||||
#### What's New
|
||||
|
39
website/docs/security/CVE-2024-21637.md
Normal file
39
website/docs/security/CVE-2024-21637.md
Normal file
@ -0,0 +1,39 @@
|
||||
# CVE-2024-21637
|
||||
|
||||
_Reported by [@lauritzh](https://github.com/lauritzh)_
|
||||
|
||||
## XSS in Authentik via JavaScript-URI as Redirect URI and form_post Response Mode
|
||||
|
||||
### Summary
|
||||
|
||||
Given an OAuth2 provider configured with allowed redirect URIs set to `*` or `.*`, an attacker can send an OAuth Authorization request using `response_mode=form_post` and setting `redirect_uri` to a malicious URI, to capture authentik's session token.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2023.8.6 and 2023.10.6 fix this issue.
|
||||
|
||||
### Impact
|
||||
|
||||
The impact depends on the attack scenario. In the following I will describe the two scenario that were identified for Authentik.
|
||||
|
||||
#### Redirect URI Misconfiguration
|
||||
|
||||
While advising that this may cause security issues, Authentik generally allows wildcards as Redirect URI. Therefore, using a wildcard-only effectively allowing arbitrary URLS is possible misconfiguration that may be present in real-world instances.
|
||||
|
||||
In such cases, unauthenticated and unprivileged attackers can perform the above described actions.
|
||||
|
||||
### User with (only) App Administration Permissions
|
||||
|
||||
A more likely scenario is an administrative user (e.g. a normal developer) having only permissions to manage applications.
|
||||
|
||||
This relatively user could use the described attacks to perform a privilege escalation.
|
||||
|
||||
### Workaround
|
||||
|
||||
It is recommended to upgrade to the patched version of authentik. If not possible, ensure that OAuth2 providers do not use a wildcard (`*` or `.*`) value as allowed redirect URI setting. (This is _not_ exploitable if part of the redirect URI has a wildcard, for example `https://foo-.*\.bar\.com`)
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
27
website/docs/security/CVE-2024-23647.md
Normal file
27
website/docs/security/CVE-2024-23647.md
Normal file
@ -0,0 +1,27 @@
|
||||
# CVE-2024-23647
|
||||
|
||||
_Reported by [@pieterphilippaerts](https://github.com/pieterphilippaerts)_
|
||||
|
||||
## PKCE downgrade attack in authentik
|
||||
|
||||
## Summary
|
||||
|
||||
PKCE is a very important countermeasure in OAuth2 , both for public and confidential clients. It protects against CSRF attacks and code injection attacks. Because of this bug, an attacker can circumvent the protection PKCE offers.
|
||||
|
||||
## Patches
|
||||
|
||||
authentik 2023.8.7 and 2023.10.7 fix this issue.
|
||||
|
||||
## Details
|
||||
|
||||
There is a bug in our implementation of PKCE that allows an attacker to circumvent the protection that PKCE offers. PKCE adds the `code_challenge’ parameter to the authorization request and adds the `code_verifier’ parameter to the token request. We recently fixed a downgrade attack (in v2023.8.5 and 2023.10.4) where if the attacker removed the `code_verifier’ parameter in the token request, authentik would allow the request to pass, thus circumventing PKCE’s protection. However, in the latest version of the software, another downgrade scenario is still possible: if the attacker removes the `code_challenge’ parameter from the authorization request, authentik will also not do the PKCE check.
|
||||
|
||||
Note that this type of downgrade enables an attacker to perform a code injection attack, even if the OAuth client is using PKCE (which is supposed to protect against code injection attacks). To start the attack, the attacker must initiate the authorization process without that `code_challenge’ parameter in the authorization request. But this is easy to do (just use a phishing site or email to trick the user into clicking on a link that the attacker controls – the authorization link without that `code_challenge’ parameter).
|
||||
|
||||
The OAuth BCP (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics) explicitly mentions this particular attack in section 2.1.1: “Authorization servers MUST mitigate PKCE Downgrade Attacks by ensuring that a token request containing a code_verifier parameter is accepted only if a code_challenge parameter was present in the authorization request, see Section 4.8.2 for details.”
|
||||
|
||||
## For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
@ -407,6 +407,8 @@ const docsSidebar = {
|
||||
},
|
||||
items: [
|
||||
"security/policy",
|
||||
"security/CVE-2024-23647",
|
||||
"security/CVE-2024-21637",
|
||||
"security/CVE-2023-48228",
|
||||
"security/GHSA-rjvp-29xq-f62w",
|
||||
"security/CVE-2023-39522",
|
||||
|
Reference in New Issue
Block a user