Comments (7)
The intended use for the generated API client would be using this
from authentik_client.models.application import Application
from authentik_client.models.o_auth2_provider import OAuth2Provider
Application()
instead of just passing a dictionary to the API calls
from authentik.
Thanks for your reply. I have been continuing to run into this problem after using authentik_client
provided models. I think I have narrowed down the problem.
When I create an OAuth Provider via the API, it throws validation errors for assigned_application_slug
, assigned_application_name
, assigned_backchannel_application_slug
, assigned_backchannel_application_name
.
I am using this package, as linked from the authentik documentation. FYI, the links to the documentation on the pypi page go to 404s (ex: https://pypi.org/project/authentik-client/docs/ProvidersApi.md#providers_oauth2_create). I cannot find a python version of authentik_client
open source on github, although maybe it is all generated by this schema.yml?)
To create a new provider, we call: provider_api_instance.providers_oauth2_create([Instance of OAuth2ProviderRequest])
.
providers_oauth2_create
expects an argument of type OAuth2ProviderRequest
(maybe defined here).
An OAuth2ProviderRequest
has these properties, which does not include assigned_application_slug
, assigned_application_name
, assigned_backchannel_application_slug
, assigned_backchannel_application_name
.
So to me, it seems impossible to send the properties an OAuth2Provider
model is expecting since even if you put the assigned_application_slug
etc in the data object to construct OAuth2ProviderRequest
, properties that aren't in the OAuth2ProviderRequest
schema are ignored.
Please let me know if I am misunderstanding something and how I can successfully create an OAuth provider with authentik-client
Here is the actual code I am using if it helpful:
expand...
with ApiClient(authentik_configuration) as api_client:
provider_api_instance = ProvidersApi(api_client)
flows_api_instance = FlowsApi(api_client)
property_mappings_api_instance = PropertymappingsApi(api_client)
try:
print("[authentik_setup] Creating oauth provider...")
authorization_flow = flows_api_instance.flows_instances_retrieve(
"default-provider-authorization-implicit-consent"
)
authetication_flow = flows_api_instance.flows_instances_retrieve(
"default-authentication-flow"
)
mappings = property_mappings_api_instance.propertymappings_scope_list()
scope_mappings = [
mapping.pk
for mapping in mappings.results
if mapping.managed
in [
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-profile",
"goauthentik.io/providers/oauth2/scope-email",
]
]
provider_data = {
"name": PROVIDER_NAME,
"authorization_flow": authorization_flow.pk,
"authentication_flow": authetication_flow.pk,
"client_id": settings.AUTHENTIK_CLIENT_ID,
"client_secret": settings.AUTHENTIK_CLIENT_SECRET,
"redirect_uris": "http://127.0.0.1:9090/auth/authentik/callback",
"property_mappings": scope_mappings,
}
provider_request = OAuth2ProviderRequest.model_construct(**provider_data)
api_provider_response = provider_api_instance.providers_oauth2_create(provider_request)
from authentik.
The assigned_*
properties are read_only and are mainly used by the frontend when the provider is connected with an application. In the backend this is defined correctly (and in theory so is it in the schema), however some client generators don't interpret this correctly
The python client is indeed generated from that schema (https://github.com/goauthentik/authentik/blob/main/.github/workflows/api-py-publish.yml) hence there currently isn't a source for it available.
Which line exactly is throwing the exception you posted above?
from authentik.
Thanks for your reply.
The lines are hard to share since I can't link to the generated python code, but here's some more details.
Backtrace
File "/Users/lc/projects/foo/api/bin/first_time_setup", line 307, in <module>
setup_authentik.do_it()
File "/Users/lc/projects/foo/api/bin/setup_authentik.py", line 30, in do_it
create_oauth_provider()
File "/Users/lc/projects/foo/api/bin/setup_authentik.py", line 110, in create_oauth_provider
api_provider_response = provider_api_instance.providers_oauth2_create(provider_request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/pydantic/validate_call_decorator.py", line 59, in wrapper_function
return validate_call_wrapper(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/pydantic/_internal/_validate_call.py", line 81, in __call__
res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/authentik_client/api/providers_api.py", line 16090, in providers_oauth2_create
foo = self.api_client.response_deserialize(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/authentik_client/api_client.py", line 316, in response_deserialize
return_data = self.deserialize(response_text, response_type)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/authentik_client/api_client.py", line 392, in deserialize
return self.__deserialize(data, response_type)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/authentik_client/api_client.py", line 437, in __deserialize
return self.__deserialize_model(data, klass)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/authentik_client/api_client.py", line 761, in __deserialize_model
return klass.from_dict(data)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/authentik_client/models/o_auth2_provider.py", line 163, in from_dict
_obj = cls.model_validate({
^^^^^^^^^^^^^^^^^^^^
File "/Users/lc/projects/foo/api/.venv/lib/python3.12/site-packages/pydantic/main.py", line 551, in model_validate
return cls.__pydantic_validator__.validate_python(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 4 validation errors for OAuth2Provider
assigned_application_slug
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.7/v/string_type
assigned_application_name
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.7/v/string_type
assigned_backchannel_application_slug
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.7/v/string_type
assigned_backchannel_application_name
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.7/v/string_type
So what is happening as far as I understand is:
I call `providers_oauth2_create`
# definition from generated authentik-client -> `providers_api.py:16027`
@validate_call
def providers_oauth2_create(
self,
o_auth2_provider_request: OAuth2ProviderRequest,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> OAuth2Provider:
"""providers_oauth2_create
OAuth2Provider Viewset
:param o_auth2_provider_request: (required)
:type o_auth2_provider_request: OAuth2ProviderRequest
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._providers_oauth2_create_serialize(
o_auth2_provider_request=o_auth2_provider_request,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'201': "OAuth2Provider",
'400': "ValidationError",
'403': "GenericError",
}
response_data = self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
We get some response_data back at the end of `providers_oauth2_create`.
# `response_data` content
{
"pk": 1,
"name": "Foo OAuth/OIDC provider",
"authentication_flow": "feb4c4d7-14fe-44e4-8036-c43c9af69a93",
"authorization_flow": "5dbc2d24-2417-43f3-ac98-16336fba7968",
"property_mappings": [
"89cb76b9-3874-4bbe-9d04-4fedd482a787",
"bbc36495-5293-436e-960e-f0745a4ee542",
"db7d0b95-2d85-4894-a0dc-4d398f9076b6"
],
"component": "ak-provider-oauth2-form",
"assigned_application_slug": null,
"assigned_application_name": null,
"verbose_name": "OAuth2/OpenID Provider",
"verbose_name_plural": "OAuth2/OpenID Providers",
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
"client_type": "confidential",
"client_id": "foobarbaz",
"client_secret": "blablabla",
"access_code_validity": "minutes=1",
"access_token_validity": "hours=1",
"refresh_token_validity": "days=30",
"include_claims_in_id_token": true,
"signing_key": null,
"redirect_uris": "http://127.0.0.1:9090/auth/authentik/callback",
"sub_mode": "hashed_user_id",
"issuer_mode": "per_provider",
"jwks_sources": []
}
As you can see, there are no assigned_backchannel_application_name
, assigned_backchannel_application_slug
, assigned_application_name
, or assigned_application_slug
in this response. We get this response back, and then providers_oauth2_create
calls api_client.response_deserialize
which then calls api_client.__deserialize
, which eventually calls api_client.__deserialize_model
.
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
definition of `api_client.response_deserialize`, `api_client.__deserialize`, and `api_client.__deserialize_model` -> api_client.py: 376 - 437, api_client.py:751
def deserialize(self, response_text, response_type):
"""Deserializes response into an object.
:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:return: deserialized object.
"""
# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
data = response_text
return self.__deserialize(data, response_type)
def __deserialize(self, data, klass):
"""Deserializes dict, list, str into an object.
:param data: dict, list or str.
:param klass: class literal, or string of class name.
:return: object.
"""
if data is None:
return None
if isinstance(klass, str):
if klass.startswith('List['):
m = re.match(r'List\[(.*)]', klass)
assert m is not None, "Malformed List type definition"
sub_kls = m.group(1)
return [self.__deserialize(sub_data, sub_kls)
for sub_data in data]
if klass.startswith('Dict['):
m = re.match(r'Dict\[([^,]*), (.*)]', klass)
assert m is not None, "Malformed Dict type definition"
sub_kls = m.group(2)
return {k: self.__deserialize(v, sub_kls)
for k, v in data.items()}
# convert str to class
if klass in self.NATIVE_TYPES_MAPPING:
klass = self.NATIVE_TYPES_MAPPING[klass]
else:
klass = getattr(authentik_client.models, klass)
if klass in self.PRIMITIVE_TYPES:
return self.__deserialize_primitive(data, klass)
elif klass == object:
return self.__deserialize_object(data)
elif klass == datetime.date:
return self.__deserialize_date(data)
elif klass == datetime.datetime:
return self.__deserialize_datetime(data)
elif issubclass(klass, Enum):
return self.__deserialize_enum(data, klass)
else:
return self.__deserialize_model(data, klass)
...
def __deserialize_model(self, data, klass):
"""Deserializes list or dict to model.
:param data: dict, list.
:param klass: class literal.
:return: model object.
"""
return klass.from_dict(data)
`__deserialize_model` calls `klass.from_dict` which in our case is `OAuth2Provider` class. So it calls `from_dict` method on `OAuth2Provider` model which calls `model_validate` requiring `assigned_application_slug`, etc. which do not exist, and therefore fails validation
models/o_auth2_provider.py:151
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of OAuth2Provider from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"pk": obj.get("pk"),
"name": obj.get("name"),
"authentication_flow": obj.get("authentication_flow"),
"authorization_flow": obj.get("authorization_flow"),
"property_mappings": obj.get("property_mappings"),
"component": obj.get("component"),
"assigned_application_slug": obj.get("assigned_application_slug"),
"assigned_application_name": obj.get("assigned_application_name"),
"assigned_backchannel_application_slug": obj.get("assigned_backchannel_application_slug"),
"assigned_backchannel_application_name": obj.get("assigned_backchannel_application_name"),
"verbose_name": obj.get("verbose_name"),
"verbose_name_plural": obj.get("verbose_name_plural"),
"meta_model_name": obj.get("meta_model_name"),
"client_type": obj.get("client_type"),
"client_id": obj.get("client_id"),
"client_secret": obj.get("client_secret"),
"access_code_validity": obj.get("access_code_validity"),
"access_token_validity": obj.get("access_token_validity"),
"refresh_token_validity": obj.get("refresh_token_validity"),
"include_claims_in_id_token": obj.get("include_claims_in_id_token"),
"signing_key": obj.get("signing_key"),
"redirect_uris": obj.get("redirect_uris"),
"sub_mode": obj.get("sub_mode"),
"issuer_mode": obj.get("issuer_mode"),
"jwks_sources": obj.get("jwks_sources")
})
return _obj
So it seems like
- The authentik API is not sending back required properties
assigned_application_slug
,assigned_application_name
,assigned_backchannel_application_name
, andassigned_backchannel_application_slug
from a create call and therefore failing the de-serialization step. - It is impossible for me to create a provider with those properties because the request object to create a provider does not accept those properties.
from authentik.
The generated API docs show assigned_application_name
, assigned_application_slug
, etc as "required" in the response, so I think the problem may actually be with the schema.yml
file (or whatever generates it) rather than the openapi generator.
from authentik.
(edit: these were for the provider creation endpoint rather than the application creation endpoint)
For reference, adding nullable
to these fields in schema.yml
and regenerating the python client bindings was enough to get past this (not sure whether that is correct, I didn't look at the returned json to determine whether these were actually null values or just not present):
diff --git a/schema.yml b/schema.yml
index baa970150..8b301609b 100644
--- a/schema.yml
+++ b/schema.yml
@@ -45767,18 +45767,22 @@ components:
assigned_application_slug:
type: string
description: Internal application name, used in URLs.
+ nullable: true
readOnly: true
assigned_application_name:
type: string
description: Application's display Name.
+ nullable: true
readOnly: true
assigned_backchannel_application_slug:
type: string
description: Internal application name, used in URLs.
+ nullable: true
readOnly: true
assigned_backchannel_application_name:
type: string
description: Application's display Name.
+ nullable: true
readOnly: true
verbose_name:
type: string
from authentik.
I am also seeing similar issues trying to create a provider via the API.
It looks like DRF doesn't honor required=False
for ReadOnlyField
s. Even though these fields have required=False
in their ModelSerializer class, they are still showing up under the required:
field list for the response object in schema.yml
. There was a similar problem with allow_null
which appears to have been resolved by encode/django-rest-framework#8536 . It doesn't seem like adding "required": False
for the field in extra_kwargs in the serializer Meta makes any difference on these. I think changes may be needed in DRF for required
so that drf_spectacular gets the metadata it needs.
A possible workaround would be to set allow_null=True
for these fields. That solves the python openapi binding issue, at least (not sure whether other libraries/bindings validate the responses the same way... it still seems like having the field not be required in the spec would be ideal).
See also: tfranzel/drf-spectacular#383
from authentik.
Related Issues (20)
- Console error when there are no outposts
- Add partial Support for 389-DS
- POST /api/v3/flows/executor/default-authenticator-webauthn-setup/ HOT 1
- AMR MFA field not being sent to OIDC client?
- Blueprint validation failures not returned to web UI
- How to use apache2 auth_openidc? HOT 1
- Redirect failing on initial login HOT 1
- 404 errors for flows on full development environment HOT 1
- authentik Embedded Outpost Proxy Doesn't work when there are many Providers
- UI is messed up when forcing light mode with system set to dark mode
- Nextcloud SAML: Found an Attribute element with duplicated Name HOT 1
- What is the TLS version of the OIDC provider of Authentik?
- Authentik occasionally times out HOT 1
- Add documentation surrounding configuring PhotoPrism in Integrations section of website HOT 1
- How about semantic versioning?
- No outposts found with given token, ensure the given token corresponds to an authenitk Outpost HOT 2
- Docker Compose Setup Does Not Work on MacOS or Other POSIX OSes
- Application icons disappear after a few weeks
- GET /api/v3/flows/executor/google-enrollment-flow/
- Discord docs expression policy doubt
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from authentik.