diff --git a/api/config/routing.py b/api/config/routing.py
new file mode 100644
index 0000000000000000000000000000000000000000..249bf51a83cfba8cc09e5660872e18cbe2404461
--- /dev/null
+++ b/api/config/routing.py
@@ -0,0 +1,17 @@
+from django.conf.urls import url
+
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
+
+from funkwhale_api.common.auth import TokenAuthMiddleware
+from funkwhale_api.music import consumers
+
+
+application = ProtocolTypeRouter({
+    # Empty for now (http->django views is added by default)
+    "websocket": TokenAuthMiddleware(
+        URLRouter([
+            url("^api/v1/test/$", consumers.MyConsumer),
+        ])
+    ),
+})
diff --git a/api/funkwhale_api/common/auth.py b/api/funkwhale_api/common/auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f99b3bba26b36fbce61dd03fb608cb2cb61f6ba
--- /dev/null
+++ b/api/funkwhale_api/common/auth.py
@@ -0,0 +1,47 @@
+from urllib.parse import parse_qs
+
+import jwt
+
+from django.contrib.auth.models import AnonymousUser
+from django.utils.encoding import smart_text
+
+from rest_framework import exceptions
+from rest_framework_jwt.settings import api_settings
+from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
+
+
+
+class TokenHeaderAuth(BaseJSONWebTokenAuthentication):
+    def get_jwt_value(self, request):
+
+        try:
+            qs = request.get('query_string', b'').decode('utf-8')
+            parsed = parse_qs(qs)
+            token = parsed['token'][0]
+        except KeyError:
+            raise exceptions.AuthenticationFailed('No token')
+
+        if not token:
+            raise exceptions.AuthenticationFailed('Empty token')
+
+        return token
+
+
+class TokenAuthMiddleware:
+    """
+    Custom middleware (insecure) that takes user IDs from the query string.
+    """
+
+    def __init__(self, inner):
+        # Store the ASGI application we were passed
+        self.inner = inner
+
+    def __call__(self, scope):
+        auth = TokenHeaderAuth()
+        try:
+            user, token = auth.authenticate(scope)
+        except exceptions.AuthenticationFailed:
+            user = AnonymousUser()
+
+        scope['user'] = user
+        return self.inner(scope)
diff --git a/api/funkwhale_api/common/consumers.py b/api/funkwhale_api/common/consumers.py
new file mode 100644
index 0000000000000000000000000000000000000000..26e57fc8ae5441cbb0b1cc91f0864da08bced672
--- /dev/null
+++ b/api/funkwhale_api/common/consumers.py
@@ -0,0 +1,11 @@
+from channels.generic.websocket import JsonWebsocketConsumer
+
+
+class JsonAuthConsumer(JsonWebsocketConsumer):
+    def connect(self):
+        try:
+            assert self.scope['user'].pk is not None
+        except (AssertionError, AttributeError, KeyError):
+            return self.close()
+
+        return self.accept()
diff --git a/api/tests/channels/test_auth.py b/api/tests/channels/test_auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2b7eaf0ca685c1c197ed339a5963aeceea215c2
--- /dev/null
+++ b/api/tests/channels/test_auth.py
@@ -0,0 +1,37 @@
+import pytest
+
+from rest_framework_jwt.settings import api_settings
+
+from funkwhale_api.common.auth import TokenAuthMiddleware
+
+jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
+jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
+
+
+@pytest.mark.parametrize('query_string', [
+    b'token=wrong',
+    b'',
+])
+def test_header_anonymous(query_string, factories):
+    def callback(scope):
+        assert scope['user'].is_anonymous
+
+    scope = {
+        'query_string': query_string
+    }
+    consumer = TokenAuthMiddleware(callback)
+    consumer(scope)
+
+
+def test_header_correct_token(factories):
+    user = factories['users.User']()
+    payload = jwt_payload_handler(user)
+    token = jwt_encode_handler(payload)
+    def callback(scope):
+        assert scope['user'] == user
+
+    scope = {
+        'query_string': 'token={}'.format(token).encode('utf-8')
+    }
+    consumer = TokenAuthMiddleware(callback)
+    consumer(scope)
diff --git a/api/tests/channels/test_consumers.py b/api/tests/channels/test_consumers.py
new file mode 100644
index 0000000000000000000000000000000000000000..f1648efb3a614fb2f74b8e941083f99491043dd0
--- /dev/null
+++ b/api/tests/channels/test_consumers.py
@@ -0,0 +1,26 @@
+from funkwhale_api.common import consumers
+
+
+def test_auth_consumer_requires_valid_user(mocker):
+    m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.close')
+    scope = {'user': None}
+    consumer = consumers.JsonAuthConsumer(scope=scope)
+    consumer.connect()
+    m.assert_called_once_with()
+
+
+def test_auth_consumer_requires_user_in_scope(mocker):
+    m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.close')
+    scope = {}
+    consumer = consumers.JsonAuthConsumer(scope=scope)
+    consumer.connect()
+    m.assert_called_once_with()
+
+
+def test_auth_consumer_accepts_connection(mocker, factories):
+    user = factories['users.User']()
+    m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.accept')
+    scope = {'user': user}
+    consumer = consumers.JsonAuthConsumer(scope=scope)
+    consumer.connect()
+    m.assert_called_once_with()