authx-identity / Runbooks / AuthX ↔ DjangoPlay Integration
DocsAuthx-IdentityRunbooksAuthX ↔ DjangoPlay Integration

AuthX ↔ DjangoPlay Integration

DjangoPlay need to communicate with the AuthX service for identity operations.

10 min readApplies to v1.1.1
On this page ▾
  1. 1. AuthX Service Connectivity
  2. Problem
  3. Debugging
  4. Service Token Validation
  5. Result
  6. Problem
  7. Debug Command
  8. Result
  9. Interpretation
  10. Result
  11. Problem
  12. Root Cause
  13. Important Observation
  14. Actual Result
  15. Problem
  16. Debugging
  17. Interpretation
  18. Problem
  19. Initial Hypothesis
  20. Private Key
  21. Public Key
  22. Result
  23. Root Cause Confirmed
  24. Validation Result
  25. Final Flow
  26. Key Lessons
  27. 1. HTTP 422 can confirm connectivity
  28. 2. Do not trust the frontend error message alone
  29. 3. Validate cryptographic material directly
  30. 4. Only \n escapes are valid in the .env representation
  31. 5. Keep the PEM normalization validator

Purpose: Record the important integration steps for connecting DjangoPlay to the AuthX identity/authentication service, including diagnostics, fixes, and validation results.


1. AuthX Service Connectivity

Problem

The DjangoPlay client is configured to use:

python
AUTHX_BASE_URL
AUTHX_SERVICE_TOKEN
AUTHX_TIMEOUT

The AuthX client ultimately calls:

text
POST /internal/identities

Debugging

Inspect the installed DjangoPlay client:

bash
python - <<'PY'
import inspect
from authx_client import AuthXClient

print("FILE:", inspect.getfile(AuthXClient))

print("\n--- __init__ ---")
print(inspect.getsource(AuthXClient.__init__))

print("\n--- create_identity ---")
print(inspect.getsource(AuthXClient.create_identity))
PY

Confirmed:

python
self._base_url = getattr(settings, "AUTHX_BASE_URL", "http://authx:8100")
self._service_token = getattr(settings, "AUTHX_SERVICE_TOKEN", "")
self._timeout = getattr(settings, "AUTHX_TIMEOUT", 10.0)

and:

python
response = client.post(
    f"{self._base_url}/internal/identities",
    json={...},
    headers=self._headers,
)

Service Token Validation

Check the token inside the AuthX container:

bash
docker exec authx-identity-authx-1 sh -lc '
python - <<'PY'
import os

token = os.getenv("AUTHX_SERVICE_TOKEN", "")

print("configured:", bool(token))
print("length:", len(token))
print("repr prefix/suffix:", repr(token[:8]), repr(token[-8:]))
print("has leading whitespace:", token != token.lstrip())
print("has trailing whitespace:", token != token.rstrip())
PY
'

Result:

text
configured: True
length: 96
has leading whitespace: False
has trailing whitespace: False

Result

The service token is correctly configured.


2. AuthX Internal API Authentication / Routing

Problem

You need to verify that the internal AuthX API is reachable and that the service-token authentication path is functioning.

Debug Command

Test an internal identity endpoint with an intentionally invalid UUID:

bash
curl -i \
  -H "X-Service-Token: ***" \
  http://localhost:8100/internal/identities/nonexistent

Result

text
HTTP/1.1 422 Unprocessable Entity

with:

json
{
  "detail": [
    {
      "type": "uuid_parsing",
      "loc": ["path", "identity_id"],
      "msg": "Input should be a valid UUID"
    }
  ]
}

Interpretation

This is actually a positive result.

The request successfully reached the AuthX application and routing layer. The 422 is generated by FastAPI because "nonexistent" is not a valid UUID.

Result

AuthX connectivity and internal endpoint routing confirmed.


3. Password Hashing Failure

Problem

During identity creation, AuthX failed while hashing the password.

The important error is :

text
(trapped) error reading bcrypt version
AttributeError: module 'bcrypt' has no attribute '__about__'

followed by:

text
ValueError: password cannot be longer than 72 bytes,
truncate manually if necessary

The stack trace showed:

text
IdentityService.create()
    ↓
hash_password()
    ↓
pwd_context.hash()
    ↓
Passlib bcrypt backend
    ↓
bcrypt

Root Cause

The installed bcrypt version is incompatible with the way the current passlib bcrypt backend detects/uses the bcrypt package.

This is identified as a dependency compatibility issue, separate from the later JWT problem.

Important Observation

This problem occurs during password hashing / identity creation, not during JWT issuance.


4. Identity Creation Confirmed

Actual Result

The account is successfully created in AuthX.

This established that:

text
DjangoPlay
    ↓
AuthXClient
    ↓
AuthX /internal/identities
    ↓
Identity creation
    ↓
Password hashing
    ↓
Database

is functioning.


5. Login Reported "No Account Found"

Problem

After successful signup, attempting to log in from DjangoPlay displays:

text
No account found with this email or username.

Please use correct username/email or create a new account.

Initially this appears to indicate an identity lookup/authentication failure.

Debugging

AuthX logs shows a different failure.

The important part of the traceback is :

text
IdentityService.authenticate(...)
    ↓
_issue_tokens(identity)
    ↓
create_access_token(...)
    ↓
jwt.encode(...)
    ↓
JWSError

Specifically:

text
jose.exceptions.JWSError:
Unable to load PEM file
...
InvalidData(Invalid symbol 92, offset 64.)

Interpretation

The identity had already been authenticated.

The failure occurrs afterward while AuthX attempts to issue the access token.

Therefore the DjangoPlay message:

text
No account found with this email or username.

is misleading for this particular failure.

The actual problem is JWT signing.


6. JWT PEM Configuration Failure

Problem

AuthX could not load the configured RSA private/public keys.

The error is :

text
Unable to load PEM file
InvalidData(Invalid symbol 92, offset 64.)

The value 92 corresponds to:

text
\

ASCII backslash.

Initial Hypothesis

The first suspicion is that .env contained literal:

text
\n

instead of actual newlines.

AuthX already had a validator intended to handle this:

python
@field_validator("jwt_private_key", "jwt_public_key", mode="before")
@classmethod
def normalize_pem(cls, value: str) -> str:
    if not value:
        return value

    return value.replace("\\n", "\n").strip()

Therefore we do not need to add another normalization mechanism.


7. Correct Settings Module Identified

The actual configuration API is :

python
@lru_cache
def get_settings() -> Settings:
    return Settings()

Therefore the correct diagnostic import is :

python
from authx.core.settings import get_settings

8. Initial PEM Validation

Inspect the private key loaded by Pydantic:

bash
docker exec authx-identity-authx-1 sh -lc '
python - <<'"'"'PY'"'"'
from authx.core.settings import get_settings

s = get_settings()
key = s.jwt_private_key

print("type:", type(key))
print("length:", len(key))
print("literal_backslash_n:", "\\n" in key)
print("real_newline:", "\n" in key)
print("starts:", repr(key[:40]))
print("ends:", repr(key[-40:]))
PY
'

The loaded value had:

text
real newline: True

and no literal \n escape sequence.

This initially suggested that newline normalization is working correctly.


9. Direct Cryptography Validation

Test the actual PEM rather than relying only on string inspection.

Private Key

bash
docker exec authx-identity-authx-1 sh -lc '
python - <<'"'"'PY'"'"'
from authx.core.settings import get_settings
from cryptography.hazmat.primitives.serialization import load_pem_private_key

s = get_settings()
key = s.jwt_private_key

try:
    load_pem_private_key(key.encode(), password=None)
    print("PRIVATE KEY: VALID")
except Exception as e:
    print("PRIVATE KEY: INVALID")
    print(type(e).__name__, str(e))
PY
'

Result:

text
PRIVATE KEY: INVALID
ValueError Unable to load PEM file
InvalidData(Invalid symbol 92, offset 64.)

Public Key

bash
docker exec authx-identity-authx-1 sh -lc '
python - <<'"'"'PY'"'"'
from authx.core.settings import get_settings
from cryptography.hazmat.primitives.serialization import load_pem_public_key

s = get_settings()
key = s.jwt_public_key

try:
    load_pem_public_key(key.encode())
    print("PUBLIC KEY: VALID")
except Exception as e:
    print("PUBLIC KEY: INVALID")
    print(type(e).__name__, str(e))
PY
'

Result:

text
PUBLIC KEY: INVALID
ValueError Unable to load PEM file
InvalidData(Invalid symbol 92, offset 64.)

Result

The keys have correct PEM framing/newlines but contains invalid characters inside the Base64 body.


10. Locate the Corrupted Characters

Search the loaded PEM values for backslashes.

The private key contained many unexpected backslashes:

text
BACKSLASH at: 92
...
BACKSLASH at: 157
...
BACKSLASH at: 222
...

Example:

text
...EjIvI\7cegv/3...

The public key also contained invalid backslashes:

text
...HoL/9\xB4...

These characters cannot occur in a valid PEM Base64 body.


11. Confirm .env Was the Source

You inspected the actual AuthX .env file:

bash
docker exec authx-identity-authx-1 sh -lc '
python - <<'PY'
from pathlib import Path

p = Path("/app/.env")

print("exists:", p.exists())

if p.exists():
    text = p.read_text()

    for name in ("JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY"):
        for line in text.splitlines():
            if line.startswith(name + "="):
                value = line.split("=", 1)[1]
                print("\n", name)
                print("raw line length:", len(value))
                print("backslashes:", value.count("\\"))
                print("first 150:", repr(value[:150]))
                break
PY
'

Result:

text
JWT_PRIVATE_KEY
raw line length: 1707
backslashes: 27

JWT_PUBLIC_KEY
raw line length: 454
backslashes: 8

Distinguish legitimate newline escapes from invalid backslashes:

bash
python - <<'PY'
from pathlib import Path

text = Path(".env").read_text()

for name in ("JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY"):
    for line in text.splitlines():
        if line.startswith(name + "="):
            value = line.split("=", 1)[1]

            print("\n" + name)
            print("backslashes:", value.count("\\"))
            print("newline escapes:", value.count("\\n"))
            print(
                "other backslashes:",
                value.count("\\") - value.count("\\n")
            )
PY

Result:

text
JWT_PRIVATE_KEY
backslashes: 27
newline escapes: 2
other backslashes: 25

JWT_PUBLIC_KEY
backslashes: 8
newline escapes: 2
other backslashes: 6

Root Cause Confirmed

The RSA keys stored in .env are corrupted.

The problem is not the Pydantic validator.

The .env contained additional backslashes embedded inside the Base64 key material.


12. RSA Key Pair Regenerated

The correct solution is to replace the corrupted RSA key pair rather than attempting to remove the invalid characters.

Generate a new private key:

bash
openssl genrsa -out jwt_private.pem 2048

Generate the matching public key:

bash
openssl rsa \
  -in jwt_private.pem \
  -pubout \
  -out jwt_public.pem

Validate the private key:

bash
openssl rsa \
  -in jwt_private.pem \
  -check \
  -noout

Expected:

text
RSA key ok

13. Generate Safe .env Values

Rather than manually copying PEM content, convert the PEM files into the escaped format expected by .env:

bash
python - <<'PY'
from pathlib import Path

def env_pem(path):
    return Path(path).read_text().strip().replace("\n", "\\n")

print(f'JWT_PRIVATE_KEY="{env_pem("jwt_private.pem")}"')
print(f'JWT_PUBLIC_KEY="{env_pem("jwt_public.pem")}"')
PY

The generated values are placed into AuthX .env.


14. Validate .env Before Restart

Run:

bash
python - <<'PY'
from pathlib import Path

text = Path(".env").read_text()

for name in ("JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY"):
    for line in text.splitlines():
        if line.startswith(name + "="):
            value = line.split("=", 1)[1]

            print("\n" + name)
            print("backslashes:", value.count("\\"))
            print("newline escapes:", value.count("\\n"))
            print(
                "other backslashes:",
                value.count("\\") - value.count("\\n")
            )
PY

Expected result:

text
JWT_PRIVATE_KEY
backslashes: 2
newline escapes: 2
other backslashes: 0

JWT_PUBLIC_KEY
backslashes: 2
newline escapes: 2
other backslashes: 0

This confirms that the only backslashes are the two intended \n separators.


15. Rebuild and Recreate AuthX

After updating .env:

bash
docker compose build authx

Then:

bash
docker compose up -d --force-recreate authx

DjangoPlay is also restarted as required:

bash
docker compose up -d --force-recreate djangoplay

16. Validate PEMs Inside the Running Container

Before testing login, validate the actual values loaded by AuthX:

bash
docker exec authx-identity-authx-1 sh -lc '
python - <<'"'"'PY'"'"'
from authx.core.settings import get_settings
from cryptography.hazmat.primitives.serialization import (
    load_pem_private_key,
    load_pem_public_key,
)

s = get_settings()

for name, loader, value in [
    ("PRIVATE", load_pem_private_key, s.jwt_private_key),
    ("PUBLIC", load_pem_public_key, s.jwt_public_key),
]:
    print(name)
    print("  length:", len(value))
    print("  literal \\\\n:", "\\\\n" in value)
    print("  real newline:", "\n" in value)

    try:
        if name == "PRIVATE":
            loader(value.encode(), password=None)
        else:
            loader(value.encode())

        print("  PEM: VALID")
    except Exception as e:
        print("  PEM: INVALID")
        print(" ", type(e).__name__, str(e))
PY
'

Validation Result

Both RSA keys are successfully loaded as valid PEM keys.


17. Final Login Validation

The login flow is tested again after recreating the containers.

The previous failure:

text
jose.exceptions.JWSError:
Unable to load PEM file
InvalidData(Invalid symbol 92, offset 64.)

is resolved.

The AuthX flow now successfully reaches JWT creation using the valid RSA private key.

Final Flow

text
DjangoPlay
    │
    │ login credentials
    ▼
AuthX
    │
    ├── find identity
    ├── verify password
    ├── authenticate identity
    │
    ▼
Issue JWT
    │
    ├── RSA private key
    ├── RS256
    │
    ▼
Access / Refresh Tokens
    │
    ▼
DjangoPlay

18. Current Status

Area Status
DjangoPlay → AuthX connectivity ✅ Working
AuthX service token ✅ Valid
Internal API routing ✅ Validated
Identity creation ✅ Working
Password hashing ✅ Working
Account creation ✅ Working
Login identity authentication ✅ Reached successfully
JWT private key ✅ Fixed
JWT public key ✅ Fixed
PEM validation ✅ Passing
JWT signing ✅ Fixed
Previous Invalid symbol 92 error ✅ Resolved

Key Lessons

1. HTTP 422 can confirm connectivity

The test:

bash
curl -i \
  -H "X-Service-Token: ***" \
  http://localhost:8100/internal/identities/nonexistent

returns 422 because the UUID is invalid. This proved the request is reaching FastAPI successfully.

2. Do not trust the frontend error message alone

The DjangoPlay message:

text
No account found with this email or username.

is misleading.

The AuthX traceback shows that authentication had already succeeded and the failure happened during:

text
_issue_tokens()
    ↓
create_access_token()
    ↓
jwt.encode()

3. Validate cryptographic material directly

String checks such as:

text
real newline: True

are insufficient.

The decisive validation is :

python
load_pem_private_key(...)
load_pem_public_key(...)

4. Only \n escapes are valid in the .env representation

For the current configuration:

text
2 backslashes = 2 newline escapes
other backslashes = 0

is the expected state.

Unexpected backslashes inside the Base64 body indicate corrupted key material.

5. Keep the PEM normalization validator

The existing:

python
return value.replace("\\n", "\n").strip()

is correct for the .env representation being used.

The problem is the corrupted RSA key values, not the normalization code.