diff --git a/aleksis/core/frontend/app/dateTimeFormats.js b/aleksis/core/frontend/app/dateTimeFormats.js
index a835b238ccb56b0cdfc465f3b7ea2341e45322f8..6a980fc8dab2fea0b74fec8ef042de413db35bee 100644
--- a/aleksis/core/frontend/app/dateTimeFormats.js
+++ b/aleksis/core/frontend/app/dateTimeFormats.js
@@ -19,6 +19,13 @@ const dateTimeFormats = {
       minute: "numeric",
       second: "numeric",
     },
+    longNumeric: {
+      year: "numeric",
+      month: "numeric",
+      day: "numeric",
+      hour: "numeric",
+      minute: "numeric",
+    },
   },
   de: {
     short: {
@@ -39,6 +46,13 @@ const dateTimeFormats = {
       minute: "numeric",
       second: "numeric",
     },
+    longNumeric: {
+      year: "numeric",
+      month: "numeric",
+      day: "numeric",
+      hour: "numeric",
+      minute: "numeric",
+    },
   },
 };
 
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplication.vue b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplication.vue
new file mode 100644
index 0000000000000000000000000000000000000000..73d581937e36ff8f42133c25b73a967bff2ef503
--- /dev/null
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplication.vue
@@ -0,0 +1,82 @@
+<template>
+  <v-expansion-panel>
+    <v-expansion-panel-header v-slot="{ open }">
+      <div class="d-flex justify-start align-center">
+        <v-avatar
+          x-large
+          v-if="accessToken.application.icon.absoluteUrl"
+          class="mr-4"
+        >
+          <img
+            :src="accessToken.application.icon.absoluteUrl"
+            :alt="accessToken.application.name"
+          />
+        </v-avatar>
+        <v-avatar x-large v-else class="mr-4" color="secondary">
+          <v-icon color="white">mdi-apps</v-icon>
+        </v-avatar>
+        <div class="subtitle-1 font-weight-medium">
+          {{ accessToken.application.name }}
+        </div>
+      </div>
+    </v-expansion-panel-header>
+    <v-expansion-panel-content>
+      <v-list dense class="pa-0">
+        <v-list-item>
+          <v-list-item-content class="body-2">
+            {{
+              $t("oauth.authorized_application.access_since", {
+                date: $d(new Date(accessToken.created), "longNumeric"),
+              })
+            }}
+            ·
+            {{
+              $t("oauth.authorized_application.valid_until", {
+                date: $d(new Date(accessToken.expires), "longNumeric"),
+              })
+            }}
+          </v-list-item-content>
+          <v-list-item-action>
+            <v-btn color="primary" @click="deleteItem(accessToken)">
+              {{ $t("oauth.authorized_application.revoke") }}
+            </v-btn>
+          </v-list-item-action>
+        </v-list-item>
+        <v-list-item v-if="accessToken.scopes && accessToken.scopes.length > 0">
+          <div class="pr-4">
+            <v-list-item-content class="body-2">
+              {{ $t("oauth.authorized_application.has_access_to") }}
+            </v-list-item-content>
+          </div>
+          <v-list dense class="pa-0 flex-grow-1">
+            <div v-for="(scope, idx) in accessToken.scopes" :key="scope.name">
+              <v-list-item>
+                <v-list-item-content class="body-2">
+                  {{ scope.description }}
+                </v-list-item-content>
+              </v-list-item>
+              <v-divider v-if="idx < accessToken.scopes.length - 1" />
+            </div>
+          </v-list>
+        </v-list-item>
+      </v-list>
+    </v-expansion-panel-content>
+  </v-expansion-panel>
+</template>
+
+<script>
+export default {
+  name: "AuthorizedApplication",
+  props: {
+    accessToken: {
+      type: Object,
+      required: true,
+    },
+  },
+  methods: {
+    deleteItem(item) {
+      this.$emit("delete-item", item);
+    },
+  },
+};
+</script>
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplications.vue b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplications.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bbf35c8e7f3c2f9e71479c03feb86799b3328d7f
--- /dev/null
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/AuthorizedApplications.vue
@@ -0,0 +1,70 @@
+<template>
+  <div>
+    <h1 class="mb-4">{{ $t("oauth.authorized_application.title") }}</h1>
+    <div v-if="$apollo.queries.accessTokens.loading">
+      <v-skeleton-loader type="card"></v-skeleton-loader>
+    </div>
+    <div v-else-if="accessTokens">
+      <v-card class="mb-4">
+        <v-card-title>
+          {{ $t("oauth.authorized_application.subtitle") }}
+        </v-card-title>
+        <v-card-text>
+          {{ $t("oauth.authorized_application.description") }}
+        </v-card-text>
+        <v-expansion-panels flat>
+          <authorized-application
+            v-for="(accessToken, index) in accessTokens"
+            :key="accessToken.id"
+            :access-token="accessToken"
+            @delete-item="openDeleteDialog"
+          />
+        </v-expansion-panels>
+      </v-card>
+    </div>
+    <delete-dialog
+      :item="deleteItem"
+      :gql-mutation="require('./revokeOauthToken.graphql')"
+      :gql-query="require('./accessTokens.graphql')"
+      v-model="deleteDialog"
+    >
+      <template #title>
+        {{ $t("oauth.authorized_application.revoke_question") }}
+      </template>
+      <template #body>
+        <span v-if="deleteItem">{{ deleteItem.application.name }}</span>
+      </template>
+      <template #deleteContent>
+        {{ $t("oauth.authorized_application.revoke") }}
+      </template>
+    </delete-dialog>
+  </div>
+</template>
+
+<script>
+import gqlAccessTokens from "./accessTokens.graphql";
+import AuthorizedApplication from "./AuthorizedApplication.vue";
+import DeleteDialog from "../generic/dialogs/DeleteDialog.vue";
+
+export default {
+  name: "AuthorizedApplications",
+  components: { DeleteDialog, AuthorizedApplication },
+  data() {
+    return {
+      deleteDialog: false,
+      deleteItem: null,
+    };
+  },
+  methods: {
+    openDeleteDialog(item) {
+      this.deleteItem = item;
+      this.deleteDialog = true;
+    },
+  },
+  apollo: {
+    accessTokens: {
+      query: gqlAccessTokens,
+    },
+  },
+};
+</script>
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/accessTokens.graphql b/aleksis/core/frontend/components/authorized_oauth_applications/accessTokens.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..68ab08b05d479b8149b3772406a81dd0673c3169
--- /dev/null
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/accessTokens.graphql
@@ -0,0 +1,19 @@
+{
+  accessTokens: oauthAccessTokens {
+    id
+    created
+    updated
+    expires
+    scopes {
+      name
+      description
+    }
+    application {
+      id
+      name
+      icon {
+        absoluteUrl
+      }
+    }
+  }
+}
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/revokeOauthToken.graphql b/aleksis/core/frontend/components/authorized_oauth_applications/revokeOauthToken.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..6b2f8adbde00d9e0e4f3419673324f8962266683
--- /dev/null
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/revokeOauthToken.graphql
@@ -0,0 +1,5 @@
+mutation ($id: ID!) {
+  revokeOauthToken(id: $id) {
+    ok
+  }
+}
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index 5235e23265346a5842cccf793e67987459a5b40f..3a046e694bda219530c46e30d32e3d9ec1b44658 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -167,8 +167,16 @@
       "title": "OAuth Application",
       "title_plural": "OAuth Applications"
     },
-    "authorized_token": {
-      "menu_title": "Authorized Applications"
+    "authorized_application": {
+      "menu_title": "Third-party Applications",
+      "title": "Third-party Applications",
+      "subtitle": "Third-party Applications With Access to Your Account",
+      "description": "The following third-party applications have access to your account. You can revoke access at any time for those you don't need or trust anymore.",
+      "valid_until": "Valid until {date}",
+      "access_since": "Access since {date}",
+      "has_access_to": "Has access to:",
+      "revoke": "Revoke Access",
+      "revoke_question": "Are you sure you want to revoke access for this application?"
     }
   },
   "people": "People",
diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js
index 215f4d1485644883eef42f14216735547abb041a..e7fc1e4fe28da824dd4d7f308154c120551b43ed 100644
--- a/aleksis/core/frontend/routes.js
+++ b/aleksis/core/frontend/routes.js
@@ -920,14 +920,14 @@ const routes = [
   },
   {
     path: "/oauth/authorized_tokens/",
-    component: () => import("./components/LegacyBaseTemplate.vue"),
-    props: {
-      byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-    },
+    component: () =>
+      import(
+        "./components/authorized_oauth_applications/AuthorizedApplications.vue"
+      ),
     name: "core.oauth.authorizedTokens",
     meta: {
       inAccountMenu: true,
-      titleKey: "oauth.authorized_token.menu_title",
+      titleKey: "oauth.authorized_application.menu_title",
       icon: "mdi-gesture-tap-hold",
       permission: "core.manage_authorized_tokens_rule",
     },
diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py
index deab0e28b5d93e854a020aad687a618817196536..24a68978aa2a40dce7aa6fbf3546e1e5929d62cb 100644
--- a/aleksis/core/schema/__init__.py
+++ b/aleksis/core/schema/__init__.py
@@ -9,7 +9,15 @@ from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
 from haystack.utils.loading import UnifiedIndex
 
-from ..models import CustomMenu, DynamicRoute, Notification, PDFFile, Person, TaskUserAssignment
+from ..models import (
+    CustomMenu,
+    DynamicRoute,
+    Notification,
+    OAuthAccessToken,
+    PDFFile,
+    Person,
+    TaskUserAssignment,
+)
 from ..util.apps import AppConfig
 from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person
 from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType
@@ -19,6 +27,7 @@ from .group import GroupType  # noqa
 from .installed_apps import AppType
 from .message import MessageType
 from .notification import MarkNotificationReadMutation, NotificationType
+from .oauth import OAuthAccessTokenType, OAuthRevokeTokenMutation
 from .pdf import PDFFileType
 from .person import PersonMutation, PersonType
 from .school_term import SchoolTermType  # noqa
@@ -59,6 +68,8 @@ class Query(graphene.ObjectType):
 
     two_factor = graphene.Field(TwoFactorType)
 
+    oauth_access_tokens = graphene.List(OAuthAccessTokenType)
+
     def resolve_ping(root, info, payload) -> str:
         return payload
 
@@ -157,6 +168,10 @@ class Query(graphene.ObjectType):
             return None
         return info.context.user
 
+    @staticmethod
+    def resolve_oauth_access_tokens(root, info, **kwargs):
+        return OAuthAccessToken.objects.filter(user=info.context.user)
+
 
 class Mutation(graphene.ObjectType):
     update_person = PersonMutation.Field()
@@ -165,6 +180,8 @@ class Mutation(graphene.ObjectType):
 
     celery_progress_fetched = CeleryProgressFetchedMutation.Field()
 
+    revoke_oauth_token = OAuthRevokeTokenMutation.Field()
+
 
 def build_global_schema():
     """Build global GraphQL schema from all apps."""
diff --git a/aleksis/core/schema/oauth.py b/aleksis/core/schema/oauth.py
new file mode 100644
index 0000000000000000000000000000000000000000..c51527d0bb316f2768a844e71697a0eeb8c29100
--- /dev/null
+++ b/aleksis/core/schema/oauth.py
@@ -0,0 +1,44 @@
+import graphene
+from graphene_django import DjangoObjectType
+
+from aleksis.core.models import OAuthAccessToken, OAuthApplication
+
+from .base import FieldFileType
+
+
+class OAuthScope(graphene.ObjectType):
+    name = graphene.String()
+    description = graphene.String()
+
+
+class OAuthApplicationType(DjangoObjectType):
+    icon = graphene.Field(FieldFileType)
+
+    class Meta:
+        model = OAuthApplication
+        fields = ["id", "name", "icon"]
+
+
+class OAuthAccessTokenType(DjangoObjectType):
+    scopes = graphene.List(OAuthScope)
+
+    @staticmethod
+    def resolve_scopes(root: OAuthAccessToken, info, **kwargs):
+        return [OAuthScope(name=key, description=value) for key, value in root.scopes.items()]
+
+    class Meta:
+        model = OAuthAccessToken
+        fields = ["id", "application", "expires", "created", "updated"]
+
+
+class OAuthRevokeTokenMutation(graphene.Mutation):
+    class Arguments:
+        id = graphene.ID()  # noqa
+
+    ok = graphene.Boolean()
+
+    @staticmethod
+    def mutate(root, info, id):  # noqa
+        token = OAuthAccessToken.objects.get(id=id, user=info.context.user)
+        token.delete()
+        return OAuthRevokeTokenMutation(ok=True)
diff --git a/aleksis/core/templates/oauth2_provider/authorized-token-delete.html b/aleksis/core/templates/oauth2_provider/authorized-token-delete.html
deleted file mode 100644
index ece2011b6e6170bd8ed845f8464302bb1e6fb8fb..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/oauth2_provider/authorized-token-delete.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends "core/base.html" %}
-
-{% load i18n %}
-
-{% block browser_title %}{% trans "Revoke access" %}{% endblock %}
-{% block page_title %}{% trans "Revoke access" %}{% endblock %}
-
-{% block content %}
-  <div class="alert info">
-    <p>
-      <i class="material-icons iconify left" data-icon="mdi:alert-outline"></i>
-      {% trans "Are you sure to revoke the access for this application?" %}
-    </p>
-  </div>
-
-  <form method="post">
-    {% csrf_token %}
-    <a class="btn waves-effect waves-light red" href="{% url "oauth2_applications" %}">
-      <i class="material-icons iconify left" data-icon="mdi:delete-outline"></i>
-      {% trans "Revoke" %}
-    </a>
-    <a class="btn waves-effect waves-light" href="{% url "oauth2_applications" %}">
-      <i class="material-icons iconify left" data-icon="mdi:close"></i>
-      {% trans "Cancel" %}
-    </a>
-  </form>
-{% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/authorized-tokens.html b/aleksis/core/templates/oauth2_provider/authorized-tokens.html
deleted file mode 100644
index dfe058056fb4ffaabd2a44eaac04adaa8aa3ad3b..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/oauth2_provider/authorized-tokens.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{% extends "core/base.html" %}
-
-{% load i18n %}
-
-{% block browser_title %}{% blocktrans %}Authorized applications{% endblocktrans %}{% endblock %}
-{% block page_title %}{% trans "Authorized applications" %}{% endblock %}
-
-{% block content %}
-  {% if authorized_tokens %}
-    <div class="row">
-      {% for authorized_token in authorized_tokens %}
-        <div class="col s12 m6 l4 xl3">
-          <div class="card">
-            <div class="card-content">
-              <div class="card-title">{{ authorized_token.application }}</div>
-              {% for scope_name, scope_description in authorized_token.scopes.items %}
-                <p>
-                  {{ scope_name }}: {{ scope_description }}
-                </p>
-              {% endfor %}
-            </div>
-            <div class="card-action">
-              <a href="{% url 'oauth2_provider:authorized-token-delete' authorized_token.pk %}">{% trans "Revoke access" %}</a>
-            </div>
-          </div>
-        </div>
-      {% endfor %}
-    </div>
-  {% else %}
-    <div class="alert info">
-      <p>
-        <i class="material-icons iconify left" data-icon="mdi:information-outline"></i>
-        {% trans "No authorized applications." %}
-      </p>
-    </div>
-  {% endif %}
-{% endblock %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index f5e5899063678abc338ac971d1df3f55db3d0430..29fe9dcb2f5e51d0c1461898efae087ac9c6ee27 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -33,6 +33,10 @@ urlpatterns = [
         ConnectDiscoveryInfoView.as_view(),
         name="oidc_configuration",
     ),
+    path("oauth/applications/", views.TemplateView.as_view(template_name="core/vue_index.html")),
+    path(
+        "oauth/authorized_tokens/", views.TemplateView.as_view(template_name="core/vue_index.html")
+    ),
     path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
     path(
         "django/",