ruvnet/RuView · error · HTTPException

Invalid authentication credentials

Error message

Invalid authentication credentials

What it means

HTTP 401 raised in verify_token when jwt.decode succeeds (correct signature and algorithm) but the payload has no 'sub' claim. The token is cryptographically valid but lacks the user-id subject this auth scheme requires, so it cannot be mapped to a user. Distinguished from signature/expiry errors, which hit the dedicated handlers below.

Source

Thrown at plans/phase2-architecture/api-architecture.md:1571

            'exp': expire,
            'iat': datetime.utcnow(),
            'scopes': scopes
        }
        
        token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
        return token
    
    async def verify_token(self, credentials: HTTPAuthorizationCredentials = Security(HTTPBearer())):
        """Verify JWT token"""
        token = credentials.credentials
        
        try:
            payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
            user_id = payload.get('sub')
            scopes = payload.get('scopes', [])
            
            if user_id is None:
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Invalid authentication credentials",
                    headers={"WWW-Authenticate": "Bearer"},
                )
            
            return {'user_id': user_id, 'scopes': scopes}
            
        except jwt.ExpiredSignatureError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Token has expired",
                headers={"WWW-Authenticate": "Bearer"},
            )
        except jwt.JWTError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token",
                headers={"WWW-Authenticate": "Bearer"},

View on GitHub (pinned to 4685618388)

Solutions

  1. Ensure the token-issuing code includes the subject: jwt.encode({'sub': user_id, 'scopes': [...], ...})
  2. Decode the token client-side (e.g. jwt.io or jwt.decode(token, options={'verify_signature': False})) and confirm 'sub' is present in the payload
  3. If the token comes from another service, obtain a token from this API's own /token endpoint instead
  4. Log the decoded payload (minus secrets) at debug level when this 401 fires to see exactly which claims arrived

Example fix

# before (issuer)
token = jwt.encode({'scopes': ['pose:read']}, secret, algorithm='HS256')

# after (issuer)
token = jwt.encode({'sub': str(user.id), 'scopes': ['pose:read']}, secret, algorithm='HS256')
Defensive patterns

Strategy: try-catch

Validate before calling

import jwt
unverified = jwt.decode(token, options={'verify_signature': False})
assert unverified.get('sub'), 'token lacks sub claim; it will be rejected with 401'

Try / catch

from fastapi import HTTPException
try:
    await client.get('/api/pose', headers={'Authorization': f'Bearer {token}'})
except HTTPException as e:
    if e.status_code == 401 and e.detail == 'Invalid authentication credentials':
        token = await obtain_new_token()  # token verified but had no 'sub'
        raise

Prevention

When it happens

Trigger: Presenting a Bearer token that decodes and verifies against secret_key but whose payload omits 'sub': tokens minted by another issuer/service in your org, hand-built JWTs, or an old token version that stopped including the subject claim.

Common situations: Reusing an ID token or service-to-service token from a different auth service on this API; issuer recently changed its token template and dropped 'sub'; tests that craft payloads manually with only 'scopes'.

Understand the failure class

Related errors


AI-assisted analysis of ruvnet/RuView@4685618388 (2026-08-16). Data as JSON: /api/errors/65b94e9fb7a887da. Report an issue: GitHub.