Über unsMediaKontaktImpressum
Christian Schulz 13. März 2019

Authentication mit Java EE 8

Tokenbasierte Standards wie OAuth2 wurden vor ein paar Jahren zum De-facto-Standard für moderne Client-Server-Authentifizierung. Durch sie soll die Anforderung des Single-Sign-On (SSO) gelöst werden. Dennoch fanden diese Varianten der Authentifizierung bis heute keine Berücksichtigung im Enterprise-Java-Standard. Aufwändig realisierte Third-Party-Bibliotheken halfen, diese Lücke zu schließen. Mit der Security API 1.0 erhielt Java EE 8 eine neue Möglichkeit der Implementierung von Authentifizierungsmethoden. Der Artikel stellt die Möglichkeiten vor. Darauf aufbauend wird am Beispiel JSON Web Token (JWT) eine exemplarische Realisierung gezeigt.

Warum muss dazu ein neuer Standard definiert werden? Können nicht bestehende Authentifizierungsmethoden genutzt werden?

Nahezu jede Java-EE-Anwendung benötigt eine Benutzerauthentifizierung. Dazu wird eine Konfiguration meist in Form einer web.xml angelegt. Dort wird die Authentifizierungsmethode (hier: FORM), sowie die Konfiguration der Login- und Error-Seite hinterlegt:

Listing 1: Konfiguration der Authentifizierungsmethode in der web.xml

<login-config>
  <auth-method>FORM</auth-method>
  <realm-name>MyCustomRealm</realm-name>
  <form-login-config>
    <form-login-page>/login.xhtml</form-login-page>
    <form-error-page>/error.xhtml</form-error-page>
  </form-login-config>
</login-config>

Eine formulargestützte Authentifizierung (FORM) wird wie im Beispiel konfiguriert. Neben der formulargestützten Methode können auch BASIC, DIGEST und CLIENT-CERT eingesetzt werden. Containerhersteller können weitere Methoden zur Verfügung stellen.

Woher stammen die Login-Informationen?

Unabhängig von der eingesetzten Methode stellt sich die Frage, wie der Container die Authentifizierungsinformationen (Benutzername/Kennwort) erhält. Durch einen fehlenden Standard ist die Angabe des zu nutzenden Realms container-abhängig. Der Tomcat benötigt beispielsweise folgende Konfiguration (in der Datei server.xml):

Listing 2: Auszug aus der server.xml eines Tomcats

<Context>
  <Realm className="org.apache.catalina.realm.JAASRealm"
         appName="MyCustomRealm" ... />
</Context>

Zusätzlich wird die Datei jaas.config benötigt:

Listing 3: Beispielhafte jaas.config

MyCustomRealm {
  de.openknowledge...CustomLoginModule required;
};

Damit der Container sie auch nutzt, muss ein entsprechender Startparameter angegeben werden: -Djava.security.auth.login.config=jaas.config.

Der Java Authentication and Authorization Service (JAAS - JSR 196) Standard definiert die Basis für das LoginModule. Er spezifiziert, wie ein LoginModule aufgebaut werden muss. In der Regel liefern die Containerhersteller verschiedene Implementierungen für die populärsten Datenquellen von Authentifizierungsdaten.

Das LoginModule ist für das Erzeugen von einem javax.security.auth.Subjects zuständig, welcher den Benutzer mit Rollen repräsentiert. Die java.security.Principals stellen eine eindeutige Identifizierung eines Benutzers dar. Die Ausführung erfolgt durch den im Standard spezifizierten Zwei-Phase-Login. Dazu werden in der ersten Phase alle konfigurierten LoginModule aufgerufen. Die Konfiguration erlaubt es, Module als erforderlich und optional zu kennzeichnen, was Auswirkungen auf das Verhalten hat, wenn der Benutzer nicht authentifiziert werden kann. Die zweite Phase startet nachdem alle Module abgearbeitet worden sind und es keinen Fehlschlag gab. Sie schreibt alle erstellten Principals der Module in das Subject.

Die mitgelieferten Implementierungen decken die meisten Anforderungen ab. Sollte das nicht ausreichend sein, weil z. B. eine nicht unterstützte Datenquelle für die Login-Informationen herangezogen werden soll, kann ein Module mit Hilfe des Interface javax.security.auth.spi.LoginModule implementiert werden. Die Aufgabe des Modules ist das Erstellen von einem Principal, wozu im Standard die Callbacks vorgesehen sind:

Listing 4: Benutzerdaten mit Hilfe von Callbacks auslesen

principal = null;
Callback[] callbacks = new Callback [] {
  new NameCallback("Username"),
  new PasswordCallback("Password", false)
};
callbackHandler.handle(callbacks);
String username = ((NameCallback)callbacks[0]).getName();
String password = new String(((PasswordCallback)callbacks[1]).getPassword());
principal = createPrincipal(username, password);

Die Realisierung von einem LoginModule ist alles andere als einfach – wie die Implementierung zeigt. Die nicht intuitive Benutzung der API durch die Callbacks stellt eine Hürde dar.

Selbst wenn das LoginModule umgesetzt wurde, gibt es noch eine weitere Schwierigkeit: Ein LoginModule kann nur durch eine Anpassung der Serverkonfiguration eingesetzt werden. Dies stellt spätestens im Falle der Cloud ein Problem dar, da hier die Änderung der Server-Konfiguration nicht mehr möglich ist.

Java Authentication Service Provider Interface for Containers

Mit dem Standard Java Authentication Service Provider Interface for Containers (JASPIC/JSR 196) wurde dieser Nachteil ausgeräumt. Das Konzept sieht vor, dass ein Anwendungsarchiv alles enthalten kann, was für die Authentizifierung notwendig ist. Dadurch ist eine Unabhängigkeit zum Container gegeben.

Durch das Interface ServerAuthModule steht die Möglichkeit frei, eine eigene Authentifizierungsmethode für JASPIC selber zu realisieren. Es ermöglicht die Umsetzung von nahezu jedem Szenario der Authentifizierung, dazu zählen zum Beispiel HTTP, RMI/Remote-EJB, JMS und weitere. Auch wenn der Standard schon einige Jahre existiert und von gängigen Containern unterstützt wird, findet er in der Praxis selten Anwendung. Der generische Ansatz des Standards gestaltet die Realisierung sehr aufwändig und umständlich.

Security API 1.0

Mit JASPIC wurden die Grundlagen geschaffen, welche von der neuen Security API (JSR 375) genutzt wird. Durch eine weitere Abstraktion wird eine auf HTTP ausgelegte vereinfachte API angeboten. Durch die Wiederverwendung des bestehenden Standards ist die API komplett unabhängig von Java Enterprise 8 und kann auch in einem Java-Enterprise-7-Server betrieben werden. Für die Benutzung muss in die Anwendung eine Implementierung der API integriert werden. Mit Soteria wurde eine Referenzimplementierung geschaffen. Auf GitHub kann der Quellcode eingesehen werden [1].

Für das maven-gestützte Projekt kann mit dem folgenden Snippet die Abhängigkeit eingebunden werden:

Listing 5: Maven-Koordinaten für Soteria

<dependency>
  <groupId>org.glassfish.soteria</groupId>
  <artifactId>jakarta.security.enterprise</artifactId>
  <version>1.0.1</version>
</dependency>

Der Artikel wird nun die neu eingeführten Interfaces und Klassen des Standards näher erläutern. Eigene Implementierungen von Authentifizierungsmethoden nutzen sie als Basis. Zusätzlich zu der Möglichkeit relativ einfach eigene Implementierungen umzusetzen, bringt die neue API eine Unterstützung für "Remember me" sowie eine automatische Weiterleitung zur Anmeldung mit dem "Login to Continue" mit.

IdentityStore

Listing 6: Interface IdentityStore

public interface IdentityStore {

  CredentialValidationResult validate(Credential credential);

  Set<String> getCallerGroups(CredentialValidationResult result);

  int priority();

  Set<ValidationType> validationTypes();

  enum ValidationType { VALIDATE, PROVIDE_GROUPS }
}

Über das Interface IdentityStore werden Quellen für Authentifizierungsdaten definiert. An sich wurde das Interface für die Benutzung mit HttpAuthenticationMechanism-Implementierungen entworfen, dennoch ist es möglich, das Interface mit anderen Methoden zu nutzen. Durch den Rückgabewert von validationTypes() kann definiert werden, wofür die Implementierung zuständig ist. Es besteht die Möglichkeit, nur die Validierung der Logindaten, nur die Vergabe von Rollen oder beides zu übernehmen.

Die möglichen Werte werden durch das Enum ValidationType beschrieben:

  • VALIDATE – Gibt an, dass der IdentityStore zur Validierung der Logindaten genutzt wird.
  • PROVIDE_GROUPS – Gibt an, dass der IdentityStore als Provider für die Rollen (Gruppen) genutzt wird.

Mit der priority()-Methode kann die Reihenfolge der IdentityStores-Bearbeitung beeinflusst werden. Über die validate()-Methode erhält der IdentityStore im Falle der Validierung von Logindaten die benötigten Informationen. Eine Instanz von CredentialValidationResult wird als Ergebnis zurückgegeben. Sie wird im nächsten Abschnitt erläutert. Wenn der IdentityStore als Provider genutzt werden soll, gibt die Methode getCallerGroups() die zugehörigen Rollen (Gruppen) zurück.

Bei der Verwendung der Standard-Authentifizierungsmechanismen (BASIC, FORM-BASED, ...), werden alle verfügbaren IdentityStores zur Validierung der Logindaten konsultiert. Die Anbindung einer eigenen Datenquelle würde dann durch die Implementierung des IdentityStore-Interfaces erfolgen. Sobald eine Validierung der Logindaten erfolgreich war, werden erneut alle IdentityStores konsultiert, um die Gruppen abzufragen. Das Interface IdentityStoreHandler kapselt den Zugriff auf die IdentityStores und übernimmt auch die Kontrolle der Abarbeitung.

Der Standard definiert zwei Annotationen, welche sofort eingesetzt werden können:

Listing 7: Standardannotationen für den IdentityStore

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:3268",
  bindDn = "readonly@openknowledge",
  bindDnPassword = "password"
)

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:jboss/datasources/ExampleDS",
  callerQuery = "SELECT password from USERS where name = ?"
)

Soteria liefert noch eine weitere Annotation:

Listing 8: Soteria-spezifische Annotation für einen IdentityStore

@EmbeddedIdentityStoreDefinition({
  @Credentials(callerName = "admin", password = "secret1", groups = { "admin", "user" }),
  @Credentials(callerName = "user", password = "password", groups = { "user" }),
})

CredentialValidationResult

Listing 9: Klasse CredentialValidationResult

public class CredentialValidationResult {

  public Status getStatus() {...}

  public CallerPrincipal getCallerPrincipal() {...}

  public Set<String> getCallerGroups() {...}

  public enum Status { NOT_VALIDATED, INVALID, VALID }
}

Das Validierungsergebnis wird mit Hilfe der Container-Klasse CredentialValidationResult abgebildet. Nebem dem Status enthält sie die genutzten Logininformationen sowie optional die Rollen (Gruppen), wenn der IdentityStore als Provider konfiguriert worden ist. Es gibt drei Zustände, welche der Status annehmen kann. Die beiden VALID und INVALID sind vom Namen her selbsterklärend. Der dritte Status NOT_VALIDATED gibt an, dass kein zuständiger IdentityStore gefunden werden konnte, um die Logininformationen zu verarbeiten.

HttpAuthenticationMechanism

Die vorher beschriebenen Klassen und Interfaces werden dazu eingesetzt, eine Validierung durchzuführen, jedoch die Ausführung der Validierung fehlt noch. Dazu muss das Interface HttpAuthenticationMechanism implementiert werden.

Listing 10: Interface HttpAuthenticationMechanism

public interface HttpAuthenticationMechanism {

  AuthenticationStatus validateRequest(
    HttpServletRequest request,
    HttpServletResponse response,
    HttpMessageContext httpMessageContext) throws AuthenticationException;

  AuthenticationStatus secureResponse(...) throws AuthenticationException;

  void cleanSubject(...) throws AuthenticationException;
}

Das JASPIC ServerAuthModule ruft im Zuge der Authentifizierung die Methode validateRequest() auf. In dieser wird die Validierung der Logininformationen umgesetzt, welche im Normalfall den IdentityStoreHandler als seine Datenquelle einsetzt. Um alle notwendigen Informationen zu erhalten, wird der HTTP-Request, HTTP-Response sowie der HttpMessageContext in die Methode eingefügt. Der HttpMessageContext wird im nächsten Abschnitt genauer betrachtet.

Um unnötigen Aufwand einzusparen, definiert der Standard auch hier zu implementierende Varianten des HttpAuthenticationMechanisms. Diese können mit dem Einsatz der passenden Annotationen direkt genutzt werden. Wird eine Implementierung zur Verfügung gestellt, entfällt der entsprechende Eintrag in der web.xml.

Listing 11: Annotationen für Standard HttpAuthenticationMechanism

@BasicAuthenticationMechanismDefinition

@CustomFormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue())

@FormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue())

Alle Implementierungen des Interfaces von Soteria nutzen den zuvor erklärten IdentityStore.

HttpMessageContext

Listing 12: Interface HttpMessageContext

public interface HttpMessageContext {

  // ...

  boolean isProtected();

  AuthenticationStatus responseUnauthorized();

  AuthenticationStatus responseNotFound();

  AuthenticationStatus notifyContainerAboutLogin(String callername, Set<String> groups);
  AuthenticationStatus notifyContainerAboutLogin(Principal principal, Set<String> groups);
  AuthenticationStatus notifyContainerAboutLogin(CredentialValidationResult result);

  AuthenticationStatus doNothing();

  // ...
}

Damit unter anderem eine Kommunikation mit der JASPIC-Implementierung möglich ist, wurde der HttpMessageContext eingeführt. Der Container kann mit Hilfe von verschiedenen Methoden über den aktuellen Status der Authentifizierung informiert werden. Die Methode notifyContainerAboutLogin() informiert über den erfolgreichen Login.

Soll der Authentifizierungsprozess übersprungen werden, wie z. B. bei einem nicht geschützten Pfad, wird die Methode doNothing() genutzt. Das Handling der Standardfehler 401 und 404 kann mit Hilfe der Methoden responseUnauthorized() bzw. responseNotFound() direkt eingesetzt werden.

Durch den Servlet-Security-Standard ist es möglich, Constraints für Pfade festzulegen, diese können mit der Methode isProtected() abgefragt werden.

Um die neue API sinnvoll einsetzen zu können, fehlt noch die Variante zur Übertragung von Benutzerdaten. Im Hinblick auf Web API eignen sich tokenbasierte Varianten wie der JWT sehr gut.

JSON Web Token

Um die passwortbasierte Authentifizierung zu ersetzen, wurden für verteilte Anwendungen in den vergangenen Jahren verschiedene Standards entwickelt, um das Verfahren zu revolutionieren unter anderem auch OAuth 2. Sie boten den entscheidenen Vorteil, dass die Anwendung unabhängig von der Anmeldung gestartet werden konnte. Google oder Facebook konnten zum Beispiel als große Authentifizierungsquellen ohne Aufwand eingesetzt werden. Doch konzeptbedingt entstehen viele unnötige Anfragen zum Aussteller des Tokens, da bei jedem Request überprüft werden muss, ob der Token noch gültig ist.

Auf JSON basierend ist der JWT ein leichtgewichtiges Tokenformat. Die kompakte Größe sowie die Art der Validierung vom Token stellen hier große Vorteile dar. Der Token trägt alle Informationen zum Validieren seiner Gültigkeit und es entfallen wie bei OAuth 2 benötigte Anfragen gegenüber einem Authentifizierungsserver.

Der JWT besteht aus drei Teilen, dem Kopf (Zeile 1), der Nutzlast (Zeile 3) und der Signatur (Zeile 5).

Listing 13: Beispiel JSON Web Token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiJjaHJpc3RpYW4uc2NodWx6IiwibmFtZSI6IkNocmlzdGlhbiBTY2h1bHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvamF4cnMtc2VjdXJpdHktand0Iiwicm9sZXMiOlsiQURNSU4iXSwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9
.
tYbW6ojcRWymQpKNLOiUL3KwGx7v-Ekoksqmf1W0TYk

Um einen Token validieren zu können, muss die Verschlüsselungsmethode enthalten sein. Durch seine Konzeption muss der Token keine weiteren Informationen verpflichtend mitführen, dennoch ist es üblich, den Issuer (Aussteller vom Token), das Subject (Benutzerinformation), die Expiration Time (Ablaufdatum) und ähnliche mitzuliefern. Durch sein offenes Format können auch benutzerdefinierte Informationen (Private Claims) übertragen werden. Typischerweise werden Rollen auch im Token mitgeschickt.

Listing 14: Geparster JSON Web Token

{
  "alg": "HS256",
  "typ": "JWT"
}
.
{
  "sub": "christian.schulz",
  "name": "Christian Schulz",
  "iss": "http://localhost:8080/jaxrs-security-jwt",
  "roles": ["ADMIN"],
  "iat": 1516239022,
  "exp": 1516242622
}

HttpAuthenticationMechanism und JSON Web Token

Nach dem Exkurs durch die Grundlagen von JWT und den neuen APIs soll die praktische Anwendung am Beispiel von JWT gezeigt werden. Für eine bessere Übersicht wird der Ablauf in Abb. 1 verdeutlicht. Das JASPIC ServerAuthModule stellt den Einstiegspunkt dar. Dort wird nach einer Implementierung der Klasse HttpAuthenticationMechanism gesucht. In der Instanz wird die validateRequest()-Methode aufgerufen.

In diesem Artikel wird die implementierende Klasse JwtAuthenticationMechanism genannt. Diese soll den JWT validieren. Das Ergebnis wird mit Hilfe des AuthenticationStatus an das ServerAuthModule zurückgegeben, um den Ausgang der Authentifizierung zu beschreiben. Die Benutzung von einem IdentityStore ist nicht notwendig, da der Token schon alle benötigten Informationen enthält. Eine Abfrage von weiteren Informationen mit Hilfe von einem IdentityStore ist weiterhin möglich, z. B. schützenswerte Informationen.

Fachlich betrachtet kann die Implementierung des JwtAuthenticationMechanism in mehrere Teile untergliedert werden. Als erstes wird überprüft, ob der angeforderte Pfad überhaupt geschützt werden muss. Dieser Bereich kann beispielsweise in der web.xml definiert werden:

Listing 15: web.xml mit Security Constraint

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>protect ping</web-resource-name>
      <url-pattern>/api/protected/*</url-pattern>
    </web-resource-collection>
  </security-constraint>
</web-app>

Mit Hilfe der schon vorgestellten Methode isProtected() kann überprüft werden, ob dieser Bereich geschützt ist:

Listing 16: Kontextüberprüfung

if (context.isProtected()) {
  // api path is not protected
  return context.doNothing();
}

Der Token wird aus dem Header ausgelesen und muss auf das richtige Format überprüft werden. Die weitere Verarbeitung wird mit dem Fehler 401 abgebrochen, falls der Token nicht im richtigen Format übertragen worden ist, da es sich bei der angeforderten Ressource um einen geschützten Bereich handelt.

Listing 17: Token Pattern-Überprüfung

private static final Pattern PATTERN_AUTHORIZATION_HEADER =
    Pattern.compile("^Bearer [a-zA-Z0-9\\-_\\.]+$", Pattern.CASE_INSENSITIVE);

// ...

String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
  LOGGER.log(Level.WARNING, "Authorization header is missing");
  return context.responseUnauthorized();
}

if (!PATTERN_AUTHORIZATION_HEADER.matcher(header).matches()) {
  LOGGER.log(Level.WARNING, "Authorization header is invalid");
  return context.responseUnauthorized();
}

Zur Überprüfung des Tokens wird eine Bibliothek von Auth0 eingesetzt. Es können auch Alternativen wie z. B. Nimbus Jose JWT oder Google Json Token eingesetzt werden.

Listing 18: Maven-Koordination für auth0-JWT-Bibliothek

<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.4.0</version>
</dependency>

Der Benutzername kann als Subject aus dem Token ausgelesen werden und wird dem Container mitgeteilt. Dies bedeutet, dass der Login erfolgreich war. Der Status 401 wird als Fallback zurückgegeben, wenn bei der Verarbeitung ein Fehler aufgetreten ist (z. B. Token ist abgelaufen).

Listing 19: JWT parsen und Informationen auslesen

try {
  String[] headerComponents = header.split(" ");
  String token = headerComponents[1];

  DecodedJWT jwt = JWT.require(Algorithm.HMAC256(secretKey))
    .withIssuer("http://localhost:8080/jaxrs-security-jwt")
    .build()
    .verify(token);

  String username = jwt.getSubject();

  return context.notifyContainerAboutLogin(username, new HashSet<>();
} catch (JWTVerificationException e) {
  LOGGER.log(Level.WARNING, "JWT token verification failed", e);
}

return context.responseUnauthorized();

Fazit zum Login

Mit Hilfe der Security-API können eigene Implementierungen von Authentifizierungsmechanismen ohne großen Aufwand entwickelt und als Teil der Anwendung geliefert werden. Dies wurde am Beispiel von JWT exemplarisch gezeigt. Der gesamte Code ist Bestandteil der Anwendung. Dadurch ist keine Konfiguration des Containers oder der Benutzerquelle mehr notwendig.

Autorisierung

Die gezeigte Implementierung erlaubt es, dass sich Benutzer mit einem JWT authentifizieren können. In vielen Anwendungen ist diese Art der Benutzerkontrolle nicht ausreichend – es werden verschiedene Berechtigungen für die Benutzer benötigt. Rollenbasierte Authentifizierung gehört zu den meist genutzten Varianten, sie erlaubt die Zuordnung von Berechtigungen zu einer Rolle und sie werden dann dem Benutzer zu geordnet. Für Web-Ressourcen wurden durch den Servlet-Standard schon rollenbasierte Berechtigungen definiert. Der Zugriff kann in der deklarativen Form durch die web.xml oder programmatisch mit Hilfe von Annotationen gesteuert werden:

Listing 20. Rollenberechtigung in der web.xml

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>protect ping</web-resource-name>
      <url-pattern>/api/protected/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>USER</role-name>
    </auth-constraint>
  </security-constraint>
  <security-role>
    <role-name>USER</role-name>
  </security-role>
</web-app>

Listing 21. Rollenberechtigung mit Hilfe von Annotationen

@ServletSecurity(@HttpConstraint(rolesAllowed = {"USER"}))

Der Java-Enterprise-Standard definiert für EJBs die RolesAllowed-Annotation.

Listing 22: RollesAllowed-Annotation im Einsatz

@RolesAllowed({"USER"})
public class CustomerService {
}

Die RolesAllowed-Annotation existiert schon lange in dem Standard, wurde aber noch nicht für JAX-RS spezifiziert. Der aktuelle Arbeitsstand kann in dem offenen Proposal angeschaut werden [2]. Standards brauchen immer ihre Zeit und das Problem kann relativ einfach auch so umgesetzt werden. Damit das möglich ist, müssen zunächst die Rollen ermittelt werden.

Rollen und JSON Web Token

Wie erwähnt erlaubt das Format von JWT das Anreichern von benutzerdefinierten Informationen. Hiermit lassen sich zum Beispiel Rollen übertragen, welche für eine Authentifizierung genutzt werden können. Durch die vorhandene JwtAuthenticationMechanism-Implementierung wird der Token schon decodiert, es müssen nun lediglich die Rollen ausgelesen werden.

In der vorherigen Implementierung wurde mit Hilfe der Methode notifyContainerAboutLogin() ein leeres Set an den Container übergeben. Dieses muss durch das Set der Rollen ersetzt werden:

Listing 23: Rollen aus dem JWT auslesen

String username = jwt.getSubject();
List<String> roles = jwt.getClaim("roles").asList(String.class);

return context.notifyContainerAboutLogin(username, new HashSet<>(roles));

Die Sicherheitsrichtlinien des Servlet-Standards als Zugriffskontrolle für JAX-RS ist umständlich, da sie durch Annotationen oder mit Hilfe der web.xml umgesetzt wird. Da die Berechtigungen pro Pfad gesetzt werden, gibt es hier eine gravierende Einschränkung. Sollen für unterschiedliche HTTP-Methoden (z. B. POST und GET) unterschiedliche Berechtigungen gesetzt werden, z. B. der Benutzer darf nichts erstellen aber lesen, kann diese Anforderung nicht umgesetzt werden.

Durch die fehlende Integration für den JAX-RS-Standard muss der Filter selber realisiert werden. Er soll ähnlich wie die RolesAllowed-Annotation einsetzbar sein und damit die Zugriffskontrolle auf die jeweilige Resource übernehmen.

RolesAllowedFiler

Das Filtersystem von JAX-RS erlaubt den Eingriff in jeden Request an eine Resource. An dieser Stelle soll die Implementierung ansetzen. Als Einstiegspunkt wird das Interface ContainerRequestFilter eingesetzt. Das Interface schreibt vor, dass die Methode filter() implementiert werden muss. Als Parameter erhält sie eine Instanz des ContainerRequestContexts. Damit können Anfragen z. B. vorzeitig abgebrochen werden, um den Container anzuweisen, dass der Benutzer keinen Zugriff hat.

Listing 24: Exemplarische Implementierung des RolesAllowedFilters

@Provider
@Priority(Priorities.AUTHENTICATION)
public class RolesAllowedFilter implements ContainerRequestFilter {

  @Context
  private ResourceInfo resourceInfo;

  @Inject
  private SecurityContext securityContext;

  @Override
  public void filter(ContainerRequestContext requestContext) throws IOException {
    Class<?> resourceClass = resourceInfo.getResourceClass();
    Method resourceMethod = resourceInfo.getResourceMethod();

    RolesAllowed rolesAllowed = resourceClass.getAnnotation(RolesAllowed.class);
    if (resourceMethod.getAnnotation(RolesAllowed.class) != null) {
      rolesAllowed = resourceMethod.getAnnotation(RolesAllowed.class);
    }

    if (rolesAllowed != null &&
          Arrays.stream(rolesAllowed.value())
          .noneMatch(s -> securityContext.isCallerInRole(s))) {
      requestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build());
    }
  }
}

Die aktuell aufgerufene Methode in der Ressource kann mit Hilfe der ResourceInfo abgefragt werden. Damit kann dann die Annotation ausgelesen werden.

Die Spezifikation gibt vor, dass Annotationen auf Methodenebene die Klassenebene überschreibt. Ist keine Annotation auf der Methodenebene vorhanden, wird die eventuell vorhandene Annotation auf Klassenebene ausgelesen. Der Filter soll überprüfen, ob sich der Benutzer in einer angegebenen Rolle befindet, ist dies nicht der Fall kann der Fehler 403 zurückgegeben werden.

Mit der Security-API wurde noch ein weiteres Interface eingeführt, der SecurityContext. Dieser soll einen einfachen Zugriff auf Sicherheitsmethoden für den aktuellen Benutzer sowie dessen Mitgliedschaft in einer Rolle erlauben. Hintergrund für die Einführung waren die vielen unterschiedlichen Varianten der verschiedenen Standards (wie z. B. HttpServletRequest#isUserInRole). Der Filter kann mit der aus der Java-Security bekannten Annotation RolesAllowed an einer Ressource aktiviert werden.

Wird RESTEasy eingesetzt, muss diese Implementierung nicht umgesetzt werden, da ein ähnliches Feature direkt mitgeliefert wird. In der Standardkonfiguration ist es nicht aktiv, kann aber mit Hilfe eines Eintrags in der web.xml eingeschaltet werden:

Listing 25: Aktivierung des RESTeasys RolesAllowed-Feature in der web.xml

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
  <context-param>
    <param-name>resteasy.role.based.security</param-name>
    <param-value>true</param-value>
  </context-param>
</web-app>

Fazit

Im Artikel wurde mit Hilfe einer JWT-Unterstützung für die Authentifizerung und Autorisierung gezeigt, was sich mit der neuen Security-API umsetzen lässt. Eine komplette Implementierung kann online gefunden werden [3].

Autor

Christian Schulz

Christian Schulz verfügt über mehrjährige Erfahrung als Entwickler im Enterprise- und SPA-Umfeld. Zu seinen Interessenschwerpunkten gehören API-Design und -Dokumentation via Swagger und das SPA-Framework Angular.
>> Weiterlesen
Kommentare (1)

Neuen Kommentar schreiben