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]
current_version = 2021.6.3
current_version = 2021.6.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)

View File

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

26
Pipfile.lock generated
View File

@ -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": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)
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()

View File

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

View File

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

View File

@ -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": {

View File

@ -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, **_):

View File

@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
"expires",
"scope",
"id_token",
"revoked",
]
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)
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)})
// 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)}})

View File

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

View File

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

View File

@ -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"],

88
web/package-lock.json generated
View File

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

View File

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

View File

@ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware {
post(context: ResponseContext): Promise<Response | void> {
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);
}

View File

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

View File

@ -12,6 +12,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
if (this.isInViewport) {
this.loadInstance(value).then(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 {
// if we're in viewport now and haven't loaded AND have a PK set, load now
if (this.isInViewport && !this._initialLoad && this._instancePk) {

View File

@ -34,6 +34,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
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<RefreshTokenModel> {
html`<a href="#/core/providers/${item.provider?.pk}">
${item.provider?.name}
</a>`,
html`${item.revoked ? t`Yes` : t`No`}`,
html`${item.expires?.toLocaleString()}`,
html`${item.scope.join(", ")}`,
html`

View File

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

View File

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

View File

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

View File

@ -103,10 +103,18 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
<dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-description-list__group">
<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>
<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>
</div>
<div class="pf-c-description-list__group">

View File

@ -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`<ak-spinner></ak-spinner>`;
}
if (this.outpostHealth.length === 0) {

View File

@ -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<User> {
@ -128,27 +129,33 @@ export class UserListPage extends TablePage<User> {
</li>
</ul>
</ak-dropdown>
<ak-action-button
.apiRequest=${() => {
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`}
</ak-action-button>
${until(tenant().then(te => {
if (te.flowRecovery) {
return html`
<ak-action-button
.apiRequest=${() => {
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`}
</ak-action-button>`;
}
return html``;
}))}
<a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}">
${t`Impersonate`}
</a>`,

View File

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

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`
- 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/<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',
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
attribute_statements: {

View File

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

View File

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

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