diff --git a/openlobby/core/api/schema.py b/openlobby/core/api/schema.py
index 4bd463243cc9c44ea140f4a45419967e6475e65e..cc99dac02427a68f12e8ec93a5492be90e34cf09 100644
--- a/openlobby/core/api/schema.py
+++ b/openlobby/core/api/schema.py
@@ -9,6 +9,15 @@ from .sanitizers import extract_text
 from .. import search
 from ..models import User, Report
 
+AUTHOR_SORT_LAST_NAME_ID = 1
+AUTHOR_SORT_TOTAL_REPORTS_ID = 2
+
+class AuthorSortEnum(graphene.Enum):
+    LAST_NAME = AUTHOR_SORT_LAST_NAME_ID
+    TOTAL_REPORTS = AUTHOR_SORT_TOTAL_REPORTS_ID
+
+    class Meta:
+        description = 'Sort by field.'
 
 class AuthorsConnection(relay.Connection):
     total_count = graphene.Int()
@@ -38,6 +47,8 @@ class Query:
     authors = relay.ConnectionField(
         AuthorsConnection,
         description='List of Authors. Returns first 10 nodes if pagination is not specified.',
+        sort=AuthorSortEnum(),
+        reversed=graphene.Boolean(default_value=False, description="Reverse order of sort")
     )
     search_reports = relay.ConnectionField(
         SearchReportsConnection,
@@ -59,10 +70,12 @@ class Query:
         paginator = Paginator(**kwargs)
 
         total = User.objects.filter(is_author=True).count()
-        authors = User.objects.filter(is_author=True)\
-            .annotate(total_reports=Count('report', filter=Q(report__is_draft=False)))\
-            .order_by('last_name', 'first_name')[
-            paginator.slice_from:paginator.slice_to]
+
+        authors = User.objects\
+                      .sorted(**kwargs)\
+                      .filter(is_author=True)\
+                    .annotate(total_reports=Count('report', filter=Q(report__is_draft=False)))[
+                  paginator.slice_from:paginator.slice_to]
 
         page_info = paginator.get_page_info(total)
 
diff --git a/openlobby/core/models.py b/openlobby/core/models.py
index 470de4c2f75960aba1845fb0006b208c7e3d9210..7a4ccb87c711f604b31d181e5554d6dfc7467861 100644
--- a/openlobby/core/models.py
+++ b/openlobby/core/models.py
@@ -1,10 +1,24 @@
 from django.conf import settings
+from django.contrib.auth.base_user import BaseUserManager
 from django.db import models
 from django.contrib.auth.models import AbstractUser
 from django.contrib.postgres.fields import JSONField
 from django.utils import timezone
 import time
 
+class UserManager(BaseUserManager):
+
+    def sorted(self, **kwargs):
+        # inline import intentionally
+        from openlobby.core.api.schema import AUTHOR_SORT_LAST_NAME_ID, AUTHOR_SORT_TOTAL_REPORTS_ID
+        qs = self.get_queryset()
+        sort_field = kwargs.get('sort', AUTHOR_SORT_LAST_NAME_ID)
+        if sort_field == AUTHOR_SORT_LAST_NAME_ID:
+            return qs.order_by('{}last_name'.format('-' if kwargs.get('reversed', False) else ''), 'first_name')
+        elif sort_field == AUTHOR_SORT_TOTAL_REPORTS_ID:
+            return qs.order_by('{}total_reports'.format('' if kwargs.get('reversed', False) else '-'), 'last_name')
+        raise NotImplemented("Other sort types are not implemented")
+
 
 class User(AbstractUser):
     """Custom user model. For simplicity we store OpenID 'sub' identifier in
@@ -14,6 +28,7 @@ class User(AbstractUser):
     extra = JSONField(null=True, blank=True)
     is_author = models.BooleanField(default=False)
     has_colliding_name = models.BooleanField(default=False)
+    objects = UserManager()
 
     def save(self, *args, **kwargs):
         # deal with first name and last name collisions
diff --git a/tests/schema/snapshots/snap_test_authors.py b/tests/schema/snapshots/snap_test_authors.py
index af1983302e23288e0d90904421bb8aa855bb14f8..cf9fba0e09487210dbca4670b052d2d65f9c3fd3 100644
--- a/tests/schema/snapshots/snap_test_authors.py
+++ b/tests/schema/snapshots/snap_test_authors.py
@@ -266,3 +266,248 @@ snapshots['test_with_reports 1'] = {
         }
     }
 }
+
+snapshots['test_sort_by_last_name 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_last_name_reversed 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_total_reports 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_total_reports_reversed 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_default_reversed 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
diff --git a/tests/schema/test_authors.py b/tests/schema/test_authors.py
index 0e83176eb619f6d23935f667e1eb9bd0e4a50734..5c59a37386187dff0949ef7bb8dedee57d827897 100644
--- a/tests/schema/test_authors.py
+++ b/tests/schema/test_authors.py
@@ -199,3 +199,148 @@ def test_with_reports(client, snapshot):
     """
     response = call_api(client, query)
     snapshot.assert_match(response)
+
+def test_sort_by_last_name(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: LAST_NAME) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_last_name_reversed(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: LAST_NAME, reversed: true) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_total_reports(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: TOTAL_REPORTS, reversed: false) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_total_reports_reversed(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: TOTAL_REPORTS, reversed: true) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_default_reversed(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(reversed: true) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
\ No newline at end of file