Es ist durchaus gängige Praxis geworden, REST Services über JWT (Java Web Token) abzusichern. JWTs sind sicherer als herkömmliche BASIC Tokens, die aus unverschlüsselten aber encodierten Benutzernamen und Kennwörtern bestehen. Und sie sind flexibler, weil man mit ihnen beispielsweise eine Gültigkeitsdauer festlegen und obendrein eigene Metadaten mit auf den Weg geben kann. Die komplette Spezifikation ist auf den Seiten der IETF zu finden. Etliche APIs für Java und andere Programmiersprachen vereinfachen die Erzeugung und Validierung solcher Token.
Die Oracle REST Data Services (ORDS) bieten eine OAUTH genannte API an, um Clients zu definieren, die sich über eine bereitgestellte URL einen Token abholen und sich damit eine gewisse Zeit lang am gleichen System authentisieren können. Eine Anleitung dazu bietet ein Blog Eintrag auf oracle-base.com.
Dieses Vorgehen bedeutet aber auch, daß die Benutzerverwaltung innerhalb einer Oracle Datenbank erfolgt. Auch die Zuordnung dieser Benutzer, "clients" genannt, zu Anwendungsrollen erfolgt über interne Datenbanktabellen. Das funktioniert gut für sogenannte technische Benutzer, künstliche Clients die angelegt werden um beispielsweise Anwendungen miteinander kommunizieren zu lassen, oder auch falls eine solche eher lokale Benutzerverwaltung für eigene Zwecke denkbar ist. Wenn aber beispielsweise
die Benutzerverwaltung über LDAP bzw. Active Directory erfolgen soll
die JWTs an anderer Stelle erzeugt werden sollen als innerhalb von ORDS
die JWTs dadurch andere Metadaten enthalten wie vorgesehen, z.B. komplex verschlüsselte Signaturen
die JWTs eigene Metadaten enthalten sollen ("custom claims" genannt), wie z.B. Gruppennamen
die JWTs stärker geprüft werden sollen als vorgesehen, z.B. ob der prüfende Server überhaupt zur Liste der gültigen Server gehört ("audience" genannt)
dann empfiehlt es sich, einen anderen oder gar selbst geschriebenen Mechanismus einzubinden. Oftmals kommen zu diesem Zwecke sogenannte "API Gateways" zum Einsatz wie "KrakenD" , das Oracle Cloud API Gateway, oder Single SignOn Server wie Keycloak und der Oracle Identity Cloud Service. Diese sichern REST Services auf umfassende Weise ab indem sie sich zwischen REST Service und Aufrufer schalten, ganz im Sinne eines Proxy. Diese geben nach dem "clearing" die nötigen Daten an den eigentlichen REST Service weiter, beispielsweise der hinter einem JWT stehende Benutzername oder weitere, intern abgestimmte Header-Informationen. Ganz so aufwändig und umfassend muß es jedoch auch nicht gleich sein. Denn eine solche Proxy-Funktion, gleich einem Torwächter, steht einerseits innerhalb von ORDS zur Verfügung, andererseits einem jeden Applikationsserver, der ORDS betreibt.
Der benutzerdefinierte Weg über ORDS
Im Falle von ORDS ruft ein sogenannter "pre-Hook" vor der eigentlichen Abarbeitung des REST Services eine beliebige Datenbank-Funktion auf, welche neben einer LDAP Authentisierung mit Datenbank-Mitteln auch eine Prüfung der JWT Token vornehmen könnte. Dies bedeutet wiederum eine gewisse Menge an Handarbeit, die durch Beispiel-Codes im Internet unterstützt werden kann. Im Detail müßte die prehook-Funktion folgende Aufgaben erfüllen:
Auslesen des JWT Token aus dem HTTP Header (owa_util.get_cgi_env('Authorization')😉
Base64URL-decoding der JWT Bestandteile Header, Payload, Signatur (ein normales Base64 decoding mit utl_encode.base64_decode reicht leider nicht aus und führt zu Fehlern bei der späteren Signatur-Prüfung)
Prüfung, d.h. Nachbauen der Signatur im JWT Token anhand des benannten Algorithmus im JWT Header und eines Schlüssels oder Zertifikats, das dem prüfenden Server bekannt ist (z.B. dbms_crypto.mac wenn im JWT Header ein "algo: SHA256" steht)
Prüfung der zeitlichen Gültigkeit, wobei als Zeitformat das "Epoch" Zeitformat gilt, d.h. verstrichene Millisekunden seit dem 1.1.1970 00:00 Uhr
Auslesen des Benutzernamens aus dem JWT Payload (json_value (jwtpayload, '$.sub'))
LDAP Zugriff für das Auslesen der Gruppen, die für den eben ausgelesenen Benutzer im Token hinterlegt sind (dbms_ldap und zugehörige Netzwerk-ACL, ein API Beispiel in einem Blog auf oracle-base.com)
Setzen der internen Header für den angemeldeten Benutzer und dessen Gruppen htp.prn('X-ORDS-HOOK-USER: ' || identified_user);
htp.prn('X-ORDS-HOOK-ROLES: ' || identified_roles);
Auf github habe ich Beispiel-Code in PL/SQL hinterlegt, der nur sinngemäß ein JWT Token in seine drei Bestandteile zerlegt und die Signatur -vorerst erfolglos mangels Base64URL encoding- versucht nachzuempfinden.
Der letzte Schritt in der Aufgaben-Liste wird auch Identity Assertion genannt, die Annahme und Übergabe der Benutzerdaten ins Backend im Gegensatz zu deren vorangehender Prüfung. Das führt uns zum nächsten Abschnitt:
Der benutzerdefinierte Weg über WebLogic Identity Asserter
Zum gleichen Zweck und mit gleicher Funktion können in einen WebLogic Server vorgefertigte oder selbst erstellte Identity Asserter eingebaut werden. Zudem gibt es sehr komfortable Java APIs für die Erzeugung und Validierung von JWT, so daß die für PL/SQL beschriebenen Einzelschritte durch einen einzigen "validate" Aufruf (mit entsprechender Parametrisierung) an die API erfolgen kann. In Kombination mit zahlreichen verfügbaren Identity Providern für beispielsweise Active Directory oder generischem LDAP erübrigt sich auch die programmatische Einbindung von LDAP Servern.
Aus Gründen des Komforts und einer sauberen Aufgabentrennung zwischen Geschäftslogik und Security Context habe ich mich entschieden, diesen letzteren Weg nicht nur zu beschreiben, sondern auch beispielhaft zu implementieren und Ihnen auf github zur Verfügung zu stellen.
Das github-Projekt besteht aus mehreren Unterprojekten, in denen Sie sich gerne umschauen dürfen. Drei davon sind für den WebLogic Identity Asserter interessant bis relevant:
1) das Projekt "JWTTokenGenerator" enthält eine Web Applikation, einen REST Service, der anhand eines auf herkömmlichem Wege authentisierten Benutzers ein JWT erzeugt und zurückgibt. Dieses Token kann dann verwendet werden, um sich damit bei
2) der Web Anwendung "JWTprotectedHiWorldApp" anzumelden und das darin enthaltene HalloWelt-Servlet aufzurufen. Damit die Anmeldung mit dem JWT funktioniert, muß vorher jedoch der IdentityAsserter aus dem Projekt
3) "JWTIdentityAsserter" in den WebLogic Server eingerichtet werden. Funktioniert alles, kann auch die ORDS-Anwendung mittels JWT geschützt werden, lediglich eine kleine Anpassung in deren Konfiguration (web.xml Datei) ist vonnöten.
Schritt 0: Download des Projektes
Um zu beginnen, laden Sie sich das github Projekt herunter. Entweder via
oder im Browser auf dem github-Projekt durch klick auf "Code" und dann "Download ZIP". Entpacken Sie das .zip in ein Verzeichnis Ihrer Wahl und wechseln Sie per Kommandozeile dorthin.
Für alle weiteren Schritte benötigen Sie das Tool "Maven" bzw. "mvn" sowie ein aktuelles JDK 8 in Ihrem Pfad.
Schritt 1: Das Projekt "JWTTokenGenerator"
Wechseln Sie in das Unterverzeichnis JWTtokenGenerator
Mittels "mvn package" generieren Sie das Anwendungs-WAR File JWTtokenGenerator-1.0.war im Unterverzeichnis "target".
Führen Sie ein Deployment der WAR Datei durch in einen WebLogic Server Ihrer Wahl. Ich verwende
zu Testzwecken immer eine WebLogic Domäne mit nur einem AdminServer darin, ohne Managed Server.
Gehen Sie z.B. im Browser über "Deployments", dann "Install". Navigieren Sie in das eben generierte "target" Verzeichnis und wählen Sie die Datei JWTTokenGenerator-1.0.war aus.
Installieren Sie die Datei als eine Applikation, nicht als eine Bibliothek/Library, und belassen Sie sonst alle Defaults.
Der REST Service steht Ihnen nun zur Verfügung über die URI
/jwtTokenGen/resources/token/noGroups
Rufen Sie den Service auf und lassen Sie sich einen Token generieren. Bitte verwenden Sie für die Anmeldung einen Benutzer, der dem WebLogic Server bekannt ist, z.B. "weblogic". Sonst erhalten Sie eine Rückmeldung der Art "Unauthorized".
C:\>curl http://127.0.0.1:7001/jwtTokenGen/resources/token/noGroups
Unauthorized
C:\>curl http://127.0.0.1:7001/jwtTokenGen/resources/token/noGroups -u weblogic
Enter host password for user 'weblogic':
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJodHRwOi8vTVBGRUlGRVItWU9HQTM3Iiwic3ViIjoid2VibG9naWMiLCJpc3MiOiJNUEZFSUZFUi1ZT0dBMzciLCJleHAiOjE2MzM2ODg2MDgsImlhdCI6MTYzMzY4NTAwOH0.Y_OG9Ym627yPDNFDb0f58YmQTniGME4SewesatGNAps
Das Token verwendet eine simple SHA256-basierte Signatur mit dem Erzeuger-Hostnamen ("issuer" oder "iss") als Schlüssel. Das zu ändern bzw. konfigurierbar zu machen wäre eine sinnvolle Anforderung, ginge aber über ein Beispiel hinaus.
Die gleiche Anwendung ist auch in der Lage, das Token zu validieren, z.B. ob es noch zeitlich gültig ist oder bereits abgelaufen. Sie können das erzeugte Token in den "Authorization" Header einsetzen und an den zweiten REST Service übergeben mit der URI "/jwtTokenGen/resources/token/validate"
C:\>curl http://127.0.0.1:7001/jwtTokenGen/resources/token/validate --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJodHRwOi8vTVBGRUlGRVItWU9HQTM3Iiwic3ViIjoid2VibG9naWMiLCJpc3MiOiJNUEZFSUZFUi1ZT0dBMzciLCJleHAiOjE2MzM2ODg2MDgsImlhdCI6MTYzMzY4NTAwOH0.Y_OG9Ym627yPDNFDb0f58YmQTniGME4SewesatGNAps"
{"valid" : true}
C:\>curl http://127.0.0.1:7001/jwtTokenGen/resources/token/validate --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJodHRwOi8vTVBGRUlGRVItWU9HQTM3Iiwic3ViIjoid2VibG9naWMiLCJpc3MiOiJNUEZFSUZFUi1ZT0dBMzciLCJleHAiOjE2MzM2ODg2MDgsImlhdCI6MTYzMzY4NTAwOH0.Y_OG9Ym627yPDNFDb0f58YmQTniGME4SewesatGNApFEHLERMITABSICHT"
{"valid" : false, "reason":"The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256"}
Schritt 2: Das Projekt "JWTIdentityAsserter"
Wechseln Sie in das Unterverzeichnis JWTtokenIdentityAsserter
Ändern Sie die Datei "pom.xml" mit einem Texteditor, indem Sie dort die Pfade für Ihre WebLogic-Installation und für das verwendete JDK8 eintragen. Suchen Sie nach folgendem Schnipsel (ca. im letzten Viertel der Datei) Zwecks Anpassung:
Mittels "mvn package" generieren Sie nun das Anwendungs-JAR File JWTtokenAuthenticator-1.0.jar im Unterverzeichnis "target". Dieser Compile-Vorgang dauert etwas länger, denn es werden WebLogic-interne Tools aufgerufen (z.B. der MBeanCompiler), die das Security Plugin mit einigen Metadaten erzeugen und einbindbar machen.
Kopieren Sie zwei Dateien in Ihre WebLogic Installation:
Datei target\JWTtokenAuthenticator-1.0.jar nach <mdw.home>\wlserver\server\lib\mbeantypes
Datei target\lib\java-jwt-3.18.1.jar nach <domain.home>\lib
zum Beispiel:
Das gesamte Beispiel nutzt die "auth0" Java Bibliothek zur Erzeugung und Validierung von JWT Token. Sie befindet sich in dem eben kopierten jar "java-jwt-3.18.1.jar"
Starten Sie nun den WebLogic Server durch und aktivieren Sie den JWTIdentityAsserter. D.h.
Klicken Sie in der WebLogic Console auf "Security Realms", dann "myrealm", dann "Providers", Button "New".
Wurde der neue IdentityAsserter beim Start erkannt, steht er Ihnen in einer ziemlich großen Popup-Liste nun zur Auswahl. Bitte wählen Sie den neuen "JWTtokenIdentityAsserter" aus der Liste aus und benennen ihn am besten ebenso:
Achten Sie bitte darauf, daß der DefaultAuthenticator (der z.B. den Benutzer "weblogic" kennt) das Flag "SUFFICIENT" erhält, also keinesfalls "REQUIRED", damit WebLogic auch die anderen vorhandenen Provider durchführt.
Starten Sie den WebLogic Server abermals durch. In der Ausgabe bzw. im Log File müßte sich der Asserter bei dessen Initialisierung relativ am Anfang des Startvorgangs melden:
<Okt 8, 2021 11:10:10,004 AM MESZ> <Notice> <Security> <BEA-090946> <Security pre-initializing using security realm: myrealm>
Okt 08, 2021 11:10:10 AM com.svi.asserter.JWTtokenIdentityAsserterProviderImpl initialize
WARNING: JWTtokenIdentityAsserterProviderImpl.initialize
<Okt 8, 2021 11:10:10,911 AM MESZ> <Notice> <Security> <BEA-090947> <Security post-initializing using security realm: myrealm>
<Okt 8, 2021 11:10:14,115 AM MESZ> <Notice> <Security> <BEA-090082> <Security initialized using administrative security realm: myrealm>
Fertig – nun gilt es, den Asserter zu testen. Dies tun wir mit:
Schritt 3: Das Projekt "JWTprotectedHiWorldApp"
Wechseln Sie in das Unterverzeichnis JWTprotectedHiWorldApp
Mittels "mvn package" generieren Sie das Anwendungs-WAR File JWTprotectedHiWorldApp-1.0.war im Unterverzeichnis "target".
Deployen Sie die Anwendung äquivalent so, wie Sie bereits den Token Generator deployed haben.
D.h. als Anwendung (nicht Bibliothek) und mit Beibelassung aller Defaults:
Nach erfolgtem Deployment testen Sie den Aufruf der Anwendung, mal mit und mal ohne JWT Token.
Das Servlet gibt Ihnen einige Infos aus, z.B. angemeldeter Benutzer und ob Sie bestimmten Gruppen angehören wie den "Administrators" oder einer Gruppe "jwtAuthenticated", letztere dürfen Sie gern ignorieren.
curl http://127.0.0.1:7001/jwtHi/hi
C:\Projects\svi_jwt\JWTprotectedHiWorldApp>curl http://127.0.0.1:7001/jwtHi/hi
<!DOCTYPE html>
<html>
<head>
<title>Servlet HiWorldServlet</title>
</head>
<body>
<h1>Servlet HiWorldServlet at /jwtHi</h1>
<h1>Logged in as user: null</h1>
<h1>Member of group jwtAuthenticated : NO</h1>
<h1>Member of group Administrators : NO</h1>
</body>
</html>
C:\Projects\svi_jwt\JWTprotectedHiWorldApp>curl http://127.0.0.1:7001/jwtHi/hi --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJodHRwOi8vTVBGRUlGRVItWU9HQTM3Iiwic3ViIjoid2VibG9naWMiLCJpc3MiOiJNUEZFSUZFUi1ZT0dBMzciLCJleHAiOjE2MzM2OTYwMzcsImlhdCI6MTYzMzY5MjQzN30.XCeqfyUkn4krQAs20DJUNbaeeEltoVrXQtEsQXNlRKE"
<!DOCTYPE html>
<html>
<head>
<title>Servlet HiWorldServlet</title>
</head>
<body>
<h1>Servlet HiWorldServlet at /jwtHi</h1>
<h1>Logged in as user: weblogic</h1>
<h1>Member of group jwtAuthenticated : NO</h1>
<h1>Member of group Administrators : YES</h1>
</body>
</html>
Das wäre der erfolgreiche Test. Sie können, wenn Sie möchten, zusätzliche Identity Provider hinzukonfigurieren, z.B. den LDAPIdentityProvider, um Zugriff auf Ihre Unternehmens-Benutzerdatenbank zu haben statt nur auf die WebLogic-Benutzer. Setzen Sie diese ebenfalls auf den Wert SUFFICIENT, damit ein Login mit dem Benutzer "weblogic" nach wie vor funktioniert und platzieren Sie diese am besten hinter dem DefaultAuthenticator in der Liste der Provider.
Kommen wir zum finalen Schritt:
Schritt 4: Anpassen der ORDS-Anwendung
Auch die ORDS-Anwendung liegt in Form einer .WAR Datei vor, die man, sobald sie einmal korrekt konfiguriert und mit einer Datenbank verbunden wurde, einfach in den WebLogic Server deployen kann.
Bitte öffnen Sie diese fertig konfigurierte WAR Datei mit einem ZIP Tool Ihrer Wahl und navigieren Sie dort ins Verzeichnis WEB-INF
Editieren Sie die Datei web.xml im eben erreichten Verzeichnis und fügen Sie einen Authentisierungsmechanismus ein. Ohne diesen Eintrag reagiert ORDS nur auf die klassische "BASIC" Authentisierung.
Gleich nach der zweiten Zeile, beginnend mit "<display-name>…." fügen Sie ein:
Sie können den eben erklärten Schritt gerne automatisch durchführen lassen. Es gibt mittlerweile ein Konfigurations-Kommando für ORDS, welches die ORDS.WAR auf identische Weise anpaßt: java -jar ords.war oam-config
Sollte das ORDS.war bereits im angepaßten WebLogic deployed sein, so starten Sie ihn neu damit das .war file ausgepackt und geprüft wird. Falls nicht, deployen Sie das eben angepaßte ORDS.war genau so wie die anderen beiden .WAR Dateien aus den vorigen Projekten als Anwendung, nicht als Bibliothek, und belassen alle Defaults. Die ORDS Awendung steht Ihnen dann unter der URI "/ords" zur Verfügung, zum Beispiel: http://127.0.0.1:7001/ords/<dbuser>/<modul>/<service>
Bitte achten Sie darauf, daß die ORDS Anwendung bzw. das ORDS.war bereits im Vorfeld fertig konfiguriert ist, d.h. mit einer Oracle Datenbank verküpft wurde (java -jar ords.war install)
Nun können Sie einen REST Service in der Datenbank definieren, der z.B. den angemeldeten Benutzernamen zurückgibt, und ihn mit einem gültigen Token aufrufen. Folgendes PL/SQL habe ich in meiner mit ORDS verknüpften Datenbank unter dem Benutzer "demo" ausgeführt:
Letztlich können Sie den gleichen REST Service schützen, indem Sie eine Rolle definieren, die genau so heißt wie die Benutzergruppe im WebLogic Server (das darf später auch eine LDAP Gruppe sein), und ein Privileg, das den Zugriff auf den Service nur über diese Rollenzugehörigkeit erlaubt.
Der Aufruf ohne Token kommt dann mit einer HTML-Seite zurück mit sinngemäßem Inhalt "Unauthorized", mit Token funktioniert der Aufruf dann wieder wie weiter oben getestet.
Viel Erfolg und Spaß beim Spiel mit JWTs!
Authors
Marcel Boermann-pfeifer
Master Principal Solution Engineer
Doing System Integration, Data Integration, System Integration Frameworks and Data Integration Frameworks since so many years and moved from Transaction Monitors to Application Servers to Cloud Infrastructure, Kubernetes and AI - all in the name of Oracle.