diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 1d73b29deac76506c8ac3359e6b9712e240cae68..914ec92144ea1169b55a730878571c2d1353998a 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -870,6 +870,7 @@ REST_FRAMEWORK = {
     ),
     "DEFAULT_AUTHENTICATION_CLASSES": (
         "funkwhale_api.common.authentication.OAuth2Authentication",
+        "funkwhale_api.common.authentication.ApplicationTokenAuthentication",
         "funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS",
         "funkwhale_api.common.authentication.BearerTokenHeaderAuth",
         "funkwhale_api.common.authentication.JSONWebTokenAuthentication",
diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py
index f826b0c122fccea619d91c7f26b097490e30f2c3..11447ce23ace35646c2ab8085307e6cd9f04aef3 100644
--- a/api/funkwhale_api/common/authentication.py
+++ b/api/funkwhale_api/common/authentication.py
@@ -12,6 +12,8 @@ from rest_framework import exceptions
 from rest_framework_jwt import authentication
 from rest_framework_jwt.settings import api_settings
 
+from funkwhale_api.users import models as users_models
+
 
 def should_verify_email(user):
     if user.is_superuser:
@@ -46,6 +48,36 @@ class OAuth2Authentication(BaseOAuth2Authentication):
             resend_confirmation_email(request, e.user)
 
 
+class ApplicationTokenAuthentication(object):
+    def authenticate(self, request):
+        try:
+            header = request.headers["Authorization"]
+        except KeyError:
+            return
+
+        if "Bearer" not in header:
+            return
+
+        token = header.split()[-1].strip()
+
+        try:
+            application = users_models.Application.objects.exclude(user=None).get(
+                token=token
+            )
+        except users_models.Application.DoesNotExist:
+            return
+        user = users_models.User.objects.all().for_auth().get(id=application.user_id)
+        if not user.is_active:
+            msg = _("User account is disabled.")
+            raise exceptions.AuthenticationFailed(msg)
+
+        if should_verify_email(user):
+            raise UnverifiedEmail(user)
+
+        request.scopes = application.scope.split()
+        return user, None
+
+
 class BaseJsonWebTokenAuth(object):
     def authenticate(self, request):
         try:
diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py
index ea079f47b95b391beb195ffdcbe6eb8ff9bf8945..7b15043282803b4cc54a5aab450fc40aa6a18d04 100644
--- a/api/funkwhale_api/users/factories.py
+++ b/api/funkwhale_api/users/factories.py
@@ -129,6 +129,7 @@ class SuperUserFactory(UserFactory):
 class ApplicationFactory(factory.django.DjangoModelFactory):
     name = factory.Faker("name")
     redirect_uris = factory.Faker("url")
+    token = factory.Faker("uuid4")
     client_type = models.Application.CLIENT_CONFIDENTIAL
     authorization_grant_type = models.Application.GRANT_AUTHORIZATION_CODE
     scope = "read"
diff --git a/api/funkwhale_api/users/migrations/0020_application_token.py b/api/funkwhale_api/users/migrations/0020_application_token.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8728eb564c4085844fefe1dd6edd00bac578c6f
--- /dev/null
+++ b/api/funkwhale_api/users/migrations/0020_application_token.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.8 on 2020-08-19 08:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0019_auto_20200718_0741'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='application',
+            name='token',
+            field=models.CharField(blank=True, max_length=50, null=True, unique=True),
+        ),
+    ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index dfb8a3f550ced2a5df4b4134db00a5321e6b9ce1..e4a26899b7ab81981d67d5a63023a687c735aca2 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -31,8 +31,8 @@ from funkwhale_api.federation import models as federation_models
 from funkwhale_api.federation import utils as federation_utils
 
 
-def get_token():
-    return binascii.b2a_hex(os.urandom(15)).decode("utf-8")
+def get_token(length=15):
+    return binascii.b2a_hex(os.urandom(length)).decode("utf-8")
 
 
 PERMISSIONS_CONFIGURATION = {
@@ -350,6 +350,7 @@ class Invitation(models.Model):
 
 class Application(oauth2_models.AbstractApplication):
     scope = models.TextField(blank=True)
+    token = models.CharField(max_length=50, blank=True, null=True, unique=True)
 
     @property
     def normalized_scopes(self):
diff --git a/api/funkwhale_api/users/oauth/serializers.py b/api/funkwhale_api/users/oauth/serializers.py
index 4788ba220a8077d2fd3c84fd555216c446cd8581..b95d576248bbb9e93cd796f562000b5c634fa038 100644
--- a/api/funkwhale_api/users/oauth/serializers.py
+++ b/api/funkwhale_api/users/oauth/serializers.py
@@ -10,6 +10,12 @@ class ApplicationSerializer(serializers.ModelSerializer):
         model = models.Application
         fields = ["client_id", "name", "scopes", "created", "updated"]
 
+    def to_representation(self, obj):
+        repr = super().to_representation(obj)
+        if obj.user_id:
+            repr["token"] = obj.token
+        return repr
+
 
 class CreateApplicationSerializer(serializers.ModelSerializer):
     name = serializers.CharField(required=True, max_length=255)
@@ -27,3 +33,9 @@ class CreateApplicationSerializer(serializers.ModelSerializer):
             "redirect_uris",
         ]
         read_only_fields = ["client_id", "client_secret", "created", "updated"]
+
+    def to_representation(self, obj):
+        repr = super().to_representation(obj)
+        if obj.user_id:
+            repr["token"] = obj.token
+        return repr
diff --git a/api/funkwhale_api/users/oauth/views.py b/api/funkwhale_api/users/oauth/views.py
index 8fd88d908224df2e8686176bdde9529c814f9153..3260dc031959c91f5f1d7cc9f495dc1c2cf08d67 100644
--- a/api/funkwhale_api/users/oauth/views.py
+++ b/api/funkwhale_api/users/oauth/views.py
@@ -4,7 +4,8 @@ import urllib.parse
 from django import http
 from django.utils import timezone
 from django.db.models import Q
-from rest_framework import mixins, permissions, views, viewsets
+from rest_framework import mixins, permissions, response, views, viewsets
+from rest_framework.decorators import action
 
 from oauth2_provider import exceptions as oauth2_exceptions
 from oauth2_provider import views as oauth_views
@@ -32,6 +33,7 @@ class ApplicationViewSet(
         "destroy": "write:security",
         "update": "write:security",
         "partial_update": "write:security",
+        "refresh_token": "write:security",
         "list": "read:security",
     }
     lookup_field = "client_id"
@@ -54,6 +56,7 @@ class ApplicationViewSet(
             client_type=models.Application.CLIENT_CONFIDENTIAL,
             authorization_grant_type=models.Application.GRANT_AUTHORIZATION_CODE,
             user=self.request.user if self.request.user.is_authenticated else None,
+            token=models.get_token(15) if self.request.user.is_authenticated else None,
         )
 
     def get_serializer(self, *args, **kwargs):
@@ -70,10 +73,31 @@ class ApplicationViewSet(
 
     def get_queryset(self):
         qs = super().get_queryset()
-        if self.action in ["list", "destroy", "update", "partial_update"]:
+        if self.action in [
+            "list",
+            "destroy",
+            "update",
+            "partial_update",
+            "refresh_token",
+        ]:
             qs = qs.filter(user=self.request.user)
         return qs
 
+    @action(
+        detail=True,
+        methods=["post"],
+        url_name="refresh_token",
+        url_path="refresh-token",
+    )
+    def refresh_token(self, request, *args, **kwargs):
+        app = self.get_object()
+        if not app.user_id or request.user != app.user:
+            return response.Response(status=404)
+        app.token = models.get_token(15)
+        app.save(update_fields=["token"])
+        serializer = serializers.CreateApplicationSerializer(app)
+        return response.Response(serializer.data, status=200)
+
 
 class GrantViewSet(
     mixins.RetrieveModelMixin,
diff --git a/api/tests/common/test_authentication.py b/api/tests/common/test_authentication.py
index e249d526047bf5b128fed5cda69f70956e82f9bd..5678abcf6a89517a735aef1e473a91d061db6758 100644
--- a/api/tests/common/test_authentication.py
+++ b/api/tests/common/test_authentication.py
@@ -60,3 +60,13 @@ def test_json_webtoken_auth_verify_email_validity(
             auth.authenticate(request)
 
     should_verify.assert_called_once_with(user)
+
+
+def test_app_token_authentication(factories, api_request):
+    user = factories["users.User"]()
+    app = factories["users.Application"](user=user, scope="read write")
+    request = api_request.get("/", HTTP_AUTHORIZATION="Bearer {}".format(app.token))
+
+    auth = authentication.ApplicationTokenAuthentication()
+    assert auth.authenticate(request)[0] == app.user
+    assert request.scopes == ["read", "write"]
diff --git a/api/tests/users/oauth/test_views.py b/api/tests/users/oauth/test_views.py
index bf78c83b4cfe6f58db32f6576f8cc6fdb968b2e4..96f594ec2038fc38696e01dbd0eb96ff6fa6a448 100644
--- a/api/tests/users/oauth/test_views.py
+++ b/api/tests/users/oauth/test_views.py
@@ -47,6 +47,8 @@ def test_apps_post_logged_in_user(logged_in_api_client, db):
     assert response.data == serializers.CreateApplicationSerializer(app).data
     assert app.scope == "read write:profile"
     assert app.user == logged_in_api_client.user
+    assert app.token is not None
+    assert response.data["token"] == app.token
 
 
 def test_apps_list_anonymous(api_client, db):
@@ -120,6 +122,31 @@ def test_apps_get_owner(preferences, logged_in_api_client, factories):
 
     assert response.status_code == 200
     assert response.data == serializers.CreateApplicationSerializer(app).data
+    assert response.data["token"] == app.token
+
+
+def test_apps_refresh_token(preferences, logged_in_api_client, factories):
+    app = factories["users.Application"](user=logged_in_api_client.user)
+    old_token = app.token
+    url = reverse(
+        "api:v1:oauth:apps-refresh_token", kwargs={"client_id": app.client_id}
+    )
+    response = logged_in_api_client.post(url)
+
+    app.refresh_from_db()
+    assert response.status_code == 200
+    assert response.data == serializers.CreateApplicationSerializer(app).data
+    assert app.token != old_token
+
+
+def test_apps_refresh_token_not_owner(preferences, logged_in_api_client, factories):
+    app = factories["users.Application"]()
+    url = reverse(
+        "api:v1:oauth:apps-refresh_token", kwargs={"client_id": app.client_id}
+    )
+    response = logged_in_api_client.post(url)
+
+    assert response.status_code == 404
 
 
 def test_authorize_view_post(logged_in_client, factories):