diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index cef045d867bd838acc8443c17eb695a48e90b31a..3a92a82b810622b63f36a467b876bf61ccdaa873 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -132,3 +132,7 @@ class EditTermForm(forms.ModelForm):
     class Meta:
         model = SchoolTerm
         fields = ["caption", "date_start", "date_end"]
+
+
+class NextcloudServerForm(forms.Form):
+    url = forms.URLField(required=True, label=_("URL of your Nextcloud instance"))
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index 65d0a584858322665bbfad34af67aebd539f6b4f..3a06bdae36ae3df8cfd19dd81ec2055796614b7b 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -87,6 +87,15 @@ MENUS = {
                         "menu_generator.validators.is_superuser",
                     ],
                 },
+                {
+                    "name": _("Third-party services"),
+                    "url": "third_party_services",
+                    "icon": "share",
+                    "validators": [
+                        "menu_generator.validators.is_authenticated",
+                        "menu_generator.validators.is_superuser",
+                    ],
+                },
                 {
                     "name": _("Backend Admin"),
                     "url": "admin:index",
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 522695ea6ce9aa1dacc6904a701cc7b8d4230370..95d9287643c6382353822ff8c4923423f750bff6 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -371,6 +371,9 @@ CONSTANCE_CONFIG = {
     "IMPRINT_URL": ("", _("Link to imprint"), "url_field"),
     "ADRESSING_NAME_FORMAT": ("german", _("Name format of adresses"), "adressing-select"),
     "NOTIFICATION_CHANNELS": (["email"], _("Channels to allow for notifications"), "notifications-select"),
+    "NEXTCLOUD_TALK_SERVER": ("", _("Nextcloud Talk server configured by connection service"), "url_field"),
+    "NEXTCLOUD_TALK_LOGIN_NAME": ("", _("Nextcloud Talk server configured by connection service"), str),
+    "NEXTCLOUD_TALK_APP_PASSWORD": ("", _("Nextcloud Talk server configured by connection service"), str),
 }
 CONSTANCE_CONFIG_FIELDSETS = {
     "General settings": ("SITE_TITLE",),
diff --git a/aleksis/core/static/js/nextcloud_talk.js b/aleksis/core/static/js/nextcloud_talk.js
new file mode 100644
index 0000000000000000000000000000000000000000..14fca65317a1b5a95026557e8c1af52aa9e19b63
--- /dev/null
+++ b/aleksis/core/static/js/nextcloud_talk.js
@@ -0,0 +1,29 @@
+var POLL_INTERVAL = 500;
+var w = null;
+
+function poll() {
+    $.ajax({
+        url: Urls.pollNextcloudTalk(),
+    }).done(function (data) {
+        if (data.done) {
+            console.log("Polling done");
+
+            // Redirect to third-party services home view
+            window.location.href = Urls.thirdPartyServices();
+            w.close();
+        } else {
+            window.setTimeout(poll, POLL_INTERVAL);
+        }
+    }).fail(function () {
+        window.setTimeout(poll, POLL_INTERVAL);
+    })
+}
+
+$(document).ready(function () {
+    // Open login URL
+    var loginData = getJSONScript("login_data");
+    w = window.open(loginData.login);
+
+    console.log("Start polling");
+    poll();
+});
diff --git a/aleksis/core/templates/core/connect_nextcloud_talk.html b/aleksis/core/templates/core/connect_nextcloud_talk.html
new file mode 100644
index 0000000000000000000000000000000000000000..e47ff7ae2dd4249a5a9e4c7f83f69c636f7ee438
--- /dev/null
+++ b/aleksis/core/templates/core/connect_nextcloud_talk.html
@@ -0,0 +1,39 @@
+{# -*- engine:django -*- #}
+{% extends "core/base.html" %}
+{% load i18n material_form static %}
+
+{% block browser_title %}{% blocktrans %}Connect to Nextcloud Talk{% endblocktrans %}{% endblock %}
+
+{% block page_title %}{% blocktrans %}Connect to Nextcloud Talk{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  {% if step == 1 %}
+    <form action="" method="post">
+      {% csrf_token %}
+      {% form form=form %}{% endform %}
+
+      <button class="btn green waves-effect waves-light" type="submit" name="step-1">
+        <i class="material-icons left">open_in_new</i>
+        {% trans "Open login page" %}
+      </button>
+    </form>
+  {% elif step == 2 %}
+    {{ login_data|json_script:"login_data" }}
+
+    <p class="flow-text">
+      {% blocktrans %}
+        Waiting for successful login ...
+      {% endblocktrans %}
+    </p>
+
+    <p>
+      {% blocktrans with url=login_data.login %}
+        Login window has not opened?
+        <a href="{{ url }}" target="_blank">Try again.</a>
+      {% endblocktrans %}
+    </p>
+
+    <script src="{% static "js/helper.js" %}"></script>
+    <script src="{% static "js/nextcloud_talk.js" %}"></script>
+  {% endif %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/third_party_services.html b/aleksis/core/templates/core/third_party_services.html
new file mode 100644
index 0000000000000000000000000000000000000000..31753139307a8455a3b48bdc36dc013143955dd8
--- /dev/null
+++ b/aleksis/core/templates/core/third_party_services.html
@@ -0,0 +1,46 @@
+{# -*- engine:django -*- #}
+{% extends "core/base.html" %}
+{% load i18n %}
+
+{% block browser_title %}{% blocktrans %}Third-party services{% endblocktrans %}{% endblock %}
+
+{% block page_title %}{% blocktrans %}Third-party services{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <div class="card">
+    <div class="card-content">
+      {% if nextcloud.connected %}
+        <span class="badge new green right">{% trans "Connected" %}</span>
+      {% else %}
+        <span class="badge new red right">{% trans "Not connected" %}</span>
+      {% endif %}
+
+      <span class="card-title">{% trans "Nextcloud Talk" %}</span>
+
+      {% if nextcloud.connected %}
+        <p>
+          {% blocktrans with login_name=nextcloud.login_name %}
+            Connected Nextcloud user: {{ login_name }}
+          {% endblocktrans %}
+        </p>
+        <br/>
+        <a class="btn red waves-effect waves-light" href="{% url "disconnect_nextcloud_talk" %}">
+          <i class="material-icons left">leak_remove</i>
+          {% trans "Disconnect" %}
+        </a>
+      {% else %}
+        <p>
+          {% blocktrans %}
+            In order to send notifications by Nextcloud Talk you must connect a Nextcloud user which should send the
+            notifications to the users.
+          {% endblocktrans %}
+        </p>
+        <br/>
+        <a class="btn green waves-effect waves-light" href="{% url "connect_nextcloud_talk" %}">
+          <i class="material-icons left">leak_add</i>
+          {% trans "Connect" %}
+        </a>
+      {% endif %}
+    </div>
+  </div>
+{% endblock %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 6f1d4275e0fd15f2b1cd315353beb56d219342fd..054e38247f476cc8d49b481efdd70218f8c83758 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -19,6 +19,10 @@ urlpatterns = [
     path("admin/", admin.site.urls),
     path("data_management/", views.data_management, name="data_management"),
     path("status/", views.system_status, name="system_status"),
+    path("third_party/", views.third_party_services, name="third_party_services"),
+    path("third_party/nextcloud_talk/", views.connect_nextcloud_talk, name="connect_nextcloud_talk"),
+    path("third_party/nextcloud_talk/disconnect/", views.disconnect_nextcloud_talk, name="disconnect_nextcloud_talk"),
+    path("third_party/nextcloud_talk/poll/", views.poll_nextcloud_talk, name="poll_nextcloud_talk"),
     path("school_management", views.school_management, name="school_management"),
     path("school/information/edit", views.edit_school, name="edit_school_information"),
     path("school/term/edit", views.edit_schoolterm, name="edit_school_term"),
diff --git a/aleksis/core/util/nextcloud.py b/aleksis/core/util/nextcloud.py
new file mode 100644
index 0000000000000000000000000000000000000000..0595dbfd45c46f73709b8412b999435e54f07ec6
--- /dev/null
+++ b/aleksis/core/util/nextcloud.py
@@ -0,0 +1,34 @@
+from typing import Union
+
+import requests
+from constance import config
+
+HEADERS = {
+    "User-Agent": "AlekSIS",
+}
+INITIATE_LOGIN_PROCESS_URL = "index.php/login/v2"
+
+
+def initiate_login_process(nextcloud_url: str) -> dict:
+    url = nextcloud_url + INITIATE_LOGIN_PROCESS_URL
+
+    r = requests.post(url, headers=HEADERS)
+
+    return r.json()
+
+
+def login_process_poll(endpoint: str, token: str) -> Union[dict, bool]:
+    r = requests.post(endpoint, headers=HEADERS, data={"token": token})
+
+    if r.status_code != 200:
+        return False
+
+    return r.json()
+
+
+def is_connected() -> bool:
+    return (
+        config.NEXTCLOUD_TALK_SERVER != ""
+        and config.NEXTCLOUD_TALK_LOGIN_NAME != ""
+        and config.NEXTCLOUD_TALK_APP_PASSWORD
+    )
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index c610ec8abf640246355fd04e56130e15f103f483..3957e4ed0cc5cde996e82ab5172584a62c152dff 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1,8 +1,9 @@
 from typing import Optional
 
+from constance import config
 from django.contrib.auth.decorators import login_required
 from django.core.exceptions import PermissionDenied
-from django.http import Http404, HttpRequest, HttpResponse
+from django.http import Http404, HttpRequest, HttpResponse, JsonResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.translation import ugettext_lazy as _
 
@@ -15,10 +16,11 @@ from .forms import (
     EditSchoolForm,
     EditTermForm,
     PersonsAccountsFormSet,
-)
+    NextcloudServerForm)
 from .models import Activity, Group, Notification, Person, School, DashboardWidget
 from .tables import GroupsTable, PersonsTable
-from .util import messages
+from .util import messages, nextcloud
+from .util.nextcloud import initiate_login_process, login_process_poll
 
 
 @person_required
@@ -264,3 +266,80 @@ def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse:
         raise PermissionDenied(_("You are not allowed to mark notifications from other users as read!"))
 
     return redirect("index")
+
+
+@admin_required
+def third_party_services(request: HttpRequest) -> HttpResponse:
+    context = {}
+
+    context["nextcloud"] = {
+        "connected": nextcloud.is_connected(),
+        "login_name": config.NEXTCLOUD_TALK_LOGIN_NAME
+    }
+
+    return render(request, "core/third_party_services.html", context)
+
+
+@admin_required
+def connect_nextcloud_talk(request: HttpRequest) -> HttpResponse:
+    context = {}
+
+    if request.method == "GET":
+        form = NextcloudServerForm()
+
+        context["step"] = 1
+        context["form"] = form
+
+    elif request.method == "POST":
+        if "step-1" in request.POST:
+            form = NextcloudServerForm(request.POST)
+
+            if form.is_valid():
+                url = form.cleaned_data["url"]
+                if url[-1] != "/":
+                    url += "/"
+
+                r = initiate_login_process(url)
+
+                request.session["nextcloud_poll"] = True
+                request.session["nextcloud_login_data"] = r
+
+                context["login_data"] = r
+                context["step"] = 2
+
+    return render(request, "core/connect_nextcloud_talk.html", context)
+
+
+@admin_required
+def disconnect_nextcloud_talk(request: HttpRequest) -> HttpResponse:
+    config.NEXTCLOUD_TALK_SERVER = ""
+    config.NEXTCLOUD_TALK_LOGIN_NAME = ""
+    config.NEXTCLOUD_TALK_APP_PASSWORD = ""
+
+    messages.success(request, _("The Nextcloud user was successfully disconnected ."))
+
+    return redirect("third_party_services")
+
+
+@admin_required
+def poll_nextcloud_talk(request: HttpRequest) -> HttpResponse:
+    done = False
+
+    if request.session.get("nextcloud_poll", False):
+        login_data = request.session["nextcloud_login_data"]
+
+        r = login_process_poll(login_data["poll"]["endpoint"], login_data["poll"]["token"])
+
+        done = r != False
+
+        if done:
+            config.NEXTCLOUD_TALK_SERVER = r["server"]
+            config.NEXTCLOUD_TALK_LOGIN_NAME = r["loginName"]
+            config.NEXTCLOUD_TALK_APP_PASSWORD = r["appPassword"]
+
+            messages.success(request, _("The Nextcloud user was successfully connected."))
+
+            del request.session["nextcloud_login_data"]
+            del request.session["nextcloud_poll"]
+
+    return JsonResponse({"done": done})