Secure Litestar APIs with OKTA
Tags: tutorial, python, api, security
Published: 2024-02-10 20:08:09


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](https://developer.okta.com/blog/authors/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5n3pswjhkulldplf9f5t.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vchz84nydn97hs2qcqbt.png)
### Step 3 ###
Give the integration a name
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u47f1u2gbkf5g3w55e5i.png)Result Screen
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sjdcun93vr6utbvggl65.png)
### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ts27wqi0ndy1r9nytsau.png)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h66p8dilmrnj0f12y94u.png)
### Step 7 ###
Try the 'Metadata URI' in a new tab in you web browser to see something like:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3zzqizbwsayqfcwdjd50.png)
### Step 8 ###
Click 'Scopes' -> 'Add Scope'
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lr2kmr2njvxrx1sc3rgz.png)
### Step 9 ###
Add a scope such as what is shown below:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ofusyayh5tdaunblyt42.png)
### 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](https://developer.okta.com/docs/reference/api/oidc/#introspect) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gujinm6u6o9mx95a8vju.png)
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]