ory / sdk

The place where ORY's SDKs are being auto-generated
Apache License 2.0
140 stars 84 forks source link

pyhton authentication issues with session cookie #63

Closed ms42Q closed 2 years ago

ms42Q commented 3 years ago

Note: I already reached out to the ory community on slack pointing out that i would be willing to write a patch for this if someone can point me into the right direction.

kratos version and environment

Kratos: oryd/kratos:v0.5.4-alpha.1-sqlite and v0.5.5-alpha.1-pre.1-sqlite

SDK: 0.5.4-alpha1 python (via pip)

Description of my issues

Requests from flask to kratos's settings endpoint return 'unauthorized' even tho I am passing the session cookie of a user that has been redirected to flask using the recovery link.

I mostly followed the docs of the python sdk:

From https://github.com/ory/sdk/blob/master/clients/kratos/python/docs/PublicApi.md#get_self_service_settings_flow

# Configure API key authorization: sessionToken
configuration = ory_kratos_client.Configuration(
    host = "http://localhost",
    api_key = {
        'X-Session-Token': 'YOUR_API_KEY'
    }
)
# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
# configuration.api_key_prefix['X-Session-Token'] = 'Bearer'

# Enter a context with an instance of the API client
with ory_kratos_client.ApiClient(configuration) as api_client:
    # Create an instance of the API class
    api_instance = ory_kratos_client.PublicApi(api_client)
    id = 'id_example' # str | ID is the Settings Flow ID  The value for this parameter comes from `flow` URL Query parameter sent to your application (e.g. `/settings?flow=abcde`).

    try:
        # Get Settings Flow
        api_response = api_instance.get_self_service_settings_flow(id)

I implemented it like this:

    @kratos_is_ready
    def get_settings(self, flow_id, session_key):
        ## Add user session key to kratos api object
        self.kratos_config.api_key = {
            "X-Session-Token": session_key
        }
        self.kratos_config.api_key_prefix["X-Session-Token"] = "Bearer"
        kratos = ory_kratos_client.ApiClient(self.kratos_config)
        kratos_admin = ory_kratos_client.PublicApi(kratos)
        try:
            settings_flow_object = kratos_admin.get_self_service_settings_flow(flow_id)
        except ApiException as e:
            raise BackendConnectionError(e.reason)
        except MaxRetryError as error:
            raise BackendConnectionError(str(error.reason))
        password_method = settings_flow_object.methods["password"]

And I have written a test which expects this:

def test_get_service_variables(httpserver):
    kratos_url = httpserver.url_for("")
    httpserver.expect_request("/health/ready").respond_with_json({})
    httpserver.expect_request(
        "/self-service/settings/flows",
        query_string=f"id=fake-flow-id",
        headers={"X-Session-Token": "Bearer fake-session-key"},
    ).respond_with_json(
        {
            "id": "fake-flow-id",
            "type": "browser",
            "expires_at": "2021-03-17T16:09:37.457574217Z",
            "issued_at": "2021-03-17T15:09:37.457574217Z",
            "request_url": "http://ignored:4433/self-service/settings/browser",
            <---------------- some other lines that are not important here --------------->
        }
    )
    login_flow = LoginFlow(kratos_url)
    settings_form_data = login_flow.get_settings(
        "fake-flow-id", "fake-session-key"
    )
    assert settings_form_data.csrf_token == "insecure_csrf_token"
    assert settings_form_data.identity == "test@tester.org"

The test passes so everything seems good, right?

testing with the actual server

However the API returns an unauthorized when I try the code against the real server.

using the recovery function i get a link via email, which looks like this:

 http://127.0.0.1:4433/self-service/recovery/methods/link?token=M3FV0w95kDIurOGpzW9zK24iLEgDqpoT

just as expected, when i click on the link the kratos endpoint creates a session (cookie is created) and redirects to my /settings endpoint. my settings endpoint then extracts the session cookie and triggers the get_settings function from above by doing this:

    if "ory_kratos_session" in request.cookies:
        kratos_session = request.cookies.get("ory_kratos_session")
        form_data = kratos.get_settings(
            flow_id=flow, session_key=kratos_session
        )

I already debugged through the whole python sdk and I can't spot anything obvious. While reading through the code of kratoss sdk I found some things that confuse me.

First this line:

# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
# configuration.api_key_prefix['X-Session-Token'] = 'Bearer'

Do I need this or not?

Then there is another one in the implementation of Configuration https://github.com/ory/sdk/blob/master/clients/kratos/python/ory_kratos_client/configuration.py:

    :Example:
    API Key Authentication Example.
    Given the following security scheme in the OpenAPI specification:
      components:
        securitySchemes:
          cookieAuth:         # name for the security scheme
            type: apiKey
            in: cookie
            name: JSESSIONID  # cookie name
    You can programmatically set the cookie:
conf = ory_kratos_client.Configuration(
    api_key={'cookieAuth': 'abc123'}
    api_key_prefix={'cookieAuth': 'JSESSIONID'}
)
    The following cookie will be added to the HTTP request:
       Cookie: JSESSIONID abc123
    """

But looking at the code around line 331 we find:

    def auth_settings(self):
        """Gets Auth Settings dict for api client.
        :return: The Auth Settings information dict.
        """
        auth = {}
        if 'X-Session-Token' in self.api_key:
            auth['sessionToken'] = {
                'type': 'api_key',
                'in': 'header',
                'key': 'X-Session-Token',
                'value': self.get_api_key_with_prefix('X-Session-Token')
            }
        return auth

So i assume X-Session-Token is the only header that works. Also i think an exception should be thrown here if authentication is required instead of silenty ignoring other auuth headers.

anyways, cookie auth doesn't seem to be supported anymore. And maybe this is where I am doing something wrong?

additional information

the auth cookie which I am using (ory_kratos_session) looks like this:

image

stacktraces

Traceback (most recent call last):
  File "/usr/src/app/service.py", line 139, in get_settings_form_data
    settings_flow_object = kratos_admin.get_self_service_settings_flow(flow_id)
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/api/public_api.py", line 1465, in get_self_service_settings_flow
    return self.get_self_service_settings_flow_with_http_info(id, **kwargs)  # noqa: E501
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/api/public_api.py", line 1554, in get_self_service_settings_flow_with_http_info
    collection_formats=collection_formats)
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/api_client.py", line 369, in call_api
    _preload_content, _request_timeout, _host)
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/api_client.py", line 188, in __call_api
    raise e
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/api_client.py", line 185, in __call_api
    _request_timeout=_request_timeout)
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/api_client.py", line 393, in request
    headers=headers)
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/rest.py", line 234, in GET
    query_params=query_params)
  File "/usr/local/lib/python3.7/site-packages/ory_kratos_client/rest.py", line 224, in request
    raise ApiException(http_resp=r)
ory_kratos_client.exceptions.ApiException: (401)
Reason: Unauthorized
HTTP response headers: HTTPHeaderDict({'Cache-Control': 'private, no-cache, no-store, must-revalidate', 'Content-Type': 'application/json', 'Set-Cookie': 'csrf_token=jILDmHeQE7FchT370+J3kCFXr5UGsA98/oSB6TlD0cc=; Path=/; Domain=127.0.0.1; Max-Age=31536000; HttpOnly; SameSite=Lax', 'Vary': 'Origin, Cookie', 'Date': 'Thu, 18 Mar 2021 14:57:42 GMT', 'Content-Length': '166'})
HTTP response body: {"error":{"code":401,"status":"Unauthorized","reason":"A valid ORY Session Cookie or ORY Session Token is missing.","message":"The request could not be authorized"}}

image

aeneasr commented 3 years ago

Thank you for the detailed report. Given that this works in other frameworks, could this be a problem with the generator template from https://openapi-generator.tech ?

ms42Q commented 3 years ago

I used wireshark to record all of the communication between the the secure-app (from the quickstart guide) and the kratos server.

Here is what happens:

Screenshot from 2021-03-30 12-38-23

Screenshot from 2021-03-30 12-46-24

And the rest is as I expect. It retrieves the data, displays it to the user and so on.

So the secure app is using the admin endpoint that doesn't need authentication, which is different from what you suggest in the guide. So now I am wondering if the public API is working at all like i understood from reading the documentation. In your comment above you are stating that the sdk works in with other frameworks. Can you point me to a frameworks that use the public api to authenticate a request?

update: I rephrased this comment a bit for clarity

ms42Q commented 3 years ago

I also noticed that the sdk is inconsistent with the api

https://github.com/ory/sdk/blob/master/clients/kratos/python/docs/AdminApi.md#get_self_service_settings_flow

Here it says

The client must configure the authentication and authorization parameters in accordance with the API server security policy. Examples for each auth method are provided below, use the example that satisfies your auth use case.

but in the api docs we find

When accessing this endpoint through ORY Kratos' Public API you must ensure that either the ORY Kratos Session Cookie or the ORY Kratos Session Token are set. The public endpoint does not return 404 status codes but instead 403 or 500 to improve data privacy.

You can access this endpoint without credentials when using ORY Kratos' Admin API.

So as far as I understand I can't even use the API to bypass authentication like the secure app is doing (see above comment). As a workaround i probably need to use the api via a low level http library (i.e. requests) to do a /session/whoami request and if the session token is valid, a kratos-admin/self-service/settings request to get the form data, right?

aeneasr commented 3 years ago

Hey there, I'm not sure I follow. I think the slack community or github discussions would be a good place to get some help as it looks like you're struggling with the ui integration. Anyways, if there is actually a bug in the Python SDK or if the Python SDK is not properly generated and is lacking something (e.g. setting the cookie) we can definitely work on improving this! Generally, the settings endpoint changes the settings of a user, which is why you need to have authentication to do that. The session endpoint accepts X-Session-Token and the Cookie HTTP header as documented in the ory documentation. The SDK documentation is auto-generated and not always up to date or accurate - changes to improve that can be made against the ory/kratos repository!

Hope this helps

ms42Q commented 3 years ago

i see. I already posted something in slack some time ago and I only opened this issue here because no one on slack was able to point me into the right direction.

You are right that I am struggling with the UI integration. I was hoping to find the cause of my issue by looking at how the reference implementation communicates with kratos (thats my last comment with the wireshark screenshots). The point of my last comment was that the reference implementation is not using the self-service/settings/ public-api endpoint as it is documented in the docs. This caught me by surprise.

As far as I understand the settings endpoint is not used to change the user settings but to get the form data (including the csrf token) which I then display to the user, right? Because changing the data will be handled by kratos which receives the form data submitted by the user and not by doing an api call directly.

aeneasr commented 3 years ago

Right! The settings flow endpoint is available both via admin as well as public port. For admin, no authorization is required - but be careful! This could mean that people who guess the flow ID could theoretically extract user info. That’s not very likely, because UUIDs are extremely hard to guess - but it is a possibility.

If you use the public endpoint, you can pass throught the „Cookie“ header which should contain the „ory_kratos_session“ cookie to the session endpoint as a HTTP header.

Hope this helps!

ms42Q commented 3 years ago

I can confirm that the following works: instead of using the sdk i now use requests to make the calls for this specific endpoint. Instead of using the admin api, I use the public api and pass the session secret as a cookie, just like you suggested.

    @kratos_is_ready
    def get_settings_form_data(self, flow_id, session_key):
        ## We need to use requests here because ory_kratos_client can't handle authentication via cookie
        ## https://github.com/ory/sdk/issues/63
        request_headers = {"Accept": "application/json"}
        ## Add session key for authentication
        request_cookies = {"ory_kratos_session": session_key}
        request_url = f"{self.kratos_url}/self-service/settings/flows?id={flow_id}"
        response = requests.get(request_url, headers=request_headers, cookies=request_cookies)
        response_data = response.json()
        return response_data["methods"]["password"]
aeneasr commented 3 years ago

Hello, I believe this should have improved in the most recent releases of the SDK (v0.6.3 in particular). Could you take a look? :)

fdelbos commented 3 years ago

I m using the latest version with Go and i think there is still the same problem (works fine when using the endpoint in the browser)

    cookie := r.Header.Get("Cookie")
    if cookie == "" {
        LoginFlowRedirect(w, r)
        log.Info().Msg("invalid kratos session (no cookie)")
        return
    }

    _, _, err := Public().ToSession(r.Context()).XSessionToken(cookie).Execute()
    if err != nil {
        LoginFlowRedirect(w, r)
        log.Info().Err(err).Str("token", cookie).Msg("invalid kratos session (no session)")
        return
    }
        // never reach
aeneasr commented 3 years ago

https://github.com/ory/kratos/issues/1405

github-actions[bot] commented 2 years ago

Hello contributors!

I am marking this issue as stale as it has not received any engagement from the community or maintainers a year. That does not imply that the issue has no merit! If you feel strongly about this issue

Throughout its lifetime, Ory has received over 10.000 issues and PRs. To sustain that growth, we need to prioritize and focus on issues that are important to the community. A good indication of importance, and thus priority, is activity on a topic.

Unfortunately, burnout has become a topic of concern amongst open-source projects.

It can lead to severe personal and health issues as well as opening catastrophic attack vectors.

The motivation for this automation is to help prioritize issues in the backlog and not ignore, reject, or belittle anyone.

If this issue was marked as stale erroneous you can exempt it by adding the backlog label, assigning someone, or setting a milestone for it.

Thank you for your understanding and to anyone who participated in the conversation! And as written above, please do participate in the conversation if this topic is important to you!

Thank you 🙏✌️