OAuth2 – PKCE

PKCE ist eine Erweiterung von OAuth2, um die Authorization-Code Methode in Szenarien verwenden zu können, bei denen Nachrichten auf dem Rückkanal abgefangen werden könnten. Dies kann etwa bei Public Clients auf Smartphones der Fall sein.

Zur Wiederholung: Ein Client gilt als Confidential Client, wenn er kryptographische Geheimnisse bewahren kann und nicht, wie etwa bei einer reinen JavaScript-Anwendung, im Klartext vorliegt. Ein Public Client kann dagegen keine Geheimnisse bewahren.

PKCE erweitert das allgemeine Authorization-Code-Verfahren, welches bereits in diesem Blog-Post beschrieben wurde. Es beinhaltet zwei Stufen: Zunächst wird ein Authorization-Code angefordert, welcher dann im nächsten Schritt gegen ein Access-Token getauscht wird. Mit diesem Access-Token kann dann auf die API zugegriffen werden.

Das Problem

Die Idee hinter PKCE ist es, eine sichere Kommunikation von der Client-Anwendung bis zum Authorization Server des Dienstes zu ermöglichen. Normalerweise wird diese sichere Kommunikation zwischen dem Client und dem Authorization Server durch die Verwendung von Transport Layer Security (TLS) gewährleistet.

Das Problem ist hier der Redirect, der für die Kommunikation des Authorization Codes vom Authorization Server an den Client verwendet wird. Dieser könnte nämlich über einen Applicationlink (Custom URL Schema) geschehen, um den Authorization Code gleich an die richtige App weiterzuleiten. Die dabei entstehenden Inter-Process-Messages könnten nun von einem potenziell bösartigen Angreifer abgefangen werden, indem dieser beispielsweise seine eigene Anwendung auf die gleichen Applicationlinks registriert. Dadurch wird der Redirect mit dem Authorization Code nicht länger an die legitime App, sondern an die App des Angreifers weitergeleitet. Der kann dann mit dem Authorization Code die Identität des legitimen Clients stehlen. Dies ist in der folgenden Abbildung visualisiert.

OAuth2 Angriffsszenario

OAuth2 Angriffsszenario

Der legitime Client schickt also über die Betriebssystemschnittstelle eine Authorisierungsanfrage an den Authorizationserver. Dieser antwortet mit einem Authorization Code. Wenn die Redirect-URL in OAuth2 nun auf einen Application Link zeigt, kann eine andere Anwendung, die auf dem gleichen Gerät läuft, sich selbst auf den Application-Link registrieren und den Authorization Code abfangen. Diesen Authorization Code kann die böswillige Anwendung dann beim Authorization Server in ein valides Access Token umtauschen.

Um diesen Angriff zu verhindern, ist in OAuth2 das sogenannte ClientSecret vorgesehen.
Das ClientSecret sorgt dafür, dass nur eine legitime App sich gegenüber einem Authorization Server als legitim authentifizieren kann. Denn nach dem Setup-Prozess (siehe OAuth2-Artikel) hat der Authorization Server neben der öffentlichen ClientId auch ein geheimes ClientSecret, welches nur er und der Entwickler kennen. Bei einem Confidential Client, wie etwa einer Web-App, kann der Entwickler dieses Secret auf dem Webserver ablegen und von außen unzugänglich machen. Damit bleibt das ClientSecret weiterhin geheim und dem Angreifer nutzt auch ein abgefangener Authorization Code wenig. Damit ist der oben beschriebene Angriff bei einem Confidential Client gar nicht möglich.

In einem Public Client sieht die Situation allerdings völlig anders aus. Hier kann dieses ClientSecret nicht verwendet werden, da ein Public Client ja per Definition öffentlich ist und damit jeder das abgelegte ClientSecret sehen und missbrauchen könnte.

Ohne das ClientSecret kann sich die App, welche den Authorization-Code in ein Access Token umtauschen möchte, gegenüber dem Authorization Server nicht länger als legitime App authentifizieren. Jeder könnte ja daherkommen und mit der öffentlichen ClientId die Identität des legitimen Clients stehlen. Bei dem Anfordern des Authorization-Codes ist dies noch kein Problem, da dieser ja ohnehin nicht direkt zurückgeschickt wird, sondern der Client an die RedirectUri weitergeleitet wird. Im oben beschriebenen Fall mit einer lokalen RedirectUri und Inter-Application-Communication, in der Praxis etwa Applicationlinks auf einem Smartphone, könnte theoretisch jedoch jede beliebige, laufende Anwendung den Authorization-Code abfangen und sich damit beim Dienst anmelden.

Wie kann man also sicherstellen, dass nur der Client den Authorization-Code in ein Access-Token umtauschen kann, der den Code initial angefordert hat? Hier kommt PKCE ins Spiel.

PKCE im Detail

Um das Problem zu lösen, wird mit PKCE noch ein weiteres Geheimnis verwendet, welches lokal und nur zur Laufzeit auf dem Public Client generiert und verwendet wird. Dazu die folgende Abbildung, welche den veränderten Authorization-Code-Workflow zeigt und in den nächsten Absätzen chronologisch abgearbeitet wird:

OAuth2: - Authorization-Code-Workflow mit PKCE

OAuth2: – Authorization-Code-Workflow mit PKCE

Im ersten Schritt wird zur Laufzeit das private und lokale Secret generiert. In diesem Fall der ASCII-String ‘TEST’ (in der Abbildung links oben). Laut Spezifikation sollte dieser String zwischen 43 und 128 Zeichen lang sein. In diesem Beispiel ist er zur Vereinfachung nur 4 Zeichen lang. Dieses Secret existiert nur im Arbeitsspeicher und während der Client läuft und wird beim Starten generiert. Außerdem ist es lokal und wird zunächst nicht an den Authorization Server oder sonstwohin verschickt.

Nun bauen wir unsere GET-Request an den Authorization Server zusammen.

GET https://auth-server.tld/authorize
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=REDIRECT_URI
&scope=user:email
&state=test1337
&code_challenge=OTRFRTA1OTMzNUU1ODdFNTAxQ0M0QkY5MDYxM0UwODE0RjAwQTdCMDhCQzdDNjQ4RkQ4NjVBMkFGNkEyMkNDMg==
&code_challenge_method=S256

Wie bereits im letzten Artikel beschrieben werden zunächst response_type (code für Authorization-Code), client_id und eine optionale redirect_uri als Parameter mitgeschickt. Als scope wird wieder ‘user:email’ übergeben und als state ‘test1337’.

Der Unterschied liegt in den nächsten zwei Parametern: Hier wird das lokale Secret ‘TEST’ mit der SHA-256-Hashfunktion gehasht, anschließend in Base64 konvertiert und als code_challenge mitgeschickt. Außerdem wird die code_challenge_method auf ‘S256’ für SHA-256 gesetzt.

Nachdem der Client nach dem Absenden der GET-Anfrage erfolgreich mit dem Authorization-Code redirectet wurde, wird der erhaltene Authorization-Code mittels eines POST-Aufrufs an /token gegen ein Access Token getauscht:

POST https://auth-server.tld/token
?grant_type=authorization_code
&client_id=CLIENT_ID
&redirect_uri=REDIRECT_URI
&code_verifier=TEST

Hierbei wird zusätzlich zu den aus dem letzten OAuth2-Artikel bekannten Parametern grant-type, cliend_id und ggf. redirect_uri über den Parameter code_verifier das lokale Secret angehängt. Dies stellt kein Sicherheitsproblem dar, da ja nur der Rückkanal mit dem Redirect, nicht der Hinkanal vom Angreifer mitgelesen werden kann.

Warum ist das jetzt sicher?

Dazu treten wir zunächst einmal einen Schritt zurück. Das Problem war ja, dass ein potenzieller Angreifer den Authorization Code erhalten kann, indem er den Rückkanal abhört und den Redirect an sich selbst umleitet. Im nächsten Schritt gibt er sich dann als legitimer Client beim Authorization Server aus und tauscht den Code gegen ein Access Token.

Der Authorization Server “glaubt” also, dass der, der den Authorization Code hat, automatisch ein legitimer Client ist. Dass jemand anderes als der anfragende Client den Authorization Code erhält, ist nicht vorgesehen.

Diese problematische Annahme ist mit PKCE nun ausgeräumt. Der Authorization Server tauscht den Authorization Code nun ausschließlich dann um, wenn der korrekte code_verifier mitgeschickt wird, der zu dem Hashwert aus der ersten Anfrage passt. Sofern der Angreifer nicht etwa über Seitenangriffe an das lokale Secret (‘TEST’) und damit an den code_verifier im Speicher kommt, kann der Authorization Server nun davon ausgehen, dass es sich bei dem Client, der die Umtauschanfrage gesendet hat um den gleichen Client handelt, der vorher den Code angefordert hat.

Der Angreifer kann nun zwar den Authorization Code abfangen. Für den Umtausch des Authorization Codes in ein Access-Token benötigt er jedoch das lokale Secret im Klartext, da dieses ja als Parameter als code_verifier mitgesendet werden muss. Ohne das Secret nützt der abgefangene Authorization-Code dem Angreifer wenig, da er es ohne Secret nicht umtauschen kann. Der Authorization Server kann also davon ausgehen, dass nur der Client, welcher initial die Anfrage nach dem Authorization-Code gestellt hat, auch ein Access-Token erhält.

Selbst wenn es dem Angreifer irgendwie gelingen sollte, an den Hashwert zu kommen, kann er damit recht wenig anfangen. Denn die Sicherheit einer Hashfunktion basiert darauf, dass es schwer ist, sie umzukehren. Wer den Klartext kennt, kann relativ einfach den dazugehörigen Hashwert berechnen. Aus diesem Hashwert ist es jedoch nicht so einfach möglich, den Klartext zurückzurechnen. Stattdessen kann der Klartext nur durch Ausprobieren erraten werden, wofür jedoch mit hoher Wahrscheinlichkeit eine große Menge an Versuchen benötigt wird. Sofern das lokale Secret ausreichend lang und zufällig gewählt ist, sollte die SHA-256-Hashfunktion vorerst als sicher gelten. Wenn dies einmal nicht mehr der Fall sein sollte, kann für PKCE statt SHA-256 auch eine andere Hashfunktion verwendet werden. Der zur Hashfunktion gehörende Identifier muss dann natürlich entsprechend über den ‘code_challenge_method’-Parameter angepasst werden.

Short URL for this post: https://wp.me/p4nxik-38T
Steffen Jacobs

About Steffen Jacobs

Java Consultant & Developer at Orientation in Objects GmbH. Follow me on Twitter and find me on LinkedIn and Xing. Some of the source code associated with the blog articles can be found on GitHub.
This entry was posted in Security and tagged , , , . Bookmark the permalink.

Leave a Reply