diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 9ba1ecd09..8cefc6899 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -609,6 +609,8 @@ OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "users.AccessToken" OAUTH2_PROVIDER_GRANT_MODEL = "users.Grant" OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "users.RefreshToken" +SCOPED_TOKENS_MAX_AGE = 60 * 60 * 24 * 3 + # LDAP AUTHENTICATION CONFIGURATION # ------------------------------------------------------------------------------ AUTH_LDAP_ENABLED = env.bool("LDAP_ENABLED", default=False) diff --git a/api/funkwhale_api/common/auth.py b/api/funkwhale_api/common/auth.py index 7717c836b..736364337 100644 --- a/api/funkwhale_api/common/auth.py +++ b/api/funkwhale_api/common/auth.py @@ -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) diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 80cf286af..93635c11b 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -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" diff --git a/api/funkwhale_api/users/authentication.py b/api/funkwhale_api/users/authentication.py new file mode 100644 index 000000000..cb9ec8a8e --- /dev/null +++ b/api/funkwhale_api/users/authentication.py @@ -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