Sichern einer Anwendung mit Spring Security 5.0

1. Hinzufügen der Dependencies

"org.springframework.boot:spring-boot-starter-sercurity",
"org.springframework.security:spring-security-oauth2-resource-server"

Mit dem Hinzufügen der Dependency spring-boot-starter-sercurity werden automatisch alle Endpoints gesichert.

Es muss spring-security-oauth2-resource-server Libary eingebunden werden, damit die Anwendung mit JWT encoded Barear Token gesichert werden kann. Dieses muss deklariert werden, dann verwandelt sich die Anwendung in einen Resource Server. Wenn ein Authorization Header in einem Request vorhanden und in diesem Barear Schema und das Token vorhanden ist, dann wird das JWT geparst und festgestellt ob der Request authorisiert war oder nicht.

2. Anpassen der application.yaml

Die application.yaml muss angepasst werden. Es muss die URI angegeben werden wo der uaa Server läuft. Hier in diesem Beispiel läuft der Server auf dem Localhost auf Port 8090.

   security:
     oauth2:
       resourceserver:
         jwt:
           issuer-uri: http://localhost:8090/uaa/oauth/token 

Beim Starten der Anwendung werden von der standarisierten URI http://localhost:8090/uaa/oauth/token/.well-known/openid-configuration weitere Konfigrationseinstellungen geladen. In den Konfigurationseinstellungen wird u.a. auch die URI für jwks-Token angegeben.

Über die URI (http://localhost:8090/uaa/token_keys) kann der PublicKey geladen werden, um z.B. die Signierung durch den Authorisationsserver zu überprüfen und ob das Token seit der Generierung im Server modifiziert worden ist.

Damit ist die minimale Konfiguration abgeschlossen, um die Anwendung in einen Resourceserver zu verwandeln.

Exkurs: JWT

JWT (jawt ausgesprochen) steht für JSON Web Token. Auf jwt.io gibt es einen Decoder der die beide Teile Header und Payload base64 decodiert und als JSON darstellt. Im Header wird der Algorythmus und der Typ (z.B. JWT) angegeben. In der Payload können die Attribute (hier Claims genannt) übertragen werden. JWT wird als Bearer Token im HTTP Authorization Header übertragen. Der Header und die Payload wird als Singnatur mit RSASHA256 angehangen.

UAA

OAuth Authorization Code FLow

Resource Owner –> Application –> Authorization Server –> Resource Server

  1. Der Resource Owner (Hier der Anwender) fragt bei der Application an.
  2. Die Application frag den Anwender nach den Rechten. Dieser gewährt der Anwendung diese.
  3. Die Authentifizierung und die Authorisierung werden an den Authorisationsserver geschickt.
  4. Der Authorisationsserver schickt der Anwendung einen Authorisationscode (OTC) zu. Dieser ist kurzlebig.
  5. Nun tauscht die Anwendung den OTC Code zusammen mit den Client Credentials (Das ist die Authentifizierung der Anwendung) mit dem Authorisationsserver für das Token aus.
  6. Der Authorisationsserver antwortet mit dem Token für die Anwendung.
  7. Nun kann die Anwendung mit dem Token auf den Resource Server nach einer Resource anfragen.
  8. Der Resource Server schickt nach Validierung des Tokens die angeforderte Resource an die Anwendung.

Das Token ist mächtig und daher verbleit es im Backend der Anwendung und wird somit geschützt. Sollte das Token offengelegt (geleakt) werden, so hat man den vollen Zugriff auf die API mit Rechten des Anwenders. Deshalb sollte die Gültigkeit des Token nur kurz sein und es regelmäßig aktualisiert werden.

OAuth Client Credentials Flow

Buildship 3

Die neue Version des Buildship Plugins wird die Version 3 sein. Die aktuelle stable Version ist 2.2.1.

Offen Punkte die noch zu fixen sind, sind in den Issues zu finden.

Neue Installationsquelle hinzufügen

http://download.eclipse.org/buildship/updates/e49/snapshots/3.x/

Installation überprüfen

Die erfolgreiche Installation kann in About Eclipse überprüft werden.

Neues Projekt mit Spring Tool Suite 4 erstellen

Nach dem Neustart von Eclipse kann jetzt ein Projekt mit dem Starter erstellt werden. Es kann nun Gradle Buildship 3.x ausgewählt werden.

Spring Tools 4.4.2

Es ist mal wieder eine neue Version von den Spring Tools 4 erschienen.

  • Verbesserung: Neue actions um connect,refresh,disconnect live hover Informationen von laufenden Spring Boot Projecten zu steuern. Diese sind nun auch in den Boot Dashboard Menüs vornahen
  • Verbesserung: Über das Kontextmenü sind weitere Voreinstellungen für live hover möglich
  • Verbesserung: Die Console-View kann nun mit der Dashboard Auswahl verknüpft werden
  • Fehlerbehebung: Live hover werden wieder bei neueren Versionen von Spring Boot angezeigt, wenn sie auf CF deployt worden sind
  • Fehlerbehebung: Für Properties Keys wurde die Sortierung in @Value Annotationparametern gefixt

Weitere Änderungen

Wie immer habe ich hier nicht alle Änderugen aufgeführt. Diese könnt ihr auf der Release Info sehen.

Neues in Gradle 6.0

  • Mit Gradle 6 wird nun JDK 13 unterstützt!
  • Stark erweitertes Dependency Management
  • Die Module Metadata werden jetzt per default erstellt und publiziert
  • Out of the box Unterstüzung von JavaDoc.jar und source.jar

Erstes Update nach dem Point Release

Kurz nach dem Release 6.0 kam auch schon das erste Fix für Gradle 6.0 heraus.

Im allgemeinen wurden kleinere Fehler behoben. Weitere Informationen können sie unter den Gradle 6.0.1 Release Notes erfahren.

Wrapper updaten

./gradlew wrapper --gradle-version=6.0.1

Download

Die aktuellen Versionen von Gradle lassen sich auf der Release Page herunterladen.

Certbot und DuckDNS

Der Certbot kann die Eingerichten Domänen auch automatisch aktualisieren. Dazu ist für ausgefallene Anwendungszwecke ein Hooksystem integriert, sodass das Aktualisieren der Zertifikate auch unter besonderen Umständen funktioniert.

Wie der Titelschon beschreibt, will ich hier die Automatisation der Erneuerung der Zertifikate mit dem Certbot auf einer DuckDNS Domäne demonstrieren. In diesem Artikel habe ich die Einrichtung von DuckDNS bereits erleutert. Als Zeitgeber setze ich den Systemd Timer ein.

Manuelles ReNew

Die Zertifikate die LetsEncrypt ausstellt haben immer nur einen Gültigskeitszeitraum von genau 3 Monaten. Das heißt alle 3 Monate muss das Zertifikat erneuert und in den WebServer deployt werden, da sonst weiterhin die Aĺten abgelaufenen Zertifikate ausgeliefert werden und somit die Seiten von den Browsern als nicht vertrauenswürdig eingestuft werden.

Für DuckDNS bietet sich die Verifikationsmethode DNS01 an. Bei dieser Methode wird ein TXT Record im DNS Server erwartet. Dieses sogenannte Challenge muss vor der Überprüfung gesetzt werden. Hierfür wird das PRE Hook verwendet, um das Secret in DuckDNS zu setzen.

Die Informationen zu den Hooks kann man hier nachlesen.

Die Skripte die in den Hooks eingesetzt werden in einem Unterverzeichnis /duckdns im Ordner /etc/letsencrypt abgelegt.

Der PRE Hook

Dieses Skript wird in der Vorbereitung der Verifikation der Domäne ausgeführt. Es muss beim dem DNS01 Verfahren, also zum setzen der Challenge im DNS Server, genutzt werden. Dieses Skript hängen wir in den PRE Hook mit dem Parameter –manual-auth-hook ein. Dieser erwartet Pfad zu dem folgenden Skript…

Die Datei set_acme_challenge.sh in /etc/letsencrypt/duckdns/

#!/bin/bash

token="TTTTTTTT-TTTT-TTTT-TTTT-TTTTTTTTTTTT"

# log
echo "renewal of $CERTBOT_DOMAIN with challenge $CERTBOT_VALIDATION" 

# set acme challenge
curl -s "https://www.duckdns.org/update?domains=$CERTBOT_DOMAIN&token=$token&txt=$CERTBOT_VALIDATION"

Im Log werden in den meisten Fällen aufgeführt werden, dass das Aktualisieren nicht durchgeführt worden ist, weil das Zertifikat noch länger als 30 Tage gültig ist. Erst wenn die Gültigkeitsdauer klein 30 Tage ist, dann wird der Certbot eine Aktualisierung durchführen.

DNS Server Aufräumen

Nachdem das Challenge verifiziert worden ist, wird das Challenge auf dem DNS Server von DuckDNS nicht mehr benötigt. Das heißt es kann wieder gelöscht werden. Dazu verwenden wird den Hook –manual-cleanup-hook Parameter.

Die Datei clanup_acme_challenge.sh in /etc/letsencrypt/duckdns/

#!/bin/bash

# DuckDNS Token
token="TTTTTTTT-TTTT-TTTT-TTTT-TTTTTTTTTTTT" 

# clear acme challenge
curl -s "https://www.duckdns.org/update?domains=$CERTBOT_DOMAIN&token=$token&txt=$CERTBOT_VALIDATION&clear=true

Das aktuelle Zertifikat dem WebServer bekannt machen

Nachdem nun der Certbot das aktuelle Zertifikat bereitgestellt hat, dann muss das dem WebServer bekannt gemacht werden. Da ich den NginX WebServer einsetze, muss ich also ein systemctl reload nginx.service ausführen. Dies übergeben wir einfach direkt dem Deploy-Hook mit

--deploy-hook "systemctl reload nginx.service"

Der komplette Aufruf

Wenn wir jetzt alles zusammenführen, dann ergibt sich folgender Aufruf:

certbot renew --manual-auth-hook=/etc/letsencrypt/duckdns/set_acme_challenge.sh --manual-cleanup-hook=/etc/letsencrypt/duckdns/clear_acme_challenge.sh --deploy-hook "systemctl reload nginx.service"

Für Debuggingzwecke empfiehlt es sich sich noch den DRY RUN Modus mit dem Parameter –dry-run zu aktivieren.

Den Systemd Timer anlegen

Der certbot.service:

[Unit]
Description=Let's Encrypt renewal

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --manual-auth-hook=/etc/letsencrypt/duckdns/set_acme_challenge.sh --manual-cleanup-hook=/etc/letsencrypt/duckdns/clear_acme_challenge.sh --deploy-hook "systemctl reload nginx.service"

Der certbot.timer:

[Unit]
Description=Twice daily renewal of Let's Encrypt's certificates

[Timer]
OnCalendar=0/12:00:00
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Log

Das Log kann mit journalctl -n50 -fu certbot.service eingesehen werden:

Nov 24 14:14:04 server systemd[1]: Starting Let's Encrypt renewal...
Nov 24 14:14:07 server certbot[2160]: Saving debug log to /var/log/letsencrypt/letsencrypt.log
Nov 24 14:14:07 server certbot[2160]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nov 24 14:14:07 server certbot[2160]: Processing /etc/letsencrypt/renewal/XXXX.duckdns.org.conf
Nov 24 14:14:07 server certbot[2160]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nov 24 14:14:08 server certbot[2160]: Cert not yet due for renewal
Nov 24 14:14:08 server certbot[2160]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nov 24 14:14:08 server certbot[2160]: The following certs are not due for renewal yet:
Nov 24 14:14:08 server certbot[2160]:   /etc/letsencrypt/live/XXXX.duckdns.org/fullchain.pem expires on 2020-02-21 (skipped)
Nov 24 14:14:08 server certbot[2160]: No renewals were attempted.
Nov 24 14:14:08 server certbot[2160]: No hooks were run.
Nov 24 14:14:08 server certbot[2160]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nov 24 14:14:08 server systemd[1]: certbot.service: Succeeded.
Nov 24 14:14:08 server systemd[1]: Started Let's Encrypt renewal.

Ein Plugin für SEO

Google hat ein offizielles Plugin SiteKit für WordPress vorgestellt mit dem man einen einfachen Zugriff auf diverse Metriken von Google bekommen kann. Es werden verschiedene Dienste von Google integriert, so dass man einen schnellen überblick über die Page bekommt ohne WordPress zu verlassen.

Die Installation

Die Installation ist super einfach und nach ein paar Mausklicks auch schon abgeschlossen.

Die Metriken

Das Google SiteKit Plugin zeigt übersichtlich die Metriken von Google Analytics, Search Console und PageSpeed Insight hübsch aufbereitet an.

Resümee

Das Plugin macht – trotz der noch recht jungen Geschichte – schon einen sehr guten Eindruck und hilft dem User Zeit zu sparen, da alle wichtigen Informationen auf einen Blick angezeigt werden.

Die Lombok Anwendung kann nicht gestartet werden

Wer neuere Java Features einsetzt und daher neuere Java Versionen verwendet, kommt oftmals mit neuen Fehler die mit dem Modulsystem von dem Projekt Jigsaw zusammenhängen.

Ich verwende gerne das Lombok und auch hier zeigen sich immer noch einmal hier und dort einige Fehler die auftauchen können. Sollte Euer Programm mit folgender Fehlermeldung abbrechen:

Error occurred during initialization of boot layer
java.lang.module.ResolutionException: Module lombok does not read a module that exports org.mapstruct.ap.spi

So fehlt Lombok eine Dependency. Es muss org.mapstruct:mapstruct-processor mit aufgenommen werden. Siehe issue 2125

dependencies {
    // fix for Lombok see https://github.com/rzwitserloot/lombok/issues/2125
    implementation 'org.mapstruct:mapstruct-processor:1.3.1.Final'
}

Jetzt sollte das Programm inklusive Lombok normal gestartet werden können.

Das Problem mit dem Mixed-Content

Mixed-Content wird in den gängigen Browsern seit geraumer Zeit geblockt. Leider taucht das Problem nach dem Update von dem Docker Image von KeyCloak in der Version 7.0 nach 8.0 auf. Ein Blick mit dem Browser zeigt schnell warum die Console nicht mehr lädt.

Zuvor hatte ich das Problem innerhalb des Containers gelöst (siehe KeyCloak). Die nun gezeigte Variante halte ich für sauberer.

Wie kann das Mixed-Content Problem gelöst werden?

Wie ich in KeyCloak Artikel beschrieben habe, muss das Anlegen des Docker Containers angepasst werden. Es muss zusätzlich die Environmentvariable PROXY_ADDRESS_FORWARDING auf true gesetzt werden.

Danach lädt die Console von KeyCloak in der neusten Version wieder einwandfrei.

Eclipse und der Modulepath

Mit Java 9 wurden die Module neu eingeführt (Projekt Jigsaw). Wenn man ein Projekt mit Modulen baut, dann müssen die Module auch in dem Modulepath vorhanden sein.

Das Buildship-Plugin von Eclipse setzt leider nicht alle Module automatisch auf den Modulepath, sodass bei jeder Änderung am gradle.build Skript man von Hand die Einträge im Buildpath Konfigurationsdialog verschieben muss.

Folgendes kleines Skript erledigt die Aufgabe für uns, wenn der Gradle Task eclipse ausgeführt wird.

//
// Will be executed when gradle Task Eclipse was called
//

eclipse {
    project {
        natures 'org.eclipse.buildship.core.gradleprojectnature'
    }

    classpath {
        file {
            whenMerged {
                entries.findAll { isModule(it) }.each {
                    it.entryAttributes['module'] = 'true'
                }

                entries.findAll { isSource(it) && isTestScope(it) }.each {
                    it.entryAttributes['test'] = 'true'
                }

                entries.findAll { isLibrary(it) && isTestScope(it) }.each {
                    it.entryAttributes['test'] = 'true'
                }
            }
        }

        defaultOutputDir = file('build')
        downloadSources = true
        downloadJavadoc = true
    }
}

boolean isLibrary(entry) { return entry.properties.kind.equals('lib') }
boolean isTestScope(entry) { return entry.entryAttributes.get('gradle_used_by_scope').equals('test'); }
boolean isModule(entry) { isLibrary(entry) && !isTestScope(entry); }
boolean isSource(entry) { return entry.properties.kind.equals('src'); }