Backend: a GreetingToken model stores only a SHA-256 hash of a j621r_…
key (shown once at creation) plus label, prefix, created/last-used. A
dedicated GreetingTokenAuthentication understands the usual
'Authorization: Token …' header but is registered only on RandomItemView
(alongside the normal token auth), so a greeting token authenticates
/api/random/ and is rejected with 401 everywhere else — exactly the scope
shell greetings need. Endpoints: GET/POST /api/auth/greeting-tokens/ and
DELETE /api/auth/greeting-tokens/{id}/ (own tokens only; the list never
returns keys or hashes).
Frontend: /tokens page (Account → Shell tokens card, command palette entry)
lists tokens with label, prefix, created/last-used and revoke (shared
confirm dialog). Creating one shows the key with Copy and 'Copy for fish'
buttons plus a pointer to extras/fish_greeting.
Tests: apps/accounts/tests/test_greeting_tokens.py — 9 tests covering
create-once semantics and hashing, hidden keys in listings, the scope
guarantee (random 200 with a signed URL; 401 on files, storage, me, tags
cloud, delete and the token list itself), unknown/revoked keys, cross-user
revocation, last-used tracking and label limits.
Verified live: created a token, rolled /random (signed URL), got 401 from
four other endpoints, saw the list omit secrets, revoked it (204) and the
same key then 401'd on /random. Full suite: 39 tests green.
43 lines
1.5 KiB
Python
43 lines
1.5 KiB
Python
"""Authentication for scope-limited bearer tokens.
|
|
|
|
`GreetingTokenAuthentication` understands the same header a normal API token
|
|
uses (``Authorization: Token <key>``) but only resolves tokens issued for the
|
|
random-image endpoint. It is registered per-view (currently only
|
|
`RandomItemView`), so a greeting token is rejected everywhere else by the
|
|
regular DRF token authentication.
|
|
"""
|
|
|
|
from rest_framework import authentication, exceptions
|
|
|
|
from .models import GreetingToken
|
|
|
|
|
|
class GreetingTokenAuthentication(authentication.BaseAuthentication):
|
|
keyword = b"token"
|
|
|
|
def authenticate_header(self, request):
|
|
# DRF answers 401 (instead of 403) for AuthenticationFailed only when
|
|
# the first authenticator can name the scheme.
|
|
return "Token"
|
|
|
|
def authenticate(self, request):
|
|
header = authentication.get_authorization_header(request).split()
|
|
if not header or header[0].lower() != self.keyword:
|
|
return None
|
|
if len(header) != 2:
|
|
raise exceptions.AuthenticationFailed("Invalid token header.")
|
|
try:
|
|
key = header[1].decode()
|
|
except UnicodeError:
|
|
raise exceptions.AuthenticationFailed("Invalid token header.")
|
|
|
|
# Not one of ours: let the regular token authentication handle it.
|
|
if not key.startswith(GreetingToken.PREFIX):
|
|
return None
|
|
|
|
token = GreetingToken.resolve(key)
|
|
if token is None:
|
|
raise exceptions.AuthenticationFailed("Invalid token.")
|
|
token.touch()
|
|
return (token.user, token)
|