Use scoped tokens to load <audio> urls instead of JWT

This commit is contained in:
Agate 2020-05-11 10:06:35 +02:00
parent 13d28f7b0c
commit ec8dfdb740
17 changed files with 265 additions and 39 deletions

View file

@ -29,6 +29,7 @@ class TokenAuthMiddleware:
self.inner = inner
def __call__(self, scope):
# XXX: 1.0 remove this, replace with websocket/scopedtoken
auth = TokenHeaderAuth()
try:
user, token = auth.authenticate(scope)

View file

@ -30,6 +30,7 @@ from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.tags.models import Tag, TaggedItem
from funkwhale_api.tags.serializers import TagSerializer
from funkwhale_api.users.oauth import permissions as oauth_permissions
from funkwhale_api.users.authentication import ScopedTokenAuthentication
from . import filters, licenses, models, serializers, tasks, utils
@ -571,7 +572,7 @@ class ListenViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
serializer_class = serializers.TrackSerializer
authentication_classes = (
rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES
+ [SignatureAuthentication]
+ [SignatureAuthentication, ScopedTokenAuthentication]
)
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"

View file

@ -0,0 +1,74 @@
from django.conf import settings
from django.core import signing
from rest_framework import authentication
from rest_framework import exceptions
from django.core.exceptions import ValidationError
from .oauth import scopes as available_scopes
from . import models
def generate_scoped_token(user_id, user_secret, scopes):
if set(scopes) & set(available_scopes.SCOPES_BY_ID) != set(scopes):
raise ValueError("{} contains invalid scopes".format(scopes))
return signing.dumps(
{
"user_id": user_id,
"user_secret": str(user_secret),
"scopes": list(sorted(scopes)),
},
salt="scoped_tokens",
)
def authenticate_scoped_token(token):
try:
payload = signing.loads(
token, salt="scoped_tokens", max_age=settings.SCOPED_TOKENS_MAX_AGE,
)
except signing.BadSignature:
raise exceptions.AuthenticationFailed("Invalid token signature")
try:
user_id = int(payload["user_id"])
user_secret = str(payload["user_secret"])
scopes = list(payload["scopes"])
except (KeyError, ValueError, TypeError):
raise exceptions.AuthenticationFailed("Invalid scoped token payload")
try:
user = (
models.User.objects.all()
.for_auth()
.get(pk=user_id, secret_key=user_secret, is_active=True)
)
except (models.User.DoesNotExist, ValidationError):
raise exceptions.AuthenticationFailed("Invalid user")
return user, scopes
class ScopedTokenAuthentication(authentication.BaseAuthentication):
"""
Used when signed token returned by generate_scoped_token are provided via
token= in GET requests. Mostly for <audio src=""> urls, since it's not possible
to override headers sent by the browser when loading media.
"""
def authenticate(self, request):
data = request.GET
token = data.get("token")
if not token:
return None
try:
user, scopes = authenticate_scoped_token(token)
except exceptions.AuthenticationFailed:
raise exceptions.AuthenticationFailed("Invalid token")
setattr(request, "scopes", scopes)
setattr(request, "actor", user.actor)
return user, None

View file

@ -77,6 +77,10 @@ class ScopePermission(permissions.BasePermission):
if isinstance(token, models.AccessToken):
return self.has_permission_token(token, required_scope)
elif getattr(request, "scopes", None):
return should_allow(
required_scope=required_scope, request_scopes=set(request.scopes)
)
elif request.user.is_authenticated:
user_scopes = scopes.get_from_permissions(**request.user.get_permissions())
return should_allow(

View file

@ -22,6 +22,7 @@ from funkwhale_api.moderation import utils as moderation_utils
from . import adapters
from . import models
from . import authentication as users_authentication
@deconstructible
@ -220,6 +221,7 @@ class UserReadSerializer(serializers.ModelSerializer):
class MeSerializer(UserReadSerializer):
quota_status = serializers.SerializerMethodField()
summary = serializers.SerializerMethodField()
tokens = serializers.SerializerMethodField()
class Meta(UserReadSerializer.Meta):
fields = UserReadSerializer.Meta.fields + [
@ -227,6 +229,7 @@ class MeSerializer(UserReadSerializer):
"instance_support_message_display_date",
"funkwhale_support_message_display_date",
"summary",
"tokens",
]
def get_quota_status(self, o):
@ -237,6 +240,13 @@ class MeSerializer(UserReadSerializer):
return
return common_serializers.ContentSerializer(o.actor.summary_obj).data
def get_tokens(self, o):
return {
"listen": users_authentication.generate_scoped_token(
user_id=o.pk, user_secret=o.secret_key, scopes=["read:libraries"]
)
}
class PasswordResetSerializer(PRS):
def get_email_options(self):