From bb8af2f19b6446e060116b2b9fd1b6e20836380a Mon Sep 17 00:00:00 2001 From: Jens L Date: Thu, 31 Mar 2022 00:30:55 +0200 Subject: [PATCH] providers/oauth2: add client_assertion_type jwt bearer support (#2618) --- authentik/core/models.py | 3 + authentik/core/tasks.py | 29 ++- authentik/core/tests/test_tasks.py | 50 +++++ authentik/core/tests/test_token_api.py | 12 - authentik/providers/oauth2/api/provider.py | 1 + authentik/providers/oauth2/constants.py | 4 + ...uth2provider_verification_keys_and_more.py | 36 +++ authentik/providers/oauth2/models.py | 11 +- .../test_token_client_credentials_jwt.py | 206 ++++++++++++++++++ authentik/providers/oauth2/views/jwks.py | 1 - authentik/providers/oauth2/views/token.py | 84 ++++++- authentik/root/settings.py | 5 + authentik/sources/saml/apps.py | 2 +- authentik/sources/saml/processors/response.py | 23 +- authentik/sources/saml/settings.py | 10 - authentik/sources/saml/signals.py | 10 +- authentik/sources/saml/tasks.py | 42 ---- schema.yml | 32 ++- web/src/locales/de.po | 10 + web/src/locales/en.po | 10 + web/src/locales/es.po | 10 + web/src/locales/fr_FR.po | 10 + web/src/locales/pl.po | 10 + web/src/locales/pseudo-LOCALE.po | 10 + web/src/locales/tr.po | 10 + web/src/locales/zh-Hans.po | 10 + web/src/locales/zh-Hant.po | 10 + web/src/locales/zh_TW.po | 10 + .../crypto/CertificateKeyPairListPage.ts | 2 +- .../providers/oauth2/OAuth2ProviderForm.ts | 35 +++ .../providers/oauth2/client_credentials.md | 53 +++++ .../providers/{oauth2.md => oauth2/index.md} | 18 +- website/docs/releases/v0.10.md | 2 +- website/sidebars.js | 9 +- 34 files changed, 681 insertions(+), 99 deletions(-) create mode 100644 authentik/core/tests/test_tasks.py create mode 100644 authentik/providers/oauth2/migrations/0009_oauth2provider_verification_keys_and_more.py create mode 100644 authentik/providers/oauth2/tests/test_token_client_credentials_jwt.py delete mode 100644 authentik/sources/saml/settings.py delete mode 100644 authentik/sources/saml/tasks.py create mode 100644 website/docs/providers/oauth2/client_credentials.md rename website/docs/providers/{oauth2.md => oauth2/index.md} (67%) diff --git a/authentik/core/models.py b/authentik/core/models.py index 6e8a8295b..d3bda4bf2 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -36,6 +36,9 @@ from authentik.policies.models import PolicyBindingModel LOGGER = get_logger() USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" +USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated" +USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires" +USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout" USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources" USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username" diff --git a/authentik/core/tasks.py b/authentik/core/tasks.py index e84b946b2..2a78987b1 100644 --- a/authentik/core/tasks.py +++ b/authentik/core/tasks.py @@ -1,10 +1,18 @@ """authentik core tasks""" +from datetime import datetime, timedelta + from django.contrib.sessions.backends.cache import KEY_PREFIX from django.core.cache import cache from django.utils.timezone import now from structlog.stdlib import get_logger -from authentik.core.models import AuthenticatedSession, ExpiringModel +from authentik.core.models import ( + USER_ATTRIBUTE_EXPIRES, + USER_ATTRIBUTE_GENERATED, + AuthenticatedSession, + ExpiringModel, + User, +) from authentik.events.monitored_tasks import ( MonitoredTask, TaskResult, @@ -42,3 +50,22 @@ def clean_expired_models(self: MonitoredTask): LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount) messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}") self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) + + +@CELERY_APP.task(bind=True, base=MonitoredTask) +@prefill_task +def clean_temporary_users(self: MonitoredTask): + """Remove temporary users created by SAML Sources""" + _now = datetime.now() + messages = [] + deleted_users = 0 + for user in User.objects.filter(**{f"attributes__{USER_ATTRIBUTE_GENERATED}": True}): + delta: timedelta = _now - datetime.fromtimestamp( + user.attributes.get(USER_ATTRIBUTE_EXPIRES) + ) + if delta.total_seconds() > 0: + LOGGER.debug("User is expired and will be deleted.", user=user, delta=delta) + user.delete() + deleted_users += 1 + messages.append(f"Successfully deleted {deleted_users} users.") + self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) diff --git a/authentik/core/tests/test_tasks.py b/authentik/core/tests/test_tasks.py new file mode 100644 index 000000000..2212dbccc --- /dev/null +++ b/authentik/core/tests/test_tasks.py @@ -0,0 +1,50 @@ +"""Test tasks""" +from time import mktime + +from django.utils.timezone import now +from guardian.shortcuts import get_anonymous_user +from rest_framework.test import APITestCase + +from authentik.core.models import ( + USER_ATTRIBUTE_EXPIRES, + USER_ATTRIBUTE_GENERATED, + Token, + TokenIntents, + User, +) +from authentik.core.tasks import clean_expired_models, clean_temporary_users +from authentik.core.tests.utils import create_test_admin_user +from authentik.lib.generators import generate_id + + +class TestTasks(APITestCase): + """Test token API""" + + def setUp(self) -> None: + super().setUp() + self.user = User.objects.create(username="testuser") + self.admin = create_test_admin_user() + self.client.force_login(self.user) + + def test_token_expire(self): + """Test Token expire task""" + token: Token = Token.objects.create( + expires=now(), user=get_anonymous_user(), intent=TokenIntents.INTENT_API + ) + key = token.key + clean_expired_models.delay().get() + token.refresh_from_db() + self.assertNotEqual(key, token.key) + + def test_clean_temporary_users(self): + """Test clean_temporary_users task""" + username = generate_id + User.objects.create( + username=username, + attributes={ + USER_ATTRIBUTE_GENERATED: True, + USER_ATTRIBUTE_EXPIRES: mktime(now().timetuple()), + }, + ) + clean_temporary_users.delay().get() + self.assertFalse(User.objects.filter(username=username)) diff --git a/authentik/core/tests/test_token_api.py b/authentik/core/tests/test_token_api.py index 904e4eb0f..eea1c88e1 100644 --- a/authentik/core/tests/test_token_api.py +++ b/authentik/core/tests/test_token_api.py @@ -2,12 +2,10 @@ from json import loads from django.urls.base import reverse -from django.utils.timezone import now from guardian.shortcuts import get_anonymous_user from rest_framework.test import APITestCase from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User -from authentik.core.tasks import clean_expired_models from authentik.core.tests.utils import create_test_admin_user @@ -53,16 +51,6 @@ class TestTokenAPI(APITestCase): self.assertEqual(token.intent, TokenIntents.INTENT_API) self.assertEqual(token.expiring, False) - def test_token_expire(self): - """Test Token expire task""" - token: Token = Token.objects.create( - expires=now(), user=get_anonymous_user(), intent=TokenIntents.INTENT_API - ) - key = token.key - clean_expired_models.delay().get() - token.refresh_from_db() - self.assertNotEqual(key, token.key) - def test_list(self): """Test Token List (Test normal authentication)""" token_should: Token = Token.objects.create( diff --git a/authentik/providers/oauth2/api/provider.py b/authentik/providers/oauth2/api/provider.py index fde5e6dd1..a534d0648 100644 --- a/authentik/providers/oauth2/api/provider.py +++ b/authentik/providers/oauth2/api/provider.py @@ -34,6 +34,7 @@ class OAuth2ProviderSerializer(ProviderSerializer): "sub_mode", "property_mappings", "issuer_mode", + "verification_keys", ] diff --git a/authentik/providers/oauth2/constants.py b/authentik/providers/oauth2/constants.py index be1b96038..e8beade85 100644 --- a/authentik/providers/oauth2/constants.py +++ b/authentik/providers/oauth2/constants.py @@ -4,6 +4,10 @@ GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code" GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials" +CLIENT_ASSERTION_TYPE = "client_assertion_type" +CLIENT_ASSERTION = "client_assertion" +CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + PROMPT_NONE = "none" PROMPT_CONSNET = "consent" PROMPT_LOGIN = "login" diff --git a/authentik/providers/oauth2/migrations/0009_oauth2provider_verification_keys_and_more.py b/authentik/providers/oauth2/migrations/0009_oauth2provider_verification_keys_and_more.py new file mode 100644 index 000000000..8503b060b --- /dev/null +++ b/authentik/providers/oauth2/migrations/0009_oauth2provider_verification_keys_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.0.3 on 2022-03-29 19:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_crypto", "0003_certificatekeypair_managed"), + ("authentik_providers_oauth2", "0008_rename_rsa_key_oauth2provider_signing_key_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="oauth2provider", + name="verification_keys", + field=models.ManyToManyField( + help_text="JWTs created with the configured certificates can authenticate with this provider.", + related_name="+", + to="authentik_crypto.certificatekeypair", + verbose_name="Allowed certificates for JWT-based client_credentials", + ), + ), + migrations.AlterField( + model_name="oauth2provider", + name="signing_key", + field=models.ForeignKey( + help_text="Key used to sign the tokens. Only required when JWT Algorithm is set to RS256.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="authentik_crypto.certificatekeypair", + verbose_name="Signing Key", + ), + ), + ] diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index a103fee26..b70f669dc 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -212,7 +212,7 @@ class OAuth2Provider(Provider): signing_key = models.ForeignKey( CertificateKeyPair, - verbose_name=_("RSA Key"), + verbose_name=_("Signing Key"), on_delete=models.SET_NULL, null=True, help_text=_( @@ -220,6 +220,15 @@ class OAuth2Provider(Provider): ), ) + verification_keys = models.ManyToManyField( + CertificateKeyPair, + verbose_name=_("Allowed certificates for JWT-based client_credentials"), + help_text=_( + "JWTs created with the configured certificates can authenticate with this provider." + ), + related_name="+", + ) + def create_refresh_token( self, user: User, scope: list[str], request: HttpRequest ) -> "RefreshToken": diff --git a/authentik/providers/oauth2/tests/test_token_client_credentials_jwt.py b/authentik/providers/oauth2/tests/test_token_client_credentials_jwt.py new file mode 100644 index 000000000..e6a012621 --- /dev/null +++ b/authentik/providers/oauth2/tests/test_token_client_credentials_jwt.py @@ -0,0 +1,206 @@ +"""Test token view""" +from datetime import datetime, timedelta +from json import loads + +from django.test import RequestFactory +from django.urls import reverse +from jwt import decode + +from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group +from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow +from authentik.lib.generators import generate_id, generate_key +from authentik.managed.manager import ObjectManager +from authentik.policies.models import PolicyBinding +from authentik.providers.oauth2.constants import ( + GRANT_TYPE_CLIENT_CREDENTIALS, + SCOPE_OPENID, + SCOPE_OPENID_EMAIL, + SCOPE_OPENID_PROFILE, +) +from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping +from authentik.providers.oauth2.tests.utils import OAuthTestCase + + +class TestTokenClientCredentialsJWT(OAuthTestCase): + """Test token (client_credentials, with JWT) view""" + + def setUp(self) -> None: + super().setUp() + ObjectManager().run() + self.factory = RequestFactory() + self.cert = create_test_cert() + self.provider: OAuth2Provider = OAuth2Provider.objects.create( + name="test", + client_id=generate_id(), + client_secret=generate_key(), + authorization_flow=create_test_flow(), + redirect_uris="http://testserver", + signing_key=self.cert, + ) + self.provider.verification_keys.set([self.cert]) + self.provider.property_mappings.set(ScopeMapping.objects.all()) + self.app = Application.objects.create(name="test", slug="test", provider=self.provider) + self.user = create_test_admin_user("sa") + self.user.attributes[USER_ATTRIBUTE_SA] = True + self.user.save() + + def test_invalid_type(self): + """test invalid type""" + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", + "client_id": self.provider.client_id, + "client_assertion_type": "foo", + "client_assertion": "foo.bar", + }, + ) + self.assertEqual(response.status_code, 400) + body = loads(response.content.decode()) + self.assertEqual(body["error"], "invalid_grant") + + def test_invalid_jwt(self): + """test invalid JWT""" + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", + "client_id": self.provider.client_id, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": "foo.bar", + }, + ) + self.assertEqual(response.status_code, 400) + body = loads(response.content.decode()) + self.assertEqual(body["error"], "invalid_grant") + + def test_invalid_signautre(self): + """test invalid JWT""" + token = self.provider.encode( + { + "sub": "foo", + "exp": datetime.now() + timedelta(hours=2), + } + ) + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", + "client_id": self.provider.client_id, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": token + "foo", + }, + ) + self.assertEqual(response.status_code, 400) + body = loads(response.content.decode()) + self.assertEqual(body["error"], "invalid_grant") + + def test_invalid_expired(self): + """test invalid JWT""" + token = self.provider.encode( + { + "sub": "foo", + "exp": datetime.now() - timedelta(hours=2), + } + ) + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", + "client_id": self.provider.client_id, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": token, + }, + ) + self.assertEqual(response.status_code, 400) + body = loads(response.content.decode()) + self.assertEqual(body["error"], "invalid_grant") + + def test_invalid_no_app(self): + """test invalid JWT""" + self.app.provider = None + self.app.save() + token = self.provider.encode( + { + "sub": "foo", + "exp": datetime.now() + timedelta(hours=2), + } + ) + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", + "client_id": self.provider.client_id, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": token, + }, + ) + self.assertEqual(response.status_code, 400) + body = loads(response.content.decode()) + self.assertEqual(body["error"], "invalid_grant") + + def test_invalid_access_denied(self): + """test invalid JWT""" + group = Group.objects.create(name="foo") + PolicyBinding.objects.create( + group=group, + target=self.app, + order=0, + ) + token = self.provider.encode( + { + "sub": "foo", + "exp": datetime.now() + timedelta(hours=2), + } + ) + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", + "client_id": self.provider.client_id, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": token, + }, + ) + self.assertEqual(response.status_code, 400) + body = loads(response.content.decode()) + self.assertEqual(body["error"], "invalid_grant") + + def test_successful(self): + """test successful""" + token = self.provider.encode( + { + "sub": "foo", + "exp": datetime.now() + timedelta(hours=2), + } + ) + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", + "client_id": self.provider.client_id, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": token, + }, + ) + self.assertEqual(response.status_code, 200) + body = loads(response.content.decode()) + self.assertEqual(body["token_type"], "bearer") + _, alg = self.provider.get_jwt_key() + jwt = decode( + body["access_token"], + key=self.provider.signing_key.public_key, + algorithms=[alg], + audience=self.provider.client_id, + ) + self.assertEqual( + jwt["given_name"], "Autogenerated user from application test (client credentials JWT)" + ) + self.assertEqual(jwt["preferred_username"], "test-foo") diff --git a/authentik/providers/oauth2/views/jwks.py b/authentik/providers/oauth2/views/jwks.py index 5e4912e49..3b9ed0ec7 100644 --- a/authentik/providers/oauth2/views/jwks.py +++ b/authentik/providers/oauth2/views/jwks.py @@ -36,7 +36,6 @@ class JWKSView(View): if signing_key: private_key = signing_key.private_key - print(type(private_key)) if isinstance(private_key, RSAPrivateKey): public_key: RSAPublicKey = private_key.public_key() public_numbers = public_key.public_numbers() diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py index 96925dbfc..9e1ca0c81 100644 --- a/authentik/providers/oauth2/views/token.py +++ b/authentik/providers/oauth2/views/token.py @@ -5,14 +5,27 @@ from hashlib import sha256 from typing import Any, Optional from django.http import HttpRequest, HttpResponse +from django.utils.timezone import datetime, now from django.views import View +from jwt import InvalidTokenError, decode from structlog.stdlib import get_logger -from authentik.core.models import USER_ATTRIBUTE_SA, Application, Token, TokenIntents, User +from authentik.core.models import ( + USER_ATTRIBUTE_EXPIRES, + USER_ATTRIBUTE_GENERATED, + USER_ATTRIBUTE_SA, + Application, + Token, + TokenIntents, + User, +) from authentik.events.models import Event, EventAction from authentik.lib.utils.time import timedelta_from_string from authentik.policies.engine import PolicyEngine from authentik.providers.oauth2.constants import ( + CLIENT_ASSERTION, + CLIENT_ASSERTION_TYPE, + CLIENT_ASSERTION_TYPE_JWT, GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_REFRESH_TOKEN, @@ -21,6 +34,7 @@ from authentik.providers.oauth2.errors import TokenError, UserAuthError from authentik.providers.oauth2.models import ( AuthorizationCode, ClientTypes, + JWTAlgorithms, OAuth2Provider, RefreshToken, ) @@ -187,6 +201,8 @@ class TokenParams: raise TokenError("invalid_grant") def __post_init_client_credentials(self, request: HttpRequest): + if request.POST.get(CLIENT_ASSERTION_TYPE, "") != "": + return self.__post_init_client_credentials_jwt(request) # Authenticate user based on credentials username = request.POST.get("username") password = request.POST.get("password") @@ -222,6 +238,72 @@ class TokenParams: if not result.passing: LOGGER.info("User not authenticated for application", user=self.user, app=app) raise TokenError("invalid_grant") + return None + + def __post_init_client_credentials_jwt(self, request: HttpRequest): + assertion_type = request.POST.get(CLIENT_ASSERTION_TYPE, "") + if assertion_type != CLIENT_ASSERTION_TYPE_JWT: + raise TokenError("invalid_grant") + + client_secret = request.POST.get("client_secret", None) + assertion = request.POST.get(CLIENT_ASSERTION, client_secret) + if not assertion: + raise TokenError("invalid_grant") + + token = None + for cert in self.provider.verification_keys.all(): + LOGGER.debug("verifying jwt with key", key=cert.name) + try: + token = decode( + assertion, + cert.certificate.public_key(), + algorithms=[JWTAlgorithms.RS256, JWTAlgorithms.EC256], + options={ + "verify_aud": False, + }, + ) + except (InvalidTokenError, ValueError) as last_exc: + LOGGER.warning("failed to validate jwt", last_exc=last_exc) + if not token: + raise TokenError("invalid_grant") + + exp = datetime.fromtimestamp(token["exp"]) + # Non-timezone aware check since we assume `exp` is in UTC + if datetime.now() >= exp: + LOGGER.info("JWT token expired") + raise TokenError("invalid_grant") + + app = Application.objects.filter(provider=self.provider).first() + if not app or not app.provider: + LOGGER.info("client_credentials grant for provider without application") + raise TokenError("invalid_grant") + + engine = PolicyEngine(app, self.user, request) + engine.request.context["JWT"] = token + engine.build() + result = engine.result + if not result.passing: + LOGGER.info("JWT not authenticated for application", app=app) + raise TokenError("invalid_grant") + self.user, _ = User.objects.update_or_create( + username=f"{self.provider.name}-{token.get('sub')}", + defaults={ + "attributes": { + USER_ATTRIBUTE_GENERATED: True, + USER_ATTRIBUTE_EXPIRES: token["exp"], + }, + "last_login": now(), + "name": f"Autogenerated user from application {app.name} (client credentials JWT)", + }, + ) + + Event.new( + action=EventAction.LOGIN, + PLAN_CONTEXT_METHOD="jwt", + PLAN_CONTEXT_METHOD_ARGS={ + "jwt": token, + }, + ).from_http(request, user=self.user) class TokenView(View): diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 918901f7c..b8fedb4c8 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -345,6 +345,11 @@ CELERY_BEAT_SCHEDULE = { "schedule": crontab(hour="*/24", minute=0), "options": {"queue": "authentik_scheduled"}, }, + "user_cleanup": { + "task": "authentik.core.tasks.clean_temporary_users", + "schedule": crontab(minute="*/5"), + "options": {"queue": "authentik_scheduled"}, + }, } CELERY_TASK_CREATE_MISSING_QUEUES = True CELERY_TASK_DEFAULT_QUEUE = "authentik" diff --git a/authentik/sources/saml/apps.py b/authentik/sources/saml/apps.py index 5b3615517..746d2d45b 100644 --- a/authentik/sources/saml/apps.py +++ b/authentik/sources/saml/apps.py @@ -6,7 +6,7 @@ from django.apps import AppConfig class AuthentikSourceSAMLConfig(AppConfig): - """authentik saml_idp app config""" + """authentik saml source app config""" name = "authentik.sources.saml" label = "authentik_sources_saml" diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 53a28560a..8859bbc61 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -1,5 +1,6 @@ """authentik saml source processor""" from base64 import b64decode +from time import mktime from typing import TYPE_CHECKING, Any import xmlsec @@ -7,9 +8,16 @@ from defusedxml.lxml import fromstring from django.core.cache import cache from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse +from django.utils.timezone import now from structlog.stdlib import get_logger -from authentik.core.models import User +from authentik.core.models import ( + USER_ATTRIBUTE_DELETE_ON_LOGOUT, + USER_ATTRIBUTE_EXPIRES, + USER_ATTRIBUTE_GENERATED, + USER_ATTRIBUTE_SOURCES, + User, +) from authentik.flows.models import Flow from authentik.flows.planner import ( PLAN_CONTEXT_PENDING_USER, @@ -19,6 +27,7 @@ from authentik.flows.planner import ( FlowPlanner, ) from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN +from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.urls import redirect_with_qs from authentik.policies.utils import delete_none_keys from authentik.sources.saml.exceptions import ( @@ -124,9 +133,19 @@ class ResponseProcessor: on logout and periodically.""" # Create a temporary User name_id = self._get_name_id().text + expiry = mktime( + (now() + timedelta_from_string(self._source.temporary_user_delete_after)).timetuple() + ) user: User = User.objects.create( username=name_id, - attributes={"saml": {"source": self._source.pk.hex, "delete_on_logout": True}}, + attributes={ + USER_ATTRIBUTE_GENERATED: True, + USER_ATTRIBUTE_SOURCES: [ + self._source.name, + ], + USER_ATTRIBUTE_DELETE_ON_LOGOUT: True, + USER_ATTRIBUTE_EXPIRES: expiry, + }, ) LOGGER.debug("Created temporary user for NameID Transient", username=name_id) user.set_unusable_password() diff --git a/authentik/sources/saml/settings.py b/authentik/sources/saml/settings.py deleted file mode 100644 index 3e3dc45cf..000000000 --- a/authentik/sources/saml/settings.py +++ /dev/null @@ -1,10 +0,0 @@ -"""saml source settings""" -from celery.schedules import crontab - -CELERY_BEAT_SCHEDULE = { - "saml_source_cleanup": { - "task": "authentik.sources.saml.tasks.clean_temporary_users", - "schedule": crontab(minute="*/5"), - "options": {"queue": "authentik_scheduled"}, - } -} diff --git a/authentik/sources/saml/signals.py b/authentik/sources/saml/signals.py index cf6b92709..cd9b1f408 100644 --- a/authentik/sources/saml/signals.py +++ b/authentik/sources/saml/signals.py @@ -4,7 +4,7 @@ from django.dispatch import receiver from django.http import HttpRequest from structlog.stdlib import get_logger -from authentik.core.models import User +from authentik.core.models import USER_ATTRIBUTE_DELETE_ON_LOGOUT, User LOGGER = get_logger() @@ -15,8 +15,6 @@ def on_user_logged_out(sender, request: HttpRequest, user: User, **_): """Delete temporary user if the `delete_on_logout` flag is enabled""" if not user: return - if "saml" in user.attributes: - if "delete_on_logout" in user.attributes["saml"]: - if user.attributes["saml"]["delete_on_logout"]: - LOGGER.debug("Deleted temporary user", user=user) - user.delete() + if user.attributes.get(USER_ATTRIBUTE_DELETE_ON_LOGOUT, False): + LOGGER.debug("Deleted temporary user", user=user) + user.delete() diff --git a/authentik/sources/saml/tasks.py b/authentik/sources/saml/tasks.py deleted file mode 100644 index cb72d55d0..000000000 --- a/authentik/sources/saml/tasks.py +++ /dev/null @@ -1,42 +0,0 @@ -"""authentik saml source tasks""" -from django.utils.timezone import now -from structlog.stdlib import get_logger - -from authentik.core.models import AuthenticatedSession, User -from authentik.events.monitored_tasks import ( - MonitoredTask, - TaskResult, - TaskResultStatus, - prefill_task, -) -from authentik.lib.utils.time import timedelta_from_string -from authentik.root.celery import CELERY_APP -from authentik.sources.saml.models import SAMLSource - -LOGGER = get_logger() - - -@CELERY_APP.task(bind=True, base=MonitoredTask) -@prefill_task -def clean_temporary_users(self: MonitoredTask): - """Remove temporary users created by SAML Sources""" - _now = now() - messages = [] - deleted_users = 0 - for user in User.objects.filter(attributes__saml__isnull=False): - sources = SAMLSource.objects.filter(pk=user.attributes.get("saml", {}).get("source", "")) - if not sources.exists(): - LOGGER.warning("User has an invalid SAML Source and won't be deleted!", user=user) - messages.append(f"User {user} has an invalid SAML Source and won't be deleted!") - continue - source = sources.first() - source_delta = timedelta_from_string(source.temporary_user_delete_after) - if ( - _now - user.last_login >= source_delta - and not AuthenticatedSession.objects.filter(user=user).exists() - ): - LOGGER.debug("User is expired and will be deleted.", user=user, delta=source_delta) - user.delete() - deleted_users += 1 - messages.append(f"Successfully deleted {deleted_users} users.") - self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) diff --git a/schema.yml b/schema.yml index 3b1416e1d..31df40770 100644 --- a/schema.yml +++ b/schema.yml @@ -23091,7 +23091,6 @@ components: type: string format: uuid nullable: true - title: RSA Key description: Key used to sign the tokens. Only required when JWT Algorithm is set to RS256. redirect_uris: @@ -23106,6 +23105,15 @@ components: allOf: - $ref: '#/components/schemas/IssuerModeEnum' description: Configure how the issuer field of the ID Token should be filled. + verification_keys: + type: array + items: + type: string + format: uuid + title: Allowed certificates for JWT-based client_credentials + title: Allowed certificates for JWT-based client_credentials + description: JWTs created with the configured certificates can authenticate + with this provider. required: - assigned_application_name - assigned_application_slug @@ -23116,6 +23124,7 @@ components: - pk - verbose_name - verbose_name_plural + - verification_keys OAuth2ProviderRequest: type: object description: OAuth2Provider Serializer @@ -23163,7 +23172,6 @@ components: type: string format: uuid nullable: true - title: RSA Key description: Key used to sign the tokens. Only required when JWT Algorithm is set to RS256. redirect_uris: @@ -23178,9 +23186,19 @@ components: allOf: - $ref: '#/components/schemas/IssuerModeEnum' description: Configure how the issuer field of the ID Token should be filled. + verification_keys: + type: array + items: + type: string + format: uuid + title: Allowed certificates for JWT-based client_credentials + title: Allowed certificates for JWT-based client_credentials + description: JWTs created with the configured certificates can authenticate + with this provider. required: - authorization_flow - name + - verification_keys OAuth2ProviderSetupURLs: type: object description: OAuth2 Provider Metadata serializer @@ -27488,7 +27506,6 @@ components: type: string format: uuid nullable: true - title: RSA Key description: Key used to sign the tokens. Only required when JWT Algorithm is set to RS256. redirect_uris: @@ -27503,6 +27520,15 @@ components: allOf: - $ref: '#/components/schemas/IssuerModeEnum' description: Configure how the issuer field of the ID Token should be filled. + verification_keys: + type: array + items: + type: string + format: uuid + title: Allowed certificates for JWT-based client_credentials + title: Allowed certificates for JWT-based client_credentials + description: JWTs created with the configured certificates can authenticate + with this provider. PatchedOAuthSourceRequest: type: object description: OAuth Source Serializer diff --git a/web/src/locales/de.po b/web/src/locales/de.po index e9474da12..f0feeda4b 100644 --- a/web/src/locales/de.po +++ b/web/src/locales/de.po @@ -2327,6 +2327,7 @@ msgstr "Interne Konten ausblenden" #: src/pages/events/RuleForm.ts #: src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2582,6 +2583,10 @@ msgstr "Ausstellermodus" #~ msgid "JWT Algorithm" #~ msgstr "JWT Algorithmus" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "Schlüssel zum Signieren der Token." @@ -2745,6 +2750,7 @@ msgstr "Wird geladen" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -5906,6 +5912,10 @@ msgstr "Überprüfung" msgid "Verification Certificate" msgstr "Zertifikat zur Überprüfung" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Überprüfen Sie die E-Mail-Adresse des Benutzers, indem Sie ihm einen einmaligen Link senden. Kann auch für die Wiederherstellung verwendet werden, um die Authentizität des Benutzers zu überprüfen." diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 9b7930227..46fa28eea 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -2360,6 +2360,7 @@ msgstr "Hide service-accounts" #: src/pages/events/RuleForm.ts #: src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2624,6 +2625,10 @@ msgstr "Issuer mode" #~ msgid "JWT Algorithm" #~ msgstr "JWT Algorithm" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "JWTs signed by certificates configured here can be used to authenticate to the provider." + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "Key used to sign the tokens." @@ -2789,6 +2794,7 @@ msgstr "Loading" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -6022,6 +6028,10 @@ msgstr "Verification" msgid "Verification Certificate" msgstr "Verification Certificate" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "Verification certificates" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." diff --git a/web/src/locales/es.po b/web/src/locales/es.po index bcdc84d9d..68e169265 100644 --- a/web/src/locales/es.po +++ b/web/src/locales/es.po @@ -2318,6 +2318,7 @@ msgstr "Ocultar cuentas de servicio" #: src/pages/events/RuleForm.ts #: src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2575,6 +2576,10 @@ msgstr "Modo emisor" #~ msgid "JWT Algorithm" #~ msgstr "algoritmo JWT" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "Clave utilizada para firmar los tokens." @@ -2738,6 +2743,7 @@ msgstr "Cargando" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -5900,6 +5906,10 @@ msgstr "Verificación" msgid "Verification Certificate" msgstr "Certificado de verificación" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Verifique la dirección de correo electrónico del usuario enviándole un enlace único. También se puede utilizar para la recuperación para verificar la autenticidad del usuario." diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index ac9355e5b..3ee425b9d 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -2344,6 +2344,7 @@ msgstr "Cacher les comptes de service" #: src/pages/events/RuleForm.ts #: src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2605,6 +2606,10 @@ msgstr "Mode de l'émetteur" #~ msgid "JWT Algorithm" #~ msgstr "Algorithme JWT" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "" @@ -2769,6 +2774,7 @@ msgstr "Chargement en cours" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -5961,6 +5967,10 @@ msgstr "Vérification" msgid "Verification Certificate" msgstr "Certificat de validation" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Vérifier le courriel de l'utilisateur en lui envoyant un lien à usage unique. Peut également être utilisé lors de la récupération afin de vérifier l'authenticité de l'utilisateur." diff --git a/web/src/locales/pl.po b/web/src/locales/pl.po index 1a565311d..c0bf8681c 100644 --- a/web/src/locales/pl.po +++ b/web/src/locales/pl.po @@ -2315,6 +2315,7 @@ msgstr "Ukryj konta serwisowe" #: src/pages/events/RuleForm.ts #: src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2572,6 +2573,10 @@ msgstr "Tryb wystawcy" #~ msgid "JWT Algorithm" #~ msgstr "Algorytm JWT" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "Klucz używany do podpisywania tokenów." @@ -2735,6 +2740,7 @@ msgstr "Ładowanie" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -5897,6 +5903,10 @@ msgstr "Weryfikacja" msgid "Verification Certificate" msgstr "Certyfikat weryfikacji" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Zweryfikuj adres e-mail użytkownika, wysyłając mu jednorazowy link. Może być również używany do odzyskiwania w celu weryfikacji autentyczności użytkownika." diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 6c1a1332e..ab1ca26a9 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -2352,6 +2352,7 @@ msgstr "" #: src/pages/events/RuleForm.ts #: src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2614,6 +2615,10 @@ msgstr "" #~ msgid "JWT Algorithm" #~ msgstr "" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "" @@ -2779,6 +2784,7 @@ msgstr "" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -6002,6 +6008,10 @@ msgstr "" msgid "Verification Certificate" msgstr "" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "" diff --git a/web/src/locales/tr.po b/web/src/locales/tr.po index 121ee0d11..794d0cf39 100644 --- a/web/src/locales/tr.po +++ b/web/src/locales/tr.po @@ -2318,6 +2318,7 @@ msgstr "Hizmet hesaplarını gizle" #: src/pages/events/RuleForm.ts #: src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2576,6 +2577,10 @@ msgstr "Yayımcı kipi" #~ msgid "JWT Algorithm" #~ msgstr "JWT Algoritması" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "Anahtar belirteçleri imzalamak için kullanılır." @@ -2739,6 +2744,7 @@ msgstr "Yükleniyor" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -5902,6 +5908,10 @@ msgstr "Doğrulama" msgid "Verification Certificate" msgstr "Doğrulama Sertifikası" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Kullanıcının e-posta adresini bir kerelik bağlantı göndererek doğrulayın. Kullanıcının orijinalliğini doğrulamak için kurtarma için de kullanılabilir." diff --git a/web/src/locales/zh-Hans.po b/web/src/locales/zh-Hans.po index 6cde8bdf6..79eb7b857 100644 --- a/web/src/locales/zh-Hans.po +++ b/web/src/locales/zh-Hans.po @@ -2333,6 +2333,7 @@ msgstr "隐藏服务账户" #: src/pages/events/RuleForm.ts src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2612,6 +2613,10 @@ msgstr "Issuer 模式" #~ msgid "JWT Algorithm" #~ msgstr "JWT 算法" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "用于签名令牌的密钥。" @@ -2771,6 +2776,7 @@ msgstr "正在加载" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -6015,6 +6021,10 @@ msgstr "验证" msgid "Verification Certificate" msgstr "验证证书" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "" "Verify the user's email address by sending them a one-time-link. Can also be" diff --git a/web/src/locales/zh-Hant.po b/web/src/locales/zh-Hant.po index 9bfb7c479..233e4da96 100644 --- a/web/src/locales/zh-Hant.po +++ b/web/src/locales/zh-Hant.po @@ -2332,6 +2332,7 @@ msgstr "隐藏服务账户" #: src/pages/events/RuleForm.ts src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2611,6 +2612,10 @@ msgstr "Issuer mode" #~ msgid "JWT Algorithm" #~ msgstr "JWT 算法" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "用于对令牌进行签名的密钥。" @@ -2770,6 +2775,7 @@ msgstr "正在加载" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -6015,6 +6021,10 @@ msgstr "验证" msgid "Verification Certificate" msgstr "验证证书" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "" "Verify the user's email address by sending them a one-time-link. Can also be" diff --git a/web/src/locales/zh_TW.po b/web/src/locales/zh_TW.po index 808ee2e65..8eea86c77 100644 --- a/web/src/locales/zh_TW.po +++ b/web/src/locales/zh_TW.po @@ -2332,6 +2332,7 @@ msgstr "隐藏服务账户" #: src/pages/events/RuleForm.ts src/pages/outposts/OutpostForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts @@ -2611,6 +2612,10 @@ msgstr "Issuer mode" #~ msgid "JWT Algorithm" #~ msgstr "JWT 算法" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "JWTs signed by certificates configured here can be used to authenticate to the provider." +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Key used to sign the tokens." msgstr "用于对令牌进行签名的密钥。" @@ -2770,6 +2775,7 @@ msgstr "正在加载" #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts @@ -6015,6 +6021,10 @@ msgstr "验证" msgid "Verification Certificate" msgstr "验证证书" +#: src/pages/providers/oauth2/OAuth2ProviderForm.ts +msgid "Verification certificates" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "" "Verify the user's email address by sending them a one-time-link. Can also be" diff --git a/web/src/pages/crypto/CertificateKeyPairListPage.ts b/web/src/pages/crypto/CertificateKeyPairListPage.ts index b55d5df79..27f147903 100644 --- a/web/src/pages/crypto/CertificateKeyPairListPage.ts +++ b/web/src/pages/crypto/CertificateKeyPairListPage.ts @@ -117,7 +117,7 @@ export class CertificateKeyPairListPage extends TablePage { } renderExpanded(item: CertificateKeyPair): TemplateResult { - return html` + return html`
diff --git a/web/src/pages/providers/oauth2/OAuth2ProviderForm.ts b/web/src/pages/providers/oauth2/OAuth2ProviderForm.ts index df2cbc2fd..c2eb01cc3 100644 --- a/web/src/pages/providers/oauth2/OAuth2ProviderForm.ts +++ b/web/src/pages/providers/oauth2/OAuth2ProviderForm.ts @@ -292,6 +292,41 @@ ${this.instance?.redirectUris} + + +

+ ${t`JWTs signed by certificates configured here can be used to authenticate to the provider.`} +

+

+ ${t`Hold control/command to select multiple items.`} +

+