diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4f9ebc8a9..8d9171fc6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2021.6.3 +current_version = 2021.6.4 tag = True commit = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)\-?(?P.*) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c522ec40..35987a23d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,14 +33,14 @@ jobs: with: push: ${{ github.event_name == 'release' }} tags: | - beryju/authentik:2021.6.3, + beryju/authentik:2021.6.4, beryju/authentik:latest, - ghcr.io/goauthentik/server:2021.6.3, + ghcr.io/goauthentik/server:2021.6.4, ghcr.io/goauthentik/server:latest platforms: linux/amd64,linux/arm64 context: . - 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: | docker pull beryju/authentik:latest docker tag beryju/authentik:latest beryju/authentik:stable @@ -75,14 +75,14 @@ jobs: with: push: ${{ github.event_name == 'release' }} tags: | - beryju/authentik-proxy:2021.6.3, + beryju/authentik-proxy:2021.6.4, beryju/authentik-proxy:latest, - ghcr.io/goauthentik/proxy:2021.6.3, + ghcr.io/goauthentik/proxy:2021.6.4, ghcr.io/goauthentik/proxy:latest file: proxy.Dockerfile platforms: linux/amd64,linux/arm64 - 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: | docker pull beryju/authentik-proxy:latest docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable @@ -117,14 +117,14 @@ jobs: with: push: ${{ github.event_name == 'release' }} tags: | - beryju/authentik-ldap:2021.6.3, + beryju/authentik-ldap:2021.6.4, beryju/authentik-ldap:latest, - ghcr.io/goauthentik/ldap:2021.6.3, + ghcr.io/goauthentik/ldap:2021.6.4, ghcr.io/goauthentik/ldap:latest file: ldap.Dockerfile platforms: linux/amd64,linux/arm64 - 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: | docker pull beryju/authentik-ldap:latest docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable @@ -176,7 +176,6 @@ jobs: SENTRY_PROJECT: authentik SENTRY_URL: https://sentry.beryju.org with: - version: authentik@2021.6.3 + version: authentik@2021.6.4 environment: beryjuorg-prod sourcemaps: './web/dist' - finalize: false diff --git a/Pipfile.lock b/Pipfile.lock index 1bce7fdf7..9dcd52355 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -122,19 +122,19 @@ }, "boto3": { "hashes": [ - "sha256:055f9dc07f95f202a4dc25196a3a9f1e2f137171ee364cf980e4673de75fb529", - "sha256:bc9b278e362ec9b531511a498262297f074c4f5ca9560455919a0af1a4698615" + "sha256:3b35689c215c982fe9f7ef78d748aa9b0cd15c3b2eb04f9b460aaa63fe2fbd03", + "sha256:b1cbeb92123799001b97f2ee1cdf470e21f1be08314ae28fc7ea357925186f1c" ], "index": "pypi", - "version": "==1.17.104" + "version": "==1.17.105" }, "botocore": { "hashes": [ - "sha256:23aa3238c004319f78423eb8cbba2813b62ee64d0e3bab04e0a00e067f99542a", - "sha256:95ab472c8254b8d2cfa6d719b433e511fbcf80895b4cd18e4219c1efa0b78270" + "sha256:b0fda4edf8eb105453890700d49011ada576d0cc7326a0699dfabe9e872f552c", + "sha256:b5ba72d22212b0355f339c2a98b3296b3b2202a48e6a2b1366e866bc65a64b67" ], "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": { "hashes": [ @@ -778,11 +778,11 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], "index": "pypi", - "version": "==20.9" + "version": "==21.0" }, "prometheus-client": { "hashes": [ @@ -1585,7 +1585,7 @@ "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56", "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" }, "lazy-object-proxy": { @@ -1632,11 +1632,11 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], "index": "pypi", - "version": "==20.9" + "version": "==21.0" }, "pathspec": { "hashes": [ diff --git a/authentik/__init__.py b/authentik/__init__.py index 059f9706e..7b41254ae 100644 --- a/authentik/__init__.py +++ b/authentik/__init__.py @@ -1,3 +1,3 @@ """authentik""" -__version__ = "2021.6.3" +__version__ = "2021.6.4" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 379958bd5..f334fd8db 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -2,12 +2,11 @@ from json import loads from django.db.models.query import QuerySet -from django.http.response import Http404 from django.urls import reverse_lazy from django.utils.http import urlencode from django_filters.filters import BooleanFilter, CharFilter 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 rest_framework.decorators import action from rest_framework.fields import CharField, JSONField, SerializerMethodField @@ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): @extend_schema( responses={ "200": LinkSerializer(many=False), - "404": OpenApiResponse(description="No recovery flow found."), + "404": LinkSerializer(many=False), }, ) @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 flow = tenant.flow_recovery if not flow: - raise Http404 + return Response({"link": ""}, status=404) user: User = self.get_object() token, __ = Token.objects.get_or_create( identifier=f"{user.uid}-password-reset", diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index 380208e08..87aa87260 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -14,7 +14,9 @@ def is_dict(value: Any): """Ensure a value is a dictionary, useful for JSONFields""" if isinstance(value, dict): return - raise ValidationError("Value must be a dictionary.") + raise ValidationError( + "Value must be a dictionary, and not have any duplicate keys." + ) class PassiveSerializer(Serializer): diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py index 81c8f2b6e..0d5f97830 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer): fields = [ "pk", "name", - "fingerprint", + "fingerprint_sha256", + "fingerprint_sha1", "certificate_data", "key_data", "cert_expiry", diff --git a/authentik/crypto/models.py b/authentik/crypto/models.py index 1cfd71c79..37d875dc9 100644 --- a/authentik/crypto/models.py +++ b/authentik/crypto/models.py @@ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel): return self._private_key @property - def fingerprint(self) -> str: + def fingerprint_sha256(self) -> str: """Get SHA256 Fingerprint of certificate_data""" return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode( "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 def kid(self): """Get Key ID used for JWKS""" diff --git a/authentik/events/middleware.py b/authentik/events/middleware.py index 1543fb8ef..4a8838a0f 100644 --- a/authentik/events/middleware.py +++ b/authentik/events/middleware.py @@ -3,6 +3,7 @@ from functools import partial from typing import Callable from django.conf import settings +from django.core.exceptions import SuspiciousOperation from django.db.models import Model from django.db.models.signals import post_save, pre_delete from django.http import HttpRequest, HttpResponse @@ -63,7 +64,15 @@ class AuditMiddleware: if settings.DEBUG: 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( EventAction.SYSTEM_EXCEPTION, request, diff --git a/authentik/flows/migrations/0022_alter_flowstagebinding_invalid_response_action.py b/authentik/flows/migrations/0022_alter_flowstagebinding_invalid_response_action.py new file mode 100644 index 000000000..bb858cc3b --- /dev/null +++ b/authentik/flows/migrations/0022_alter_flowstagebinding_invalid_response_action.py @@ -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.", + ), + ), + ] diff --git a/authentik/flows/views.py b/authentik/flows/views.py index 39c9a1662..2a6ab7b39 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -134,7 +134,7 @@ class FlowExecutorView(APIView): message = exc.__doc__ if exc.__doc__ else str(exc) 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: # Early check if theres an active Plan for the current 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", "")) # We don't save the Plan after getting the next stage # as it hasn't been successfully passed yet - next_binding = self.plan.next(self.request) + 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) + 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: self._logger.debug("f(exec): no more stages, flow is done.") return self._flow_done() diff --git a/authentik/outposts/api/outposts.py b/authentik/outposts/api/outposts.py index 11a6c5fbf..2e42265ba 100644 --- a/authentik/outposts/api/outposts.py +++ b/authentik/outposts/api/outposts.py @@ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer): raise ValidationError( ( f"Outpost type {self.initial_data['type']} can't be used with " - f"{type(provider)} providers." + f"{provider.__class__.__name__} providers." ) ) return providers diff --git a/authentik/outposts/controllers/docker.py b/authentik/outposts/controllers/docker.py index aa7a6781e..c233073db 100644 --- a/authentik/outposts/controllers/docker.py +++ b/authentik/outposts/controllers/docker.py @@ -36,8 +36,10 @@ class DockerController(BaseController): def _get_env(self) -> dict[str, str]: return { - "AUTHENTIK_HOST": self.outpost.config.authentik_host, - "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure), + "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(), + "AUTHENTIK_INSECURE": str( + self.outpost.config.authentik_host_insecure + ).lower(), "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 to be rebuilt.""" 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(): - if key not in container_env: - continue - if container_env[key] != expected_value: + entry = f"{key.upper()}={expected_value}" + if entry not in container_env: return True return False @@ -62,14 +63,17 @@ class DockerController(BaseController): # When the container isn't running, the API doesn't report any port mappings if container.status != "running": 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: key = f"{port.inner_port or port.port}/{port.protocol.lower()}" if key not in container.ports: return True host_matching = False 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: return True return False @@ -79,7 +83,7 @@ class DockerController(BaseController): try: return self.client.containers.get(container_name), False except NotFound: - self.logger.info("Container does not exist, creating") + self.logger.info("(Re-)creating container...") image_name = self.get_container_image() self.client.images.pull(image_name) container_args = { @@ -107,6 +111,7 @@ class DockerController(BaseController): try: container, has_been_created = self._get_container() if has_been_created: + container.start() return None # Check if the container is out of date, delete it and retry if len(container.image.tags) > 0: @@ -164,6 +169,7 @@ class DockerController(BaseController): self.logger.info("Container is not running, restarting...") container.start() return None + self.logger.info("Container is running") return None except DockerException as exc: raise ControllerException(str(exc)) from exc diff --git a/authentik/outposts/settings.py b/authentik/outposts/settings.py index 617274295..bd0e9a94e 100644 --- a/authentik/outposts/settings.py +++ b/authentik/outposts/settings.py @@ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = { }, "outposts_service_connection_check": { "task": "authentik.outposts.tasks.outpost_service_connection_monitor", - "schedule": crontab(minute="*/60"), + "schedule": crontab(minute="*/5"), "options": {"queue": "authentik_scheduled"}, }, "outpost_token_ensurer": { diff --git a/authentik/outposts/signals.py b/authentik/outposts/signals.py index 85667cf37..9a712613e 100644 --- a/authentik/outposts/signals.py +++ b/authentik/outposts/signals.py @@ -1,7 +1,7 @@ """authentik outpost signals""" from django.core.cache import cache 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 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) +@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) # pylint: disable=unused-argument def post_save_update(sender, instance: Model, **_): diff --git a/authentik/providers/oauth2/api/tokens.py b/authentik/providers/oauth2/api/tokens.py index 8d77ff4ce..05037de6b 100644 --- a/authentik/providers/oauth2/api/tokens.py +++ b/authentik/providers/oauth2/api/tokens.py @@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer): "expires", "scope", "id_token", + "revoked", ] depth = 2 diff --git a/authentik/providers/oauth2/migrations/0015_auto_20210703_1313.py b/authentik/providers/oauth2/migrations/0015_auto_20210703_1313.py new file mode 100644 index 000000000..79654b521 --- /dev/null +++ b/authentik/providers/oauth2/migrations/0015_auto_20210703_1313.py @@ -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), + ), + ] diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index 8ada987b9..97b6bc077 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -318,6 +318,7 @@ class BaseGrantModel(models.Model): provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE) user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE) _scope = models.TextField(default="", verbose_name=_("Scopes")) + revoked = models.BooleanField(default=False) @property def scope(self) -> list[str]: @@ -473,9 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel): # Convert datetimes into timestamps. now = int(time.time()) iat_time = now - exp_time = int( - now + timedelta_from_string(self.provider.token_validity).total_seconds() - ) + exp_time = int(dateformat.format(self.expires, "U")) # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time auth_events = Event.objects.filter( action=EventAction.LOGIN, user=get_user(user) diff --git a/authentik/providers/oauth2/tests/test_token.py b/authentik/providers/oauth2/tests/test_token.py index fc9c39c67..41a48981e 100644 --- a/authentik/providers/oauth2/tests/test_token.py +++ b/authentik/providers/oauth2/tests/test_token.py @@ -6,6 +6,8 @@ from django.urls import reverse from django.utils.encoding import force_str 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.providers.oauth2.constants import ( GRANT_TYPE_AUTHORIZATION_CODE, @@ -39,7 +41,8 @@ class TestToken(OAuthTestCase): client_id=generate_client_id(), client_secret=generate_client_secret(), authorization_flow=Flow.objects.first(), - redirect_uris="http://local.invalid", + redirect_uris="http://testserver", + rsa_key=CertificateKeyPair.objects.first(), ) header = b64encode( f"{provider.client_id}:{provider.client_secret}".encode() @@ -53,11 +56,13 @@ class TestToken(OAuthTestCase): data={ "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, "code": code.code, - "redirect_uri": "http://local.invalid", + "redirect_uri": "http://testserver", }, 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) def test_request_refresh_token(self): @@ -68,6 +73,7 @@ class TestToken(OAuthTestCase): client_secret=generate_client_secret(), authorization_flow=Flow.objects.first(), redirect_uris="http://local.invalid", + rsa_key=CertificateKeyPair.objects.first(), ) header = b64encode( f"{provider.client_id}:{provider.client_secret}".encode() @@ -87,7 +93,9 @@ class TestToken(OAuthTestCase): }, 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) def test_auth_code_view(self): @@ -98,6 +106,7 @@ class TestToken(OAuthTestCase): client_secret=generate_client_secret(), authorization_flow=Flow.objects.first(), redirect_uris="http://local.invalid", + rsa_key=CertificateKeyPair.objects.first(), ) # Needs to be assigned to an application for iss to be set self.app.provider = provider @@ -141,6 +150,7 @@ class TestToken(OAuthTestCase): client_secret=generate_client_secret(), authorization_flow=Flow.objects.first(), redirect_uris="http://local.invalid", + rsa_key=CertificateKeyPair.objects.first(), ) # Needs to be assigned to an application for iss to be set self.app.provider = provider @@ -193,6 +203,7 @@ class TestToken(OAuthTestCase): client_secret=generate_client_secret(), authorization_flow=Flow.objects.first(), redirect_uris="http://local.invalid", + rsa_key=CertificateKeyPair.objects.first(), ) header = b64encode( 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() + ) diff --git a/authentik/providers/oauth2/utils.py b/authentik/providers/oauth2/utils.py index a022ef04d..c97cf67d6 100644 --- a/authentik/providers/oauth2/utils.py +++ b/authentik/providers/oauth2/utils.py @@ -10,6 +10,7 @@ from django.http.response import HttpResponseRedirect from django.utils.cache import patch_vary_headers from structlog.stdlib import get_logger +from authentik.events.models import Event, EventAction from authentik.providers.oauth2.errors import BearerTokenError from authentik.providers.oauth2.models import RefreshToken @@ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s if not allowed: LOGGER.warning( "CORS: Origin is not an allowed origin", - requested=origin, + requested=received_origin, allowed=allowed_origins, ) return response @@ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]): raise BearerTokenError("invalid_token") try: - kwargs["token"] = RefreshToken.objects.get( + token: RefreshToken = RefreshToken.objects.get( access_token=access_token ) except RefreshToken.DoesNotExist: LOGGER.debug("Token does not exist", access_token=access_token) raise BearerTokenError("invalid_token") - if kwargs["token"].is_expired: + if token.is_expired: LOGGER.debug("Token has expired", access_token=access_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( "Scope missmatch.", required=set(scopes), - token_has=set(kwargs["token"].scope), + token_has=set(token.scope), ) raise BearerTokenError("insufficient_scope") except BearerTokenError as error: @@ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]): "WWW-Authenticate" ] = f'error="{error.code}", error_description="{error.description}"' return response - + kwargs["token"] = token return view(request, *args, **kwargs) return view_wrapper diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py index 7c78eed16..d192cba50 100644 --- a/authentik/providers/oauth2/views/token.py +++ b/authentik/providers/oauth2/views/token.py @@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse from django.views import View from structlog.stdlib import get_logger +from authentik.events.models import Event, EventAction from authentik.lib.utils.time import timedelta_from_string from authentik.providers.oauth2.constants import ( GRANT_TYPE_AUTHORIZATION_CODE, @@ -30,6 +31,7 @@ LOGGER = get_logger() @dataclass +# pylint: disable=too-many-instance-attributes class TokenParams: """Token params""" @@ -40,6 +42,8 @@ class TokenParams: state: str scope: list[str] + provider: OAuth2Provider + authorization_code: Optional[AuthorizationCode] = None refresh_token: Optional[RefreshToken] = None @@ -47,35 +51,34 @@ class TokenParams: raw_code: InitVar[str] = "" raw_token: InitVar[str] = "" + request: InitVar[Optional[HttpRequest]] = None @staticmethod - def from_request(request: HttpRequest) -> "TokenParams": - """Extract Token Parameters from http request""" - client_id, client_secret = extract_client_auth(request) - + def parse( + request: HttpRequest, + provider: OAuth2Provider, + client_id: str, + client_secret: str, + ) -> "TokenParams": + """Parse params for request""" 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_secret=client_secret, redirect_uri=request.POST.get("redirect_uri", ""), 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", ""), scope=request.POST.get("scope", "").split(), # PKCE parameter. code_verifier=request.POST.get("code_verifier"), ) - def __post_init__(self, raw_code, raw_token): - 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") - + def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest): if self.provider.client_type == ClientTypes.CONFIDENTIAL: if self.provider.client_secret != self.client_secret: LOGGER.warning( @@ -87,7 +90,6 @@ class TokenParams: if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: self.__post_init_code(raw_code) - elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN: if not raw_token: LOGGER.warning("Missing refresh token") @@ -107,7 +109,14 @@ class TokenParams: token=raw_token, ) 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: LOGGER.warning("Invalid grant type", grant_type=self.grant_type) raise TokenError("unsupported_grant_type") @@ -159,13 +168,14 @@ class TokenParams: class TokenView(View): """Generate tokens for clients""" + provider: Optional[OAuth2Provider] = None params: Optional[TokenParams] = None def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: response = super().dispatch(request, *args, **kwargs) allowed_origins = [] - if self.params: - allowed_origins = self.params.provider.redirect_uris.split("\n") + if self.provider: + allowed_origins = self.provider.redirect_uris.split("\n") cors_allow(self.request, response, *allowed_origins) return response @@ -175,19 +185,32 @@ class TokenView(View): def post(self, request: HttpRequest) -> HttpResponse: """Generate tokens for clients""" 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: - return TokenResponse(self.create_code_response_dic()) + return TokenResponse(self.create_code_response()) 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}") except TokenError as error: return TokenResponse(error.create_dict(), status=400) except UserAuthError as error: 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""" 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. self.params.authorization_code.delete() - response_dict = { + return { "access_token": refresh_token.access_token, "refresh_token": refresh_token.refresh_token, "token_type": "bearer", @@ -223,9 +246,7 @@ class TokenView(View): "id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()), } - return response_dict - - def create_refresh_response_dic(self) -> dict[str, Any]: + def create_refresh_response(self) -> dict[str, Any]: """See https://tools.ietf.org/html/rfc6749#section-6""" unauthorized_scopes = set(self.params.scope) - set( @@ -253,10 +274,11 @@ class TokenView(View): # Store the refresh_token. refresh_token.save() - # Forget the old token. - self.params.refresh_token.delete() + # Mark old token as revoked + self.params.refresh_token.revoked = True + self.params.refresh_token.save() - dic = { + return { "access_token": refresh_token.access_token, "refresh_token": refresh_token.refresh_token, "token_type": "bearer", @@ -267,5 +289,3 @@ class TokenView(View): ), "id_token": self.params.provider.encode(refresh_token.id_token.to_dict()), } - - return dic diff --git a/authentik/sources/oauth/views/callback.py b/authentik/sources/oauth/views/callback.py index fa6d9b735..122f94804 100644 --- a/authentik/sources/oauth/views/callback.py +++ b/authentik/sources/oauth/views/callback.py @@ -1,4 +1,5 @@ """OAuth Callback Views""" +from json import JSONDecodeError from typing import Any, Optional from django.conf import settings @@ -10,6 +11,7 @@ from django.views.generic import View from structlog.stdlib import get_logger 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.views.base import OAuthClientMixin @@ -42,8 +44,16 @@ class OAuthCallback(OAuthClientMixin, View): if "error" in token: return self.handle_login_failure(token["error"]) # Fetch profile info - raw_info = client.get_profile_info(token) - if raw_info is None: + try: + raw_info = client.get_profile_info(token) + if raw_info is None: + 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) if identifier is None: diff --git a/authentik/stages/user_write/stage.py b/authentik/stages/user_write/stage.py index 554cd36d6..0e726e05f 100644 --- a/authentik/stages/user_write/stage.py +++ b/authentik/stages/user_write/stage.py @@ -24,6 +24,10 @@ LOGGER = get_logger() class UserWriteStageView(StageView): """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: """Save data in the current flow to the currently pending user. If no user is pending, a new user is created.""" diff --git a/docker-compose.yml b/docker-compose.yml index 27c788de1..68df291d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: networks: - internal 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 command: server environment: @@ -44,7 +44,7 @@ services: - "0.0.0.0:9000:9000" - "0.0.0.0:9443:9443" 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 command: worker networks: diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 8d23d675e..999accc4b 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -17,4 +17,4 @@ func OutpostUserAgent() string { return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD()) } -const VERSION = "2021.6.3" +const VERSION = "2021.6.4" diff --git a/internal/outpost/ldap/instance_search.go b/internal/outpost/ldap/instance_search.go index 0515fe470..e7aa81f23 100644 --- a/internal/outpost/ldap/instance_search.go +++ b/internal/outpost/ldap/instance_search.go @@ -99,6 +99,11 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { } 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/superuser", Values: []string{BoolToString(u.IsSuperuser)}}) diff --git a/internal/web/web_static.go b/internal/web/web_static.go index 06d30eddd..ccee1c98d 100644 --- a/internal/web/web_static.go +++ b/internal/web/web_static.go @@ -10,15 +10,19 @@ import ( func (ws *WebServer) configureStatic() { 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 { ws.log.Debug("Using local static files") - ws.lh.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/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist")))) + statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik")))) + statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs)) } else { statRouter.Use(ws.staticHeaderMiddleware) 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)))) - ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik)))) + statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist)))) + 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) { rw.Header()["Content-Type"] = []string{"text/plain"} @@ -30,8 +34,6 @@ func (ws *WebServer) configureStatic() { rw.WriteHeader(200) 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 { diff --git a/schema.yml b/schema.yml index 5d5b53fcb..63945edfa 100644 --- a/schema.yml +++ b/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: authentik - version: 2021.6.3 + version: 2021.6.4 description: Making authentication simple. contact: email: hello@beryju.org @@ -3096,7 +3096,11 @@ paths: $ref: '#/components/schemas/Link' description: '' '404': - description: No recovery flow found. + content: + application/json: + schema: + $ref: '#/components/schemas/Link' + description: '' '400': $ref: '#/components/schemas/ValidationError' '403': @@ -18637,7 +18641,10 @@ components: title: Kp uuid name: type: string - fingerprint: + fingerprint_sha256: + type: string + readOnly: true + fingerprint_sha1: type: string readOnly: true cert_expiry: @@ -18660,7 +18667,8 @@ components: - cert_expiry - cert_subject - certificate_download_url - - fingerprint + - fingerprint_sha1 + - fingerprint_sha256 - name - pk - private_key_available @@ -26707,6 +26715,8 @@ components: id_token: type: string readOnly: true + revoked: + type: boolean required: - id_token - is_expired diff --git a/tests/e2e/test_provider_ldap.py b/tests/e2e/test_provider_ldap.py index 8e95ad13e..fd0b3d056 100644 --- a/tests/e2e/test_provider_ldap.py +++ b/tests/e2e/test_provider_ldap.py @@ -195,6 +195,8 @@ class TestProviderLDAP(SeleniumTestCase): "goauthentik.io/ldap/user", ], "memberOf": [], + "accountStatus": ["true"], + "superuser": ["false"], "goauthentik.io/ldap/active": ["true"], "goauthentik.io/ldap/superuser": ["false"], "goauthentik.io/user/override-ips": ["true"], @@ -218,6 +220,8 @@ class TestProviderLDAP(SeleniumTestCase): "memberOf": [ "cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io" ], + "accountStatus": ["true"], + "superuser": ["true"], "goauthentik.io/ldap/active": ["true"], "goauthentik.io/ldap/superuser": ["true"], "extraAttribute": ["bar"], diff --git a/web/package-lock.json b/web/package-lock.json index 532459321..680d55dfc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -26,7 +26,7 @@ "@rollup/plugin-typescript": "^8.2.1", "@sentry/browser": "^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/grecaptcha": "^3.0.2", "@typescript-eslint/eslint-plugin": "^4.28.1", @@ -35,11 +35,11 @@ "authentik-api": "file:api", "babel-plugin-macros": "^3.1.0", "base64-js": "^1.5.1", - "chart.js": "^3.4.0", + "chart.js": "^3.4.1", "chartjs-adapter-moment": "^1.0.0", "codemirror": "^5.62.0", "construct-style-sheets-polyfill": "^2.4.16", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-config-google": "^0.14.0", "eslint-plugin-custom-elements": "0.0.2", "eslint-plugin-lit": "^1.5.1", @@ -61,12 +61,13 @@ "typescript": "^4.3.5", "webcomponent-qr-code": "^1.0.5", "yaml": "^1.10.2" - } + }, + "devDependencies": {} }, "api": { "name": "authentik-api", - "version": "0.0.1", - "dependencies": { + "version": "1.0.0", + "devDependencies": { "typescript": "^3.6" } }, @@ -74,6 +75,7 @@ "version": "3.9.9", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1739,6 +1741,24 @@ "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": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", @@ -2434,9 +2454,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@types/chart.js": { - "version": "2.9.32", - "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz", - "integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==", + "version": "2.9.33", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz", + "integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==", "dependencies": { "moment": "^2.10.2" } @@ -3316,9 +3336,9 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/chart.js": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz", - "integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ==" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz", + "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g==" }, "node_modules/chartjs-adapter-moment": { "version": "1.0.0", @@ -3861,12 +3881,13 @@ } }, "node_modules/eslint": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz", - "integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz", + "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==", "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.2", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -9191,6 +9212,21 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", "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": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", @@ -9780,9 +9816,9 @@ } }, "@types/chart.js": { - "version": "2.9.32", - "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz", - "integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==", + "version": "2.9.33", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz", + "integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==", "requires": { "moment": "^2.10.2" } @@ -10170,7 +10206,8 @@ "typescript": { "version": "3.9.9", "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==" }, "chart.js": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.0.tgz", - "integrity": "sha512-mJsRm2apQm5mwz2OgYqGNG4erZh/qljcRZkWSa0kLkFr3UC3e1wKRMgnIh6WdhUrNu0w/JT9PkjLyylqEqHXEQ==" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz", + "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g==" }, "chartjs-adapter-moment": { "version": "1.0.0", @@ -10899,12 +10936,13 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz", - "integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz", + "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==", "requires": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.2", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", diff --git a/web/package.json b/web/package.json index c0470b7e7..8d78f4ec8 100644 --- a/web/package.json +++ b/web/package.json @@ -55,7 +55,7 @@ "@rollup/plugin-typescript": "^8.2.1", "@sentry/browser": "^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/grecaptcha": "^3.0.2", "@typescript-eslint/eslint-plugin": "^4.28.1", @@ -64,11 +64,11 @@ "authentik-api": "file:api", "babel-plugin-macros": "^3.1.0", "base64-js": "^1.5.1", - "chart.js": "^3.4.0", + "chart.js": "^3.4.1", "chartjs-adapter-moment": "^1.0.0", "codemirror": "^5.62.0", "construct-style-sheets-polyfill": "^2.4.16", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-config-google": "^0.14.0", "eslint-plugin-custom-elements": "0.0.2", "eslint-plugin-lit": "^1.5.1", diff --git a/web/src/api/Config.ts b/web/src/api/Config.ts index eb2dbc3f0..530d0f5fd 100644 --- a/web/src/api/Config.ts +++ b/web/src/api/Config.ts @@ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware { post(context: ResponseContext): Promise { 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); } diff --git a/web/src/constants.ts b/web/src/constants.ts index 288e1aa08..15ca917ec 100644 --- a/web/src/constants.ts +++ b/web/src/constants.ts @@ -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 = "2021.6.3"; +export const VERSION = "2021.6.4"; export const PAGE_SIZE = 20; export const EVENT_REFRESH = "ak-refresh"; export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle"; diff --git a/web/src/elements/forms/ModelForm.ts b/web/src/elements/forms/ModelForm.ts index bc272990d..59d06dd38 100644 --- a/web/src/elements/forms/ModelForm.ts +++ b/web/src/elements/forms/ModelForm.ts @@ -12,6 +12,7 @@ export abstract class ModelForm extends Form if (this.isInViewport) { this.loadInstance(value).then(instance => { this.instance = instance; + this.requestUpdate(); }); } } @@ -37,6 +38,11 @@ export abstract class ModelForm extends Form }); } + resetForm(): void { + this.instance = undefined; + this._initialLoad = false; + } + render(): TemplateResult { // if we're in viewport now and haven't loaded AND have a PK set, load now if (this.isInViewport && !this._initialLoad && this._instancePk) { diff --git a/web/src/elements/oauth/UserRefreshList.ts b/web/src/elements/oauth/UserRefreshList.ts index ef58053bf..7356eb6d3 100644 --- a/web/src/elements/oauth/UserRefreshList.ts +++ b/web/src/elements/oauth/UserRefreshList.ts @@ -34,6 +34,7 @@ export class UserOAuthRefreshList extends Table { columns(): TableColumn[] { return [ new TableColumn(t`Provider`, "provider"), + new TableColumn(t`Revoked?`, "revoked"), new TableColumn(t`Expires`, "expires"), new TableColumn(t`Scopes`, "scope"), new TableColumn(""), @@ -62,6 +63,7 @@ export class UserOAuthRefreshList extends Table { html` ${item.provider?.name} `, + html`${item.revoked ? t`Yes` : t`No`}`, html`${item.expires?.toLocaleString()}`, html`${item.scope.join(", ")}`, html` diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index 993e3b924..d65b5e6a9 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -225,7 +225,14 @@ export abstract class Table extends LitElement { renderToolbar(): TemplateResult { return html``; @@ -241,7 +248,12 @@ export abstract class Table extends LitElement { } return html` { this.search = value; - this.fetch(); + this.dispatchEvent( + new CustomEvent(EVENT_REFRESH, { + bubbles: true, + composed: true, + }) + ); }}>  `; } @@ -274,7 +286,15 @@ export abstract class Table extends LitElement { { this.page = page; this.fetch(); }}> + .pageChangeHandler=${(page: number) => { + this.page = page; + this.dispatchEvent( + new CustomEvent(EVENT_REFRESH, { + bubbles: true, + composed: true, + }) + ); + }}> @@ -300,7 +320,15 @@ export abstract class Table extends LitElement { { this.page = page; this.fetch(); }}> + .pageChangeHandler=${(page: number) => { + this.page = page; + this.dispatchEvent( + new CustomEvent(EVENT_REFRESH, { + bubbles: true, + composed: true, + }) + ); + }}> `; } diff --git a/web/src/locales/en.po b/web/src/locales/en.po index a4ff46ed9..ecd51bc6c 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -488,8 +488,12 @@ msgid "Certificate" msgstr "Certificate" #: src/pages/crypto/CertificateKeyPairListPage.ts -msgid "Certificate Fingerprint" -msgstr "Certificate Fingerprint" +msgid "Certificate Fingerprint (SHA1)" +msgstr "Certificate Fingerprint (SHA1)" + +#: src/pages/crypto/CertificateKeyPairListPage.ts +msgid "Certificate Fingerprint (SHA256)" +msgstr "Certificate Fingerprint (SHA256)" #: src/pages/crypto/CertificateKeyPairListPage.ts msgid "Certificate Subjet" @@ -2346,6 +2350,7 @@ msgstr "Negates the outcome of the binding. Messages are unaffected." msgid "New version available!" msgstr "New version available!" +#: src/elements/oauth/UserRefreshList.ts #: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/groups/GroupListPage.ts @@ -3044,6 +3049,10 @@ msgstr "Return home" msgid "Return to device picker" msgstr "Return to device picker" +#: src/elements/oauth/UserRefreshList.ts +msgid "Revoked?" +msgstr "Revoked?" + #: src/pages/property-mappings/PropertyMappingSAMLForm.ts msgid "SAML Attribute Name" msgstr "SAML Attribute Name" @@ -4544,6 +4553,7 @@ msgstr "" msgid "X509 Subject" msgstr "X509 Subject" +#: src/elements/oauth/UserRefreshList.ts #: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/groups/GroupListPage.ts diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index b5dab2646..2659bbc15 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -484,7 +484,11 @@ msgid "Certificate" msgstr "" #: -msgid "Certificate Fingerprint" +msgid "Certificate Fingerprint (SHA1)" +msgstr "" + +#: +msgid "Certificate Fingerprint (SHA256)" msgstr "" #: @@ -2350,6 +2354,7 @@ msgstr "" #: #: #: +#: msgid "No" msgstr "" @@ -3036,6 +3041,10 @@ msgstr "" msgid "Return to device picker" msgstr "" +#: +msgid "Revoked?" +msgstr "" + #: msgid "SAML Attribute Name" msgstr "" @@ -4539,6 +4548,7 @@ msgstr "" #: #: #: +#: msgid "Yes" msgstr "" diff --git a/web/src/pages/crypto/CertificateKeyPairListPage.ts b/web/src/pages/crypto/CertificateKeyPairListPage.ts index 921259f6d..f9c2d0a82 100644 --- a/web/src/pages/crypto/CertificateKeyPairListPage.ts +++ b/web/src/pages/crypto/CertificateKeyPairListPage.ts @@ -103,10 +103,18 @@ export class CertificateKeyPairListPage extends TablePage {
- ${t`Certificate Fingerprint`} + ${t`Certificate Fingerprint (SHA1)`}
-
${item.fingerprint}
+
${item.fingerprintSha1}
+
+
+
+
+ ${t`Certificate Fingerprint (SHA256)`} +
+
+
${item.fingerprintSha256}
diff --git a/web/src/pages/outposts/OutpostHealth.ts b/web/src/pages/outposts/OutpostHealth.ts index fed6f4b51..8c3395a6f 100644 --- a/web/src/pages/outposts/OutpostHealth.ts +++ b/web/src/pages/outposts/OutpostHealth.ts @@ -15,7 +15,7 @@ export class OutpostHealthElement extends LitElement { outpostId?: string; @property({attribute: false}) - outpostHealth: OutpostHealth[] = []; + outpostHealth?: OutpostHealth[]; static get styles(): CSSResult[] { return [PFBase, AKGlobal]; @@ -23,7 +23,8 @@ export class OutpostHealthElement extends LitElement { constructor() { super(); - this.addEventListener(EVENT_REFRESH, () => { + window.addEventListener(EVENT_REFRESH, () => { + this.outpostHealth = undefined; this.firstUpdated(); }); } @@ -38,7 +39,7 @@ export class OutpostHealthElement extends LitElement { } render(): TemplateResult { - if (!this.outpostId) { + if (!this.outpostId || !this.outpostHealth) { return html``; } if (this.outpostHealth.length === 0) { diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts index 826fff144..c0ec93361 100644 --- a/web/src/pages/users/UserListPage.ts +++ b/web/src/pages/users/UserListPage.ts @@ -9,13 +9,14 @@ import "../../elements/buttons/ActionButton"; import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; import { CoreApi, User } from "authentik-api"; -import { DEFAULT_CONFIG } from "../../api/Config"; +import { DEFAULT_CONFIG, tenant } from "../../api/Config"; import "../../elements/forms/DeleteForm"; import "./UserActiveForm"; import "./UserForm"; import { showMessage } from "../../elements/messages/MessageContainer"; import { MessageLevel } from "../../elements/messages/Message"; import { first } from "../../utils"; +import { until } from "lit-html/directives/until"; @customElement("ak-user-list") export class UserListPage extends TablePage { @@ -128,27 +129,33 @@ export class UserListPage extends TablePage { - { - return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({ - id: item.pk || 0, - }).then(rec => { - showMessage({ - level: MessageLevel.success, - message: t`Successfully generated recovery link`, - description: rec.link - }); - }).catch((ex: Response) => { - ex.json().then(() => { - showMessage({ - level: MessageLevel.error, - message: t`No recovery flow is configured.`, - }); - }); - }); - }}> - ${t`Reset Password`} - + ${until(tenant().then(te => { + if (te.flowRecovery) { + return html` + { + return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryRetrieve({ + id: item.pk || 0, + }).then(rec => { + showMessage({ + level: MessageLevel.success, + message: t`Successfully generated recovery link`, + description: rec.link + }); + }).catch((ex: Response) => { + ex.json().then(() => { + showMessage({ + level: MessageLevel.error, + message: t`No recovery flow is configured.`, + }); + }); + }); + }}> + ${t`Reset Password`} + `; + } + return html``; + }))} ${t`Impersonate`} `, diff --git a/website/docs/installation/docker-compose.md b/website/docs/installation/docker-compose.md index 2675a9c97..b71de8a22 100644 --- a/website/docs/installation/docker-compose.md +++ b/website/docs/installation/docker-compose.md @@ -6,17 +6,17 @@ This installation method is for test-setups and small-scale productive setups. ## 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-compose ## 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 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: diff --git a/website/docs/integrations/services/gitlab/index.md b/website/docs/integrations/services/gitlab/index.md index 4ff1177f4..32b061d2b 100644 --- a/website/docs/integrations/services/gitlab/index.md +++ b/website/docs/integrations/services/gitlab/index.md @@ -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` - Audience: `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 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 gitlab_rails['omniauth_enabled'] = true @@ -46,7 +47,7 @@ gitlab_rails['omniauth_providers'] = [ assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback', # 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_sso_target_url: 'https://authentik.company/application/saml//sso/binding/post/', + idp_sso_target_url: 'https://authentik.company/application/saml//sso/binding/redirect/', issuer: 'https://gitlab.company', name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', attribute_statements: { diff --git a/website/docs/outposts/manual-deploy-docker-compose.md b/website/docs/outposts/manual-deploy-docker-compose.md index 195008468..d3d79cc73 100644 --- a/website/docs/outposts/manual-deploy-docker-compose.md +++ b/website/docs/outposts/manual-deploy-docker-compose.md @@ -11,7 +11,7 @@ version: "3.5" services: authentik_proxy: - image: ghcr.io/goauthentik/proxy:2021.6.3 + image: ghcr.io/goauthentik/proxy:2021.6.4 ports: - 4180:4180 - 4443:4443 @@ -21,7 +21,7 @@ services: AUTHENTIK_TOKEN: token-generated-by-authentik # Or, for the LDAP Outpost authentik_proxy: - image: ghcr.io/goauthentik/ldap:2021.6.3 + image: ghcr.io/goauthentik/ldap:2021.6.4 ports: - 389:3389 environment: diff --git a/website/docs/outposts/manual-deploy-kubernetes.md b/website/docs/outposts/manual-deploy-kubernetes.md index 041e17eef..7d287d202 100644 --- a/website/docs/outposts/manual-deploy-kubernetes.md +++ b/website/docs/outposts/manual-deploy-kubernetes.md @@ -14,7 +14,7 @@ metadata: app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/name: authentik-proxy - app.kubernetes.io/version: 2021.6.3 + app.kubernetes.io/version: 2021.6.4 name: authentik-outpost-api stringData: authentik_host: "__AUTHENTIK_URL__" @@ -29,7 +29,7 @@ metadata: app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/name: authentik-proxy - app.kubernetes.io/version: 2021.6.3 + app.kubernetes.io/version: 2021.6.4 name: authentik-outpost spec: ports: @@ -54,7 +54,7 @@ metadata: app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/name: authentik-proxy - app.kubernetes.io/version: 2021.6.3 + app.kubernetes.io/version: 2021.6.4 name: authentik-outpost spec: selector: @@ -62,14 +62,14 @@ spec: app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/name: authentik-proxy - app.kubernetes.io/version: 2021.6.3 + app.kubernetes.io/version: 2021.6.4 template: metadata: labels: app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/name: authentik-proxy - app.kubernetes.io/version: 2021.6.3 + app.kubernetes.io/version: 2021.6.4 spec: containers: - env: @@ -88,7 +88,7 @@ spec: secretKeyRef: key: authentik_host_insecure name: authentik-outpost-api - image: ghcr.io/goauthentik/proxy:2021.6.3 + image: ghcr.io/goauthentik/proxy:2021.6.4 name: proxy ports: - containerPort: 4180 @@ -110,7 +110,7 @@ metadata: app.kubernetes.io/instance: __OUTPOST_NAME__ app.kubernetes.io/managed-by: goauthentik.io app.kubernetes.io/name: authentik-proxy - app.kubernetes.io/version: 2021.6.3 + app.kubernetes.io/version: 2021.6.4 name: authentik-outpost spec: rules: diff --git a/website/docs/releases/v2021.6.md b/website/docs/releases/v2021.6.md index 8ce50f86c..dab310e0f 100644 --- a/website/docs/releases/v2021.6.md +++ b/website/docs/releases/v2021.6.md @@ -139,6 +139,28 @@ slug: "2021.6" - web/admin: fix only recovery flows being selectable for unenrollment flow in tenant form - 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 This release does not introduce any new requirements.