Skip to content

Commit

Permalink
Merge branch 'main' into 5165-password-strength-indicator
Browse files Browse the repository at this point in the history
* main: (23 commits)
  web: bump API Client version (#5935)
  sources/ldap: add support for cert based auth (#5850)
  ci: replace status with state for auto-deployment
  ci: don't write CI status to file
  ci: add workflow to automatically update next branch (#5921)
  providers/ldap: fix Outpost provider listing excluding backchannel providers (#5933)
  root: revert to use secret_key for JWT signing (#5934)
  sources/ldap: fix duplicate bind when authenticating user directly to… (#5927)
  web: bump core-js from 3.30.2 to 3.31.0 in /web (#5928)
  core: bump pytest from 7.3.1 to 7.3.2 (#5929)
  web: bump @rollup/plugin-commonjs from 25.0.0 to 25.0.1 in /web (#5931)
  web: bump @formatjs/intl-listformat from 7.3.0 to 7.4.0 in /web (#5932)
  core: bump github.com/go-ldap/ldap/v3 from 3.4.4 to 3.4.5 (#5930)
  website/integrations: Fix header in dokuwiki instructions (#5926)
  providers/oauth2: launch url: if URL parsing fails, return no launch URL (#5918)
  web: bump @babel/core from 7.22.1 to 7.22.5 in /web (#5909)
  web: bump @babel/plugin-proposal-decorators from 7.22.3 to 7.22.5 in /web (#5910)
  web: bump @babel/preset-typescript from 7.21.5 to 7.22.5 in /web (#5912)
  web: bump @babel/preset-env from 7.22.4 to 7.22.5 in /web (#5915)
  core: bump requests-mock from 1.10.0 to 1.11.0 (#5911)
  ...
  • Loading branch information
kensternberg-authentik committed Jun 12, 2023
2 parents 4ea9b69 + e5576d4 commit a75c943
Show file tree
Hide file tree
Showing 39 changed files with 1,421 additions and 819 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/release-next-branch.yml
@@ -0,0 +1,25 @@
name: authentik-on-release-next-branch

on:
schedule:
- cron: "0 12 * * *" # every day at noon
workflow_dispatch:

permissions:
contents: write

jobs:
update-next:
runs-on: ubuntu-latest
environment: internal-production
steps:
- uses: actions/checkout@v3
with:
ref: main
- id: main-state
run: |
state=$(curl -fsSL -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ github.token }}" "https://api.github.com/repos/${{ github.repository }}/commits/HEAD/state" | jq -r '.state')
echo "state=${state}" >> $GITHUB_OUTPUT
- if: ${{ steps.main-state.outputs.state == 'success' }}
run: |
git push origin next --force
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -151,7 +151,7 @@ web-check-compile:
cd web && npm run tsc

web-i18n-extract:
cd web && npm run extract
cd web && npm run extract-locales

#########################
## Website
Expand Down
4 changes: 2 additions & 2 deletions authentik/core/models.py
Expand Up @@ -376,10 +376,10 @@ def get_meta_icon(self) -> Optional[str]:
def get_launch_url(self, user: Optional["User"] = None) -> Optional[str]:
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
url = None
if provider := self.get_provider():
url = provider.launch_url
if self.meta_launch_url:
url = self.meta_launch_url
elif provider := self.get_provider():
url = provider.launch_url
if user and url:
if isinstance(user, SimpleLazyObject):
user._setup()
Expand Down
4 changes: 3 additions & 1 deletion authentik/providers/ldap/api.py
Expand Up @@ -105,7 +105,9 @@ class Meta:
class LDAPOutpostConfigViewSet(ReadOnlyModelViewSet):
"""LDAPProvider Viewset"""

queryset = LDAPProvider.objects.filter(application__isnull=False)
queryset = LDAPProvider.objects.filter(
Q(application__isnull=False) | Q(backchannel_application__isnull=False)
)
serializer_class = LDAPOutpostConfigSerializer
ordering = ["name"]
search_fields = ["name"]
Expand Down
Empty file.
52 changes: 52 additions & 0 deletions authentik/providers/ldap/tests/test_api.py
@@ -0,0 +1,52 @@
"""LDAP Provider API tests"""
from json import loads

from django.urls import reverse
from rest_framework.test import APITestCase

from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.ldap.models import LDAPProvider


class TestLDAPProviderAPI(APITestCase):
"""LDAP Provider API tests"""

def test_outpost_application(self):
"""Test outpost-like provider retrieval (direct connection)"""
provider = LDAPProvider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
)
Application.objects.create(
name=generate_id(),
slug=generate_id(),
provider=provider,
)
user = create_test_admin_user()
self.client.force_login(user)
res = self.client.get(reverse("authentik_api:ldapprovideroutpost-list"))
self.assertEqual(res.status_code, 200)
data = loads(res.content.decode())
self.assertEqual(data["pagination"]["count"], 1)
self.assertEqual(len(data["results"]), 1)

def test_outpost_application_backchannel(self):
"""Test outpost-like provider retrieval (backchannel connection)"""
provider = LDAPProvider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
)
app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
app.backchannel_providers.add(provider)
user = create_test_admin_user()
self.client.force_login(user)
res = self.client.get(reverse("authentik_api:ldapprovideroutpost-list"))
self.assertEqual(res.status_code, 200)
data = loads(res.content.decode())
self.assertEqual(data["pagination"]["count"], 1)
self.assertEqual(len(data["results"]), 1)
2 changes: 1 addition & 1 deletion authentik/providers/ldap/urls.py
Expand Up @@ -2,6 +2,6 @@
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet

api_urlpatterns = [
("outposts/ldap", LDAPOutpostConfigViewSet),
("outposts/ldap", LDAPOutpostConfigViewSet, "ldapprovideroutpost"),
("providers/ldap", LDAPProviderViewSet),
]
11 changes: 9 additions & 2 deletions authentik/providers/oauth2/models.py
Expand Up @@ -17,6 +17,7 @@
from django.utils.translation import gettext_lazy as _
from jwt import encode
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger

from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
from authentik.crypto.models import CertificateKeyPair
Expand All @@ -26,6 +27,8 @@
from authentik.providers.oauth2.id_token import IDToken, SubModes
from authentik.sources.oauth.models import OAuthSource

LOGGER = get_logger()


def generate_client_secret() -> str:
"""Generate client secret with adequate length"""
Expand Down Expand Up @@ -251,8 +254,12 @@ def launch_url(self) -> Optional[str]:
if self.redirect_uris == "":
return None
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
launch_url = urlparse(main_url)._replace(path="")
return urlunparse(launch_url)
try:
launch_url = urlparse(main_url)._replace(path="")
return urlunparse(launch_url)
except ValueError as exc:
LOGGER.warning("Failed to format launch url", exc=exc)
return None

@property
def component(self) -> str:
Expand Down
13 changes: 13 additions & 0 deletions authentik/providers/oauth2/tests/test_api.py
@@ -1,5 +1,7 @@
"""Test OAuth2 API"""
from json import loads
from sys import version_info
from unittest import skipUnless

from django.urls import reverse
from rest_framework.test import APITestCase
Expand Down Expand Up @@ -42,3 +44,14 @@ def test_setup_urls(self):
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["issuer"], "http://testserver/application/o/test/")

# https://github.com/goauthentik/authentik/pull/5918
@skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up")
def test_launch_url(self):
"""Test launch_url"""
self.provider.redirect_uris = (
"https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/\n"
)
self.provider.save()
self.provider.refresh_from_db()
self.assertIsNone(self.provider.launch_url)
2 changes: 1 addition & 1 deletion authentik/providers/proxy/urls.py
Expand Up @@ -2,6 +2,6 @@
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet

api_urlpatterns = [
("outposts/proxy", ProxyOutpostConfigViewSet),
("outposts/proxy", ProxyOutpostConfigViewSet, "proxyprovideroutpost"),
("providers/proxy", ProxyProviderViewSet),
]
2 changes: 1 addition & 1 deletion authentik/providers/radius/urls.py
Expand Up @@ -2,6 +2,6 @@
from authentik.providers.radius.api import RadiusOutpostConfigViewSet, RadiusProviderViewSet

api_urlpatterns = [
("outposts/radius", RadiusOutpostConfigViewSet),
("outposts/radius", RadiusOutpostConfigViewSet, "radiusprovideroutpost"),
("providers/radius", RadiusProviderViewSet),
]
13 changes: 3 additions & 10 deletions authentik/root/middleware.py
@@ -1,5 +1,4 @@
"""Dynamically set SameSite depending if the upstream connection is TLS or not"""
from functools import lru_cache
from hashlib import sha512
from time import time
from timeit import default_timer
Expand All @@ -17,16 +16,10 @@
from structlog.stdlib import get_logger

from authentik.lib.utils.http import get_client_ip
from authentik.root.install_id import get_install_id

LOGGER = get_logger("authentik.asgi")
ACR_AUTHENTIK_SESSION = "goauthentik.io/core/default"


@lru_cache
def get_signing_hash():
"""Get cookie JWT signing hash"""
return sha512(get_install_id().encode()).hexdigest()
SIGNING_HASH = sha512(settings.SECRET_KEY.encode()).hexdigest()


class SessionMiddleware(UpstreamSessionMiddleware):
Expand Down Expand Up @@ -54,7 +47,7 @@ def decode_session_key(key: str) -> str:
# for testing setups, where the session is directly set
session_key = key if settings.TEST else None
try:
session_payload = decode(key, get_signing_hash(), algorithms=["HS256"])
session_payload = decode(key, SIGNING_HASH, algorithms=["HS256"])
session_key = session_payload["sid"]
except (KeyError, PyJWTError):
pass
Expand Down Expand Up @@ -121,7 +114,7 @@ def process_response(self, request: HttpRequest, response: HttpResponse) -> Http
}
if request.user.is_authenticated:
payload["sub"] = request.user.uid
value = encode(payload=payload, key=get_signing_hash())
value = encode(payload=payload, key=SIGNING_HASH)
if settings.TEST:
value = request.session.session_key
response.set_cookie(
Expand Down
15 changes: 15 additions & 0 deletions authentik/sources/ldap/api.py
Expand Up @@ -8,6 +8,7 @@
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import DictField, ListField
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
Expand All @@ -16,6 +17,7 @@
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.crypto.models import CertificateKeyPair
from authentik.events.monitored_tasks import TaskInfo
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
from authentik.sources.ldap.tasks import SYNC_CLASSES
Expand All @@ -24,6 +26,15 @@
class LDAPSourceSerializer(SourceSerializer):
"""LDAP Source Serializer"""

client_certificate = PrimaryKeyRelatedField(
allow_null=True,
help_text="Client certificate to authenticate against the LDAP Server's Certificate.",
queryset=CertificateKeyPair.objects.exclude(
key_data__exact="",
),
required=False,
)

def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
"""Check that only a single source has password_sync on"""
sync_users_password = attrs.get("sync_users_password", True)
Expand All @@ -42,9 +53,11 @@ class Meta:
fields = SourceSerializer.Meta.fields + [
"server_uri",
"peer_certificate",
"client_certificate",
"bind_cn",
"bind_password",
"start_tls",
"sni",
"base_dn",
"additional_user_dn",
"additional_group_dn",
Expand Down Expand Up @@ -75,7 +88,9 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"server_uri",
"bind_cn",
"peer_certificate",
"client_certificate",
"start_tls",
"sni",
"base_dn",
"additional_user_dn",
"additional_group_dn",
Expand Down
4 changes: 2 additions & 2 deletions authentik/sources/ldap/auth.py
Expand Up @@ -57,13 +57,13 @@ def auth_user_by_bind(self, source: LDAPSource, user: User, password: str) -> Op
# Try to bind as new user
LOGGER.debug("Attempting to bind as user", user=user)
try:
temp_connection = source.connection(
# source.connection also attempts to bind
source.connection(
connection_kwargs={
"user": user.attributes.get(LDAP_DISTINGUISHED_NAME),
"password": password,
}
)
temp_connection.bind()
return user
except LDAPInvalidCredentialsResult as exc:
LOGGER.debug("invalid LDAP credentials", user=user, exc=exc)
Expand Down
@@ -0,0 +1,45 @@
# Generated by Django 4.1.7 on 2023-06-06 18:33

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0004_alter_certificatekeypair_name"),
("authentik_sources_ldap", "0002_auto_20211203_0900"),
]

operations = [
migrations.AddField(
model_name="ldapsource",
name="client_certificate",
field=models.ForeignKey(
default=None,
help_text="Client certificate to authenticate against the LDAP Server's Certificate.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="ldap_client_certificates",
to="authentik_crypto.certificatekeypair",
),
),
migrations.AddField(
model_name="ldapsource",
name="sni",
field=models.BooleanField(
default=False, verbose_name="Use Server URI for SNI verification"
),
),
migrations.AlterField(
model_name="ldapsource",
name="peer_certificate",
field=models.ForeignKey(
default=None,
help_text="Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="ldap_peer_certificates",
to="authentik_crypto.certificatekeypair",
),
),
]

0 comments on commit a75c943

Please sign in to comment.