AuthX ↔ DjangoPlay Integration
DjangoPlay need to communicate with the AuthX service for identity operations.
On this page ▾
- 1. AuthX Service Connectivity
- Problem
- Debugging
- Service Token Validation
- Result
- Problem
- Debug Command
- Result
- Interpretation
- Result
- Problem
- Root Cause
- Important Observation
- Actual Result
- Problem
- Debugging
- Interpretation
- Problem
- Initial Hypothesis
- Private Key
- Public Key
- Result
- Root Cause Confirmed
- Validation Result
- Final Flow
- Key Lessons
- 1. HTTP 422 can confirm connectivity
- 2. Do not trust the frontend error message alone
- 3. Validate cryptographic material directly
- 4. Only \n escapes are valid in the .env representation
- 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:
AUTHX_BASE_URL
AUTHX_SERVICE_TOKEN
AUTHX_TIMEOUTThe AuthX client ultimately calls:
POST /internal/identitiesDebugging
Inspect the installed DjangoPlay client:
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))
PYConfirmed:
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:
response = client.post(
f"{self._base_url}/internal/identities",
json={...},
headers=self._headers,
)Service Token Validation
Check the token inside the AuthX container:
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:
configured: True
length: 96
has leading whitespace: False
has trailing whitespace: FalseResult
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:
curl -i \
-H "X-Service-Token: ***" \
http://localhost:8100/internal/identities/nonexistentResult
HTTP/1.1 422 Unprocessable Entitywith:
{
"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 :
(trapped) error reading bcrypt version
AttributeError: module 'bcrypt' has no attribute '__about__'followed by:
ValueError: password cannot be longer than 72 bytes,
truncate manually if necessaryThe stack trace showed:
IdentityService.create()
↓
hash_password()
↓
pwd_context.hash()
↓
Passlib bcrypt backend
↓
bcryptRoot 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:
DjangoPlay
↓
AuthXClient
↓
AuthX /internal/identities
↓
Identity creation
↓
Password hashing
↓
Databaseis functioning.
5. Login Reported "No Account Found"
Problem
After successful signup, attempting to log in from DjangoPlay displays:
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 :
IdentityService.authenticate(...)
↓
_issue_tokens(identity)
↓
create_access_token(...)
↓
jwt.encode(...)
↓
JWSErrorSpecifically:
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:
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 :
Unable to load PEM file
InvalidData(Invalid symbol 92, offset 64.)The value 92 corresponds to:
\ASCII backslash.
Initial Hypothesis
The first suspicion is that .env contained literal:
\ninstead of actual newlines.
AuthX already had a validator intended to handle this:
@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 :
@lru_cache
def get_settings() -> Settings:
return Settings()Therefore the correct diagnostic import is :
from authx.core.settings import get_settings8. Initial PEM Validation
Inspect the private key loaded by Pydantic:
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:
real newline: Trueand 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
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:
PRIVATE KEY: INVALID
ValueError Unable to load PEM file
InvalidData(Invalid symbol 92, offset 64.)Public Key
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:
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:
BACKSLASH at: 92
...
BACKSLASH at: 157
...
BACKSLASH at: 222
...Example:
...EjIvI\7cegv/3...The public key also contained invalid backslashes:
...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:
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:
JWT_PRIVATE_KEY
raw line length: 1707
backslashes: 27
JWT_PUBLIC_KEY
raw line length: 454
backslashes: 8Distinguish legitimate newline escapes from invalid backslashes:
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")
)
PYResult:
JWT_PRIVATE_KEY
backslashes: 27
newline escapes: 2
other backslashes: 25
JWT_PUBLIC_KEY
backslashes: 8
newline escapes: 2
other backslashes: 6Root 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:
openssl genrsa -out jwt_private.pem 2048Generate the matching public key:
openssl rsa \
-in jwt_private.pem \
-pubout \
-out jwt_public.pemValidate the private key:
openssl rsa \
-in jwt_private.pem \
-check \
-nooutExpected:
RSA key ok13. Generate Safe .env Values
Rather than manually copying PEM content, convert the PEM files into the escaped format expected by .env:
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")}"')
PYThe generated values are placed into AuthX .env.
14. Validate .env Before Restart
Run:
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")
)
PYExpected result:
JWT_PRIVATE_KEY
backslashes: 2
newline escapes: 2
other backslashes: 0
JWT_PUBLIC_KEY
backslashes: 2
newline escapes: 2
other backslashes: 0This confirms that the only backslashes are the two intended \n separators.
15. Rebuild and Recreate AuthX
After updating .env:
docker compose build authxThen:
docker compose up -d --force-recreate authxDjangoPlay is also restarted as required:
docker compose up -d --force-recreate djangoplay16. Validate PEMs Inside the Running Container
Before testing login, validate the actual values loaded by AuthX:
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:
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
DjangoPlay
│
│ login credentials
▼
AuthX
│
├── find identity
├── verify password
├── authenticate identity
│
▼
Issue JWT
│
├── RSA private key
├── RS256
│
▼
Access / Refresh Tokens
│
▼
DjangoPlay18. 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:
curl -i \
-H "X-Service-Token: ***" \
http://localhost:8100/internal/identities/nonexistentreturns 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:
No account found with this email or username.is misleading.
The AuthX traceback shows that authentication had already succeeded and the failure happened during:
_issue_tokens()
↓
create_access_token()
↓
jwt.encode()3. Validate cryptographic material directly
String checks such as:
real newline: Trueare insufficient.
The decisive validation is :
load_pem_private_key(...)
load_pem_public_key(...)4. Only \n escapes are valid in the .env representation
For the current configuration:
2 backslashes = 2 newline escapes
other backslashes = 0is the expected state.
Unexpected backslashes inside the Base64 body indicate corrupted key material.
5. Keep the PEM normalization validator
The existing:
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.