Java EE Security API

Soteria: Heilsbringer für die Sicherheit von Java EE?

Niko Köbler

©Shutterstock / Paul shuang

Mit dem Java-EE-8-Release im letzten Jahr kam nur wenig komplett Neues auf den Tisch der Enterprise-Spezifikation. Das Java EE Security API unter dem JSR-375 ist eine dieser Neuheiten. Security ist erfahrungsgemäß ein Thema, mit dem sich nur wenige Entwickler gerne intensiv und ausführlich auseinandersetzen, gab es in der Java-EE-Spezifikation in den letzten Jahren doch keinen einheitlichen Standard für die Plattform an sich.

Alle bis dato vorhandenen Möglichkeiten wie JAAS (Java Authentication and Authorization Service), JACC (Java Authorization Contract for Containers) und JASPIC (Java Authentication Service Provider Interface for Containers) waren hersteller- bzw. containerspezifische Implementierungen, die nicht oder nur schwer portiert werden konnten und kein Teil der Anwendung waren. Zusätzlich war die Verwendung von CDI innerhalb dieser Spezifikationen nicht möglich. Nicht zuletzt, weil es die existierenden Securitymöglichkeiten schon deutlich vor der Zeit gab, seit der CDI in der EE-Spezifikation enthalten ist. Wer modernere Mechanismen in seiner Anwendung verwenden wollte, musste in der Vergangenheit auf mehr oder weniger proprietäre Frameworks innerhalb der Anwendung zurückgreifen.

Die Standardisierung und Vereinheitlichung von Securitymechanismen für die Java-EE-Plattform hat sich der JSR-375, das Java EE Security API, zum Ziel gesetzt. Da die Containerkonfiguration entfällt und sich die verschiedenen Sicherheitsaspekte im Code per Annotationen setzen und konfigurieren lassen, werden Java-EE-Anwendungen dadurch kompatibler zwischen verschiedenen Application-Servern. Da außerdem keine zusätzlichen Frameworks mehr verwendet werden müssen, werden die Anwendungen leichtgewichtiger und bringen weniger Abhängigkeiten mit. Warum die JSR Expert Group aber diese Features speziell als Cloudready bezeichnet, bleibt rätselhaft und ist wohl eher in der Marketingecke anzusiedeln. Damit eine Anwendung Cloud-ready wird, ist mehr notwendig, als nur ein paar Annotationen einzuführen. Eine verteilte oder Cloud-Anwendung setzt hinsichtlich Sicherheit auf mehr als nur Containerunabhängigkeit.

Die Referenzimplementierung des JSR-375 nennt sich Soteria und ist, wie auch die Spezifikation selbst, bei Git-Hub zu finden. Das Wort „Soteria“ kommt aus dem Griechischen und bedeutet so viel wie „Rettung“, „Wohl“ oder „Heil“. Der Begriff wird häufig in Verbindung mit Einrichtungen zur Behandlung psychischer Probleme oder Krisen genannt. Eine Verbindung zwischen dem Einsatz von Security in Java-EE-Anwendungen und dem persönlichen Gemütszustand ist sicherlich rein spekulativ und muss von jedem für sich selbst beurteilt werden.

Authentication Mechanism

Zentraler Bestandteil des Security API ist der Authentication Mechanism, der sich, wie der Name schon sagt, um den eigentlichen Authentifizierungsvorgang eines Nutzers kümmert. Übermittelt ein Anwender seine Credentials wie Benutzername und Passwort, fragt der Authentication Mechanism beim Identity Store nach, ob es einen registrierten und gültigen Benutzer mit diesen Daten gibt. Ist das der Fall, wird die vom Provider zurückgelieferte Identität für den weiteren Authentifizierungsvorgang verwendet. Wird keine gültige Identität gefunden, schlägt die Authentifizierung fehl und der Authentication Mechanism bricht den Vorgang mit einem entsprechenden Statuscode ab.

Grundlage für eine Implementierung des Authentication Mechanism ist das Interface HttpAuthenticationMechanism, das den jeweiligen Mechanismus als CDI Bean zur Verfügung stellt. Die zu implementierende Methode validateRequest(…) liefert ein AuthenticationStatus-Objekt zurück, das den Status der jeweiligen Authentifizierung enthält. Die Spezifikation sieht vor, dass jede Implementierung einen fertigen Mechanismus für Basic Authentication und Form-based Authentication mitbringt. Beide Authentifizierungsmöglichkeiten sind bereits aus der Vergangenheit aus der Servlet-Spezifikation bekannt. Zusätzlich muss eine Custom-form-basierte Authentifizierung zur Verfügung gestellt werden, die von der in der Servlet Spec beschriebenen form-based Authentifizierung abweichen kann und im mitgelieferten SecurityContext implementiert wird.

Soll einer dieser Mechanismen in der eigenen Anwendung verwendet werden, müssen wir diese lediglich mit der Annotation @BasicAuthenticationMechanismDefinition oder @FormAuthenticationMechansimDefinition (bzw. @CustomFormAuth…) versehen (Listing 1). Wird kein AuthenticationMechanism per Annotation definiert, muss eine eigene Implementierung in der eigenen Anwendung existieren, die dann verwendet wird.

@ApplicationPath("/demo")
@BasicAuthenticationMechanismDefinition(realmName = "Java Magazin")
public class JaxRsApplication extends Application {
}

Der Authentifizierungsmechanismus bringt aber noch mehr Optionen mit als die bislang beschriebenen. Wenn ein Benutzer noch nicht authentifiziert ist, lässt sich über die Annotation @LoginToContinue an der Mechanismusimplementierung deklarativ festlegen, welche Ressourcen bzw. Pfade während des Log-in-Vorgangs für die Log-in- oder auch Fehlerseite verwendet werden sollen, und ob zur Log-in-Seite ein Redirect oder ein Forward stattfinden soll (Attribut useForwardToLogin). Wird diese Annotation nicht verwendet, müssen im ankommenden und zu validierenden Request bereits die Auth-Informationen enthalten sein. Eine Um- oder Weiterleitung an eine Log-in-Seite findet dann nicht statt. Dies ist im Kontext einer reinen JAX-RS-Anwendung (oder ähnlichen Applikationen) sicherlich sehr sinnvoll, wenn es aber ein User Interface gibt, mit dem Benutzer arbeiten sollen, eher nicht.

Mit der Annotation @AutoApplySession wird dem AuthenticationMechanism deklarativ ermöglicht, sich des javax.servlet.http.registerSession-Verhaltens aus der JASPI-Spezifikation zu bemächtigen. Damit müssen Requests, für die bereits eine erfolgreiche Authentifizierung durchgeführt wurde und die mit einer Session verknüpft wurden, nicht bei jeder neuen Anfrage erneut überprüft werden.

Mit den bisher zur Verfügung stehenden Mechanismen war es nicht möglich, eine Remember-me-Option anzubieten, dass also Benutzer, auch wenn sie längere Zeit nicht mit der Anwendung gearbeitet haben, automatisch wieder erkannt und eingeloggt werden, ohne dass sie sich erneut mit ihren Credentials authentifizieren müssen. Wer das realisieren wollte, musste es aufwendig und kompliziert, schlimmstenfalls noch unsicher, an der Security vorbei implementieren. Diese Zeiten sind nun glücklicherweise vorbei, denn das Java EE Security API bringt diese Option in Form der Annotation @RememberMe mit. Mit dieser Annotation lässt sich definieren, dass diese Option überhaupt verwendet werden kann (isRememberMe), in welchem Cookie (`cookieName`) diese Wiedererkennungsdaten zu finden sind, wie das RememberMe-Cookie zugänglich (secureOnly und httpOnly) und wie lange es gültig ist (cookieMaxAgeSeconds).

Ist eine solche Annotation am AuthenticationMechanism enthalten, versucht er, die Benutzer über die dort angegebenen Informationen zu authentifizieren. Erst wenn dies nicht gelingt, wird der Benutzer zur definierten Log-in-Seite weitergeleitet. Ein eigener AuthenticationMechanism mit den genannten Annotationen findet sich in Listing 2. Der im nächsten Abschnitt beschriebene Identity Store wird über CDI in die Klasse injiziert, da der AuthenticationMechanism selbst auch eine CDI Bean ist.

@AutoApplySession
@LoginToContinue(
  loginPage = "/soteria/login",
  useForwardToLogin = false
)
@RememberMe(
  cookieMaxAgeSeconds = 60 * 60 * 24 * 14,
  cookieSecureOnly = true,
  isRememberMeExpression = "#{self.isRememberMe(httpMessageContext)}"
)
@ApplicationScoped
public class DemoAuthenticationMechanism implements HttpAuthenticationMechanism {

  @Inject
  private IdentityStore identityStore;

  @Override
  public AuthenticationStatus validateRequest(HttpServletRequest request,
                                              HttpServletResponse response,
                                              HttpMessageContext httpMessageContext)
                                              throws AuthenticationException {
    Credential credential = httpMessageContext.getAuthParameters().getCredential();

    if (credential != null) {
      return httpMessageContext.notifyContainerAboutLogin(
        identityStore.validate(credential));
    } else {
      return httpMessageContext.doNothing();
    }
  }

  public Boolean isRememberMe(HttpMessageContext httpMessageContext) {
    return httpMessageContext.getAuthParameters().isRememberMe();
  }
}

Identity Store

Der Identity Store hält alle bekannten Identitäten, die auf die Anwendung zugreifen dürfen und bei dem der Authentication Mechanism diese anfragt. Typischerweise hat eine Identität einen Benutzernamen und wird über ein Passwort verifiziert. Die Passwortüberprüfung übernimmt ausschließlich der Identity Store. In Listing 2 ist der Aufruf über identityStore.validate(credential) zu sehen. Aber nicht jede Identität besteht aus diesen Standard-Credentials. Im Fall der RememberMe-Option wird der Benutzer nur anhand einer ID (aus dem RememberMe-Cookie) erkannt und an den AuthenticationMechanism zurückgeliefert. Hierfür gibt es neben dem normalen IdentityStore-Interface das RememberMeIndentityStore-Pendant.

Wie bei den Authentication Mechanisms gibt die Spezifikation für JSR-371-Implementierungen auch für die Identity Stores Annotationen vor, die implementiert zur Verfügung gestellt werden müssen. So lassen sich einfache datenbankgestützte (@DataBaseIdentityStoreDefinition, Listing 3) oder LDAP-basierte Stores (@LdapIdentityStoreDefinition, Listing 4) einfach per deklarativer Konfiguration nutzen.

@DataBaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/DefaultDataSource",
  callerQuery = "select password from caller where name = ?",
  groupsQuery = "select group_name from caller_groups where caller_name = ?"
  hashAlgorithm = "Pbkdf2"
)
@LdapIdentityStoreDefinition(
  url = "ldap://ldap.example.com:389",
  callerBaseDn = "ou=user,dc=javamagazin,dc=de",
  groupBaseDn = "ou=group,dc=javamagazin,dc=de"
)

Die Referenzimplementierung Soteria bringt zusätzlich noch die Annotation @EmbeddedIdentityStoreDefinition (Listing 5) mit, die einen statisch eingebetteten In-Memory Store ermöglicht, der manuell deklarierte Credentials enthält. Dies ist für frühe Testzwecke gut geeignet, wenn das eigentliche Backend-System noch nicht zur Verfügung steht. Für produktive Umgebungen sollte dieser Store nicht verwendet werden, da die Passwörter hier ausschließlich in Klartext angegeben werden können.

@EmbeddedIdentityStoreDefinition({
  @Credentials(callerName = "dasniko", password = "secret", groups = {"user", "admin"}),
  @Credentials(callerName = "john", password = "doe", groups = {"user"}),
  @Credentials(callerName = "foo", password = "bar", groups = {"fizz", "buzz"})
})

Ist keiner der vordefinierten Identity Stores passend, können wir natürlich auch einen eigenen Store implementieren. Hierfür wird einfach das Interface IdentityStore implementiert und die Methode validate() ausprogrammiert. Der Store steht natürlich auch wieder als CDI Bean zur Verfügung und kann darüber im Authentication Mechanism verwendet werden. Ein Beispiel eines selbstimplementierten Identity Stores ist in Listing 6 zu finden.

public class DemoIdentityStore implements IdentityStore {
  @Inject
  private AccountService accountService;

  @Override
  public CredentialValidationResult validate(Credential credential) {
    try {
      String username = ((UsernamePasswordCredential) credential).getCaller();
      String password = ((UsernamePasswordCredential) credential).getPasswordAsString();

      if (accountService.isValid(username, password)) {
        return new CredentialValidationResult(username);
      } else {
        return CredentialValidationResult.INVALID_RESULT;
      }
    } catch (SecurityException e) {
      return CredentialValidationResult.NOT_VALIDATED_RESULT;
    }
  }
}

Security Context

In den bisherigen Securityimplementierungen gab es ebenfalls noch keinen containerübergreifenden, einheitlichen Zugriff auf Sicherheitsfunktionen wie „Mit welchem Principal (Benutzer) wird die Anfrage gerade durchgeführt?“ oder „Welche Rollen hat der aktuelle Benutzer?“ etc. Dies hält jetzt über das Interface SecurityContext Einzug in Java EE, natürlich auch wieder als CDI Bean, und ist somit an allen Stellen der Anwendung nutzbar. Bislang haben nur verschiedene proprietäre Frameworks ihre eigenen Security Contexts mitgebracht, diese waren aber in keiner Weise kompatibel zueinander.

Über die Methode getCallerPrincipal() wird das aktuelle Principal-Objekt, also der Benutzer, zurückgeliefert (Listing 7). Wenn die Anwendung verschiedene Principal-Typen verwendet, können mit getPrincipalsByType() alle Principals zu einem Typ zurückgeliefert werden, die der aktuelle Benutzer hat. Die Methode isCallerInRole() gibt wenig überraschend an, ob ein Benutzer eine bestimmte Rolle hat oder nicht. Ob der aktuelle Benutzer Zugriff auf eine bestimmte Ressource hat, lässt sich über hasAccessToWebResource() herausfinden. In bestimmten Fällen kann es notwendig sein, den Authentifizierungsvorgang manuell bzw. programmatisch zu initiieren. Ist dies der Fall, so muss lediglich die Methode authenticate() im SecurityContext aufgerufen werden.

0Auth2, OpenID Connect und JWT

Das Java EE Security API mit JSR-375 hat es sich auf die (Marketing-)Fahne geschrieben, Cloud-ready zu sein. Doch im Cloud-Zeitalter der verteilten Anwendungen werden Identitäten und Zugriffsautorisierungen über Quasistandards wie OAuth2, OpenID-Connect und JWT (JSON Web Token) kommuniziert. Ein einmal erstelltes und verifiziertes Token von einer Anwendung zu einer anderen Anwendung und damit den Benutzerkontext weiterzugeben, ist nichts Neues, das wurde vor fünfzehn Jahren schon mit SAML gemacht. OAuth2, JWT und Co. sind in etwa gleichzusetzen mit SAML, basieren nur auf anderen Formaten, die besser mit Webtechnologien zu verarbeiten sind.

Im neuen Security-API ist nichts dergleichen zu finden, kein OAuth2, kein OpenID-Connect und kein JWT. Dabei äußerte die Community bereits 2014 den Wunsch nach OAuth2-Unterstützung des JSR-375. 2015 fand sich das Thema sogar auf einer Folie in einem Oracle-Vortrag auf der damaligen JavaOne-Konferenz. Davon war kurz danach aber nichts mehr zu hören oder lesen. Erst Ende 2017 kam die Diskussion nach der finalen Abstimmung der Version 1.0 wieder auf die Mailingliste der Spec. Offiziell werden Zeitgründe genannt.

Rudy De Busscher, Expert-Group-Mitglied des JSR-375, hat ein Proof-of-Concept für eine JWT-Integration mit Soteria entwickelt. In Wirklichkeit ist es auch nicht schwer, mit den neuen Interfaces und Methoden genau das zu realisieren: eine Authentifizierung über JSON Web Tokens, die ja in sich selbst über die Signatur auf Manipulationen oder Kompromittierungen überprüfbar sind und mit jedem Request an die Anwendung übermittelt werden. Dass genau diese Optionen in der Version 1.0 fehlen, ist mehr als enttäuschend. Gerade die Nutzung von JWT hätte für die Spezifikation das Label Cloud-ready wirklich gerechtfertigt. So aber ist das Security-API zwar ein guter Anfang, aber noch weit davon entfernt, wirklich in verteilten (Cloud-)Architekturen gewinnbringend eingesetzt zu werden. Da können alternative Frameworks und Projekte, z. B. JBoss Keycloak, bereits deutlich mehr.

Fazit

Eine Vereinheitlichung der Authentifizierungs- und Autorisierungsmöglichkeiten in Java EE war mehr als überfällig. Aus dieser Perspektive ist das neue Security API unter dem JSR-375 sehr zu begrüßen. Eine sinnvolle Architektur in Form von CDI-Beans der relevanten Komponenten wie Identity Store, Authentication Mechanism und Security Context machen es leicht, die notwendige Absicherung der eigenen Anwendung vorzunehmen. Fertig einsetzbare Implementierungen per Annotation erleichtern die Integration in die eigene Umgebung, ohne sich mit viel Code auseinandersetzen zu müssen. Wollen wir dennoch eine davon abweichende oder weitergehende Implementierung vornehmen, ist dies einfach machbar. Zwei Beispielprojekte mit unterschiedlicher Anwendung der Referenzimplementierung Soteria des Security API sind bei GitHub zu finden: Eine JAX-RS-Anwendung und eine komplette Webanwendung mit User Interface auf Basis von MVC/Ozark und komplett selbst implementiertem Authentifizierungs-Flow. Zieht man die Möglichkeiten in Betracht, die das Security API und Soteria hinsichtlich verteilter Architekturen und Cloud-Betrieb anbieten, wird man leider enttäuscht. Hier ist noch viel Nachbesserungsbedarf, damit möglichst bald aktuelle Spezifikationen wie OAuth2, OpenID-Connect und JWT unterstützt werden.

Geschrieben von
Niko Köbler
Niko Köbler
Niko Köbler ist freiberuflicher Software-Architekt, Developer & Trainer für Java & JavaScript-(Enterprise-)Lösungen, Integrationen und Webdevelopment. Er ist Co-Lead der JUG Darmstadt, schreibt Artikel für Fachmagazine und ist regelmäßig als Sprecher auf internationalen Fachkonferenzen anzutreffen. Niko twittert @dasniko.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: