Streaming von geschützten Inhalten
Bei Subato müssen geschützte Dateien für die Clients zugänglich sein und heruntergeladen werden können. Das ist beispielsweise in den folgenden Szenarien der Fall:
- Download von Kursdateien, Aufgabendateien oder Lösungsdateien
- Einbettung von Bildern und anderen Medien in Kurs-, Übungs- und Aufgabenbeschreibungen
- Einbettung von Downloadlinks (zu Kursdateien, ...) in Kurs-, Übungs- und Aufgabenbeschreibungen
Da es sich um ein (sehr!) komplexes Problem handelt, wird es nachfolgend erläutert. Anschließend werden betrachtete Lösungsansätze angesprochen und zuletzt erfolgt die Erklärung der aktuellen Umsetzung in Subato.
Das Problem
Üblicherweise werden Downloads (über Links) durch Angabe des Links und dem download
-Attribut gestartet:
<a [href]="getDownloadLink(file)" download> ... </a>
Dabei wird der Access-Token nicht automatisch mitgegeben, da der Download vom Browser inittiert wird. Da die Inhalte über eine REST-API bezogen werden, muss dem Request aber zwangsläufig der Access-Token hinzugefügt werden können. Ein ähnliches Problem ergibt sich mit img
, iframe
und object
-Elementen, bei denen eine URL zur geschützten Ressourcen über das src
-Attribut angegeben wird. Auch hier wird der Download über den Browser initiiert.
Lösungsansätze
Zur Lösung des Problems gibt es unterschiedliche Lösungsansätze:
- Klick auf einen Link abfangen und einen eigenen HTTP-Request mit dem Access-Token im Header durchführen. Die Ressource wird anschließend über
createObjectURL
im Cache abspeichert. Das Abfangen funktioniert jedoch nur bei Links, nicht bei anderen Elementen (wieimg
undiframe
). Weiterhin ist das bei größeren Dateien nicht mehr praktibel. Darüber hinaus können Links nicht mit anderen geteilt werden. - Signierung von URLs. Dabei werden temporäre Access-Tokens (nachfolgend als Stream-Token bezeichnet) erzeugt. Bei der Signierung wird der Ursprungslink geändert, indem ein Query-Parameter mit dem Token angehängt wird. Jeder, der die URL kennt, kann damit die Ressource ohne Access-Token im Header herunterladen. Problematisch ist jedoch, dass die signierten URLs schnell ihre Gültigkeit verlieren (müssen, wegen Sicherheit und so). Zusätzlich muss darauf geachtet werden, dass signierte URLs in Eingabemasken wieder zu regulären URLs überführt werden. Weiterhin ist ein Teilen von Links ebenfalls unmöglich, da ein Zugriff im Browser (ohne den Stream-Token) immer zu einem Autorisierungsfehler führt.
- Verwendung von Cookies. Der Access-Token kann nach dem Login über die Single-Page Application als Cookie gesetzt werden. Bei Anfragen an das Backend wird der Cookie dann automatisch mitgesendet und kann dort (als Fallback zum
Authorization
-Header) gelesen werden. Der Cookie kann zwar etwas gesichert werden (Zeitbeschränkung + Domänenbeschränkung), wobei es trotzdem mögliche Angriffsszenarien gibt, indem andere Skripte auf der selben Seite auf den Access Token zugreifen können. Zudem muss der Access-Token regelmäßig (durch Aktualisierung mit dem Refresh-Token) erneuert werden. Wird die Single-Page Application also nicht immer offen gehalten, kommt es bei einer Anfrage im Backend zu Fehlern, wenn der Access-Token abgelaufen ist.
Subato verwendete bis v2.0.0-beta15 den 1. Lösungsansatz. Das führte immer wieder zu Problemen, wobei die angesprochenen Limitierungen nur einen Teil davon ausmachen.
Umsetzung in Subato
Seit v2.0.1 verwendet Subato eine etwas sichere Kombination der Lösungsansätze 2 und 3.
Für eingebettete Bilder wird eine serverseitige Signierung von URLs über Stream-Tokens verwendet. Bei der Anfrage einer Aufgabenbeschreibung (HTML) an das Backend werden URLs zu Bildern in der Aufgabenbeschreibung signiert. Die originalen URLs werden mit den signierten URLs ersetzt und die neue Aufgabenbeschreibung wird an den Client ausgeliefert. Dieser kann die Aufgabenbeschreibung dann anzeigen. Der Browser wird die Ressourcen dann über die signierten URLs ohne Angabe des Access-Tokens laden. Darüber hinaus kann eine URL manuell signiert werden, indem ein POST-Request an den /sign
-Endpunkt gestellt wird. Dieser nimmt eine URL entgegen, signiert sie und sendet diese dann an den Client zurück. Dadurch kann der Browser in anderen Szenarien (z.B. bei der Anzeige von PDF-Dateien aus einer Lösung über das object-Element
) auf die Ressource zugreifen.
Damit Links geteilt werden können und ein Zugriff auf Dateien auch über den Direktlink funktioniert, wurden im Backend Sessions konfiguriert. Für die Erzeugung einer Session wurde ein OAuth2-Client eingerichtet, der über Single Sign On verfügt. Bei direktem Zugriff auf eine Datei im Browser wird somit zunächst auf die Login-Seite von Keycloak weitergeleitet. Sollte der Benutzer bereits angemeldet sein (da eine Anmeldung bereits in der Single-Page Application erfolgt ist), erfolgt direkt die Weiterleitung zurück an die Ressource und der Download startet. Dabei wird der Access-Token in der Session serverseitig abgespeichert, sodass bei Folgeanfragen direkt festgestellt werden kann, ob ein Zugriff erlaubt ist oder nicht. Dieser Mechanismus ermöglicht auch, dass Links in der Single-Page Application ohne Stream-Token eingebunden werden. Bei Klick auf einen Link kommt es ebenfalls zum oben beschriebenen Ablauf. Insgesamt ist dieser Ansatz auch nicht zu 100% sicher, da Cookies verwendet werden. Der Access-Token wird dabei jedoch nicht direkt im Cookie gespeichert sondern vom Backend verwaltet. Das macht es also etwas sicherer.
Implementierung
Die Implementierung im Code wird hier nur kurz angesprochen. Weitere Informationen gibt die Dokumentation im Code. Die Klasse StreamTokenManager
verwaltet den Zustand der Stream-Tokens und ist für die Signierung und Aktivierung zuständig. Eingehende HTTP-Requests werden über den StreamFilter
abgefangen und anschließend für die Aktivierung an den StreamTokenManager
weitergeleitet. Die Signierung erfolgt entweder in den Controllern direkt über den StreamTokenManager
oder über den HtmlSigner
, wenn mehrere URLs in HTML-Dokumenten signiert werden sollen. Darüber hinaus sorgt SignedUrlSanitizer
dafür, dass signierte Links, die versehentlich oder unwissensichtlich in Eingabemasken eingegeben werden, automatisch in den Originallink umgewandelt werden.