Um das Benutzererlebnis der cloudbasierten IAM Lösung OneLogin komplett zu machen, muss irgendwann halt auch mal der Logout folgen.
Das hatte ich bisher nicht so wirklich auf dem Schirm, schließlich heißt es ja "OneLogin" und nicht "OneLogout".
Der Logout stellte uns erstmal vor große Probleme, denn irgendwie wollte kein Ansatz so richtig funktionieren. Irgendwann hatte ich dann aber die richtigen Informationsteile richtig zusammengepuzzelt und dann war es super einfach.
Konfiguration OneLogin
In der Konfiguration im OneLogin unter OIDC -> Configuration -> Post Logout Redirect URI setzen.
In der Applikation selbst muss eigentlich nichts konfiguriert werden.
Es muss lediglich ein Link aufgerufen werden.
In der Konfiguration des vorgeschalteten Reverse Proxies ist folgender Block für den OIDC-Connector:
#this is required by mod_auth_openidc
OIDCCryptoPassphrase <INSERT-HERE a-random-secret>
OIDCProviderMetadataURL <INSERT-HERE>
OIDCClientID <INSERT-HERE>
OIDCClientSecret <INSERT-HERE>
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI https://www.meinetolleanwendung.de/private/redirect_uri
## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"
Wir benötigen die OIDCRedirectURI und fügen lediglich ein ?logout für den logout an, gefolgt von = https://www.meinetolleanwendung.de/logoutseite.html für den Redirect zur Post-Logoutseite.
Der Fehler trat auf, nachdem ich meinen PoC von meinem Entwicklungsrechner in die Cloud transferierte und dort laufen ließ und dort testen wollte.
Auf der Console des Servers sieht der Aufruf mittels "curl http://localhost/private/index.html" auch erstmal gut aus.
Der Zugriff aus dem Browser mittels IP "http://<server-ip>/private/index.html" schlägt mit obiger Fehlermeldung komplett fehl.
Lokal nachstellen
Um das Problem lokal auf dem Entwicklerlaptop nachstellen zu können, muss erstmal die lokale (Docker-)IP ermittelt werden:
ping host.docker.internal
Ping wird ausgeführt für host.docker.internal [192.168.2.149] mit 32 Bytes Daten:
Antwort von 192.168.2.149: Bytes=32 Zeit<1ms TTL=128
Antwort von 192.168.2.149: Bytes=32 Zeit<1ms TTL=128
Antwort von 192.168.2.149: Bytes=32 Zeit<1ms TTL=128
Der Aufruf der PRIVATE Page, nach OneLogin Login versteht sich, funktioniert über localhost:
Der Aufruf der PRIVATE Page funktioniert nicht über die IP:
Auszug aus dem Logfile des Apache HTTP Servers:
reverseproxy_1 | [Thu Jun 09 13:00:54.048377 2022] [auth_openidc:error] [pid 256:tid 140563693147904] [client 172.24.0.1:33902] oidc_authenticate_user:
the URL hostname (localhost) of the configured OIDCRedirectURI does not match the URL hostname of the URL being accessed (192.168.2.149): the "state" a
nd "session" cookies will not be shared between the two!, referer: http://192.168.2.149/
reverseproxy_1 | 172.24.0.1 - - [09/Jun/2022:13:00:54 +0000] "GET /private/index.html HTTP/1.1" 500 531
Der Fehler besagt also, "the URL hostname (localhost) of the configured OIDCRedirectURI does not match the URL hostname of the URL being accessed (192.168.2.149)".
OneLogin Konfigurationsanpassung
Die Redirect URIs Liste muss um die IP des Servers erweitert werden:
Allerdings ist http als Protokoll lediglich für localhost zugelassen, nicht aber für alle anderen URIs, wie zB unsere Server IP oder später der Domain Name des Servers.
Die Redirect URI muss also mit https eingetragen werden.
Daraus ergibt sich allerdings auch, dass unser Server über https erreichbar sein muss!
Apache mit HTTPS
Ein selbstsigniertes Zertifikat ist für unseren PoC vollkommen ausreichend. Eine Signierung durch zB Let's Encrypt ist nicht notwendig.
Für diesen PoC brauchen wir lediglich den Reverse Proxy, der im ersten Schritt eine Seite über http und https ausliefern kann. Später kommt dann die Authentifizierung mit OneLogin hinzu.
Schritt 1: http
Das Dockerfile des Apache aus den vorherigen PoCs uA schon mit OpenID Module:
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
Die Einbindung der Konfigurationsdateien erfolgt über reverseproxy_httpd.conf, die in den Container als httpd.conf hineinkopiert wird:
# load original configuration first
Include conf/container_httpd.conf
# customized configuration
ServerName reverseproxy
Include conf/reverseproxy.conf
Include conf/reverseproxy-ssl.conf
Die Konfiguration für http erfolgt in reverseproxy.conf:
Die Seite ist über http://localhost:443 erreichbar. Richtig: http, nicht https! Verschlüsselt wird hier noch nix.
Schritt 3: https
Als ersten Teilschritt wird die reverseproxy-ssl.conf mit der Default-SSL-Konfiguration überschrieben und alle Kommentare gelöscht.
Folgende Änderungen werden gemacht:
ServerName localhost:443
ServerAdmin deringo@github.com
SSLEngine off
#SSLCertificateFile "/usr/local/apache2/conf/server.crt"
#SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"
Server neu starten, anschließend ist die Seite weiterhin unverschlüsselt über http://localhost:443 erreichbar.
Als nächster Teilschritt wird SSLEngine on gestellt und die beiden CertificateFile Einträge wieder ent-kommentiert. Außerdem müssen die Module mod_socache_shmcb.so und mod_ssl.so geladen werden.
Server neu durchstarten und der unverschlüsselte Aufruf wird abgewiesen:
Der verschlüsselte Aufruf über https://localhost funktioniert:
Allerdings wird die Seite als "Nicht sicher" angezeigt und es musste einmalig ein entsprechender Hinweis weggeklickt werden, da das Zertifikat nicht von einer vertrauenswürdigen Stelle signiert wurde.
Da am Ende praktisch nichts von der Standard-Konfiguration für SSL geändert wurde, wird die Konfiguration wieder vereinfacht:
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
Include conf/extra/httpd-ssl.conf
Schritt 4: OneLogin
Die OneLogin Konfiguration wird hinzugefügt:
## Default SSL
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
Include conf/extra/httpd-ssl.conf
##
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
<VirtualHost localhost:443>
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ServerName localhost:443
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase <INSERT-HERE a-random-secret>
OIDCProviderMetadataURL <INSERT-HERE>
OIDCClientID <INSERT-HERE>
OIDCClientSecret <INSERT-HERE>
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI https://localhost/private/redirect_uri
## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"
<Location /private/>
AuthType openid-connect
Require valid-user
</Location>
</VirtualHost>
Alle Seiten sind über https erreichbar.
Aber leider auch die Private Page OHNE dass vorher ein Login über OneLogin erfolgen musste.
Irgendwie klappt das nicht mit dem überschreiben/erweitern der default SSL Konfiguration. Also doch alles in eine Datei:
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
Listen 443
SSLCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLProxyCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLHonorCipherOrder on
SSLProtocol all -SSLv3
SSLProxyProtocol all -SSLv3
SSLPassPhraseDialog builtin
SSLSessionCache "shmcb:/usr/local/apache2/logs/ssl_scache(512000)"
SSLSessionCacheTimeout 300
##
## SSL Virtual Host Context
##
<VirtualHost _default_:443>
# General setup for the virtual host
ServerName localhost:443
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ErrorLog /proc/self/fd/2
TransferLog /proc/self/fd/1
# SSL Engine Switch:
# Enable/Disable SSL for this virtual host.
SSLEngine on
SSLCertificateFile "/usr/local/apache2/conf/server.crt"
SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"
<FilesMatch "\.(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
<Directory "/usr/local/apache2/cgi-bin">
SSLOptions +StdEnvVars
</Directory>
BrowserMatch "MSIE [2-5]" \
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0
CustomLog /proc/self/fd/1 \
"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase <INSERT-HERE a-random-secret>
OIDCProviderMetadataURL <INSERT-HERE>
OIDCClientID <INSERT-HERE>
OIDCClientSecret <INSERT-HERE>
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI https://localhost/private/redirect_uri
## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"
<Location /private/>
AuthType openid-connect
Require valid-user
</Location>
</VirtualHost>
So funktioniert es wie gewünscht und die Private Page wird erst nach Login angezeigt.
Allerdings nur über localhost. Nicht über die IP.
Die OIDCRedirectURI wird von https://localhost/private/redirect_uri auf https://192.168.2.149/private/redirect_uri geändert und schon funktioniert es genau anders herum: Nur über die IP, nicht über localhost.
Das ist nicht ganz so, wie ich mir das vorgestellt habe, aber für den PoC sollte es ausreichend sein.
Schritt 5: Nur https
Alles was über http rein kommt, soll über https weitergeleitet werden:
Die Generierung der Zertifikate erfolgte im laufenden Container. Dieser Schritt soll schon beim Erzeugen des Images erfolgen.
Ein Problem war, dass dies interaktiv erfolgen musste. Mit dem Parameter -subj "/" können Key & Zertifikat ohne Interaktion generiert werden. Im Subject könnten Daten eingetragen werden, dies ist aber für den PoC gar nicht erforderlich.
Einmal der Befehl vollständig, wie er im Container ausgeführt werden könnte:
Wir wollen unsere Anwendung durch einen Filter schützen, der nur Anfragen von eingeloggten Benutzern mit der richtigen Rolle hindurch lässt.
Für diesen PoC bauen wir einen Reverse Proxy für den Login und einen Anwendungsserver für den Filter. Der Anwendungsserver bekommt einen Zugang über den RP und einen Zugang ohne RP, um so zu zeigen, dass nur über den RP auf die Anwendung zugegriffen werden kann.
In produktiven Umgebungen darf es den Zugang ohne RP so ungeschützt nicht geben, da es sehr einfach ist, zB über ein Browser-Plugin, den RP mittels selbstgesetzter Header zu faken.
Reverse Proxy
Der Reverse Proxy bekommt grundsätzlich die gleiche Konfiguration wie in den Beispielen zuvor.
Hier müssen wir aber noch den Scope params hinzufügen, um so die OneLogin-Rollen des Benutzers zu übertragen:
## OIDCScope params
## to put params including roles into header
OIDCScope "openid email profile groups params"
Im nächsten Fenster des Dialoges die Value ("User Roles") setzen:
Access Filter
Der Access Filter prüft, ob eine UserID vorhanden ist. Das ist in unserem Beispiel die Email, es könnte aber auch der Preferred Username genommen werden.
Des weiteren prüft der Access Filter, ob der Benutzer die erforderliche Rolle user hat.
Sind beide Bedingungen erfüllt, kann auf die Anwendung zugegriffen werden, ansonsten wird lediglich "Access denied" angezeigt.
Im Docker File des Anwendungsservers kopieren wir erst nur das Maven POM, lassen dann Maven bauen, um so die Abhängigkeiten herunterzuladen. Erst danach kopieren wir das gesamte Projekt in den Container und lassen die Anwendung bauen. So wird nach Code Änderungen, ohne Anpassungen in Maven, der Bau des Images beschleunigt, da nicht jedes Mal die Bibliotheken heruntergeladen werden müssen.
FROM tomcat:8.5-jdk8-openjdk-slim
RUN apt update && apt install -y \
maven
COPY pom.xml /app/pom.xml
WORKDIR /app
RUN mvn package
COPY . /app
RUN mvn package
WORKDIR $CATALINA_HOME
RUN mv /app/target/ROOT.war webapps
EXPOSE 8080
CMD ["catalina.sh", "run"]
Test
Zugriff auf den Anwendungsserver über den Reverse Proxy nach Anmeldung:
http://localhost/private/test.html
Direkter Zugriff auf den Anwendungsserver ohne Anmeldung:
Im vorherigen Post haben wir gesehen, dass ein neuer Benutzer sich selbst registrieren kann, aber um den Zugang zur Anwendung zu erhalten, er noch durch einen User-Admin freigeschaltet/der Anwendung hinzugefügt werden muss.
Das Hinzufügen des Benutzers erfolgt in OneLogin, so dass der User-Admin auch in diesem Tool geschult werden müsste.
Um den Schulungsaufwand zu vermeiden und ein flüssigeres Arbeiten, ohne Wechsel der Anwendungen, zu ermöglichen, wollen wir einige Funktionalität von OneLogin in unserer Anwendung ermöglichen. Dazu gibt es in OneLogin eine API-Schnittstelle, die wir mit einem Java-Client ansteuern werden.
Der Java-Client logt sich einmalig mit Client ID und Secret (API Credentials! siehe unten) ein und erhält so ein Access Token, mit dem die weiteren API-Funktionen ausgeführt werden können.
Die API-Dokumentation, für Version 2, findet sich hier: https://developers.onelogin.com/api-docs/2/getting-started/dev-overview
Vorbereitung / Daten sammeln
API-Subdomain
Die API-Domain des Testprojekt ist: deringo-dev.onelogin.com
Die API-Sub-Domain lautet foglich: deringo-dev
API ID & Secret
Für die Benutzung der API wird Client ID und Client Secret benötigt. Das kennen wir schon aus einem vorherigen Post zu OneLogin:
Für den Zugriff über die API benötigen wir aber ein anderes Client ID & Secret Paar! Dieses findet sich in dem Developers-Tab unter API Credentials.
Dort ist ein entsprechender Zugang einzurichten, für das Test-Projekt haben wir mit Manage All eingerichtet:
OneLogin App ID
Die ID unserer Test-Anwendung findet sich in der URL nachdem man auf Applications -> Applications die App auswählt, in unserem Beispiel "1739301":
Java Programm aufsetzen
Abhängigkeiten
Das Projekt wird für Java 8 aufgesetzt und benötigt Apache HttpClient 4.5 und Json:
Zum Testen der API-Funktionen wird eine Test-Klasse mit den oben gesammelten Informationen angelegt und dann jeweils eine eigene Methode in der Klasse verwendet.
public class TestMain {
private static final String ONELOGIN_SUBDOMAIN = "deringo-dev";
// OneLogin Administration -> Applications -> select Application, get ID from URL
private static final String ONELOGIN_APP_ID = "1739301";
// OneLogin Administration -> Developers -> API Credentials
private static final String ONELOGIN_CLIENT_ID = "12345<geheim>67890";
private static final String ONELOGIN_CLIENT_SECRET = "12345<geheim>67890";
public static void main(String[] args) {
TestMain main = new TestMain();
main.sayHello();
}
private void sayHello() {
System.out.println("Hello World!");
}
}
Wenn zB nur die Rollen für unsere Test-App angezeigt werden sollen und der Rollen Name ein "u" enthalten muss, setzt sich der resourceURL-String wie folgt zusammen:
Wollen wir uns anstelle der gesamten User-Information lediglich Vor- und Nachname zurückgeben lassen, neben der ID, dann brauchen wir lediglich den Query Parameter anpassen:
Seitenweises Vor- oder Zurückblättern ist möglich, dazu gibt es den Parameter "cursor". Im Header der ersten Paginierten Response gibt es, falls vorhanden, After-Curser und Before-Curser. Über diese Werte kann vor- und zurückgeblättert werden.
Beispielsweise um die erste Seite zu erhalten und anschließend eine Seite weiter blättern:
Es soll zuerst ein neuer User angelegt werden. Anschließend bearbeitet, der Anwendung hinzugefügt und wieder entfernt und abschließend gelöscht werden.
HTTP/1.1 201 Created
{"firstname":null,"updated_at":"2022-06-01T14:34:52.423Z","role_ids":[],"invitation_sent_at":null,"member_of":null,"distinguished_name":null,"email":null,"id":178924216,"password_changed_at":null,"manager_ad_id":null,"group_id":null,"invalid_login_attempts":0,"phone":null,"title":null,"preferred_locale_code":null,"department":null,"samaccountname":null,"lastname":null,"custom_attributes":{"my_role":null},"created_at":"2022-06-01T14:34:52.423Z","directory_id":null,"locked_until":null,"status":7,"company":null,"activated_at":null,"last_login":null,"trusted_idp_id":null,"manager_user_id":null,"username":"min.requirements","comment":null,"external_id":null,"state":1,"userprincipalname":null}
new User ID: 178924216
Nächstes Beispiel mit Vor-, Nach- und Usernamen, Passwort und schönerem JSON:
HTTP/1.1 201 Created
{"firstname":"Happy","updated_at":"2022-06-01T14:43:30.646Z","role_ids":[],"invitation_sent_at":null,"member_of":null,"distinguished_name":null,"email":null,"id":178924824,"password_changed_at":"2022-06-01T14:43:30.629Z","manager_ad_id":null,"group_id":null,"invalid_login_attempts":0,"phone":null,"title":null,"preferred_locale_code":null,"department":null,"samaccountname":null,"lastname":"Gilmore","custom_attributes":{"my_role":null},"created_at":"2022-06-01T14:43:30.646Z","directory_id":null,"locked_until":null,"status":1,"company":null,"activated_at":"2022-06-01T14:43:30.644Z","last_login":null,"trusted_idp_id":null,"manager_user_id":null,"username":"happy.gilmore","comment":null,"external_id":null,"state":1,"userprincipalname":null}
new User ID: 178924824
Unser neuer User Happy Gilmore ist angelegt und kann sich mit seinem Username & Passwort auch anmelden, kommt aber, wie erwartet, noch nicht auf die Anwendung, da er ihr noch nicht zugewiesen wurde:
HTTP/1.1 200 OK
{"firstname":"Very Happy","state":1,"manager_user_id":null,"department":null,"comment":null,"created_at":"2022-06-01T14:43:30.646Z","email":null,"last_login":"2022-06-01T14:46:10.795Z","lastname":"Gilmore","activated_at":"2022-06-01T14:43:30.644Z","directory_id":null,"updated_at":"2022-06-01T14:57:22.656Z","group_id":null,"samaccountname":null,"status":1,"userprincipalname":null,"trusted_idp_id":null,"title":null,"manager_ad_id":null,"invalid_login_attempts":0,"locked_until":null,"phone":null,"role_ids":[],"invitation_sent_at":null,"company":null,"distinguished_name":null,"external_id":null,"password_changed_at":"2022-06-01T14:43:30.629Z","preferred_locale_code":null,"id":178924824,"member_of":null,"username":"happy.gilmore","custom_attributes":{"my_role":null}}
User ID: 178924824
Firstname: Very Happy
User einer Anwendung hinzufügen
Als nächstes sollte der User einer Anwendung hinzugefügt werden, um so den Zugang zu gewähren.
Überraschenderweise gibt es aber keine API, um einer App einen User hinzuzufügen. irgendwie ist das nicht ganz nachzuvollziehen, da dies über die OneLogin-Webseite möglich ist und es einen API Call gibt, um alle User einer App anzeigen zu lassen (List App Users - OneLogin API).
Ein Erklärungs- und somit Lösungsansatz lautet:
The proper way to do this is to assign applications to a Role in the admin console and then use the APIs to set the roles for the user.
This, in turn grants the user access to the application assigned to the role.
Die Überprüfung in der OneLogin Webseite ergibt, dass der User in der Tat hinzugefügt wurde, und es wurden auch keine vorhandenen User aus der Rolle entfernt, die nicht in dem Array enthalten waren.
Unser neuer User Happy Gilmore ist angelegt und kann sich mit seinem Username & Passwort auch anmelden, kommt jetzt auf die Anwendung, da er ihr über die Rolle "user" zugewiesen wurde:
Die Registrierung ist erreichbar unter: https://deringo-dev.onelogin.com/self-registration2?name=http://localhost
Die Email:
Nach klicken auf den Link erscheint folgender Text:
Es wird auf einen Login-Screen weitergeleitet. Allerdings wurde noch kein Passwort vergeben.
Währenddessen wurde die Mail zum Setzen des Passworts geschickt:
Über den Link wird das Passwort gesetzt und anschließend kann man sich einloggen in OneLogin:
Zugang in die Anwendung - 1. Versuch
Der Zugang zur Anwendung wird verwehrt:
Die Benutzerin Erika Mustermann konnte authentifiziert werden, ist aber für die Anwendung nicht authorisiert.
Benutzer Zugang zur Anwendung gewähren
Übersicht der Users:
Erika Mustermann auswählen und auf Applications gehen:
Dort wird sie der Applikation hinzugefügt.
Zugang in die Anwendung - 2. Versuch
Der Zugang zur Anwendung wird jetzt gewährt:
Admin benachrichtigen
Um den Workflow rund zu machen, benötigen wir noch eine Benachrichtigung an die User-Admins, sobald sich ein neuer Benutzer registriert hat.
Den Reiter Activity -> Notifications auswählen.
Eine neue Notification anlegen.
In Conditions als Event Self registration created (203) auswählen. Es gibt mehrere Events mit Self registration, da mir nicht klar ist, wann genau welcher Event ausgelöst wird, habe ich den 203 gewählt, der klingt so, als ob er getriggert wird, sobald die SR vollendet ist.
Als Action Email user mit Custom Field, so können wir die Email-Adresse selbst bestimmen, zB für einen Support-Postkorb.
Oben kann man der Notification Regel auch noch einen Titel vergeben, dort, wo im Screenshot Untitled steht
Nach dem Speichern der Notification hat ein neuer Benutzer Happy Gilmore den Registrierungsprozess durchlaufen. Dabei wurde aber keine Email verschickt.
Über Activity -> Events kann man die Events sehen, die dabei ausgelöst wurden:
In der Notification wird der Event auf Self registration approved (101) geändert, dann der neue Benutzer gelöscht, so dass er sich wieder neu registrieren kann.
Nachdem der Benutzer sich erneut registriert hat, wird diesmal eine Email verschickt:
Die Mail enthält den Text (Subject & Body), der vorher in der Notification hinterlegt wurde. Leider gibt es keinen Hinweis, welcher User sich registriert hat.
Ein Blick in die bereits vordefinierten Notifications zeigt, dass es Variablen gibt und diese in doppelten geschweiften Klammern gesetzt sind.
Ich hatte zuerst versucht, den Reverse Proxy über scale zu vervielfältigen, aber das funktionierte nicht, da jeder RP den selben Hostnamen zugewiesen bekommt. Laut Forenkommentaren soll man das Problem wohl mittels Scripte oder Docker Swarm lösen können, für dieses kleine Projekt war es hingegen völlig ausreichend, den Block für den RP zu duplizieren.
Load Balancer
Neu hinzugekommen ist der den beiden RPs vorgeschaltete Load Balancer.
Die Regel, nach der das Loadbalancing erfolgt, ist hier nicht relevant und wird nicht explizit gesetzt.
Der erste Versuch, bei dem die Anfragen abwechselnd auf den RPs verteilt werden funktioniert für die public Pages.
Der Login bei OneLogin funktioniert, aber nicht das öffnen der Seite. Anscheinend harmoniert der OneLogin Flow nicht mit diesem Setup, es scheint so, als ob die Antwort der Anfrage von RP1 an OneLogin von RP2 erhalten wird, dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP1 erhalten wird , dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP2 erhalten wird , dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP1 erhalten wird, [...]
Es ist also notwendig, das wir immer auf dem selben RP landen. Das Load Balancing darf nur einmal am Anfang statt finden.
Um das zu erreichen, setzten wir einen Header, der die Route zum RP enthält und setzten die Session sticky.
Im nächsten Schritt möchte ich verschiedene Netzwerke und Server verwenden.
Der User kommt aus dem Internet und geht über den Load Balancer in die DMZ, in der er über die RPs Zugang zu den Public Servern hat und nach Authentifizierung über OneLogin (Internet) gelangt er in das Interne Netz wo er Zugang auf den ShowHeaders und die Privaten Server hat.
Vorbereitet wird auch schon die Authorisierung über die RPs und OneLogin: Falls der Benutzer die Rolle user hat, bekommt er Zugang auf den User Server, falls er die Rolle admin hat, bekommt er Zugang auf den Admin Server.
In der Docker Compose Datei werden all die Server in der Services Sektion angelegt und den jeweiligen Netzwerken zugewiesen, in der darauf folgenden Networks Sektion definiert werden:
Das Public Netzwerk muss angelegt werden, anschließend kann Docker Compose gestartet werden:
docker network create public_network
docker-compose up
Test
Wie zuvor: Über die Logausgaben in dem Terminalfenster, in dem Docker Compose gestartet wurde, kann man gut nachvollziehen, welche Server aufgerufen werden.
Vor dem Login werden über Load Balancer und RPs die Seiten der beiden Public Server angezeigt.
Nach dem Login werden auch die Private und ShowHeaders Seiten angezeigt.
Außerdem werden auch die Seiten der User und Admin Server angezeigt. Das sollte nur erfolgen, wenn der eingeloggte Benutzer auch die entsprechenden Rollen hat, wird aber momentan noch nicht abgefragt. Die Umsetzung wird weiter unten beschrieben, sobald ich herausgefunden habe, wie sie zu implementieren ist.
Der Zugang zu den Seiten der User und Admin Server soll nur mit entsprechenden Rollen erfolgen.
Die Implementierung ist noch offen.
UPDATE: Inzwischen konnte ich mit einem Experten für OneLogin sprechen und wurde aufgeklärt, dass es seitens OneLogin gar nicht vorgesehen ist, dass die Anwendungs-Rollen in OneLogin gepflegt werden.
Folglich kann keine Authorisierung durch den RP mit OneLogin erfolgen.
In dem Setup hat jeder eingeloggte Benutzer Zugriff auf alle privaten Seiten.
Jetzt wird das Setup erweitert, so dass es Seiten gibt, die für eingeloggte Benutzer verfügbar sind und Seiten, die nur für Administratoren verfügbar sind.
In OneLogin gibt es, soweit ich das sehen konnte, keine einzelnen Rechte, sondern nur Rollen.
Rollen anlegen
In OneLogin zwei Rollen anlegen:
user
admin
Dazu in der OneLogin Administration auf Users -> Roles -> New Role gehen und dort die Rollen anlegen, dabei direkt der App zuweisen.
Unter Users -> Users -> User auswählen, dort unter Applications die Rollen hinzufügen:
Zu meiner Überraschung werden die Rollen von OneLogin nicht mit übergeben:
Auf Applications, Application auswählen, dort auf Access und role-specivic policy:
Im letzten Post habe ich mir OneLogin angeschaut und zwei Javascript Beispiele zum laufen gebracht.
In diesem Post möchte ich einen Reverse Proxy aufbauen, der eine öffentlich zugängliche Seite bereit stellt und eine private Seite nur für eingeloggte Mitglieder.
OneLogin
Auf der OneLogin Applications Seite sammle ich folgende Informationen ein, die später in der ReverseProxy Konfiguration benötigt werden:
Client ID
Client Secret
Issuer URL
Außerdem wird der Token Endpoint auf Basic gesetzt.
In der Configuration muss eine Redirect URI eingetragen werden, in diesem Fall: http://localhost/private/redirect_uri
Reverse Proxy
Den Reverse Proxy wird mit Docker aufgebaut.
Der Reverse Proxy wird eine Startseite bereit stellen und von dort auf eine öffentlich zugängliche Unterseite und einen geschützten Bereich verlinken.
In der ersten Version wird der geschützte Bereich lediglich eine weitere Unterseite sein.
In der zweiten Version wird ein weiterer geschützter Bereich mit der ShowHeaders App hinzugefügt.
In Eclipse habe ich zuerst das neue GitHub-Repository hinzugefügt und ausgechecked, dann händisch .project angelegt und konnte dann in Eclipse über Import das Project hinzufügen. Fühlt sich viel zu umständlich an, aber ich muss das zum Glück nicht so oft machen, als dass ich dem jetzt weiter auf dem Grund gehen müsste, wie das besser geht.
Es wird ein Docker Image angelegt, das wiederum über Docker-Compose gestartet wird, um Dateien des Filesystems einzubinden. Das ist für die Entwicklung leichter, am Ende könnte man natürlich alles in ein Image packen und starten.
Das Docker Image basiert auf dem offiziellen Apache HTTPD Image. Es wird mod_auth_openidc hinzugefügt, sowie ca-certificates um eine verschlüsselte Verbindung per HTTPS zum OneLogin-Server aufbauen zu können. Die Datei für den mod_auth_openidc muss noch an die richtige Stelle verschoben werden und ein Backup der originalen httpd.conf angelegt werden.
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
Im Docker-Compose wird das Image gebaut und die HTML Seiten sowie Konfigurationsdateien eingebunden.
Dabei wird die Datei reverseproxy_httpd.conf als httpd.conf eingebunden und über diese Datei wird die zuvor gesicherte, originale httpd.conf und anschließend die reverseproxy.conf geladen. Das ist eine einfache Möglichkeit, die ursprüngliche Konfiguration zu erhalten. Für ein produktives Setup ist das vermutlich nicht die beste Wahl.
Die Variable ${PWD} ist unter Linux verfügbar, daher starte ich den Container unter Windows WSL.
Die Datei reverseproxy_httpd.conf (bzw. httpd.conf im Container) ist simpel aufgebaut und enthält nur die Includes zur ursprünglichen httpd.conf und zu unserer reverseproxy.conf:
# load original configuration first
Include conf/container_httpd.conf
# customized configuration
ServerName reverseproxy
Include conf/reverseproxy.conf
Apache HTTPD Konfiguration
Die in der OneLogin Seite eingesammelten Werte müssen entsprechend in die Konfiguration eingetragen werden.
Geschützt wird der Bereich, der unter /private liegt.
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
<VirtualHost *:80>
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ServerName localhost
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase a-random-secret-used-by-apache-oidc-and-balancer
OIDCProviderMetadataURL https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
OIDCClientID geheim-a91c-013a-175a-02471d082b0b208817
OIDCClientSecret wirklich-ganz-geheim
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI http://localhost/private/redirect_uri
# maps the email/prefered_username claim to the REMOTE_USER environment variable
OIDCRemoteUserClaim email
#OIDCRemoteUserClaim preferred_username
<Location /private>
AuthType openid-connect
Require valid-user
</Location>
</VirtualHost>
Im laufenden Docker Container den Apache neu durchstarten:
apachectl -t && apachectl restart
Testen
In einem neuen Browserfenster, im Inkognito Modus die Seite öffnen: http://localhost.
Der Link Index Page führt auf diese Index-Seite, der Public Page Link auf die öffentlich zugängliche Seite und Private Page auf die Seite, die nur für OneLogin User zugänglich ist.
Die Public Page:
Die Private Page führt im ersten Schritt zum OneLogin Login:
Erst nach erfolgreichem Login sehen wir die private Seite:
Reverse Proxy - mit ShowHeaders
Der Reverse Proxy schreibt einige Informationen in den Header, diese werden aber nur dem Server gesendet, der Client (zB unser Webbrowser) sieht davon nichts. Um sehen zu können, welche Informationen übermittelt werden, verwende ich eine kleine App, die nichts anderes macht, als die Header anzuzeigen, daher auch der Name ShowHeaders.
Bisher existierte noch kein Dockerfile für ShowHeaders, daher habe ich das für diesen Test entwickelt und hinzugefügt:
FROM tomcat:8.5-jdk8-openjdk-slim
RUN apt update && apt install -y \
maven
RUN git clone https://github.com/DerIngo/ShowHeaders.git
WORKDIR ShowHeaders
RUN mvn package
WORKDIR $CATALINA_HOME
RUN mv ShowHeaders/target/ROOT.war webapps
EXPOSE 8080
CMD ["catalina.sh", "run"]
ShowHeaders wird in die Docker Konfiguration mit aufgenommen:
Die OIDCRemoteUserClaim-Konfiguration scheint keinen Einfluss zu haben:
# maps the email/prefered_username claim to the REMOTE_USER environment variable
OIDCRemoteUserClaim email
#OIDCRemoteUserClaim preferred_username
Auf der ShowHeaders-Seite werden oidc_claim_email und oidc_claim_preferred_username angezeigt. Hingegen wird keine Header REMOTE_USER angezeigt.
Das Entfernen der OIDCRemoteUserClaim-Konfiguration hat auch keinen Einfluss auf die angezeigten Header.
Anscheinend macht diese Konfiguration nicht das, was ich erwartet hatte, daher entferne ich sie wieder. Weitere Recherchen dazu sind für diesen Test nicht notwendig, daher belasse ich es dabei.
Weitere Informationen zur Konfiguration des Mod Auth OpenIDC finden sich in der kommentierten Beispielkonfiguration auf GitHub.
GitHub
Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version1 und version2 zu finden.
mkdir test
cd test
git clone https://github.com/onelogin/onelogin-oidc-node.git
cd '.\onelogin-oidc-node\2. Implicit Flow\'
npm install
--> npm ERR! Unexpected token '.'
Es kommt natürlich ein Fehler, da die Anwendung noch nicht konfiguriert ist:
Application in OneLogin anlegen
Auf der OneLogin Seite (https://deringo-dev.onelogin.com/) unter Applications auf Add gehen und eine OpenId Connect App anlegen.
Name, Beschreibung und (schnell gemaltes) Square Icon vergeben und speichern.
Im darauf folgenden Screen ist mein Square Icon leider nicht mehr vorhanden. Dafür steht dort aber ein Feld Tab, in dem etwas steht, was dort definitiv nicht stehen sollte (Name eines Kunden, woher kommt das denn??)
Ich ignoriere das erstmal und notiere mir die Client ID unter SSO.
Ich muss noch die Redirect URI zu meiner App (http://localhost:3000) eintragen:
Anwendung konfigurieren
Die Client ID und meine OneLogin-Subdomain (deringo-dev) trage ich in der main.js ein.
Ich kann mich mit meinen OneLogin-Developer-Zugangsdaten einloggen.
Das Ausloggen gestaltet sich bisher als unmöglich: Weder der Neustart des Servers, noch das Löschen des Local, bzw. Session Storages führt zum erneuten Login. Cookies sind keine gesetzt und können daher nicht gelöscht werden.
Über ein neues, anonymes Browserfenster lässt sich ein erneuter Login erzwingen.
Neuer User für die Beispielanwendung
Auf der OneLogin Seite (https://deringo-dev.onelogin.com/) unter Users auf New User gehen und einen neuen Benutzer anlegen.
Ein Passwort lässt sich anscheinend nicht über die OneLogin Seite für den Benutzer setzen. Für die Passwort-Wiederherstellung wird eine Email-Adresse benötigt, das ist zumindest der einzig wählbare Authentifizierungsfaktor. Ich muss meinem neuen Benutzer auch eine Email Adresse vergeben.
Das nachträgliche setzen der Email Adresse hat nicht funktioniert. Ich habe dann einen weiteren neuen Benutzer angelegt, diesmal ohne Username, dafür aber mit Email Adresse. Mit diesem Benutzer konnte ich dann das Passwort "Zurück"setzen lassen und mich anschließend einloggen.
Der Login mit meinem Benutzer funktionierte problemlos.
Allerdings wurden anschließend auf der Sample Seite NICHT die Benutzer Daten angezeigt.
Ein Blick in die Console des Browsers zeigt den Fehler:
Da die minified Version des oidc-client.js nicht sonderlich leserlich ist, lade ich die nicht-minified Version herunter und speichere sie neben der minified Version im Projekt ab.
In der Datei index.hbs wird das oidc-client Script importiert, also passe ich dort die Referenz an.
Aber auch das debuggen mit der nicht-minified Version brachte keinen weiteren Erkenntnisgewinn.
Die Lösung fand sich schließlich in der OneLogin Konfiguration: Im SSO Teil der Applikation musste der Token Endpoint auf "None (PKCE)" umgestellt werden.