Merge branch 'master' into inbuilt-proxy

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	internal/constants/constants.go
#	outpost/pkg/version.go
This commit is contained in:
Jens Langhammer 2021-07-05 19:11:26 +02:00
commit 948db46406
46 changed files with 559 additions and 188 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2021.6.3 current_version = 2021.6.4
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)

View File

@ -33,14 +33,14 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik:2021.6.3, beryju/authentik:2021.6.4,
beryju/authentik:latest, beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.6.3, ghcr.io/goauthentik/server:2021.6.4,
ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
context: . context: .
- name: Building Docker Image (stable) - name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }} if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
run: | run: |
docker pull beryju/authentik:latest docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable docker tag beryju/authentik:latest beryju/authentik:stable
@ -75,14 +75,14 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik-proxy:2021.6.3, beryju/authentik-proxy:2021.6.4,
beryju/authentik-proxy:latest, beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.6.3, ghcr.io/goauthentik/proxy:2021.6.4,
ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:latest
file: proxy.Dockerfile file: proxy.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable) - name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }} if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
run: | run: |
docker pull beryju/authentik-proxy:latest docker pull beryju/authentik-proxy:latest
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
@ -117,14 +117,14 @@ jobs:
with: with:
push: ${{ github.event_name == 'release' }} push: ${{ github.event_name == 'release' }}
tags: | tags: |
beryju/authentik-ldap:2021.6.3, beryju/authentik-ldap:2021.6.4,
beryju/authentik-ldap:latest, beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.6.3, ghcr.io/goauthentik/ldap:2021.6.4,
ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:latest
file: ldap.Dockerfile file: ldap.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable) - name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }} if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }}
run: | run: |
docker pull beryju/authentik-ldap:latest docker pull beryju/authentik-ldap:latest
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
@ -176,7 +176,6 @@ jobs:
SENTRY_PROJECT: authentik SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org SENTRY_URL: https://sentry.beryju.org
with: with:
version: authentik@2021.6.3 version: authentik@2021.6.4
environment: beryjuorg-prod environment: beryjuorg-prod
sourcemaps: './web/dist' sourcemaps: './web/dist'
finalize: false

26
Pipfile.lock generated
View File

@ -122,19 +122,19 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:055f9dc07f95f202a4dc25196a3a9f1e2f137171ee364cf980e4673de75fb529", "sha256:3b35689c215c982fe9f7ef78d748aa9b0cd15c3b2eb04f9b460aaa63fe2fbd03",
"sha256:bc9b278e362ec9b531511a498262297f074c4f5ca9560455919a0af1a4698615" "sha256:b1cbeb92123799001b97f2ee1cdf470e21f1be08314ae28fc7ea357925186f1c"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.104" "version": "==1.17.105"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:23aa3238c004319f78423eb8cbba2813b62ee64d0e3bab04e0a00e067f99542a", "sha256:b0fda4edf8eb105453890700d49011ada576d0cc7326a0699dfabe9e872f552c",
"sha256:95ab472c8254b8d2cfa6d719b433e511fbcf80895b4cd18e4219c1efa0b78270" "sha256:b5ba72d22212b0355f339c2a98b3296b3b2202a48e6a2b1366e866bc65a64b67"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.20.104" "version": "==1.20.105"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
@ -778,11 +778,11 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.9" "version": "==21.0"
}, },
"prometheus-client": { "prometheus-client": {
"hashes": [ "hashes": [
@ -1585,7 +1585,7 @@
"sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56", "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
"sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c" "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
], ],
"markers": "python_version < '4' and python_full_version >= '3.6.1'", "markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
"version": "==5.9.1" "version": "==5.9.1"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
@ -1632,11 +1632,11 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.9" "version": "==21.0"
}, },
"pathspec": { "pathspec": {
"hashes": [ "hashes": [

View File

@ -1,3 +1,3 @@
"""authentik""" """authentik"""
__version__ = "2021.6.3" __version__ = "2021.6.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -2,12 +2,11 @@
from json import loads from json import loads
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http.response import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.http import urlencode from django.utils.http import urlencode
from django_filters.filters import BooleanFilter, CharFilter from django_filters.filters import BooleanFilter, CharFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.utils import get_anonymous_user from guardian.utils import get_anonymous_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, SerializerMethodField from rest_framework.fields import CharField, JSONField, SerializerMethodField
@ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@extend_schema( @extend_schema(
responses={ responses={
"200": LinkSerializer(many=False), "200": LinkSerializer(many=False),
"404": OpenApiResponse(description="No recovery flow found."), "404": LinkSerializer(many=False),
}, },
) )
@action(detail=True, pagination_class=None, filter_backends=[]) @action(detail=True, pagination_class=None, filter_backends=[])
@ -184,7 +183,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
# Check that there is a recovery flow, if not return an error # Check that there is a recovery flow, if not return an error
flow = tenant.flow_recovery flow = tenant.flow_recovery
if not flow: if not flow:
raise Http404 return Response({"link": ""}, status=404)
user: User = self.get_object() user: User = self.get_object()
token, __ = Token.objects.get_or_create( token, __ = Token.objects.get_or_create(
identifier=f"{user.uid}-password-reset", identifier=f"{user.uid}-password-reset",

View File

@ -14,7 +14,9 @@ def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields""" """Ensure a value is a dictionary, useful for JSONFields"""
if isinstance(value, dict): if isinstance(value, dict):
return return
raise ValidationError("Value must be a dictionary.") raise ValidationError(
"Value must be a dictionary, and not have any duplicate keys."
)
class PassiveSerializer(Serializer): class PassiveSerializer(Serializer):

View File

@ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
fields = [ fields = [
"pk", "pk",
"name", "name",
"fingerprint", "fingerprint_sha256",
"fingerprint_sha1",
"certificate_data", "certificate_data",
"key_data", "key_data",
"cert_expiry", "cert_expiry",

View File

@ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel):
return self._private_key return self._private_key
@property @property
def fingerprint(self) -> str: def fingerprint_sha256(self) -> str:
"""Get SHA256 Fingerprint of certificate_data""" """Get SHA256 Fingerprint of certificate_data"""
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode( return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
"utf-8" "utf-8"
) )
@property
def fingerprint_sha1(self) -> str:
"""Get SHA1 Fingerprint of certificate_data"""
return hexlify(
self.certificate.fingerprint(hashes.SHA1()), ":" # nosec
).decode("utf-8")
@property @property
def kid(self): def kid(self):
"""Get Key ID used for JWKS""" """Get Key ID used for JWKS"""

View File

@ -3,6 +3,7 @@ from functools import partial
from typing import Callable from typing import Callable
from django.conf import settings from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
@ -63,7 +64,15 @@ class AuditMiddleware:
if settings.DEBUG: if settings.DEBUG:
return return
if before_send({}, {"exc_info": (None, exception, None)}) is not None: # Special case for SuspiciousOperation, we have a special event action for that
if isinstance(exception, SuspiciousOperation):
thread = EventNewThread(
EventAction.SUSPICIOUS_REQUEST,
request,
message=str(exception),
)
thread.run()
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
thread = EventNewThread( thread = EventNewThread(
EventAction.SYSTEM_EXCEPTION, EventAction.SYSTEM_EXCEPTION,
request, request,

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.4 on 2021-07-03 13:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0021_flowstagebinding_invalid_response_action"),
]
operations = [
migrations.AlterField(
model_name="flowstagebinding",
name="invalid_response_action",
field=models.TextField(
choices=[
("retry", "Retry"),
("restart", "Restart"),
("restart_with_context", "Restart With Context"),
],
default="retry",
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
),
),
]

View File

@ -134,7 +134,7 @@ class FlowExecutorView(APIView):
message = exc.__doc__ if exc.__doc__ else str(exc) message = exc.__doc__ if exc.__doc__ else str(exc)
return self.stage_invalid(error_message=message) return self.stage_invalid(error_message=message)
# pylint: disable=unused-argument # pylint: disable=unused-argument, too-many-return-statements
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
# Early check if theres an active Plan for the current session # Early check if theres an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session: if SESSION_KEY_PLAN in self.request.session:
@ -167,7 +167,18 @@ class FlowExecutorView(APIView):
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", "")) request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
# We don't save the Plan after getting the next stage # We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet # as it hasn't been successfully passed yet
try:
# This is the first time we actually access any attribute on the selected plan
# if the cached plan is from an older version, it might have different attributes
# in which case we just delete the plan and invalidate everything
next_binding = self.plan.next(self.request) next_binding = self.plan.next(self.request)
except Exception as exc: # pylint: disable=broad-except
self._logger.warning(
"f(exec): found incompatible flow plan, invalidating run", exc=exc
)
keys = cache.keys("flow_*")
cache.delete_many(keys)
return self.stage_invalid()
if not next_binding: if not next_binding:
self._logger.debug("f(exec): no more stages, flow is done.") self._logger.debug("f(exec): no more stages, flow is done.")
return self._flow_done() return self._flow_done()

View File

@ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer):
raise ValidationError( raise ValidationError(
( (
f"Outpost type {self.initial_data['type']} can't be used with " f"Outpost type {self.initial_data['type']} can't be used with "
f"{type(provider)} providers." f"{provider.__class__.__name__} providers."
) )
) )
return providers return providers

View File

@ -36,8 +36,10 @@ class DockerController(BaseController):
def _get_env(self) -> dict[str, str]: def _get_env(self) -> dict[str, str]:
return { return {
"AUTHENTIK_HOST": self.outpost.config.authentik_host, "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure), "AUTHENTIK_INSECURE": str(
self.outpost.config.authentik_host_insecure
).lower(),
"AUTHENTIK_TOKEN": self.outpost.token.key, "AUTHENTIK_TOKEN": self.outpost.token.key,
} }
@ -45,11 +47,10 @@ class DockerController(BaseController):
"""Check if container's env is equal to what we would set. Return true if container needs """Check if container's env is equal to what we would set. Return true if container needs
to be rebuilt.""" to be rebuilt."""
should_be = self._get_env() should_be = self._get_env()
container_env = container.attrs.get("Config", {}).get("Env", {}) container_env = container.attrs.get("Config", {}).get("Env", [])
for key, expected_value in should_be.items(): for key, expected_value in should_be.items():
if key not in container_env: entry = f"{key.upper()}={expected_value}"
continue if entry not in container_env:
if container_env[key] != expected_value:
return True return True
return False return False
@ -62,14 +63,17 @@ class DockerController(BaseController):
# When the container isn't running, the API doesn't report any port mappings # When the container isn't running, the API doesn't report any port mappings
if container.status != "running": if container.status != "running":
return False return False
# {'6379/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '6379'}]} # {'3389/tcp': [
# {'HostIp': '0.0.0.0', 'HostPort': '389'},
# {'HostIp': '::', 'HostPort': '389'}
# ]}
for port in self.deployment_ports: for port in self.deployment_ports:
key = f"{port.inner_port or port.port}/{port.protocol.lower()}" key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
if key not in container.ports: if key not in container.ports:
return True return True
host_matching = False host_matching = False
for host_port in container.ports[key]: for host_port in container.ports[key]:
host_matching = host_port.get("HostPort") == port.port host_matching = host_port.get("HostPort") == str(port.port)
if not host_matching: if not host_matching:
return True return True
return False return False
@ -79,7 +83,7 @@ class DockerController(BaseController):
try: try:
return self.client.containers.get(container_name), False return self.client.containers.get(container_name), False
except NotFound: except NotFound:
self.logger.info("Container does not exist, creating") self.logger.info("(Re-)creating container...")
image_name = self.get_container_image() image_name = self.get_container_image()
self.client.images.pull(image_name) self.client.images.pull(image_name)
container_args = { container_args = {
@ -107,6 +111,7 @@ class DockerController(BaseController):
try: try:
container, has_been_created = self._get_container() container, has_been_created = self._get_container()
if has_been_created: if has_been_created:
container.start()
return None return None
# Check if the container is out of date, delete it and retry # Check if the container is out of date, delete it and retry
if len(container.image.tags) > 0: if len(container.image.tags) > 0:
@ -164,6 +169,7 @@ class DockerController(BaseController):
self.logger.info("Container is not running, restarting...") self.logger.info("Container is not running, restarting...")
container.start() container.start()
return None return None
self.logger.info("Container is running")
return None return None
except DockerException as exc: except DockerException as exc:
raise ControllerException(str(exc)) from exc raise ControllerException(str(exc)) from exc

View File

@ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = {
}, },
"outposts_service_connection_check": { "outposts_service_connection_check": {
"task": "authentik.outposts.tasks.outpost_service_connection_monitor", "task": "authentik.outposts.tasks.outpost_service_connection_monitor",
"schedule": crontab(minute="*/60"), "schedule": crontab(minute="*/5"),
"options": {"queue": "authentik_scheduled"}, "options": {"queue": "authentik_scheduled"},
}, },
"outpost_token_ensurer": { "outpost_token_ensurer": {

View File

@ -1,7 +1,7 @@
"""authentik outpost signals""" """authentik outpost signals"""
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save, pre_delete, pre_save from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -46,6 +46,14 @@ def pre_save_outpost(sender, instance: Outpost, **_):
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True) outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
@receiver(m2m_changed, sender=Outpost.providers.through)
# pylint: disable=unused-argument
def m2m_changed_update(sender, instance: Model, action: str, **_):
"""Update outpost on m2m change, when providers are added or removed"""
if action in ["post_add", "post_remove", "post_clear"]:
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
@receiver(post_save) @receiver(post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post_save_update(sender, instance: Model, **_): def post_save_update(sender, instance: Model, **_):

View File

@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
"expires", "expires",
"scope", "scope",
"id_token", "id_token",
"revoked",
] ]
depth = 2 depth = 2

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-07-03 13:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0014_alter_oauth2provider_rsa_key"),
]
operations = [
migrations.AddField(
model_name="authorizationcode",
name="revoked",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="refreshtoken",
name="revoked",
field=models.BooleanField(default=False),
),
]

View File

@ -318,6 +318,7 @@ class BaseGrantModel(models.Model):
provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE) provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE)
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE) user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
_scope = models.TextField(default="", verbose_name=_("Scopes")) _scope = models.TextField(default="", verbose_name=_("Scopes"))
revoked = models.BooleanField(default=False)
@property @property
def scope(self) -> list[str]: def scope(self) -> list[str]:
@ -473,9 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
# Convert datetimes into timestamps. # Convert datetimes into timestamps.
now = int(time.time()) now = int(time.time())
iat_time = now iat_time = now
exp_time = int( exp_time = int(dateformat.format(self.expires, "U"))
now + timedelta_from_string(self.provider.token_validity).total_seconds()
)
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
auth_events = Event.objects.filter( auth_events = Event.objects.filter(
action=EventAction.LOGIN, user=get_user(user) action=EventAction.LOGIN, user=get_user(user)

View File

@ -6,6 +6,8 @@ from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_AUTHORIZATION_CODE,
@ -39,7 +41,8 @@ class TestToken(OAuthTestCase):
client_id=generate_client_id(), client_id=generate_client_id(),
client_secret=generate_client_secret(), client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid", redirect_uris="http://testserver",
rsa_key=CertificateKeyPair.objects.first(),
) )
header = b64encode( header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode() f"{provider.client_id}:{provider.client_secret}".encode()
@ -53,11 +56,13 @@ class TestToken(OAuthTestCase):
data={ data={
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE, "grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
"code": code.code, "code": code.code,
"redirect_uri": "http://local.invalid", "redirect_uri": "http://testserver",
}, },
HTTP_AUTHORIZATION=f"Basic {header}", HTTP_AUTHORIZATION=f"Basic {header}",
) )
params = TokenParams.from_request(request) params = TokenParams.parse(
request, provider, provider.client_id, provider.client_secret
)
self.assertEqual(params.provider, provider) self.assertEqual(params.provider, provider)
def test_request_refresh_token(self): def test_request_refresh_token(self):
@ -68,6 +73,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(), client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
) )
header = b64encode( header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode() f"{provider.client_id}:{provider.client_secret}".encode()
@ -87,7 +93,9 @@ class TestToken(OAuthTestCase):
}, },
HTTP_AUTHORIZATION=f"Basic {header}", HTTP_AUTHORIZATION=f"Basic {header}",
) )
params = TokenParams.from_request(request) params = TokenParams.parse(
request, provider, provider.client_id, provider.client_secret
)
self.assertEqual(params.provider, provider) self.assertEqual(params.provider, provider)
def test_auth_code_view(self): def test_auth_code_view(self):
@ -98,6 +106,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(), client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
) )
# Needs to be assigned to an application for iss to be set # Needs to be assigned to an application for iss to be set
self.app.provider = provider self.app.provider = provider
@ -141,6 +150,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(), client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
) )
# Needs to be assigned to an application for iss to be set # Needs to be assigned to an application for iss to be set
self.app.provider = provider self.app.provider = provider
@ -193,6 +203,7 @@ class TestToken(OAuthTestCase):
client_secret=generate_client_secret(), client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
rsa_key=CertificateKeyPair.objects.first(),
) )
header = b64encode( header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode() f"{provider.client_id}:{provider.client_secret}".encode()
@ -230,3 +241,65 @@ class TestToken(OAuthTestCase):
), ),
}, },
) )
def test_refresh_token_revoke(self):
"""test request param"""
provider = OAuth2Provider.objects.create(
name="test",
client_id=generate_client_id(),
client_secret=generate_client_secret(),
authorization_flow=Flow.objects.first(),
redirect_uris="http://testserver",
rsa_key=CertificateKeyPair.objects.first(),
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
self.app.save()
header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode()
).decode()
user = User.objects.get(username="akadmin")
token: RefreshToken = RefreshToken.objects.create(
provider=provider,
user=user,
refresh_token=generate_client_id(),
)
# Create initial refresh token
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": token.refresh_token,
"redirect_uri": "http://testserver",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
new_token: RefreshToken = (
RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first()
)
# Post again with initial token -> get new refresh token
# and revoke old one
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": new_token.refresh_token,
"redirect_uri": "http://local.invalid",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertEqual(response.status_code, 200)
# Post again with old token, is now revoked and should error
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": new_token.refresh_token,
"redirect_uri": "http://local.invalid",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertEqual(response.status_code, 400)
self.assertTrue(
Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists()
)

View File

@ -10,6 +10,7 @@ from django.http.response import HttpResponseRedirect
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.providers.oauth2.errors import BearerTokenError from authentik.providers.oauth2.errors import BearerTokenError
from authentik.providers.oauth2.models import RefreshToken from authentik.providers.oauth2.models import RefreshToken
@ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s
if not allowed: if not allowed:
LOGGER.warning( LOGGER.warning(
"CORS: Origin is not an allowed origin", "CORS: Origin is not an allowed origin",
requested=origin, requested=received_origin,
allowed=allowed_origins, allowed=allowed_origins,
) )
return response return response
@ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]):
raise BearerTokenError("invalid_token") raise BearerTokenError("invalid_token")
try: try:
kwargs["token"] = RefreshToken.objects.get( token: RefreshToken = RefreshToken.objects.get(
access_token=access_token access_token=access_token
) )
except RefreshToken.DoesNotExist: except RefreshToken.DoesNotExist:
LOGGER.debug("Token does not exist", access_token=access_token) LOGGER.debug("Token does not exist", access_token=access_token)
raise BearerTokenError("invalid_token") raise BearerTokenError("invalid_token")
if kwargs["token"].is_expired: if token.is_expired:
LOGGER.debug("Token has expired", access_token=access_token) LOGGER.debug("Token has expired", access_token=access_token)
raise BearerTokenError("invalid_token") raise BearerTokenError("invalid_token")
if not set(scopes).issubset(set(kwargs["token"].scope)): if token.revoked:
LOGGER.warning("Revoked token was used", access_token=access_token)
Event.new(
action=EventAction.SUSPICIOUS_REQUEST,
message="Revoked refresh token was used",
token=access_token,
).from_http(request)
raise BearerTokenError("invalid_token")
if not set(scopes).issubset(set(token.scope)):
LOGGER.warning( LOGGER.warning(
"Scope missmatch.", "Scope missmatch.",
required=set(scopes), required=set(scopes),
token_has=set(kwargs["token"].scope), token_has=set(token.scope),
) )
raise BearerTokenError("insufficient_scope") raise BearerTokenError("insufficient_scope")
except BearerTokenError as error: except BearerTokenError as error:
@ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]):
"WWW-Authenticate" "WWW-Authenticate"
] = f'error="{error.code}", error_description="{error.description}"' ] = f'error="{error.code}", error_description="{error.description}"'
return response return response
kwargs["token"] = token
return view(request, *args, **kwargs) return view(request, *args, **kwargs)
return view_wrapper return view_wrapper

View File

@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
from django.views import View from django.views import View
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_AUTHORIZATION_CODE,
@ -30,6 +31,7 @@ LOGGER = get_logger()
@dataclass @dataclass
# pylint: disable=too-many-instance-attributes
class TokenParams: class TokenParams:
"""Token params""" """Token params"""
@ -40,6 +42,8 @@ class TokenParams:
state: str state: str
scope: list[str] scope: list[str]
provider: OAuth2Provider
authorization_code: Optional[AuthorizationCode] = None authorization_code: Optional[AuthorizationCode] = None
refresh_token: Optional[RefreshToken] = None refresh_token: Optional[RefreshToken] = None
@ -47,35 +51,34 @@ class TokenParams:
raw_code: InitVar[str] = "" raw_code: InitVar[str] = ""
raw_token: InitVar[str] = "" raw_token: InitVar[str] = ""
request: InitVar[Optional[HttpRequest]] = None
@staticmethod @staticmethod
def from_request(request: HttpRequest) -> "TokenParams": def parse(
"""Extract Token Parameters from http request""" request: HttpRequest,
client_id, client_secret = extract_client_auth(request) provider: OAuth2Provider,
client_id: str,
client_secret: str,
) -> "TokenParams":
"""Parse params for request"""
return TokenParams( return TokenParams(
# Init vars
raw_code=request.POST.get("code", ""),
raw_token=request.POST.get("refresh_token", ""),
request=request,
# Regular params
provider=provider,
client_id=client_id, client_id=client_id,
client_secret=client_secret, client_secret=client_secret,
redirect_uri=request.POST.get("redirect_uri", ""), redirect_uri=request.POST.get("redirect_uri", ""),
grant_type=request.POST.get("grant_type", ""), grant_type=request.POST.get("grant_type", ""),
raw_code=request.POST.get("code", ""),
raw_token=request.POST.get("refresh_token", ""),
state=request.POST.get("state", ""), state=request.POST.get("state", ""),
scope=request.POST.get("scope", "").split(), scope=request.POST.get("scope", "").split(),
# PKCE parameter. # PKCE parameter.
code_verifier=request.POST.get("code_verifier"), code_verifier=request.POST.get("code_verifier"),
) )
def __post_init__(self, raw_code, raw_token): def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
try:
provider: OAuth2Provider = OAuth2Provider.objects.get(
client_id=self.client_id
)
self.provider = provider
except OAuth2Provider.DoesNotExist:
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
raise TokenError("invalid_client")
if self.provider.client_type == ClientTypes.CONFIDENTIAL: if self.provider.client_type == ClientTypes.CONFIDENTIAL:
if self.provider.client_secret != self.client_secret: if self.provider.client_secret != self.client_secret:
LOGGER.warning( LOGGER.warning(
@ -87,7 +90,6 @@ class TokenParams:
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
self.__post_init_code(raw_code) self.__post_init_code(raw_code)
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN: elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
if not raw_token: if not raw_token:
LOGGER.warning("Missing refresh token") LOGGER.warning("Missing refresh token")
@ -107,7 +109,14 @@ class TokenParams:
token=raw_token, token=raw_token,
) )
raise TokenError("invalid_grant") raise TokenError("invalid_grant")
if self.refresh_token.revoked:
LOGGER.warning("Refresh token is revoked", token=raw_token)
Event.new(
action=EventAction.SUSPICIOUS_REQUEST,
message="Revoked refresh token was used",
token=raw_token,
).from_http(request)
raise TokenError("invalid_grant")
else: else:
LOGGER.warning("Invalid grant type", grant_type=self.grant_type) LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
raise TokenError("unsupported_grant_type") raise TokenError("unsupported_grant_type")
@ -159,13 +168,14 @@ class TokenParams:
class TokenView(View): class TokenView(View):
"""Generate tokens for clients""" """Generate tokens for clients"""
provider: Optional[OAuth2Provider] = None
params: Optional[TokenParams] = None params: Optional[TokenParams] = None
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
response = super().dispatch(request, *args, **kwargs) response = super().dispatch(request, *args, **kwargs)
allowed_origins = [] allowed_origins = []
if self.params: if self.provider:
allowed_origins = self.params.provider.redirect_uris.split("\n") allowed_origins = self.provider.redirect_uris.split("\n")
cors_allow(self.request, response, *allowed_origins) cors_allow(self.request, response, *allowed_origins)
return response return response
@ -175,19 +185,32 @@ class TokenView(View):
def post(self, request: HttpRequest) -> HttpResponse: def post(self, request: HttpRequest) -> HttpResponse:
"""Generate tokens for clients""" """Generate tokens for clients"""
try: try:
self.params = TokenParams.from_request(request) client_id, client_secret = extract_client_auth(request)
try:
self.provider = OAuth2Provider.objects.get(client_id=client_id)
except OAuth2Provider.DoesNotExist:
LOGGER.warning(
"OAuth2Provider does not exist", client_id=self.client_id
)
raise TokenError("invalid_client")
if not self.provider:
raise ValueError
self.params = TokenParams.parse(
request, self.provider, client_id, client_secret
)
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
return TokenResponse(self.create_code_response_dic()) return TokenResponse(self.create_code_response())
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN: if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
return TokenResponse(self.create_refresh_response_dic()) return TokenResponse(self.create_refresh_response())
raise ValueError(f"Invalid grant_type: {self.params.grant_type}") raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
except TokenError as error: except TokenError as error:
return TokenResponse(error.create_dict(), status=400) return TokenResponse(error.create_dict(), status=400)
except UserAuthError as error: except UserAuthError as error:
return TokenResponse(error.create_dict(), status=403) return TokenResponse(error.create_dict(), status=403)
def create_code_response_dic(self) -> dict[str, Any]: def create_code_response(self) -> dict[str, Any]:
"""See https://tools.ietf.org/html/rfc6749#section-4.1""" """See https://tools.ietf.org/html/rfc6749#section-4.1"""
refresh_token = self.params.authorization_code.provider.create_refresh_token( refresh_token = self.params.authorization_code.provider.create_refresh_token(
@ -211,7 +234,7 @@ class TokenView(View):
# We don't need to store the code anymore. # We don't need to store the code anymore.
self.params.authorization_code.delete() self.params.authorization_code.delete()
response_dict = { return {
"access_token": refresh_token.access_token, "access_token": refresh_token.access_token,
"refresh_token": refresh_token.refresh_token, "refresh_token": refresh_token.refresh_token,
"token_type": "bearer", "token_type": "bearer",
@ -223,9 +246,7 @@ class TokenView(View):
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()), "id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
} }
return response_dict def create_refresh_response(self) -> dict[str, Any]:
def create_refresh_response_dic(self) -> dict[str, Any]:
"""See https://tools.ietf.org/html/rfc6749#section-6""" """See https://tools.ietf.org/html/rfc6749#section-6"""
unauthorized_scopes = set(self.params.scope) - set( unauthorized_scopes = set(self.params.scope) - set(
@ -253,10 +274,11 @@ class TokenView(View):
# Store the refresh_token. # Store the refresh_token.
refresh_token.save() refresh_token.save()
# Forget the old token. # Mark old token as revoked
self.params.refresh_token.delete() self.params.refresh_token.revoked = True
self.params.refresh_token.save()
dic = { return {
"access_token": refresh_token.access_token, "access_token": refresh_token.access_token,
"refresh_token": refresh_token.refresh_token, "refresh_token": refresh_token.refresh_token,
"token_type": "bearer", "token_type": "bearer",
@ -267,5 +289,3 @@ class TokenView(View):
), ),
"id_token": self.params.provider.encode(refresh_token.id_token.to_dict()), "id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
} }
return dic

View File

@ -1,4 +1,5 @@
"""OAuth Callback Views""" """OAuth Callback Views"""
from json import JSONDecodeError
from typing import Any, Optional from typing import Any, Optional
from django.conf import settings from django.conf import settings
@ -10,6 +11,7 @@ from django.views.generic import View
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.sources.flow_manager import SourceFlowManager from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.events.models import Event, EventAction
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.views.base import OAuthClientMixin from authentik.sources.oauth.views.base import OAuthClientMixin
@ -42,9 +44,17 @@ class OAuthCallback(OAuthClientMixin, View):
if "error" in token: if "error" in token:
return self.handle_login_failure(token["error"]) return self.handle_login_failure(token["error"])
# Fetch profile info # Fetch profile info
try:
raw_info = client.get_profile_info(token) raw_info = client.get_profile_info(token)
if raw_info is None: if raw_info is None:
return self.handle_login_failure("Could not retrieve profile.") return self.handle_login_failure("Could not retrieve profile.")
except JSONDecodeError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message="Failed to JSON-decode profile.",
raw_profile=exc.doc,
).from_http(self.request)
return self.handle_login_failure("Could not retrieve profile.")
identifier = self.get_user_id(raw_info) identifier = self.get_user_id(raw_info)
if identifier is None: if identifier is None:
return self.handle_login_failure("Could not determine id.") return self.handle_login_failure("Could not determine id.")

View File

@ -24,6 +24,10 @@ LOGGER = get_logger()
class UserWriteStageView(StageView): class UserWriteStageView(StageView):
"""Finalise Enrollment flow by creating a user object.""" """Finalise Enrollment flow by creating a user object."""
def post(self, request: HttpRequest) -> HttpResponse:
"""Wrapper for post requests"""
return self.get(request)
def get(self, request: HttpRequest) -> HttpResponse: def get(self, request: HttpRequest) -> HttpResponse:
"""Save data in the current flow to the currently pending user. If no user is pending, """Save data in the current flow to the currently pending user. If no user is pending,
a new user is created.""" a new user is created."""

View File

@ -21,7 +21,7 @@ services:
networks: networks:
- internal - internal
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.3} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -44,7 +44,7 @@ services:
- "0.0.0.0:9000:9000" - "0.0.0.0:9000:9000"
- "0.0.0.0:9443:9443" - "0.0.0.0:9443:9443"
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.3} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
networks: networks:

View File

@ -17,4 +17,4 @@ func OutpostUserAgent() string {
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD()) return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
} }
const VERSION = "2021.6.3" const VERSION = "2021.6.4"

View File

@ -99,6 +99,11 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
} }
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)}) attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
// Old fields for backwards compatibility
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{BoolToString(*u.IsActive)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{BoolToString(u.IsSuperuser)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/active", Values: []string{BoolToString(*u.IsActive)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/active", Values: []string{BoolToString(*u.IsActive)}})
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(u.IsSuperuser)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(u.IsSuperuser)}})

View File

@ -10,15 +10,19 @@ import (
func (ws *WebServer) configureStatic() { func (ws *WebServer) configureStatic() {
statRouter := ws.lh.NewRoute().Subrouter() statRouter := ws.lh.NewRoute().Subrouter()
// Media files, always local
fs := http.FileServer(http.Dir(config.G.Paths.Media))
if config.G.Debug || config.G.Web.LoadLocalFiles { if config.G.Debug || config.G.Web.LoadLocalFiles {
ws.log.Debug("Using local static files") ws.log.Debug("Using local static files")
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist")))) statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist"))))
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik")))) statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik"))))
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
} else { } else {
statRouter.Use(ws.staticHeaderMiddleware) statRouter.Use(ws.staticHeaderMiddleware)
ws.log.Debug("Using packaged static files with aggressive caching") ws.log.Debug("Using packaged static files with aggressive caching")
ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist)))) statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist))))
ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik)))) statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik))))
statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs))
} }
ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header()["Content-Type"] = []string{"text/plain"} rw.Header()["Content-Type"] = []string{"text/plain"}
@ -30,8 +34,6 @@ func (ws *WebServer) configureStatic() {
rw.WriteHeader(200) rw.WriteHeader(200)
rw.Write(staticWeb.SecurityTxt) rw.Write(staticWeb.SecurityTxt)
}) })
// Media files, always local
ws.lh.PathPrefix("/media").Handler(http.StripPrefix("/media", http.FileServer(http.Dir(config.G.Paths.Media))))
} }
func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler { func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler {

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2021.6.3 version: 2021.6.4
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@beryju.org email: hello@beryju.org
@ -3096,7 +3096,11 @@ paths:
$ref: '#/components/schemas/Link' $ref: '#/components/schemas/Link'
description: '' description: ''
'404': '404':
description: No recovery flow found. content:
application/json:
schema:
$ref: '#/components/schemas/Link'
description: ''
'400': '400':
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
@ -18637,7 +18641,10 @@ components:
title: Kp uuid title: Kp uuid
name: name:
type: string type: string
fingerprint: fingerprint_sha256:
type: string
readOnly: true
fingerprint_sha1:
type: string type: string
readOnly: true readOnly: true
cert_expiry: cert_expiry:
@ -18660,7 +18667,8 @@ components:
- cert_expiry - cert_expiry
- cert_subject - cert_subject
- certificate_download_url - certificate_download_url
- fingerprint - fingerprint_sha1
- fingerprint_sha256
- name - name
- pk - pk
- private_key_available - private_key_available
@ -26707,6 +26715,8 @@ components:
id_token: id_token:
type: string type: string
readOnly: true readOnly: true
revoked:
type: boolean
required: required:
- id_token - id_token
- is_expired - is_expired

View File

@ -195,6 +195,8 @@ class TestProviderLDAP(SeleniumTestCase):
"goauthentik.io/ldap/user", "goauthentik.io/ldap/user",
], ],
"memberOf": [], "memberOf": [],
"accountStatus": ["true"],
"superuser": ["false"],
"goauthentik.io/ldap/active": ["true"], "goauthentik.io/ldap/active": ["true"],
"goauthentik.io/ldap/superuser": ["false"], "goauthentik.io/ldap/superuser": ["false"],
"goauthentik.io/user/override-ips": ["true"], "goauthentik.io/user/override-ips": ["true"],
@ -218,6 +220,8 @@ class TestProviderLDAP(SeleniumTestCase):
"memberOf": [ "memberOf": [
"cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io" "cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io"
], ],
"accountStatus": ["true"],
"superuser": ["true"],
"goauthentik.io/ldap/active": ["true"], "goauthentik.io/ldap/active": ["true"],
"goauthentik.io/ldap/superuser": ["true"], "goauthentik.io/ldap/superuser": ["true"],
"extraAttribute": ["bar"], "extraAttribute": ["bar"],

88
web/package-lock.json generated
View File

@ -26,7 +26,7 @@
"@rollup/plugin-typescript": "^8.2.1", "@rollup/plugin-typescript": "^8.2.1",
"@sentry/browser": "^6.8.0", "@sentry/browser": "^6.8.0",
"@sentry/tracing": "^6.8.0", "@sentry/tracing": "^6.8.0",
"@types/chart.js": "^2.9.32", "@types/chart.js": "^2.9.33",
"@types/codemirror": "5.60.1", "@types/codemirror": "5.60.1",
"@types/grecaptcha": "^3.0.2", "@types/grecaptcha": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/eslint-plugin": "^4.28.1",
@ -35,11 +35,11 @@
"authentik-api": "file:api", "authentik-api": "file:api",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"chart.js": "^3.4.0", "chart.js": "^3.4.1",
"chartjs-adapter-moment": "^1.0.0", "chartjs-adapter-moment": "^1.0.0",
"codemirror": "^5.62.0", "codemirror": "^5.62.0",
"construct-style-sheets-polyfill": "^2.4.16", "construct-style-sheets-polyfill": "^2.4.16",
"eslint": "^7.29.0", "eslint": "^7.30.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.2", "eslint-plugin-custom-elements": "0.0.2",
"eslint-plugin-lit": "^1.5.1", "eslint-plugin-lit": "^1.5.1",
@ -61,12 +61,13 @@
"typescript": "^4.3.5", "typescript": "^4.3.5",
"webcomponent-qr-code": "^1.0.5", "webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2" "yaml": "^1.10.2"
} },
"devDependencies": {}
}, },
"api": { "api": {
"name": "authentik-api", "name": "authentik-api",
"version": "0.0.1", "version": "1.0.0",
"dependencies": { "devDependencies": {
"typescript": "^3.6" "typescript": "^3.6"
} }
}, },
@ -74,6 +75,7 @@
"version": "3.9.9", "version": "3.9.9",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
"dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -1739,6 +1741,24 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.0",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
},
"engines": {
"node": ">=10.10.0"
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
},
"node_modules/@jest/types": { "node_modules/@jest/types": {
"version": "26.6.2", "version": "26.6.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
@ -2434,9 +2454,9 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}, },
"node_modules/@types/chart.js": { "node_modules/@types/chart.js": {
"version": "2.9.32", "version": "2.9.33",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz", "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==", "integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
"dependencies": { "dependencies": {
"moment": "^2.10.2" "moment": "^2.10.2"
} }
@ -3316,9 +3336,9 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
}, },
"node_modules/chart.js": { "node_modules/chart.js": {
"version": "3.4.0", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
"integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ==" "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
}, },
"node_modules/chartjs-adapter-moment": { "node_modules/chartjs-adapter-moment": {
"version": "1.0.0", "version": "1.0.0",
@ -3861,12 +3881,13 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "7.29.0", "version": "7.30.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==", "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
"dependencies": { "dependencies": {
"@babel/code-frame": "7.12.11", "@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.2", "@eslint/eslintrc": "^0.4.2",
"@humanwhocodes/config-array": "^0.5.0",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@ -9191,6 +9212,21 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
}, },
"@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
"requires": {
"@humanwhocodes/object-schema": "^1.2.0",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
}
},
"@humanwhocodes/object-schema": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w=="
},
"@jest/types": { "@jest/types": {
"version": "26.6.2", "version": "26.6.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
@ -9780,9 +9816,9 @@
} }
}, },
"@types/chart.js": { "@types/chart.js": {
"version": "2.9.32", "version": "2.9.33",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz", "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz",
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==", "integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==",
"requires": { "requires": {
"moment": "^2.10.2" "moment": "^2.10.2"
} }
@ -10170,7 +10206,8 @@
"typescript": { "typescript": {
"version": "3.9.9", "version": "3.9.9",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==" "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
"dev": true
} }
} }
}, },
@ -10461,9 +10498,9 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
}, },
"chart.js": { "chart.js": {
"version": "3.4.0", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
"integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ==" "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
}, },
"chartjs-adapter-moment": { "chartjs-adapter-moment": {
"version": "1.0.0", "version": "1.0.0",
@ -10899,12 +10936,13 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
}, },
"eslint": { "eslint": {
"version": "7.29.0", "version": "7.30.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz",
"integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==", "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==",
"requires": { "requires": {
"@babel/code-frame": "7.12.11", "@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.2", "@eslint/eslintrc": "^0.4.2",
"@humanwhocodes/config-array": "^0.5.0",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",

View File

@ -55,7 +55,7 @@
"@rollup/plugin-typescript": "^8.2.1", "@rollup/plugin-typescript": "^8.2.1",
"@sentry/browser": "^6.8.0", "@sentry/browser": "^6.8.0",
"@sentry/tracing": "^6.8.0", "@sentry/tracing": "^6.8.0",
"@types/chart.js": "^2.9.32", "@types/chart.js": "^2.9.33",
"@types/codemirror": "5.60.1", "@types/codemirror": "5.60.1",
"@types/grecaptcha": "^3.0.2", "@types/grecaptcha": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/eslint-plugin": "^4.28.1",
@ -64,11 +64,11 @@
"authentik-api": "file:api", "authentik-api": "file:api",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"chart.js": "^3.4.0", "chart.js": "^3.4.1",
"chartjs-adapter-moment": "^1.0.0", "chartjs-adapter-moment": "^1.0.0",
"codemirror": "^5.62.0", "codemirror": "^5.62.0",
"construct-style-sheets-polyfill": "^2.4.16", "construct-style-sheets-polyfill": "^2.4.16",
"eslint": "^7.29.0", "eslint": "^7.30.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.2", "eslint-plugin-custom-elements": "0.0.2",
"eslint-plugin-lit": "^1.5.1", "eslint-plugin-lit": "^1.5.1",

View File

@ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware {
post(context: ResponseContext): Promise<Response | void> { post(context: ResponseContext): Promise<Response | void> {
tenant().then(tenant => { tenant().then(tenant => {
console.debug(`authentik/api[${tenant.matchedDomain}]: ${context.response.status} ${context.init.method} ${context.url}`); let msg = `authentik/api[${tenant.matchedDomain}]: `;
msg += `${context.response.status} ${context.init.method} ${context.url}`;
if (context.response.status >= 400) {
context.response.text().then(t => {
msg += ` => ${t}`;
console.debug(msg);
});
} else {
console.debug(msg);
}
}); });
return Promise.resolve(context.response); return Promise.resolve(context.response);
} }

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2021.6.3"; export const VERSION = "2021.6.4";
export const PAGE_SIZE = 20; export const PAGE_SIZE = 20;
export const EVENT_REFRESH = "ak-refresh"; export const EVENT_REFRESH = "ak-refresh";
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle"; export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";

View File

@ -12,6 +12,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
if (this.isInViewport) { if (this.isInViewport) {
this.loadInstance(value).then(instance => { this.loadInstance(value).then(instance => {
this.instance = instance; this.instance = instance;
this.requestUpdate();
}); });
} }
} }
@ -37,6 +38,11 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
}); });
} }
resetForm(): void {
this.instance = undefined;
this._initialLoad = false;
}
render(): TemplateResult { render(): TemplateResult {
// if we're in viewport now and haven't loaded AND have a PK set, load now // if we're in viewport now and haven't loaded AND have a PK set, load now
if (this.isInViewport && !this._initialLoad && this._instancePk) { if (this.isInViewport && !this._initialLoad && this._instancePk) {

View File

@ -34,6 +34,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
columns(): TableColumn[] { columns(): TableColumn[] {
return [ return [
new TableColumn(t`Provider`, "provider"), new TableColumn(t`Provider`, "provider"),
new TableColumn(t`Revoked?`, "revoked"),
new TableColumn(t`Expires`, "expires"), new TableColumn(t`Expires`, "expires"),
new TableColumn(t`Scopes`, "scope"), new TableColumn(t`Scopes`, "scope"),
new TableColumn(""), new TableColumn(""),
@ -62,6 +63,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
html`<a href="#/core/providers/${item.provider?.pk}"> html`<a href="#/core/providers/${item.provider?.pk}">
${item.provider?.name} ${item.provider?.name}
</a>`, </a>`,
html`${item.revoked ? t`Yes` : t`No`}`,
html`${item.expires?.toLocaleString()}`, html`${item.expires?.toLocaleString()}`,
html`${item.scope.join(", ")}`, html`${item.scope.join(", ")}`,
html` html`

View File

@ -225,7 +225,14 @@ export abstract class Table<T> extends LitElement {
renderToolbar(): TemplateResult { renderToolbar(): TemplateResult {
return html`<button return html`<button
@click=${() => { this.fetch(); }} @click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
}}
class="pf-c-button pf-m-primary"> class="pf-c-button pf-m-primary">
${t`Refresh`} ${t`Refresh`}
</button>`; </button>`;
@ -241,7 +248,12 @@ export abstract class Table<T> extends LitElement {
} }
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => { return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
this.search = value; this.search = value;
this.fetch(); this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
}}> }}>
</ak-table-search>&nbsp;`; </ak-table-search>&nbsp;`;
} }
@ -274,7 +286,15 @@ export abstract class Table<T> extends LitElement {
<ak-table-pagination <ak-table-pagination
class="pf-c-toolbar__item pf-m-pagination" class="pf-c-toolbar__item pf-m-pagination"
.pages=${this.data?.pagination} .pages=${this.data?.pagination}
.pageChangeHandler=${(page: number) => { this.page = page; this.fetch(); }}> .pageChangeHandler=${(page: number) => {
this.page = page;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
}}>
</ak-table-pagination> </ak-table-pagination>
</div> </div>
</div> </div>
@ -300,7 +320,15 @@ export abstract class Table<T> extends LitElement {
<ak-table-pagination <ak-table-pagination
class="pf-c-toolbar__item pf-m-pagination" class="pf-c-toolbar__item pf-m-pagination"
.pages=${this.data?.pagination} .pages=${this.data?.pagination}
.pageChangeHandler=${(page: number) => { this.page = page; this.fetch(); }}> .pageChangeHandler=${(page: number) => {
this.page = page;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
}}>
</ak-table-pagination> </ak-table-pagination>
</div>`; </div>`;
} }

View File

@ -488,8 +488,12 @@ msgid "Certificate"
msgstr "Certificate" msgstr "Certificate"
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Certificate Fingerprint" msgid "Certificate Fingerprint (SHA1)"
msgstr "Certificate Fingerprint" msgstr "Certificate Fingerprint (SHA1)"
#: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Certificate Fingerprint (SHA256)"
msgstr "Certificate Fingerprint (SHA256)"
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Certificate Subjet" msgid "Certificate Subjet"
@ -2346,6 +2350,7 @@ msgstr "Negates the outcome of the binding. Messages are unaffected."
msgid "New version available!" msgid "New version available!"
msgstr "New version available!" msgstr "New version available!"
#: src/elements/oauth/UserRefreshList.ts
#: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
#: src/pages/groups/GroupListPage.ts #: src/pages/groups/GroupListPage.ts
@ -3044,6 +3049,10 @@ msgstr "Return home"
msgid "Return to device picker" msgid "Return to device picker"
msgstr "Return to device picker" msgstr "Return to device picker"
#: src/elements/oauth/UserRefreshList.ts
msgid "Revoked?"
msgstr "Revoked?"
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts #: src/pages/property-mappings/PropertyMappingSAMLForm.ts
msgid "SAML Attribute Name" msgid "SAML Attribute Name"
msgstr "SAML Attribute Name" msgstr "SAML Attribute Name"
@ -4544,6 +4553,7 @@ msgstr ""
msgid "X509 Subject" msgid "X509 Subject"
msgstr "X509 Subject" msgstr "X509 Subject"
#: src/elements/oauth/UserRefreshList.ts
#: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
#: src/pages/groups/GroupListPage.ts #: src/pages/groups/GroupListPage.ts

View File

@ -484,7 +484,11 @@ msgid "Certificate"
msgstr "" msgstr ""
#: #:
msgid "Certificate Fingerprint" msgid "Certificate Fingerprint (SHA1)"
msgstr ""
#:
msgid "Certificate Fingerprint (SHA256)"
msgstr "" msgstr ""
#: #:
@ -2350,6 +2354,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "No" msgid "No"
msgstr "" msgstr ""
@ -3036,6 +3041,10 @@ msgstr ""
msgid "Return to device picker" msgid "Return to device picker"
msgstr "" msgstr ""
#:
msgid "Revoked?"
msgstr ""
#: #:
msgid "SAML Attribute Name" msgid "SAML Attribute Name"
msgstr "" msgstr ""
@ -4539,6 +4548,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""

View File

@ -103,10 +103,18 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
<dl class="pf-c-description-list pf-m-horizontal"> <dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group"> <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term"> <dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Certificate Fingerprint`}</span> <span class="pf-c-description-list__text">${t`Certificate Fingerprint (SHA1)`}</span>
</dt> </dt>
<dd class="pf-c-description-list__description"> <dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${item.fingerprint}</div> <div class="pf-c-description-list__text">${item.fingerprintSha1}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Certificate Fingerprint (SHA256)`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${item.fingerprintSha256}</div>
</dd> </dd>
</div> </div>
<div class="pf-c-description-list__group"> <div class="pf-c-description-list__group">

View File

@ -15,7 +15,7 @@ export class OutpostHealthElement extends LitElement {
outpostId?: string; outpostId?: string;
@property({attribute: false}) @property({attribute: false})
outpostHealth: OutpostHealth[] = []; outpostHealth?: OutpostHealth[];
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, AKGlobal]; return [PFBase, AKGlobal];
@ -23,7 +23,8 @@ export class OutpostHealthElement extends LitElement {
constructor() { constructor() {
super(); super();
this.addEventListener(EVENT_REFRESH, () => { window.addEventListener(EVENT_REFRESH, () => {
this.outpostHealth = undefined;
this.firstUpdated(); this.firstUpdated();
}); });
} }
@ -38,7 +39,7 @@ export class OutpostHealthElement extends LitElement {
} }
render(): TemplateResult { render(): TemplateResult {
if (!this.outpostId) { if (!this.outpostId || !this.outpostHealth) {
return html`<ak-spinner></ak-spinner>`; return html`<ak-spinner></ak-spinner>`;
} }
if (this.outpostHealth.length === 0) { if (this.outpostHealth.length === 0) {

View File

@ -9,13 +9,14 @@ import "../../elements/buttons/ActionButton";
import { TableColumn } from "../../elements/table/Table"; import { TableColumn } from "../../elements/table/Table";
import { PAGE_SIZE } from "../../constants"; import { PAGE_SIZE } from "../../constants";
import { CoreApi, User } from "authentik-api"; import { CoreApi, User } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config"; import { DEFAULT_CONFIG, tenant } from "../../api/Config";
import "../../elements/forms/DeleteForm"; import "../../elements/forms/DeleteForm";
import "./UserActiveForm"; import "./UserActiveForm";
import "./UserForm"; import "./UserForm";
import { showMessage } from "../../elements/messages/MessageContainer"; import { showMessage } from "../../elements/messages/MessageContainer";
import { MessageLevel } from "../../elements/messages/Message"; import { MessageLevel } from "../../elements/messages/Message";
import { first } from "../../utils"; import { first } from "../../utils";
import { until } from "lit-html/directives/until";
@customElement("ak-user-list") @customElement("ak-user-list")
export class UserListPage extends TablePage<User> { export class UserListPage extends TablePage<User> {
@ -128,6 +129,9 @@ export class UserListPage extends TablePage<User> {
</li> </li>
</ul> </ul>
</ak-dropdown> </ak-dropdown>
${until(tenant().then(te => {
if (te.flowRecovery) {
return html`
<ak-action-button <ak-action-button
.apiRequest=${() => { .apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({ return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({
@ -148,7 +152,10 @@ export class UserListPage extends TablePage<User> {
}); });
}}> }}>
${t`Reset Password`} ${t`Reset Password`}
</ak-action-button> </ak-action-button>`;
}
return html``;
}))}
<a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}"> <a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}">
${t`Impersonate`} ${t`Impersonate`}
</a>`, </a>`,

View File

@ -6,17 +6,17 @@ This installation method is for test-setups and small-scale productive setups.
## Requirements ## Requirements
- A Linux host with at least 2 CPU cores and 4 GB of RAM. - A Linux host with at least 2 CPU cores and 2 GB of RAM.
- docker - docker
- docker-compose - docker-compose
## Preparation ## Preparation
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.6.3/docker-compose.yml). Place it in a directory of your choice. Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.6.4/docker-compose.yml). Place it in a directory of your choice.
To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env` To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env`
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.6.3 >> .env` To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.6.4 >> .env`
If this is a fresh authentik install run the following commands to generate a password: If this is a fresh authentik install run the following commands to generate a password:

View File

@ -22,13 +22,14 @@ Create an application in authentik and note the slug, as this will be used later
- ACS URL: `https://gitlab.company/users/auth/saml/callback` - ACS URL: `https://gitlab.company/users/auth/saml/callback`
- Audience: `https://gitlab.company` - Audience: `https://gitlab.company`
- Issuer: `https://gitlab.company` - Issuer: `https://gitlab.company`
- Binding: `Post` - Binding: `Redirect`
You can of course use a custom signing certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php). Under *Advanced protocol settings*, set a certificate for *Signing Certificate*.
## GitLab Configuration ## GitLab Configuration
Paste the following block in your `gitlab.rb` file, after replacing the placeholder values from above. The file is located in `/etc/gitlab`. Paste the following block in your `gitlab.rb` file, after replacing the placeholder values from above. The file is located in `/etc/gitlab`.
To get the value for `idp_cert_fingerprint`, go to the Certificate list under *Identity & Cryptography*, and expand the selected certificate.
```ruby ```ruby
gitlab_rails['omniauth_enabled'] = true gitlab_rails['omniauth_enabled'] = true
@ -46,7 +47,7 @@ gitlab_rails['omniauth_providers'] = [
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback', assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
# Shown when navigating to certificates in authentik # Shown when navigating to certificates in authentik
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A', idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
idp_sso_target_url: 'https://authentik.company/application/saml/<authentik application slug>/sso/binding/post/', idp_sso_target_url: 'https://authentik.company/application/saml/<authentik application slug>/sso/binding/redirect/',
issuer: 'https://gitlab.company', issuer: 'https://gitlab.company',
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
attribute_statements: { attribute_statements: {

View File

@ -11,7 +11,7 @@ version: "3.5"
services: services:
authentik_proxy: authentik_proxy:
image: ghcr.io/goauthentik/proxy:2021.6.3 image: ghcr.io/goauthentik/proxy:2021.6.4
ports: ports:
- 4180:4180 - 4180:4180
- 4443:4443 - 4443:4443
@ -21,7 +21,7 @@ services:
AUTHENTIK_TOKEN: token-generated-by-authentik AUTHENTIK_TOKEN: token-generated-by-authentik
# Or, for the LDAP Outpost # Or, for the LDAP Outpost
authentik_proxy: authentik_proxy:
image: ghcr.io/goauthentik/ldap:2021.6.3 image: ghcr.io/goauthentik/ldap:2021.6.4
ports: ports:
- 389:3389 - 389:3389
environment: environment:

View File

@ -14,7 +14,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.6.3 app.kubernetes.io/version: 2021.6.4
name: authentik-outpost-api name: authentik-outpost-api
stringData: stringData:
authentik_host: "__AUTHENTIK_URL__" authentik_host: "__AUTHENTIK_URL__"
@ -29,7 +29,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.6.3 app.kubernetes.io/version: 2021.6.4
name: authentik-outpost name: authentik-outpost
spec: spec:
ports: ports:
@ -54,7 +54,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.6.3 app.kubernetes.io/version: 2021.6.4
name: authentik-outpost name: authentik-outpost
spec: spec:
selector: selector:
@ -62,14 +62,14 @@ spec:
app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.6.3 app.kubernetes.io/version: 2021.6.4
template: template:
metadata: metadata:
labels: labels:
app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.6.3 app.kubernetes.io/version: 2021.6.4
spec: spec:
containers: containers:
- env: - env:
@ -88,7 +88,7 @@ spec:
secretKeyRef: secretKeyRef:
key: authentik_host_insecure key: authentik_host_insecure
name: authentik-outpost-api name: authentik-outpost-api
image: ghcr.io/goauthentik/proxy:2021.6.3 image: ghcr.io/goauthentik/proxy:2021.6.4
name: proxy name: proxy
ports: ports:
- containerPort: 4180 - containerPort: 4180
@ -110,7 +110,7 @@ metadata:
app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/instance: __OUTPOST_NAME__
app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/managed-by: goauthentik.io
app.kubernetes.io/name: authentik-proxy app.kubernetes.io/name: authentik-proxy
app.kubernetes.io/version: 2021.6.3 app.kubernetes.io/version: 2021.6.4
name: authentik-outpost name: authentik-outpost
spec: spec:
rules: rules:

View File

@ -139,6 +139,28 @@ slug: "2021.6"
- web/admin: fix only recovery flows being selectable for unenrollment flow in tenant form - web/admin: fix only recovery flows being selectable for unenrollment flow in tenant form
- web/admin: fix text color on pf-c-card - web/admin: fix text color on pf-c-card
## Fixed in 2021.6.4
- core: only show `Reset password` link when recovery flow is configured
- crypto: show both sha1 and sha256 fingerprints
- flows: handle old cached flow plans better
- g: fix static and media caching not working properly
- outposts: fix container not being started after creation
- outposts: fix docker controller not checking env correctly
- outposts: fix docker controller not checking ports correctly
- outposts: fix empty message when docker outpost controller has changed nothing
- outposts: fix permissions not being set correctly upon outpost creation
- outposts/ldap: add support for boolean fields in ldap
- outposts/proxy: always redirect to session-end interface on sign_out
- providers/oauth2: add revoked field, create suspicious event when previous token is used
- providers/oauth2: deepmerge claims
- providers/oauth2: fix CORS headers not being set for unsuccessful requests
- providers/oauth2: use self.expires for exp field instead of calculating it again
- sources/oauth: create configuration error event when profile can't be parsed as json
- stages/user_write: add wrapper for post to user_write
- web/admin: fix ModelForm not re-loading after being reset
- web/admin: show oauth2 token revoked status
## Upgrading ## Upgrading
This release does not introduce any new requirements. This release does not introduce any new requirements.