DEV Community

Pete Letkeman
Pete Letkeman

Posted on • Edited on

Secure Litestar APIs with OKTA

I borrowed a lot from https://developer.okta.com/blog/2020/12/17/build-and-secure-an-api-in-python-with-fastapi thanks to Karl Hughes for this article.

Litestar framework for Python 3.x creating APIs.
For more information about Litestar visit https://litestar.dev/

OKTA Setup

Step 1

After creating a developer account with okta click 'Applications' then 'Create App Integration' as shown in the image
Image description

Step 2

Choose 'API Services' with a description of
'Interact with Okta APIs using the scoped OAuth 2.0 access tokens for machine-to-machine authentication.'
Image description

Step 3

Give the integration a name
Image descriptionResult Screen
Image description

Step 4

In the directory for the Python project create a file named .env with the following:

OKTA_CLIENT_ID=Value From The Previous Step
OKTA_CLIENT_SECRET=Value From The Previous Step
OKTA_ISSUER="{oktaDomain}/oauth2/default"
OKTA_TOKEN="{oktaDomain}/oauth2/default/v1/token"
OKTA_INTROSPECTION="{oktaDomain}/oauth2/default/v1/introspect"
OKTA_AUDIENCE="api://default"
OKTA_SCOPE="items"

Step 5

Now go to 'Security' -> 'API' shown below:
Image description
Note that values for Audience and Issuer URI are in .env file.

Step 6

Click on the edit icon you should see something like this:
Image description

Step 7

Try the 'Metadata URI' in a new tab in you web browser to see something like:
Image description

Step 8

Click 'Scopes' -> 'Add Scope'
Image description

Step 9

Add a scope such as what is shown below:
Image description

Step 10

Complete the .env file with the correct information including replacing {oktaDomain} with your okta domain and having the correct value for OKTA_SCOPE="items", if you changed yours that is and note that OKTA_SCOPE is important for remote validation.

Python Code

You can find the final code for this here https://github.com/pbaletkeman/litestarOKTA.

Large segments of this code came from https://docs.litestar.dev/latest/usage/security/jwt.html.
To learn more about Litestar security go here https://docs.litestar.dev/latest/usage/security/index.html

Download the files to your local system and copy them to your Python project directory.

Step 11

Install the required Python libraries using

pip install -r requirements.txt

Note, that this includes all of litestar[full] which may be more than you need.

Step 12

Libraries of note:

  • base64 is used to encode your client id and client secret
  • httpx is used to connect to the OKTA server and return the JWT token
  • okta_jwt.jwt is used to validate the JWT token locally, which is quicker, but less secure than JWT token validation on the OKTA server
  • starlette.config is used to import the .env file into your application, you may want to use python-dotenv instead

Step 13

In this sample project we only ever will have one user/account which means that we can do this

API_USER_DB: dict[str, OAuthSchema] = {}

but if you have more than one account you should rethink how your database of users is stored.

Step 14

This project includes a method for local JWT token validation, but it is not used, the remote JWT token validate is used instead.
Remote validation is more secure, takes longer and increases system resource usage as this makes a call to the Introspection endpoint.
The local validation method is def validate(token: str) -> bool: and the remote validation method is def validate_remote(token: str) -> bool:

Notes

Note 1

When running this project you should have the following endpoints:

  • /login
    • uses Authorization header to login
  • /form-login
    • uses data from HTML Form to login
  • /json-login
    • uses data from JSON body to login

as shown below:
Image description
Each endpoint has it's purpose and it's up to you to choose the appropriate one.

Note 2

In json_login_handler and form_login_handler there is some funky magic happening here:

auth_header = 'Basic ' + str(base64.b64encode((data.client_id + ':' + data.client_secret).encode('ascii')))[2:-1]

To get the token we must send a authorization header of 'basic' a base 64 encoded version of the username:password. However Python prepends the letter "b" and then surrounds the value with a single quote which is why we are using [2:-1]

Top comments (0)