Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

forgot_password_generate_one_time_token not-functional in private key mode #383

Open
raymondberg opened this issue Jan 8, 2024 · 1 comment

Comments

@raymondberg
Copy link

Problem

When using "authorizationMode": "PrivateKey", the user_client.forgot_password_generate_one_time_token function fails to invoke /api/v1/users/{userId}/credentials/forgot_password due to a 400 Bad request error. This prevents any usage of that forgot password API by the SDK in this mode.

Affects

All versions of Python 3.

All versions of library that I found, but I didn't test extensively into the past.

Technical Details

The forgot_password_generate_one_time_token code in the user_client does not specify headers on its request, allowing the default headers and custom headers to mandate what headers are used

body = {}
headers = {}
form = {}
request, error = await self._request_executor.create_request(
http_method, api_url, body, headers, form, keep_empty_params=keep_empty_params
)

The problem is that the endpoint does not respond to Content-Type: application/x-www-form-urlencoded, and will return a 400 if used. Unfortunately, the get_access_token code sets that content type when it requests an access token.

oauth_req, err = await self._request_executor.create_request(
"POST", url, None, {
'Accept': "application/json",
'Content-Type': 'application/x-www-form-urlencoded'
}, oauth=True)
# TODO Make max 1 retry
# Shoot request
if err:
return (None, err)
_, res_details, res_json, err = \
await self._request_executor.fire_request(oauth_req)

But why are these related? There's a subsequent behavior ( I would call this a bug but it might be ingrained at this point) where every request to send_request in the client updates the _default_headers.

self._default_headers.update(request["headers"])

So the order of operations is: when trying to fire forgot_password_generate_one_time_token on a fresh client, first get_access_token is called which updates the _default_headers which are then used for the final call to /api/v1/users/{userId}/credentials/forgot_password which doesn't like the headers.

Steps to Reproduce

Setting up an environment

python -m venv venv
source venv/bin/activate
pip install okta==2.9.5

python -m asyncio

Inside a python shell

from okta.client import Client as OktaClient    
                                                                                                                                                                                                      
# Begin fill with real values
OKTA_ORG_URL = ""
OKTA_PRIVATE_KEY = ""
OKTA_CLIENT_ID = ""
# End fill

okta_client = OktaClient(
    {
        "orgUrl": OKTA_ORG_URL,
        "authorizationMode": "PrivateKey",
        "clientId": OKTA_CLIENT_ID,
        "privateKey": OKTA_PRIVATE_KEY,
        "scopes": [
            "okta.users.manage",
            "okta.users.read",
            "okta.groups.read",                                                                                                                                                                                                                       
            "okta.groups.manage",                                                                                                                                                                                                                     
        ],                                                                                                                                                                                                                                            
    }                                                                                                                                                                                                                                                 
)                                                                                                                                                                                                                                                     
                                                                                                                                                                                                                                                      
await okta_client.forgot_password_generate_one_time_token("some-non-existent-user@example.com")

The output should be a 404 but instead you get

(None,<okta.api_response.OktaAPIResponse at 0x104ac70a0>, {'message': 'Okta HTTP 400 E0000021 Bad request. Accept and/or Content-Type headers likely do not match supported values.\n'})

Proposed Solutions

I'm sure there are more, but I can think of three long term solutions in decreasing feasibility
order (1=easiest, 3=hardest). Although I'm sure 2 isn't easy.
I also include a workaround that has worked for my purposes.

Long Term

1 - Specify the Headers required by the endpoint

Since there is a requirement for a specific Content-Type, specify that in the headers of forgot_password_generate_one_time_token which are currently {}. This is a one-line fix
to specify Content-Type: JSON for this specific call.

2 - Non-backwards incompatible change to the API

The Okta API could become more permissive on /api/v1/users/{userId}/credentials/forgot_password.
Doing so, I believe, would not break compatibility, just add support for a new Content-Type.
Possibly not as simple as I state, and I haven't looked into all that the endpoint supports.

3 - Stop changing default

This would take significantly more testing/validation as it would affect all calls in the client,
but this does seem like the primary mistake. API calls shouldn't bleed into others, perhaps
outside of setting session information.

Workaround

Until a fix is in place, you can set the custom_headers to apply to all requests by default.

okta_client.set_custom_headers(headers={"Content-Type": "application/json"})
await okta_client.forgot_password_generate_one_time_token("some-non-existent-user@example.com")

Which yields a "successful" 404:

(None, <okta.api_response.OktaAPIResponse object at 0x1063cb760>, {'message': 'Okta HTTP 404 E0000007 Not found: Resource not found: some-non-existant-user@example.com (User)\n'})

@raymondberg
Copy link
Author

I'm happy to submit a PR for 1 if that's preferable, but I wanted to get some feedback first. 😸

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant