--- since: 1.1.1 --- # AuthX ↔ DjangoPlay Integration > **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 DjangoPlay need to communicate with the AuthX service for identity operations. 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.