mirror of
https://code.eliotberriot.com/funkwhale/funkwhale.git
synced 2025-10-03 21:19:16 +02:00
Use scoped tokens to load <audio> urls instead of JWT
This commit is contained in:
parent
13d28f7b0c
commit
ec8dfdb740
17 changed files with 265 additions and 39 deletions
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
74
api/funkwhale_api/users/authentication.py
Normal file
74
api/funkwhale_api/users/authentication.py
Normal 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
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue