Categories
Development Java

OneLogin API-Zugriff mit Java

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:

<properties>
  <maven.compiler.source>8</maven.compiler.source>
  <maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
  <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
  <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.json/json -->
  <dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20220320</version>
  </dependency>
</dependencies>

Programm Rumpf

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!");
	}
}

Access Token

Für die API Calls wird ein Access Token benötigt, wie dieser zu bekommen ist ist hier beschrieben: Generate OAuth 2.0 Tokens v2| OneLogin Developers

private String getAccessToken() {

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpPost request = new HttpPost(String.format("https://%s.onelogin.com/auth/oauth2/v2/token", ONELOGIN_SUBDOMAIN));

  String credentials = String.format("%s:%s", ONELOGIN_CLIENT_ID, ONELOGIN_CLIENT_SECRET);
  byte[] encodedAuth = Base64.getEncoder().encode(credentials.getBytes());
  String authHeader = "Basic " + new String(encodedAuth);

  request.setHeader("Authorization", authHeader);
  request.addHeader("Content-Type", "application/json");
  request.setEntity(new StringEntity("{ \"grant_type\": \"client_credentials\" }", "UTF-8"));

  try {
    CloseableHttpResponse reponse = client.execute(request);

    String content = EntityUtils.toString(reponse.getEntity());

    JSONObject json = new JSONObject(content);

    String accessToken = json.getString("access_token");
    return accessToken;

  } catch (IOException e) {
    e.printStackTrace();
    return null;
  }
}

List App Users

Use this API to return a list of users that are assigned to an App.

Doku: List App Users – OneLogin API

Sample Code:

private void listAppUsers() {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/apps/%s/users", ONELOGIN_SUBDOMAIN, ONELOGIN_APP_ID);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpGet request = new HttpGet(resourceURL);
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  try {
    CloseableHttpResponse reponse = client.execute(request);

    String content = EntityUtils.toString(reponse.getEntity());

    JSONArray jsonArray = new JSONArray(content);
    jsonArray.forEach(o -> System.out.println(o));

  } catch (IOException e) {
    e.printStackTrace();
  }
}

List Roles

Use this API to return a list of roles.
This endpoint supports pagination and sorting.

Doku: List Roles (onelogin.com)

private void listRoles() {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/roles", ONELOGIN_SUBDOMAIN);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpGet request = new HttpGet(resourceURL);
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  try {
    CloseableHttpResponse reponse = client.execute(request);

    String content = EntityUtils.toString(reponse.getEntity());

    JSONArray jsonArray = new JSONArray(content);
    jsonArray.forEach(o -> System.out.println(o));

  } catch (IOException e) {
    e.printStackTrace();
  }
}

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:

String resourceURL = String.format("https://%s.onelogin.com/api/2/roles?app_id=%s&name=u", ONELOGIN_SUBDOMAIN, ONELOGIN_APP_ID);

Query Parameter

Es gibt Query Parameter, die anscheinend von vielen Funktionen unterstützt werden: Fields, Search, Pagination & Sort

Doku: Using Query Parameters – OneLogin Developers

Als Beispiel erweitern wir das List App Users Beispiel indem wir lediglich die resourceURL anpassen und für die Query Parameter vorbereiten:

String queryParameter = "";
String resourceURL = String.format("https://%s.onelogin.com/api/2/apps/%s/users%s", ONELOGIN_SUBDOMAIN, ONELOGIN_APP_ID, queryParameter);

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:

String queryParameter = "?fields=firstname,lastname";

Suche nach allen Benutzern mit dem Nachnamen “Mustermann”:

String queryParameter = "?lastname=Mustermann";

Was NICHT funktioniert: Suche nach allen Benutzern mit dem Nachnamen “Mustermann” und dem Vornamen “Erika”:

// funktioniert NICHT:
String queryParameter = "?lastname=Mustermann,firstname=Erika";

Ups, eigene Dummheit, wenn man es richtig macht, dann geht es; Die Parameter werden natürlich mit einem “&” concatiniert und nicht mit einem “,”:

// funktioniert NICHT:
String queryParameter = "?lastname=Mustermann&firstname=Erika";

Alle Benutzer mit Vorname “Erika”, der Nachname soll mit “Muster” anfangen und wir brauchen nur Vor- und Nachname:

String queryParameter = "?lastname=Muster*&firstname=Erika&fields=firstname,lastname";

Seite 1 alle Benutzer, absteigend sortiert nach Nachnamen, bei einer Seitengröße von 1:

String queryParameter = "?page=1&limit=1&sort=-lastname";

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:

// erster Aufruf:
String queryParameter = "?page=1&limit=1";
[...]
String afterCursor = response.getFirstHeader("After-Cursor").getValue();
[...]

// zweiter Aufruf
queryParameter = "?cursor=" + afterCursor;

kleines User Management

Es soll zuerst ein neuer User angelegt werden. Anschließend bearbeitet, der Anwendung hinzugefügt und wieder entfernt und abschließend gelöscht werden.

Create User

Doku: Create User – OneLogin Developers

Erstmal ein minimales Beispiel:

private void createUser() throws Exception {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/users", ONELOGIN_SUBDOMAIN);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpPost request = new HttpPost(resourceURL);
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  String json = "{\"username\":\"min.requirements\"}";
  StringEntity entity = new StringEntity(json);
  request.setEntity(entity);

  try {
    CloseableHttpResponse response = client.execute(request);
    System.out.println(response.getStatusLine());
    String content = EntityUtils.toString(response.getEntity());
    System.out.println(content);
    JSONObject o = new JSONObject(content);
    System.out.println("new User ID: " + o.get("id"));
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Consolen Ausgabe:

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:

private void createUser() throws Exception {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/users", ONELOGIN_SUBDOMAIN);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpPost request = new HttpPost(resourceURL);
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  JSONObject json = new JSONObject()
    .put("firstname", "Happy")
    .put("lastname", "Gilmore")
    .put("username", "happy.gilmore")
    .put("password", "helloworld123")
    .put("password_confirmation", "helloworld123");
  StringEntity entity = new StringEntity(json.toString());
  request.setEntity(entity);

  try {
    CloseableHttpResponse response = client.execute(request);
    System.out.println(response.getStatusLine());
    String content = EntityUtils.toString(response.getEntity());
    System.out.println(content);
    JSONObject o = new JSONObject(content);
    System.out.println("new User ID: " + o.get("id"));
  } catch (IOException e) {
    e.printStackTrace();
  }
}
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:

Update User

Doku: Update User – OneLogin Developers

private void updateUser(Integer id) throws Exception {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/users/%d", ONELOGIN_SUBDOMAIN, id);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpPut request = new HttpPut(resourceURL);
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  JSONObject json = new JSONObject()
    .put("firstname", "Very Happy");
  StringEntity entity = new StringEntity(json.toString());
  request.setEntity(entity);

  try {
    CloseableHttpResponse response = client.execute(request);
    System.out.println(response.getStatusLine());
    String content = EntityUtils.toString(response.getEntity());
    System.out.println(content);
    JSONObject o = new JSONObject(content);
    System.out.println("User ID: " + o.get("id"));
    System.out.println("Firstname: " + o.get("firstname"));
  } catch (IOException e) {
    e.printStackTrace();
  }
}
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.

java – Onelogin API to add applications to user – Stack Overflow

Wir haben ja noch die Rolle “user”, der die App zugeordnet ist, aus einem vorhergehendem Versuch. Die sollten wir nehmen können.

Man kann es im Screenshot nicht sehen, aber in der URL ist die ID der Rolle enthalten: 546920

Add User to Role

Doku: Add Role Users – OneLogin API

private void addUserToRole(Integer roleId, Integer userId) throws Exception {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/roles/%d/users", ONELOGIN_SUBDOMAIN, roleId);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpPost request = new HttpPost(resourceURL);
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  StringEntity entity = new StringEntity("[" + userId + "]");
  request.setEntity(entity);

  try {
    CloseableHttpResponse response = client.execute(request);
    System.out.println(response.getStatusLine());
    String content = EntityUtils.toString(response.getEntity());
    System.out.println(content);
  } catch (IOException e) {
    e.printStackTrace();
  }
}
HTTP/1.1 200 OK
[{"id":178924824}

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:

Remove User from Role

Doku: Remove Role Users – OneLogin API

private void removeUserFromRole(Integer roleId, Integer userId) throws Exception {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/roles/%d/users", ONELOGIN_SUBDOMAIN, roleId);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpPost request = new HttpPost(resourceURL) {
    @Override
    public String getMethod() {
      return "DELETE";
    }
  };
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  StringEntity entity = new StringEntity("[" + userId + "]");
  request.setEntity(entity);

  try {
    CloseableHttpResponse response = client.execute(request);
    System.out.println(response.getStatusLine());
  } catch (IOException e) {
    e.printStackTrace();
  }
}
HTTP/1.1 204 No Content

Die Überprüfung in der OneLogin Webseite ergibt, dass der User aus der Rolle entfernt wurde.

Interessant ist an dem Code, dass die Klasse HttpDelete des Apache HttpClients keinen Content im Body vorsieht.

Um trotzdem ein DELETE mit Body absenden zu können, war der einfachste Weg, ein HttpPost zu nehmen und dessen Methode zu überschreiben:

HttpPost request = new HttpPost(resourceURL) {
  @Override
  public String getMethod() {
    return "DELETE";
  }
};
[...]
request.setEntity(entity);

Delete User

Doku: Delete User – OneLogin Developers

private void deleteUser(Integer userId) throws Exception {
  String resourceURL = String.format("https://%s.onelogin.com/api/2/users/%d", ONELOGIN_SUBDOMAIN, userId);

  CloseableHttpClient client = HttpClientBuilder.create().build();

  HttpDelete request = new HttpDelete(resourceURL);
  request.setHeader("Authorization", "bearer " + getAccessToken());
  request.addHeader("Content-Type", "application/json");

  try {
    CloseableHttpResponse response = client.execute(request);
    System.out.println(response.getStatusLine());
  } catch (IOException e) {
    e.printStackTrace();
  }
}
HTTP/1.1 204 No Content

Damit ist Happy Gilmore Geschichte und auch in der Überprüfung auf der OneLogin Webseite nicht mehr zu finden.

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version5 zu finden.

Categories
Development

OneLogin Self Registration

Konfiguration

Users -> Self Registration

New Profile

Profil Name vergeben: DemoSelfRegistrationProfile

Configuration auf Enabled setzen.

URL setzen auf: http://localhost

Registrierung

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:

Email Adressen sind unkenntlich gemacht

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.

Getestet wurden folgende Ausdrücke:

{{user.name}}
{{user.email}}
{{user}}
{{user.id}}
{{user.unbekanntesAttribut}}

In der Email standen dann folgende Werte:

Happy Gilmore
happy.gilmore@mymail.de
{{unknown_tag:user}}
{{unknown_tag:user.id}}
{{unknown_tag:user.unbekanntesAttribut}}

Über Name und Email lässt sich der User gezielt suchen.

Eine Dokumentation verfügbarer Variablen findet sich: <TBD, noch nicht gefunden.>

Categories
Development Java

NTLM mit Java

Um Daten von einem bestimmten Webservice beziehen zu können, muss der Aufruf durch einen authentifizierten User erfolgen. Der Webservice steht im Intranet des Kunden und der Aufruf im Browser funktioniert ohne Authentifizierung, denn im Hintergrund wird der Windows User übermittelt.

Der Aufruf über Java funktioniert nicht, da kein User übermittelt wird.

Die Authentifizierung am Webservice erfolgt lt. Ansprechpartner durch eine Windows Authentifizierung, was technischer ausgedrückt NTLM sein sollte.

Test Projekt aufsetzen

Um den Zugang zu testen ohne dabei den ganzen Ballast der großen Anwendung mitschleppen zu müssen, wird ein neues Projekt zum Testen aufgesetzt.

Das Projekt wird auf Java 8 konfiguriert und kommt mit einer einzigen Abhängigkeit aus: Dem Apache HTTPClient 4.5

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

Test 1

public static void test01() throws Exception {

  DefaultHttpClient httpclient = new DefaultHttpClient();
  httpclient.getAuthSchemes().register("ntlm", new NTLMSchemeFactory());
  httpclient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
                                                     new NTCredentials(username, password, "", ""));
  HttpGet httpGet = new HttpGet(url);
  httpGet.addHeader("accept", "application/json;odata=verbose");
  httpGet.addHeader("content-Type", "application/json;odata=verbose");
  httpGet.getParams().setBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false);
  // HttpResponse response = httpclient.execute(httpGet);
  // System.out.println("Responseeee" + response.getStatusLine());
  try (CloseableHttpResponse response2 = httpclient.execute(httpGet)) {
    StatusLine statusLine = response2.getStatusLine();
    System.out.println(statusLine.getStatusCode() + " " + statusLine.getReasonPhrase());
    String responseBody = EntityUtils.toString(response2.getEntity(), StandardCharsets.UTF_8);
    System.out.println("Response body: " + responseBody);
  }
}

Der Code funktioniert, wirft aber noch eine WARNING mit aus:

Mai 30, 2022 4:15:35 PM org.apache.http.client.protocol.RequestTargetAuthentication process 
WARNING: NEGOTIATE authentication error: No valid credentials provided (Mechanism level: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt))
200 OK
Response body: [{"Vkorg":"","VkorgDesc":"TEST Korea Limited"}]

Test 2

    public static void test02() throws Exception {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(18);
        cm.setDefaultMaxPerRoute(6);

        RequestConfig requestConfig = RequestConfig.custom()
        .setSocketTimeout(30000)
        .setConnectTimeout(30000)
        .setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.NTLM))
        .setProxyPreferredAuthSchemes(Arrays.asList(AuthSchemes.BASIC))
        .build();

        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
                new NTCredentials(username, password, "", ""));

        // Finally we instantiate the client. Client is a thread safe object and can be used by several threads at the same time. 
        // Client can be used for several request. The life span of the client must be equal to the life span of this EJB.
        CloseableHttpClient httpclient = HttpClients.custom()
        .setConnectionManager(cm)
        .setDefaultCredentialsProvider(credentialsProvider)
        .setDefaultRequestConfig(requestConfig)
        .build();
        
        HttpGet httpGet = new HttpGet(url);            
        // HttpClientContext is not thread safe, one per request must be created.
        HttpClientContext context = HttpClientContext.create();    
        try ( CloseableHttpResponse response = httpclient.execute(httpGet, context) ) {
            StatusLine statusLine = response.getStatusLine();
            System.out.println(statusLine.getStatusCode() + " " + statusLine.getReasonPhrase());
            String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            System.out.println("Response body: " + responseBody);
        }
    }

Der Code funktioniert und wirft keine Warnung mehr aus.

Categories
Development Linux

Reverse Proxy mit OneLogin hinter LoadBalancer

Die Version des Reverse Proxy mit OneLogin wird um einen vorgelagertem Load Balancer und einem zweiten Reverse Proxy erweitert:

Docker Compose

version: '3.8'
services:

  loadbalancer:
    build: ./loadbalancer
    hostname: loadbalancer
    volumes:
      - ${PWD}/loadbalancer/conf/loadbalancer_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/loadbalancer/conf/loadbalancer.conf:/usr/local/apache2/conf/loadbalancer.conf
    ports:
      - 80:80

  reverseproxy_1:
    build: ./reverseproxy
    hostname: reverseproxy_1
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf

  reverseproxy_2:
    build: ./reverseproxy
    hostname: reverseproxy_2
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf


  showheaders:
    build: ./showheaders
    hostname: showheaders     

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.

[...]
    <Proxy "balancer://myreverseproxy">
        BalancerMember http://reverseproxy_1:80
        BalancerMember http://reverseproxy_2:80
    </Proxy>
    
    ProxyPass /          balancer://myreverseproxy/
    ProxyPassReverse /   balancer://myreverseproxy/
[...]

Die geschützten Seiten ließen sich nicht öffnen.

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.

[...]
    ## Header for LoadBalancer
    Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED


    #define loadbalancer for Applicationserver
    <Proxy "balancer://myreverseproxy">
        BalancerMember http://reverseproxy_1:80 route=reverseproxy_1
        BalancerMember http://reverseproxy_2:80 route=reverseproxy_2
        ProxySet stickysession=ROUTEID
    </Proxy>

    ProxyPass /          balancer://myreverseproxy/
    ProxyPassReverse /   balancer://myreverseproxy/

[...]

Test

Über die Logausgaben in dem Terminalfenster, in dem Docker Compose gestartet wurde, kann man gut nachvollziehen, welche Server aufgerufen werden:

In der ShowHeaders Anwendung können wir sehen, welche ROUTEID gesetzt wurde und über den x-forwarded-host können wir die Route nachvollziehen.

In obigem Beispiel sind wir über RP2 gekommen. Wenn wir den ROUTID Cookie auf RP1 ändern, wird die nächste Anfrage über RP1 gerouted.

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version3 zu finden.

Getrennte Netze

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:

version: '3.8'
services:

  loadbalancer:
    build: ./loadbalancer
    hostname: loadbalancer
    volumes:
      - ${PWD}/loadbalancer/conf/loadbalancer_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/loadbalancer/conf/loadbalancer.conf:/usr/local/apache2/conf/loadbalancer.conf
    networks:
      public_network:
      dmz_network:
    ports:
      - 80:80

  reverseproxy_1:
    build: ./reverseproxy
    hostname: reverseproxy_1
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
    networks:
      dmz_network:
      private_network:

[...]

  admin: 
    image: httpd:2.4
    hostname: admin
    volumes:
      - ./admin/public_html:/usr/local/apache2/htdocs
    networks:
      private_network:

networks:
  public_network:
    external: true
  dmz_network:
    external: false
  private_network:
    external: false

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.

GitHub

Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version4 zu finden.

Authorisierung

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.

Categories
Development

Reverse Proxy mit rechtebasiertem Zugriff

Im letzten Post habe ich einen Reverse Proxy mit OneLogin aufgebaut.

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:

TBD

Das hat noch nicht weiter geholfen.

Frage auf Stack Overflow gestellt: apache – Role based authorization with mod_auth_openidc and OneLogin – Stack Overflow

UPDATE: Es geht nicht

Wie ich inzwischen erfahren durfte, ist es gar nicht in OneLogin vorgesehen, dass die Anwendungs-Rollen in OneLogin gepflegt werden.

Die Authorisierung muss also (vorerst) innerhalb der Anwendung erfolgen.

Categories
Development Linux

Reverse Proxy mit OneLogin

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.

GitHub

Es wird ein Projekt auf GitHub für diesen Post angelegt: DockerOneLoginApacheSample

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.

Apache Module OpenID

Für den Apache HTTP wird mod_auth_openidc verwendet.

Docker

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.

version: '3.8'
services:

  reverseproxy:
    build: ./reverseproxy
    hostname: reverseproxy
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
    ports:
      - 80:80

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>

Befehle

Docker Container starten:

docker-compose up

In den laufenden Docker Container einloggen:

docker exec -it dockeroneloginapachesample_reverseproxy_1 bash

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:

version: '3.8'
services:

  reverseproxy:
    build: ./reverseproxy
    hostname: reverseproxy
    volumes:
      - ./reverseproxy/public_html:/usr/local/apache2/htdocs
      - ${PWD}/reverseproxy/conf/reverseproxy_httpd.conf:/usr/local/apache2/conf/httpd.conf
      - ${PWD}/reverseproxy/conf/reverseproxy.conf:/usr/local/apache2/conf/reverseproxy.conf
    ports:
      - 80:80

  showheaders:
    build: ./showheaders
    hostname: showheaders
    ports:
      - 8080:8080

Die Reverse Proxy Konfiguration erweitern:

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>


    # showheaders block
    ProxyPass        /showheaders   http://showheaders:8080/
    ProxyPassReverse /showheaders   http://showheaders:8080/

    <Location /showheaders>
        AuthType openid-connect
        Require valid-user
    </Location>

</VirtualHost>

Die Startseite wurde um einen Link zu ShowHeaders erweitert:

Zuerst der OneLogin Login:

Es werden alle übertragenen Header angezeigt:

Headers:

host
	showheaders:8080
upgrade-insecure-requests
	1
user-agent
	Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
accept
	text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site
	cross-site
sec-fetch-mode
	navigate
sec-fetch-user
	?1
sec-fetch-dest
	document
sec-ch-ua
	" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"
sec-ch-ua-mobile
	?0
sec-ch-ua-platform
	"Windows"
accept-encoding
	gzip, deflate, br
accept-language
	de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
cookie
	mod_auth_openidc_session=13dc8a76-10b9-479f-94b1-53d7ddb760e2
oidc_claim_sub
	175995661
oidc_claim_email
	Max@Mustermann.de
oidc_claim_preferred_username
	Max@Mustermann.de
oidc_claim_name
	Max Mustermann
oidc_claim_nonce
	mXShr6JxWX49umdsGUDe2l1zmkB0eYs9Vx7Jm1We38Q
oidc_claim_at_hash
	RBPJPDQuzxFJUembbLFdLg
oidc_claim_sid
	24f09d55-0a19-4a15-9446-010c84ff4461
oidc_claim_aud
	22a153c0-a91c-013a-175a-02471d082b0b208817
oidc_claim_exp
	1652179601
oidc_claim_iat
	1652172401
oidc_claim_iss
	https://deringo-dev.onelogin.com/oidc/2
oidc_access_token
	eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVcIsIMtpZCI6IkpSY080bnhzNWpnYzhZZE43STJoTE80Vl9xbDFiZG9pTVhtY1lnSG00SHMifQ.eyJqdGkiOiJHWlJac0YyVVhrd0xrZEl3WGplNEoiLCJzdWIiOiIxNzU5OTU2NjEiLCJpc3MiOiJodHRwczovL2RlcmluZ28tZGV2Lm9uZWxvZ2luLmNvbS9vaWRjLzIiLCJpYXQiOjE2NTIxNzI0MDEsImV4cCI6MTY1MjE3NjAwMSwic2NvcGUiOiJvcGVuaWQiLCJhdWQiOiIyMmExNTNjMC1hOTFjLTAxM2EtMTc1YS0wMjQ3MWQwODJiMGIyMDg4MTcifQ.barxBngs7jirZS3nAsYMpsqBdwybrmuqzCtP1dTiwJxEkbQsRe77Z0xqdGRmXWG8sr6s65omAcyF8ZaacY51OMQiVmEriL9MUxBTG3Z4-noO9C0nq6wmMrwHBls5CG9BPhnoc-rWMB_fvsFELiP0WZk5FM4iV-POqUkJgCNTKLbyBjpkm4y6Q4IUBgCpqmuRCtgfq0jMDrXpxLkiKkya7UymtGref2pL6UurD5r0VLwvU75lV927SUyxCKnUIDOY7Mdv05BzBfrCg6KBibWunGuY9wh6xs9WWreBlu21JcZxdgYL8Vx_MPfXrTjfU-0sjhACaqn5h0nnCrwcLoK8Hw
oidc_access_token_expires
	1652176004
x-forwarded-for
	172.24.0.1
x-forwarded-host
	localhost
x-forwarded-server
	localhost
connection
	Keep-Alive

Auffälligkeit

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.

Version1 ist ohne, Version2 ist mit ShowHeaders.

Categories
Development

OneLogin

Ich setzte mich mit der cloudbasierten IAM Lösung OneLogin auseinander und möchte zum Einstieg deren simpelste Beispiel-Anwendung zum laufen bringen.

Die Entwicklerseite von OneLogin: OneLogin Developers: APIs, Documentation & Tutorials

OneLogin Developer Account erstellen

Auch für das einfachste Beispiel wird ein Account benötigt, hierzu kann ein kostenfreier Entwickler Account angelegt werden.

Ich habe mich dabei für den Domain Namen deringo-dev.onelogin.com entschieden.

Beispielanwendung

Am einfachsten scheint mir eine OpenID Connect basierte Javascript Anwendung zu sein, daher wähle ich aus den Samples die Single Page App heraus.

Zuerst Node.js updaten.

Projekt clonen und ausführen:

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 '.'

Das fängt ja mal wieder gut an 🙁

nvm use 14.17.3
npm install

Mit der alten Version hat es dann funktioniert. 🙂

npm start

Die Anwendung lässt sich jetzt über http://localhost:3000/ aufrufen.

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.

//
// OIDC Client Configuration
//
const ONELOGIN_CLIENT_ID = 'geheim-a91c-013a-175a-02471d082b0b208817';
const ONELOGIN_SUBDOMAIN = 'deringo-dev';

Die Anwendung lässt sich über http://localhost:3000/ aufrufen:

Jetzt erscheint eine Login-Seite:

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.

First-, Last-, Username eingetragen.
Beispiel-App (SinglePageApp) hinzugefügt.

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.

Beispielanwendung 2

Nach dem Implicit flow, der nicht mehr empfohlen ist, schaue ich mir den Auth Code Flow + PKCE an, der als Best Practice gilt.

Aus dem bereits geclonten Git Repository ist das das 4. Projekt.

cd '.\4. Auth Flow - PKCE\'
npm install
npm start

Die Client ID und meine OneLogin-Subdomain (deringo-dev) trage ich in der main.js ein.

//
// OIDC Client Configuration
//
const ONELOGIN_CLIENT_ID = 'geheim-a91c-013a-175a-02471d082b0b208817';
const ONELOGIN_SUBDOMAIN = 'deringo-dev';

Die Anwendung lässt sich über http://localhost:3000/ aufrufen:

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.

Categories
AWS Development

Domainname für EC2

Auf die zuvor erstellten EC2 Instanz soll ein “menschenlesbarer” Domainname den Zugang erleichtern.

Ich möchte im AWS Ökosystem bleiben und daher die Domain über Amazon Route 53 registrieren. Ansonsten hätte ich vielleicht einen anderen Anbieter gewählt, wie ich es schon für eine günstige Website gemacht hatte.

Wahl des Domainnamens

Das Projekt wird, zumindest auf meiner Infrastruktur, mutmaßlich nicht allzulange bestehen bleiben. Eine große Marktrecherche für einen tollen Namen brauche ich daher nicht, nur einprägsam sollte er sein.

Die wichtigste Anforderung ist ein günstiger Preis.

Die Preisübersicht auf der AWS Seite ist nicht sonderlich übersichtlich, eine “route 53 cheapest domain” zu googeln brachte aber auch nur den Link auf ein PDF zu Tage. In dem steht uA der “Registration and Renewal Price” und der ist für den TLD Namen “click” mit 3 Dollar am günstigsten.

Allerdings ist “click” nicht der beste Name im deutschsprachigen Raum: “Hey, besuch doch mal meine Seite meineApp.click” “Ich kann meineApp.klick nicht finden”.

Der zweitgünstigste TLD Name mit 5 Dollar ist “link”. “link” ist mir lieber als “click” und ist von der Preisdifferenz vertretbar.

Nach kurzem Brainstorming habe ich mich dann für den Namen “freigabe” und der TLD “link”, also http://freigabe.link entschieden.

Domainname registrieren

Auf die Seite des Dienstes Route 53 gehen und dort die “Domain registration” aufrufen und die gewünschte Domain eingeben:

Ab in den Shopping cart und … im nächsten Schritt muss ich meine Daten eingeben? Hey Amazon, die habt ihr doch schon!

Anschließend wird die Domain auf mich registriert, was leider bis drei Tage dauern kann.

Bis zum Abschluss der Registrierung wird hier pausiert, anschließend geht es weiter mit der

Anbindung Domain Name an EC2

Die Registrierung der Domain war zum Glück bereits nach drei Stunden abgeschlossen und nicht erst nach drei Tagen. Negativ ist zu erwähnen, dass die 5 Dollar für den Domain Namen netto sind, also noch mal 19% USt hinzu kommen.

Auf der Route 53 Seite über Domains > Registered domains die Domain freigabe.link auswählen:

Über Manage DNS geht es in die Hosted zone der Domain:

Über Create record wird der Eintrag gesetzt, dass der Domain Name auf die Public IP des EC2-Servers zeigen soll:

Nachdem ich den Web-Server gestartet hatte, funktionierte es auch sofort.

Der Web-Server war heruntergefahren. Ob ich das gestern noch gemacht hatte, weiß ich nicht mehr 100%ig.
In dem Catalina Log vom Tomcat fand sich uA folgender Eintrag:

Invalid character found in the request target [/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=md5&vars[1][]=HelloThinkPHP21 ]. The valid characters are defined in RFC 7230 and RFC 3986

Vielleicht gab es zu viele dieser Hacking Versuche?

Als nächstes kommt noch ein Reverse Proxy davor, der kann noch etwas Traffic vom Tomcat fern halten.
Vielleicht werde ich aber auch noch eine WAF vor den Server setzen? Eine kurze Recherche zu dem Thema ergab allerdings, dass das nicht direkt möglich ist, sondern ein Application Load Balancer oder CloudFront zwischengeschaltet werden muss.

EMail

Ein Nebenschauplatz ist das Thema email, so dass ich Mails an diese Domain empfangen bzw. versenden kann.

Das Thema ist leider nicht ganz so simpel gelöst, wie ich es mir erhofft hatte. Einen simplen “AWS Mail Service”, den man über Route 53 konfigurieren kann, gibt es nicht. Es gibt mit Amazon Workmail eine SaaS Lösung mit Focus auf Unternehmen und entsprechender Kostenstruktur.

Weiterhin wird Google Apps verschiedentlich empfohlen, aber auch das ist mit Kosten verbunden und wird nicht über die kostenfreien Angebote abgedeckt.

Eine SES / S3 Lösung deckt nur rudimentär den Bedarf, zB werden die Mails als Dateien auf einem S3 Bucket gespeichert. Da scheinen auch noch andere Konstellationen möglich zu sein, aber keine, die überzeugt.

Als kostenfreie WebMail-Lösung wird zB Zoho empfohlen. Eine Anleitung findet sich zB hier.

Es wäre natürlich auch möglich, einen eigenen WebMail-Server auf einem eigenen EC2 Server zu betreiben.

Als Mittelweg wäre auch ein weiter Docker Container auf dem vorhandenen EC2 Server möglich.

Komplettlösungen als Mailserver wären zB Mailcow, Mailu oder Kopano.

Jede Lösung ist mit mehr oder weniger Aufwand realisierbar, aber jede Lösung ist aufwändiger als meine momentane Motivation, oder aktueller Bedarf, und so setzte ich das erstmal auf die “wenn mal Zeit ist”-Liste.

Categories
AWS Development Linux

Docker Anwendung in AWS (EC2)

In meinem letzten Blogeintrag habe ich eine geDockerte Anwendung auf einem Server mit Ubuntu 18 zum laufen gebracht. Aus verschiedenen Gründen war das aber nur ein Zwischenschritt, um zu testen, ob die Anwendung grundsätzlich in solch einer Umgebung lauffähig ist. Neben den beschriebenen Problemen gab es noch viele weitere, die gelöst werden mussten.

Als nächsten Schritt möchte ich die Anwendung in die AWS umziehen, immerhin bin ich ja inzwischen ein zertifizierter Cloud Practitioner.

AWS User

Mit dem Stammbenutzer einen neuen IAM Nutzer für die Anwendung anlegen. Dieser bekommt erstmal umfangreiche Rechte, was nicht best Practice ist und später sollte ich diese Rechte auf das unbedingt benötigte zurücksetzen.

EC2 Server

Die Anwendung soll erstmal mit dem Docker Setup auf einem EC2 Server laufen.

Mit dem neuen IAM Nutzer wechsele ich zuerst auf die Europa Zone ec-central-1.

Ich lege eine neue EC2 Server Instanz an, wobei ich als Sparfuchs nach “nur kostenloses Kontingent” filtere und ein AMI für Ubuntu Server 20.04 LTS (x64) und Typ t2.micro auswähle.
Es wird ein neues Schlüsselpaar erzeugt und ich speichere den privaten Schlüssel.

Über EC2 > Instances > Server-Instanz auswählen.

Über Verbinden lässt sich im Browser ein Terminal öffnen. Hier lässt sich aber auch am einfachsten die öffentliche IP und vor allem der Benutzername finden:

Ich habe allerdings nicht die Web Shell verwendet, sondern die Daten, sowie den privaten Schlüssel genommen, um eine Verbindung in WinSCP einzurichten. So kann ich später leicht die Daten auf den Server kopieren und per Klick eine PuTTY-Shell öffnen.

Port Freigabe

Standardmäßig ist für den Server nur Port 22 für SSH frei gegeben.

Weitere Ports, wie zB der benötigte HTTP Port 80 oder HTTPS 443, lassen sich über die AWS Management Console frei geben.

Die EC2-Server-Instanz auswählen und unter Sicherheit findet sich die Sicherheitsgruppe:

In der Sicherheitsgruppe können die Regeln für den eingehenden Datenverkehr erweitert werden.
Dabei ist zu beachten, dass man weitere Regeln hinzufügen muss und nicht den bestehenden Typ SSH auf zB HTTP ändert und speichert, weil das diesen nur ändert und nicht als neue, weitere Regel hinzufügt. Dann kann man zwar die Seiten des Webservers bewundern, aber sich nicht mehr per SSH einloggen.

Server einrichten

Auf der Linux Konsole des EC2-Servers wird dieser eingerichtet, dazu wird Docker Compose installiert, was als Abhängigkeit Docker mitbringt.

apt list --upgradable
sudo apt update
sudo apt upgrade -y
sudo apt install docker-compose -y

sudo docker version         # -> 20.10.7
sudo docker-compose version # -> 1.25.0
sudo service docker status  # -> running

sudo docker run hello-world

Docker läuft und es werden die Daten der Anwendung auf den Server kopiert und anschließend über Docker Compose gestartet.

sudo docker-compose up 

Leider führte das zu einem Fehler, wie er schon bei der Ubuntu 18 Installation aufgetreten ist. Das zuvor gewonnene Wissen kann ich jetzt zur schnellen Fehlerbehebung anwenden:

sudo apt-get remove docker-compose -y
sudo curl -L https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
# Output:
-bash: /usr/bin/docker-compose: No such file or directory
# Lösung: neue Shell, zb per tmux, starten
# und dann nochmals testen
docker-compose --version
# Output:
Docker Compose version v2.2.3

Anschließend ließ sich die Anwendung per Docker Compose starten und per cURL, bzw. HTTPie, über localhost:80 und <öffentlicheIP>:80 aufrufen. Der Aufruf <öffentlicheIP>:80 vom Entwickler Laptop funktioniert auch.

Der Start dauerte etwas länger, die Webanwendung selbst ließ sich anschließend aber angenehm schnell bedienen. Zumindest als Test-Server scheint der “Gratis”-EC2-Server völlig auszureichen.

Ausblick

Auf dem kostenfreien Server laufen ein Tomcat Webserver, eine PostgreSQL Datenbank und PGAdmin und das, zumindest den ersten Tests nach, mit völlig ausreichender Performance.

Als nächstes möchte ich dem Docker Compose Konstrukt noch um einen Reverse Proxy erweitern, der eine (vermutlich nur selbstsignierte) verschlüsselte Verbindung per HTTPS anbietet und über Port 80 und 443 die Anwendung und den PGAdmin erreichbar macht. Außerdem soll es einen einfachen Authentifizierungs- und ggf. Authorisierungsmechanismus geben. Das wird mit einem Apache HTTP Server realisiert werden und sollte keinen besonderen Ressourcenbedarf haben.

Falls sich die Zeit findet, möchte ich das um Keycloak erweitern und den Zugriff auf Anwendung und PGAdmin erst nach erfolgreicher Authentifizierung und Authorisierung erlauben. Vielleicht ist das noch mit dem Apache HTTP Server realisierbar, ggf. werde ich aber auf zB Traefik umstellen.
Bei dem Setup kann ich mir schon vorstellen, dass die Ressourcen des kleinen Server nicht mehr ausreichen und es zu spürbaren Performanceeinbrüchen kommen wird.

Eine ansprechendere URL, anstelle der generierten AWS URL, wäre wünschenswert.

Categories
Development Linux

Docker Compose und Ubuntu 18

Docker Compose Datei vom Entwicklungsrechner auf den Server kopieren, kleinere Anpassungen vornehmen und ausführen. So einfach habe ich es mir vorgestellt, aber es gab dann leider doch noch Herausforderungen zu bewältigen:

Docker Compose Updaten

Ich habe einen Server mit dem nicht mehr ganz taufrischen Ubuntu 18 am laufen und wollte dort ein Docker Compose Skript ausführen.

Das Skript läuft auf meinem Entwicklungsrechner, aber auf dem Server wurde lediglich eine Fehlermeldung ausgegeben:

dockeruser@myServer:~/myproject$ docker-compose up
ERROR: Version in "./docker-compose.yml" is unsupported. You might be seeing this error because you're using the wrong Compose file version. Either specify a supported version (e.g "2.2" or "3.3") and place your service definitions under the `services` key, or omit the `version` key and place your service definitions at the root of the file to use version 1.
For more on the Compose file format versions, see https://docs.docker.com/compose/compose-file/

Wie sich herausstellte, war für Ubuntu 18 bei Docker Compose 1.17.1 Schluss und ich muss händisch upgraden:

Docker Compose entfernen:

sudo apt-get remove docker-compose

Die aktuelle Docker Compose Version ermitteln (heute: 2.2.3): https://github.com/docker/compose/releases

Auf dieser Seite kann man auch den direkten Link zum Download finden, falls es beim ausführen des nächsten Befehls zu Problemen kommt.

Beispielsweise ist die Versionsnummer v2.2.3, also mit einem kleinen “v” am Anfang und wenn das fehlt, schlägt der Download fehl.

So lautet der Link für mein Ubuntu:
https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64

Der Befehl zum Download:

sudo curl -L https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

Rechte setzen:

sudo chmod +x /usr/local/bin/docker-compose

Installation und Version checken:

docker-compose --version
# Output:
Docker Compose version v2.2.3

Docker Compose ohne GUI

Mit der neuesten Docker Compose Version gibt es einen neuen Fehler:

error getting credentials - err: exit status 1, out: `Cannot autolaunch D-Bus without X11 $DISPLAY`

Die Erklärung lautet:

Looks like this is because it defaults to use the secretservice executable which seems to have some sort of X11 dependency for some reason. If you install and configure pass docker will use that instead which seems to solve the problem.

Zu erst muss die aktuelle Version des Docker Credential Helpers ermittelt werden: v0.6.4

Install the Docker Credential Helper for pass:

# substitute with the latest version
url=https://github.com/docker/docker-credential-helpers/releases/download/v0.6.4/docker-credential-pass-v0.6.4-amd64.tar.gz

# download and untar the binary
wget $url
tar -xzvf $(basename $url)

# move the binary to a dir in your $PATH
sudo mv docker-credential-pass /usr/local/bin

# verify it works
docker-credential-pass list

# cleanup
rm docker-credential-pass-v0.6.4-amd64.tar.gz

Install and configure pass:

sudo apt install pass

# create a gpg2 key
gpg2 --gen-key

Und der nächste Fehler:

gpg: agent_genkey failed: Keine Berechtigung
Schlüsselerzeugung fehlgeschlagen: Keine Berechtigung

Und eine Erklärung mit Lösungsvorschlag findet sich hier:

Expected behavior. Here’s why.
At the point of failure, gen-key is about to ask the user for a passphrase. For security purposes, rather than using stdin/stdout, it wants to directly open the controlling terminal for the session and use that handle to write the prompt and receive the passphrase. When you use su to switch to some other user, the owner of the controlling terminal device file does not change; it remains associated with the user who actually logged in (i.e. received a real terminal from getty or got a pty from telnet or ssh or whatever). That device file is protected mode 600, so it can’t be opened by anyone else.

The solution is to sudo-chown the device file to the user-who-needs-to-gen-the-key before su’ing to that user. Create the key within the su’d environment, then exit back to the original environment. Then, finally, sudo-chown the terminal back to yourself.

Glücklicherweise geht es auch einfacher, indem man einfach das Programm tmux verwendet. 🙂

tmux

# create a gpg2 key
gpg2 --gen-key

# list key information
gpg2 -k

# Copy the key id (from the line labelled [uid]) and do
pass init "whatever key id you have"

Jetzt sollte der Docker Login funktionieren, aber:

docker login
# Output:
Error saving credentials: error storing credentials - err: fork/exec /usr/local/bin/docker-credential-pass: permission denied, out: ``

Auch wieder kein neues Problem, dass zB bereits hier und hier diskutiert wurde.

mkdir ~/.docker
touch ~/.docker/config.json
# brachte jeweils keine Änderung


/usr/local/bin/docker-credential-pass
# Output:
-bash: /usr/local/bin/docker-credential-pass: Keine Berechtigung


# Erfolg kam mit diesem Befehl:
sudo chmod +x /usr/local/bin/docker-credential-pass

#Zumindest funktioniert dieser Aufruf:
docker-credential-pass list


# Ein weiterer Fehler ließ sich beheben durch:
export GPG_TTY=$(tty)

Ich musste die einzelnen Images per docker pull imagename ziehen, erst danach konnte ich docker-compose ausführen.