Repository: goauthentik/authentik

Issue:
Allow tokens to be set to a specific value via the API
Necessary for blueprints to be able to implement features like `AK_ADMIN_PASS`, that is setting tokens via ENV variables.  This is necessary in order to do fully automated installs of outposts.




Discussion:
Hmm, I agree with this being required for certain setups via Blueprints, but the ability to edit the key directly is currently purposefully not allowed, as tokens circumvent any kind of MFA or extra validation, so they are seen as inherently more secure and as such I'd like to keep the current behaviour of making them (by default) 60 chars.

For this usecase, it might work creating a token via blueprint without token, and then getting the token via the `!Find` tag with the token identifier, but it still wouldn't allow for all the usecases 
>For this usecase, it might work creating a token via blueprint without token, and then getting the token via the !Find tag with the token identifier, but it still wouldn't allow for all the usecases

Definitely wouldn't work for me use case, which is using one docker-compose file to automatically deploy authentik along side some other apps that authenticate against it. 

Worth noting that this would only apply to the API, not the user interface, so it's unlikely that a user who didn't know what they were doing would set an improper token.

Resolving patch:
diff --git a/authentik/blueprints/management/commands/apply_blueprint.py b/authentik/blueprints/management/commands/apply_blueprint.py
--- a/authentik/blueprints/management/commands/apply_blueprint.py
+++ b/authentik/blueprints/management/commands/apply_blueprint.py
@@ -19,10 +19,8 @@ def handle(self, *args, **options):
         for blueprint_path in options.get("blueprints", []):
             content = BlueprintInstance(path=blueprint_path).retrieve()
             importer = Importer(content)
-            valid, logs = importer.validate()
+            valid, _ = importer.validate()
             if not valid:
-                for log in logs:
-                    getattr(LOGGER, log.pop("log_level"))(**log)
                 self.stderr.write("blueprint invalid")
                 sys_exit(1)
             importer.apply()
diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py
--- a/authentik/blueprints/v1/importer.py
+++ b/authentik/blueprints/v1/importer.py
@@ -40,6 +40,10 @@
 from authentik.outposts.models import OutpostServiceConnection
 from authentik.policies.models import Policy, PolicyBindingModel
 
+# Context set when the serializer is created in a blueprint context
+# Update website/developer-docs/blueprints/v1/models.md when used
+SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
+
 
 def is_model_allowed(model: type[Model]) -> bool:
     """Check if model is allowed"""
@@ -158,7 +162,12 @@ def _validate_single(self, entry: BlueprintEntry) -> Optional[BaseSerializer]:
             raise EntryInvalidError(f"Model {model} not allowed")
         if issubclass(model, BaseMetaModel):
             serializer_class: type[Serializer] = model.serializer()
-            serializer = serializer_class(data=entry.get_attrs(self.__import))
+            serializer = serializer_class(
+                data=entry.get_attrs(self.__import),
+                context={
+                    SERIALIZER_CONTEXT_BLUEPRINT: entry,
+                },
+            )
             try:
                 serializer.is_valid(raise_exception=True)
             except ValidationError as exc:
@@ -217,7 +226,12 @@ def _validate_single(self, entry: BlueprintEntry) -> Optional[BaseSerializer]:
         always_merger.merge(full_data, updated_identifiers)
         serializer_kwargs["data"] = full_data
 
-        serializer: Serializer = model().serializer(**serializer_kwargs)
+        serializer: Serializer = model().serializer(
+            context={
+                SERIALIZER_CONTEXT_BLUEPRINT: entry,
+            },
+            **serializer_kwargs,
+        )
         try:
             serializer.is_valid(raise_exception=True)
         except ValidationError as exc:
diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py
--- a/authentik/core/api/tokens.py
+++ b/authentik/core/api/tokens.py
@@ -16,6 +16,7 @@
 from authentik.api.authorization import OwnerSuperuserPermissions
 from authentik.api.decorators import permission_required
 from authentik.blueprints.api import ManagedSerializer
+from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
 from authentik.core.api.used_by import UsedByMixin
 from authentik.core.api.users import UserSerializer
 from authentik.core.api.utils import PassiveSerializer
@@ -29,6 +30,11 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
 
     user_obj = UserSerializer(required=False, source="user", read_only=True)
 
+    def __init__(self, *args, **kwargs) -> None:
+        super().__init__(*args, **kwargs)
+        if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
+            self.fields["key"] = CharField()
+
     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
         """Ensure only API or App password tokens are created."""
         request: Request = self.context.get("request")


Test patch: