File indexing completed on 2026-09-28 09:37:31
0001 """Per-user bearer tokens for headless clients (docs/live-data-access.md).
0002
0003 A token is ``swfr_`` followed by 43 URL-safe characters. Only its SHA-256
0004 is stored; the plaintext is shown once, on the account tokens page.
0005 ``TokenAuthMiddleware`` turns a valid ``Authorization: Bearer swfr_...``
0006 header into the token's user before the login wall runs, so a token caller
0007 passes the wall and reaches swf-monitor as that user through X-Remote-User.
0008 Tokens authenticate the MCP relay, TeamComms and stage-out ingest. The token
0009 itself never crosses the tunnel.
0010 """
0011
0012 import hashlib
0013 import secrets
0014 from datetime import timedelta
0015
0016 from django.http import JsonResponse
0017 from django.utils import timezone
0018
0019 TOKEN_PREFIX = 'swfr_'
0020
0021
0022
0023 TOKEN_PATHS = ('/mcp/', '/api/stageout/', '/teamcomms/')
0024 LAST_USED_GRANULARITY = timedelta(minutes=1)
0025
0026
0027 def _hash(raw):
0028 return hashlib.sha256(raw.encode()).hexdigest()
0029
0030
0031 def issue_token(user, label='', *, teamcomms_ai=False, teamcomms_service_kind=''):
0032 """Create a token for ``user``; returns (plaintext, ApiToken)."""
0033 from .models import ApiToken
0034 if teamcomms_service_kind not in {'', 'program', 'connector'} or (teamcomms_ai and teamcomms_service_kind):
0035 raise ValueError('Select an AI client or a service identity, not both')
0036 raw = TOKEN_PREFIX + secrets.token_urlsafe(32)
0037 token = ApiToken.objects.create(
0038 user=user, label=(label or '')[:100],
0039 prefix=raw[len(TOKEN_PREFIX):len(TOKEN_PREFIX) + 8],
0040 key_hash=_hash(raw),
0041 teamcomms_ai=teamcomms_ai,
0042 teamcomms_service_kind=teamcomms_service_kind,
0043 )
0044 return raw, token
0045
0046
0047 def resolve_token(raw):
0048 """The active user behind a plaintext token, else None."""
0049 from .models import ApiToken
0050 if not raw.startswith(TOKEN_PREFIX):
0051 return None
0052 token = (ApiToken.objects.select_related('user')
0053 .filter(key_hash=_hash(raw), revoked__isnull=True).first())
0054 if token is None or not token.user.is_active:
0055 return None
0056 now = timezone.now()
0057 if token.last_used is None or now - token.last_used > LAST_USED_GRANULARITY:
0058 ApiToken.objects.filter(pk=token.pk).update(last_used=now)
0059 return token.user
0060
0061
0062 class TokenAuthMiddleware:
0063 """Authenticate ``Authorization: Bearer swfr_...`` as the token's user.
0064
0065 Placed after AuthenticationMiddleware and before the login wall, and
0066 active only under TOKEN_PATHS. A bearer of another form is left alone
0067 for the upstream to judge; an swf-remote token that is unknown or
0068 revoked is refused here with 401.
0069 """
0070
0071 def __init__(self, get_response):
0072 self.get_response = get_response
0073
0074 def __call__(self, request):
0075 auth = request.META.get('HTTP_AUTHORIZATION', '')
0076 if (auth.startswith('Bearer ' + TOKEN_PREFIX)
0077 and request.path_info.startswith(TOKEN_PATHS)):
0078 user = resolve_token(auth[7:].strip())
0079 if user is None:
0080 return JsonResponse({'error': 'invalid or revoked token'}, status=401)
0081 request.user = user
0082 request.token_auth = True
0083 return self.get_response(request)