# python-jwtauth
Python library for JWT operations, including validating and decoding JWT Tokens.

Navigation:

- [Installation](#installation)
- [JWT Auth](#jwt-auth)
    - [Asynchronous JWT Auth](#asynchronous-jwt-auth)
    - [FastAPI or Starlette JWT Auth](#fastapi-or-starlette-jwt-auth)

## Installation

Using pip:

```shell
pip install -i https://pypi.theorchard.io/pypi/ jwtauth
```

Using Poetry:

Add orchard repository to `pyproject.toml`:

```toml
[[tool.poetry.source]]
name = "theorchard"
url = "https://pypi.theorchard.io/pypi/"
priority = "supplemental"
```

Install using poetry:

```shell
poetry add jwtauth
```

Using uv:

Add orchard repository to pyproject.toml

```toml
[tool.uv.sources]
jwtauth = { index = "theorchard" }

[[tool.uv.index]]
name = "theorchard"
url = "https://pypi.theorchard.io/pypi/"
```

Install using uv:

```shell
uv add jwtauth
```

## JWT Auth

`jwtauth` uses [PyJWT](https://pyjwt.readthedocs.io/en/stable/) to decode Json Web Tokens.

Usage:

```python
from jwtauth import JWTAuth

auth = JWTAuth(
    jwks_url="https://qalogin.theorchard.com/.well-known/jwks.json"
)

try:
    token = auth.get_token("encoded token")
except Exception as exc:
    print(exc)
```

or using default environment JWT auth factory:

```python
from jwtauth.constants import QA_ENVIRONMENT
from jwtauth.utils import jwt_auth_from_environment

auth = jwt_auth_from_environment(environment=QA_ENVIRONMENT)

try:
    token = auth.get_token("encoded token")
except Exception as exc:
    print(exc)
```

### Asynchronous JWT Auth

JWT auth also has asynchronous implementation.

Usage:

```python
from jwtauth.constants import PROD_ENVIRONMENT
from jwtauth.utils import jwt_auth_from_environment

auth = jwt_auth_from_environment(environment=PROD_ENVIRONMENT)

try:
    token = await auth.aget_token("encoded token")
except Exception as exc:
    print(exc)
```

### FastAPI or Starlette JWT Auth

Usage:

```python
from fastapi import FastAPI
from starlette.middleware import Middleware

from jwtauth.constants import PROD_ENVIRONMENT
from jwtauth.asgi.middleware import JWTAuthenticationMiddleware

app = FastAPI(middleware=[
    Middleware(
        JWTAuthenticationMiddleware,
        environment=PROD_ENVIRONMENT,
        exclude_paths=["/public/"],
        enabled=True,  # Force enable/disable
    )
])
```

## JWT Test Tooling

The test tooling provides fixtures to generate bearer tokens using credentials stored in Secrets Manager.


### Installation

You must install the testing tools as an `extra` (e.g., optional) dependency. The test tooling is not installed with
the `jwtauth` library. The `extra` ensures that testing dependencies (such as pytest and boto3) are not
accidentally included in your production Docker containers.

To install the `testing` extra using pip:

```shell
pip install -i https://pypi.theorchard.io/pypi/ "jwtauth[testing]"
```


Or using `pyproject.toml` to add as a dev dependency with Poetry:

```toml
[tool.poetry.group.dev.dependencies]
jwtauth = { extras = ["testing"], version = "^0.5.0"}
```

Or using `pyproject.toml` to add as a dev dependency with uv:

```shell
uv add "jwtauth[testing]" --dev
```

Include the `pytest_plugins` line below in the root `conftest.py` or module in your integration tests to discover the
jwtauth plugin.

```python
pytest_plugins = ["jwtauth.testing.pytest_plugin"]
```

### Fixtures

The pytest plugin has a few basic fixtures listed below. All fixtures
are [session scoped](https://docs.pytest.org/en/stable/how-to/fixtures.html#fixture-scopes), meaning that the fixture
runs once per pytest session and is cached thereafter.

#### generate_bearer_token

The fixture returns a function to generate a bearer token.

Function Arguments:
 - `get_user_creds_args`: [SecretLookupInfo](./jwtauth/testing/schemas.py) schema defining the path to the user credentials secret.
 - `get_auth0_creds_args`: [SecretLookupInfo](./jwtauth/testing/schemas.py) schema defining the path to the auth0 app credentials secret.
 - `secrets_manager`: SecretsManager object that matches the `SecretsManager` protocol


#### jwtauth_secrets_manager

This fixture returns a secrets manager instance that matches the `SecretsManager` protocol.


### Utility Functions

#### get_bearer_token_identity_uuid

Extracts the `identity_uuid` (Orchard Identity ID) from a JWT bearer token for a given environment.

```python
from jwtauth.testing import (
    get_bearer_token_identity_uuid,
)

def test_generate_bearer_token(pdp_test_bearer_token: str) -> None:
    """Verify that the generated JWT has an `orchardIdentityId`."""

    identity_uuid = get_bearer_token_identity_uuid(
        bearer_token=pdp_test_bearer_token,
        environment="qa",
    )
    assert identity_uuid == "4d5f24f5-83f9-4989-9f82-0924a5feaf88"
```

#### login_from_secrets_manager

Fetch user and auth0 application secrets to generate a bearer token.

```python
import pytest

from jwtauth.testing import (
    SecretLookupInfo,
    login_from_secrets_manager,
)

@pytest.fixture(scope="session")
def pdp_test_bearer_token(my_secrets_manager: MySecretsManager) -> str:
    """Example that uses the plugin fixtures."""
    return login_from_secrets_manager(
        get_user_creds_args=SecretLookupInfo(
            environment="qa",
            service_name="pdp-integration-test",
            secret_name="PDP_TEST_USER_CREDENTIALS",
        ),
        get_auth0_creds_args=SecretLookupInfo(
            environment="qa",
            service_name="pdp-integration-test",
            secret_name="PDP_TEST_APP_AUTH0_CREDENTIALS",
        ),
        secrets_manager=my_secrets_manager,
    )
```

#### generate_bearer_jwt_token

Generate a bearer token for the given UserCreds and Auth0Creds using the auth0 token API.

```python
from jwtauth.testing import JwtAuthSecretsManager
from jwtauth.testing.utils import (
    generate_bearer_jwt_token,
    get_auth0_creds,
    get_user_creds,
)

def do_generate_bearer_jwt_token() -> str:
    """Fetch the UserCreds and Auth0Creds secrets, then generate a bearer token."""
    secrets_manager = JwtAuthSecretsManager()

    auth0_creds = get_auth0_creds(
        secrets_manager=secrets_manager,
        service_name="test-app-name",
        secret_name="auth0-secrets-name",
        environment="qa",
    )

    user_creds = get_user_creds(
        secrets_manager=secrets_manager,
        service_name="test-app-name",
        secret_name="user-secrets-name",
        environment="qa",
    )

    return generate_bearer_jwt_token(user_creds, auth0_creds)
```


### Secret Contents
The auth0 app credentials secret must contain a JSON object matching
the [Auth0Creds](./jwtauth/testing/schemas.py) schema.

Example:
```json
{
    "auth0_client_id": "<client-id>",
    "auth0_client_secret": "<secret>"
}
```

The user credentials secret must contain a JSON object matching the
[UserCreds](./jwtauth/testing/schemas.py) schema. Set `otp_secret_key` to the value generated when setting
up the user (see ["How to integration test authenticated endpoints"](https://www.notion.so/How-to-integration-test-authenticated-endpoints-699b4ea8b0f242c1b53bc124ae76ea2b?source=copy_link#28197177520f80a4b444ef32306dc9d4)) or set it to `null`
when disabling MFA.

Example:
```json
{
    "email": "myapptester@sonymusic-pde.com",
    "password": "<password>",
    "otp_secret_key": "<base32secret3232>"
}
```


### Examples

The examples include:
 - [big_picture_demo](./examples/testing/big_picture_demo/test_big_picture_demo.py): Example showing how to test an OWS endpoint when enabling Permissions Platform authorization checks.
 - [custom_fixture_demo](./examples/testing/custom_fixture_demo/test_custom_fixture_demo.py): Example showing how to create custom fixtures using `jwtauth.testing` utility functions.


To run the examples:
```bash
awsume permissions-platform-qa
cd python-jwtauth/examples/testing
make examples
```

#### Verify both access rule and PP authorization checks

The integration tests must validate that each endpoint supports both [standalone access rule authorization checks](https://github.com/theorchard/python-owsrequest/blob/master/Readme.md#how-to-use-verify_rules_access_standalone)
that require profile-based headers and [Permissions Platform (PP) authorization checks](https://github.com/theorchard/python-pdp-sdk) that require a valid JWT.
This is a requirement until profiles are completely deprecated.


Standalone profile-based authorization checks need the following headers:

| Header Name                | Description                                                                                                                    |
|----------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| Orchard-Requestor-Service  | Is required to be present for any access rules to be checked -- it must be graphql-* or ows-grass                              |
| Orchard-Profile-Type       | The incoming request's profile type                                                                                            |
| Orchard-Profile-Id         | This header must be present, although no validation is performed to assert the profile id is associated with the profile type. |
| Orchard-Roles              | This header is used to get the incoming request's role.                                                                        |
| Orchard-Identity-Id        | This header contains the `orchardIdentityId`. PP auth checks read `orchardIdentityId` from the JWT.                            |
| Content-Type               | Must be "application/json"                                                                                                     |

Here's an example pytest fixture that returns this info:

```python
def authorized_admin_profile_headers() -> dict[str, str]:
    return {
        "Content-Type": "application/json",
        "Orchard-Identity-Id": "00dc658e-94c1-49d3-8efd-0cd14eecded1",
        "Orchard-Profile-Id": "1",
        "Orchard-Profile-Type": "AbacusProfile",
        "Orchard-Roles": "administrator",
        "Orchard-Requestor-Service": "graphql-abacus",
    }
```
