AuthX Identity — Authentication Debugging Runbook
This runbook covers the diagnostic commands.
On this page ▾
It is intended for debugging:
- AuthX container configuration
- JWT private/public key loading
- PostgreSQL connectivity
- database schema/table availability
- identity lookup
- password authentication
/token401 Unauthorizedresponses- DjangoPlay → AuthX authentication integration
1. Verify AuthX Settings Are Loading
The settings object is exposed through get_settings().
Check JWT configuration
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
from authx.core.settings import get_settings
s = get_settings()
print("JWT_ALGORITHM:", s.jwt_algorithm)
print("JWT_ISSUER:", s.jwt_issuer)
print("JWT_AUDIENCE:", s.jwt_audience)
print("APP_ENV:", s.app_env)
PY
'Do not print private keys or other secrets.
2. Verify JWT Private Key Formatting
This checks whether the value contains:
- real newline characters
- literal
\n - the expected PEM headers/footers
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 PEM:", key.startswith("-----BEGIN PRIVATE KEY-----"))
print("ends PEM:", key.endswith("-----END PRIVATE KEY-----"))
print("repr:", repr(key[:100]))
PY
'Expected:
literal_backslash_n: False
real_newline: True
starts PEM: True
ends PEM: True3. Verify the Private Key Cryptographically
Formatting alone is not sufficient.
Use cryptography to verify that the PEM is actually loadable.
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
print("PEM length:", len(key))
print("Literal \\\\n:", "\\\\n" in key)
print("Real newline:", "\n" in 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
'Expected:
PRIVATE KEY: VALIDIf you see:
InvalidData(Invalid symbol 92, offset ...)there is still a literal backslash inside the PEM payload.
4. Verify the Public Key Cryptographically
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
print("PEM length:", len(key))
print("Literal \\\\n:", "\\\\n" in key)
print("Real newline:", "\n" in 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
'Expected:
PUBLIC KEY: VALIDBoth keys must pass.
5. Inspect the .env Representation
If the cryptographic validation fails, inspect the actual .env representation.
Do not print the complete key.
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
'This helps identify malformed values such as:
\7
\xB4
\G
\zinside the Base64 payload.
Those are not valid PEM/Base64 content.
6. Inspect Backslashes in the PEM Loaded by Pydantic
If the .env representation looks suspicious, inspect the actual value after Pydantic processing.
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
from authx.core.settings import get_settings
s = get_settings()
for name in ("jwt_private_key", "jwt_public_key"):
key = getattr(s, name)
print("\n---", name, "---")
print("length:", len(key))
print("backslashes:", key.count("\\"))
print("real newlines:", key.count("\n"))
for i, char in enumerate(key):
if char == "\\":
start = max(0, i - 20)
end = min(len(key), i + 25)
print("BACKSLASH at:", i)
print("context:", repr(key[start:end]))
PY
'A correctly formatted PEM should have:
- backslashes:
0 - real newlines: multiple
7. Verify Database Configuration
Print only non-sensitive database metadata.
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
from authx.core.settings import get_settings
from sqlalchemy.engine import make_url
s = get_settings()
url = make_url(s.database_url)
print("=== AUTHX CONFIG ===")
print("DATABASE_HOST:", url.host)
print("DATABASE_PORT:", url.port)
print("DATABASE_NAME:", url.database)
print("JWT_ALGORITHM:", s.jwt_algorithm)
PY
'Avoid printing:
DATABASE_URL
DATABASE_URL_SYNC
passwords
private keys
internal tokens8. List Database Tables
Use properly quoted PostgreSQL schema names.
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from sqlalchemy import text
from authx.db.session import AsyncSessionLocal
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(text("""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN (
'\''pg_catalog'\'',
'\''information_schema'\''
)
ORDER BY table_schema, table_name
"""))
for row in result:
print(f"{row[0]}.{row[1]}")
asyncio.run(main())
PY
'Expected AuthX tables included:
public.alembic_version
public.refresh_token
public.user_identityImportant SQL mistake
This is wrong:
WHERE table_schema NOT IN (pg_catalog, information_schema)PostgreSQL interprets those as identifiers/column names.
Correct:
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')9. Inspect user_identity Columns
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from sqlalchemy import text
from authx.db.session import AsyncSessionLocal
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(text("""
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = '\''public'\''
AND table_name = '\''user_identity'\''
ORDER BY ordinal_position
"""))
for row in result:
print(f"{row[0]} ({row[1]})")
asyncio.run(main())
PY
'Important authentication columns:
email
password_hash
is_active
is_verified
deleted_at
last_login10. Find Identities Without Exposing Password Hashes
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from sqlalchemy import text
from authx.db.session import AsyncSessionLocal
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(text("""
SELECT
id,
email,
username,
is_active,
is_verified,
deleted_at,
CASE
WHEN password_hash IS NULL THEN '\''NULL'\''
WHEN password_hash = '\'''\'' THEN '\''EMPTY'\''
ELSE '\''PRESENT'\''
END AS password_status
FROM public.user_identity
ORDER BY id
"""))
rows = result.fetchall()
if not rows:
print("NO USER_IDENTITY RECORDS FOUND")
else:
for row in rows:
print(dict(row._mapping))
asyncio.run(main())
PY
'Expected healthy account:
is_active: True
is_verified: True
deleted_at: None
password_status: PRESENTDo not print the complete password_hash.
11. Verify the Exact Identity Lookup Logic
AuthX currently uses:
async def _get_by_email(self, email: str) -> UserIdentity | None:
result = await self.db.execute(
select(UserIdentity).where(
UserIdentity.email == email.lower().strip(),
UserIdentity.deleted_at.is_(None),
)
)
return result.scalar_one_or_none()Therefore an email lookup can be tested directly:
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from sqlalchemy import select
from authx.db.session import AsyncSessionLocal
from authx.models.user_identity import UserIdentity
EMAIL = "YOUR_EMAIL"
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(
select(UserIdentity).where(
UserIdentity.email == EMAIL.lower().strip(),
UserIdentity.deleted_at.is_(None),
)
)
identity = result.scalar_one_or_none()
if not identity:
print("IDENTITY: NOT FOUND")
return
print("IDENTITY: FOUND")
print("id:", identity.id)
print("email:", identity.email)
print("is_active:", identity.is_active)
print("is_verified:", identity.is_verified)
print("deleted_at:", identity.deleted_at)
print("password_hash exists:", bool(identity.password_hash))
asyncio.run(main())
PY
'12. Verify Password Independently
If the identity exists, test the password hash directly.
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from sqlalchemy import text
from authx.db.session import AsyncSessionLocal
from authx.core.security import verify_password
EMAIL = "YOUR_EMAIL"
PASSWORD = "YOUR_PASSWORD"
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(
text("""
SELECT password_hash
FROM public.user_identity
WHERE lower(email) = lower(:email)
AND deleted_at IS NULL
"""),
{"email": EMAIL},
)
row = result.mappings().first()
if not row:
print("IDENTITY: NOT FOUND")
return
hashed = row["password_hash"]
if not hashed:
print("PASSWORD HASH: EMPTY")
return
try:
result = verify_password(PASSWORD, hashed)
print("PASSWORD VERIFY:", result)
except Exception as exc:
print(
"PASSWORD VERIFY ERROR:",
type(exc).__name__,
str(exc),
)
asyncio.run(main())
PY
'Expected:
PASSWORD VERIFY: TrueIf:
PASSWORD VERIFY: Falsethe password supplied to the test does not match the stored password.
If an exception occurs, investigate the Passlib/bcrypt configuration.
13. Verify Passlib and bcrypt Versions
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import bcrypt
import passlib
print("bcrypt:", getattr(bcrypt, "__version__", "unknown"))
print("passlib:", getattr(passlib, "__version__", "unknown"))
PY
'For the environment tested during this debugging session:
bcrypt: 4.0.1
passlib: 1.7.414. Test the Entire Authentication Service
This is the most useful final diagnostic.
It reproduces the actual AuthX authentication service rather than testing individual pieces.
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from authx.db.session import AsyncSessionLocal
from authx.services.identity_service import IdentityService
EMAIL = "YOUR_EMAIL"
PASSWORD = "YOUR_PASSWORD"
async def main():
async with AsyncSessionLocal() as db:
print("=== AUTHENTICATE ===")
try:
service = IdentityService(db)
response = await service.authenticate(
EMAIL,
PASSWORD,
)
print("AUTHENTICATE: SUCCESS")
print("response type:", type(response).__name__)
if hasattr(response, "access_token"):
print(
"access_token present:",
bool(response.access_token),
)
if hasattr(response, "refresh_token"):
print(
"refresh_token present:",
bool(response.refresh_token),
)
except Exception as exc:
print("AUTHENTICATE: ERROR")
print(type(exc).__name__, str(exc))
asyncio.run(main())
PY
'15. Understand the Authentication Flow
The relevant AuthX flow is:
POST /token
│
▼
IdentityService.authenticate()
│
▼
_get_by_email()
│
├── identity not found ───────────────► 401
│
└── identity found
│
├── inactive/deleted ─────────► 401
│
├── password_hash missing ────► 401
│
▼
verify_password()
│
├── False ────────────────────► 401
│
▼
_issue_tokens()
│
▼
create_access_token()
│
▼
jwt.encode()This distinction is important:
Invalid credentials
Usually means the request failed before successful token issuance.
Unable to load PEM file
Means authentication reached JWT signing and the key material was invalid.
These are different layers of the system.
16. When DjangoPlay Returns 401
If DjangoPlay reports:
HTTP/1.1 401 Unauthorizedand AuthX returns:
{
"detail": "Invalid credentials."
}debug in this order:
1. Does user_identity exist?
↓
2. Does email exactly match after lower().strip()?
↓
3. is_active == True?
↓
4. deleted_at == NULL?
↓
5. password_hash present?
↓
6. verify_password() == True?
↓
7. IdentityService.authenticate() succeeds?
↓
8. JWT signing succeeds?Do not start with JWT debugging if the response is already:
Invalid credentials.JWT signing happens after successful password authentication.
17. Container Restart After Configuration Changes
After modifying .env, rebuild/recreate the AuthX container so the new environment is actually loaded.
For a normal Docker Compose workflow:
docker compose build authx
docker compose up -d authxIf both DjangoPlay and AuthX need to be recreated:
docker compose up -d --build authx djangoplayThen verify:
docker compose psand:
docker logs authx-identity-authx-1 --tail 10018. Basic Health Check
After restarting AuthX:
curl -i http://localhost:8100/healthExpected:
HTTP/1.1 200 OKThis confirms that the service is running.
It does not prove that:
- database authentication works
- password authentication works
- JWT signing works
- DjangoPlay integration works
Those require the deeper checks above.
19. Recommended Debugging Order
For future AuthX authentication problems, use this sequence:
┌───────────────────────────────┐
│ 1. Container running? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 2. /health returns 200? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 3. Settings loaded correctly? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 4. PEM keys cryptographically │
│ valid? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 5. Correct database? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 6. user_identity exists? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 7. Account active/not deleted?│
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 8. Password hash present? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 9. Password verifies? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 10. IdentityService succeeds? │
└──────────────┬────────────────┘
▼
┌───────────────────────────────┐
│ 11. DjangoPlay integration? │
└───────────────────────────────┘Most probable issues
PEM problem
The JWT keys could contain invalid literal backslashes inside their Base64 payload.
The important validation is not merely:
real_newline: Truebut:
cryptography.load_pem_private_key() → VALID
cryptography.load_pem_public_key() → VALIDDatabase problem
The original diagnostic queried:
identitiesbut the actual SQLAlchemy model uses:
user_identityThe model definition is the authoritative source.
SQL diagnostic problem
This:
NOT IN (pg_catalog, information_schema)is invalid because those values need string literals.
Use:
NOT IN ('pg_catalog', 'information_schema')Authentication problem
A 401 Invalid credentials does not automatically indicate a JWT problem.
The authentication service performs password verification before issuing the JWT.
Quick Reference
For better debugging, these are the highest-value commands:
Health
curl -i http://localhost:8100/healthCheck JWT PEM
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()
try:
load_pem_private_key(s.jwt_private_key.encode(), None)
print("PRIVATE KEY: VALID")
except Exception as e:
print("PRIVATE KEY: INVALID:", e)
try:
load_pem_public_key(s.jwt_public_key.encode())
print("PUBLIC KEY: VALID")
except Exception as e:
print("PUBLIC KEY: INVALID:", e)
PY
'List tables
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from sqlalchemy import text
from authx.db.session import AsyncSessionLocal
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(text("""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN (
'\''pg_catalog'\'',
'\''information_schema'\''
)
ORDER BY table_schema, table_name
"""))
for row in result:
print(f"{row[0]}.{row[1]}")
asyncio.run(main())
PY
'Test password
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from sqlalchemy import text
from authx.db.session import AsyncSessionLocal
from authx.core.security import verify_password
EMAIL = "YOUR_EMAIL"
PASSWORD = "YOUR_PASSWORD"
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(
text("""
SELECT password_hash
FROM public.user_identity
WHERE lower(email) = lower(:email)
AND deleted_at IS NULL
"""),
{"email": EMAIL},
)
row = result.mappings().first()
if not row:
print("IDENTITY: NOT FOUND")
return
print(
"PASSWORD VERIFY:",
verify_password(PASSWORD, row["password_hash"]),
)
asyncio.run(main())
PY
'Test complete AuthX authentication
docker exec authx-identity-authx-1 sh -lc '
python - <<'\''PY'\''
import asyncio
from authx.db.session import AsyncSessionLocal
from authx.services.identity_service import IdentityService
EMAIL = "YOUR_EMAIL"
PASSWORD = "YOUR_PASSWORD"
async def main():
async with AsyncSessionLocal() as db:
try:
response = await IdentityService(db).authenticate(
EMAIL,
PASSWORD,
)
print("AUTHENTICATE: SUCCESS")
print("response:", type(response).__name__)
except Exception as exc:
print("AUTHENTICATE: ERROR")
print(type(exc).__name__, str(exc))
asyncio.run(main())
PY
'Use the last command when you want to determine quickly whether the problem is actually inside AuthX authentication or somewhere in the DjangoPlay → AuthX integration.