Web Services mit UsernameToken sichern (PDF)

Autor:
Fabian Fassott
Orientation in Objects GmbH
Fabian Fassott
Fabian Fassott
Datum:November 2008

Web Services mittels UsernameToken sichern

Die Authentifzierung von menschlichen Benutzern erfolgt üblicherweise per Passwort. Dieses Tutorial beschreibt, wie Web Services durch Passwortverfahren geschützt werden können. Dabei wird auf NetBeans, Metro und GlassFish zurückgegriffen.

Web Service mittels UsernameToken sichern

Die wohl am weitesten verbreitete und beliebteste Authentifizierungsmethode für schützenswerte Ressourcen ist die Vergabe von Benutzernamen samt zugehöriger Passwörter. Für Web Services existiert für den Bereich Sicherheit die Web Service Security-Spezifikation (WSS) [1]. Diese bietet die Möglichkeit, SOAP-Nachrichten zu verschlüsseln sowie deren Integrität zu bewahren. Mit WS-Security ist die Verschlüsselung und Integrität von Daten somit unabhängig vom Transportprotokoll möglich (SSL). Zusätzlich definiert sie, wie auf sicherheitsrelevante Daten (Zertifikate, Kerberos-Tickets, Schlüssel) zu verweisen ist. WSS wird durch so genannte Profiles erweitert, die einen speziellen Sicherheitskontext abdecken. Für die Authentifizierung per Passwort existiert das UsernameTokenProfile [2]. Hauptgegenstand des UsernameTokenProfiles ist das UsernameToken, welches im SOAP-Header zum Service übertragen wird. Beispiel 1 zeigt dessen Aufbau.

<UsernameToken wsu:Id="Example-1">
  <Username>...</Username>
  <Password Type="...">...</Password>
  <Nonce EncodingType="...">...</Nonce>
  <Created>...</Created>
</UsernameToken>

Beispiel 1: Das UsernameToken-Element:

Unter Username wird der Benutzername angegeben, Password enthält je nach Wert des Type-Attributs entweder das Klartextpasswort oder den Hash-Wert des Klartextpassworts, Nonce und Created sollen Replay-Angriffe vermeiden, bei denen ein UsernameToken von einem Angreifer abgefangen und unbefugterweise von diesem selbst in SOAP-Nachrichten verwendet wird.

Replay-Attacke

Ein Angreifer könnte eine Nachricht samt UsernameToken abfangen und zu einem späteren Zeitpunkt erneut an den Service schicken. Durch das enthaltene UsernameToken wäre der Angreifer zur Nutzung des Services autorisiert. Um dies zu vermeiden, wird jedes UsernameToken mit einer eindeutigen Nummer versehen (Nonce). Der Service merkt sich diese Nummer und weist neu zugestellte UsernameTokens mit der gleichen Nummer ab. Um zu vermeiden, dass die Liste verwendeter Nummern immer weiter wächst, wird ein Zeitstempel (Created) in jedem UsernameToken übertragen. Nach Ablauf der angegebenen Zeit ist ein UsernameToken nicht mehr gültig. Somit können Nonces, deren Zeitstempel abgelaufen ist, bedenkenlos gelöscht werden.

Es ist nicht immer möglich, im Password-Element den Hash-Wert des Passworts zu übertragen, da der Service möglicherweise keinen Zugriff auf die Klartextpasswörter der Benutzer hat und somit nicht selbst deren Hash zum Vergleich berechnen kann. In solchen Fällen sollte das Klartextpasswort im Password-Element stehen. Das zu übertragende UsernameToken ist dann mittels WS-Security zu verschlüsseln. Wie dies mit dem Web Service-Stack Metro [3] erzielt werden kann, wird im Folgenden beschrieben.

Voraussetzungen

  • GlassFish V2 UR2 [4] mit Metro 1.2 [5, 6] und GlassFish-Zertifikatupdate [7]

  • NetBeans 6.1 [8]

  • GlassFish V2 UR2 in NetBeans einrichten, wofür folgende Schritte nötig sind:

  1. NetBeans 6.1 starten.

  2. Services-Tab wählen -> Rechtsklick auf Servers->Add Server…-> Server: GlassFish V2, Name: GlassFish V2 UR2 -> Next-> Platform Location: Pfad zu installiertem GlassFish V2 UR2 angeben -> Next-> evt. Admin-Daten anpassen ->Finish

Service-Implementierung

Um zu demonstrieren, wie ein Web Service mittels UsernameToken gesichert werden kann, muss dieser zunächst erstellt werden. Dabei wird kein Wert auf eine anspruchsvolle Implementierung gelegt, es geht lediglich darum, einen Web Service zu erstellen, der dann gesichert werden kann.

1. In NetBeans ist ein neues Projekt für den zu sichernden Web Service zu erstellen. File -> New Project -> Categories: Web und Projects: Web Application -> Next -> Project Name: UsernameTokenService -> Next -> Server: konfigurierten GlassFish-Server mit Metro 1.2 auswählen -> Finish.

Schritt 1: Projekt erstellen

Abbildung 1: Schritt 1: Projekt erstellen

2. Nach der Erstellung des Projektes, wird innerhalb dieses Projektes der zu schützende Web Service angelegt: Rechtsklick auf eben erstelltes Projekt-> New-> Other... -> Categories: Web Services und File Types: Web Service -> Next -> Web Service Name: UsernameTokenService, package: server -> Finish

Schritt 2: Web Service erstellen

Abbildung 2: Schritt 2: Web Service erstellen

3. Das erstellte Grundgerüst des Web Services ist nun mit Leben zu füllen: Unter Projekt den Ordner Web Services expandieren und Doppelklick auf UsernameTokenService -> Wechsel in Source-Ansicht (befindet sich links oben im zentralen Fenster). Die Implementierung so ändern, dass sie Beispiel 2 entspricht.

package server;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;

@WebService()
public class UsernameTokenService 
{
  @WebMethod(operationName = "sayHello")
  public String sayHello(@WebParam(name = "name") String name)
  {
    return "Hello " +name;
  }
}

Beispiel 2: Implementierung von UsernameTokenService

4. Der implementierte Web Service soll nun so gesichert werden, dass nur authentifizierte Benutzer ihn verwenden dürfen. Die Authentifizierung soll per UsernameToken erfolgen. Dafür sind folgende Schritte nötig: Rechtsklick auf UsernameTokenService unter Web Services-Ordner -> Edit Web Service Attributes -> Haken bei Secure Service . Nun besteht die Möglichkeit, einen Sicherungsmechanismus (Security Mechanism) auszuwählen. Um die Sicherung per UsernameToken zu erreichen, muss der entsprechende Wert ausgewählt werden-> Security Mechanism: Username Authentication with Symmetric Key. Der abschließende Haken bei Use Development Defaults bewirkt, dass die Standard-Schlüssel der GlassFish-Installation für die anzuwendenden kryptographischen Operationen zu verwenden sind. Welche Operationen genau ausgeführt werden, wird später erläutert (Client-Implementierung, Schritt 6).

Schritt 4: Sichern des Services per UsernameToken

Abbildung 3: Schritt 4: Sichern des Services per UsernameToken

5. Der gesicherte Web Service ist abschließend auf GlassFish zu deployen: Rechtsklick auf Projekt -> "Undeploy and Deploy" wählen.

Benutzer hinzufügen

Nachdem der Service deployed wurde, muss ein Benutzer für dessen Verwendung erstellt werden.

1. Zunächst muss der GlassFish-Server gestartet werden, sofern er nicht gestartet wurde. In NetBeans Services-Tab auswählen -> Servers expandieren -> Rechtsklick auf GlassFish V2 UR2 -> Start.

2. Mittels Browser sich mit http://localhost:4848 verbinden. Standardbenutzername: admin, Standardpasswort: adminadmin.

3. Im linken Fenster Configuration -> Security -> Realms -> file, dann auf Manage Users im rechten Fenster klicken.

Schritt 3: Unter dem Realm "File" einen neuen Benutzer hinzufügen.

Abbildung 4: Schritt 3: Unter dem Realm "File" einen neuen Benutzer hinzufügen.

4. Jetzt den Web Service-Benutzer erstellen: Auf New klicken -> User ID: client, Group List: leer lassen, New Password und Confirm New Password: client -> OK.

Schritt 4: Erstellen des Benutzers für den Web Service

Abbildung 5: Schritt 4: Erstellen des Benutzers für den Web Service

Client-Implementierung

Nachdem der Web Service erstellt und ein zur Nutzung befugter Benutzer erstellt wurde, kann der Client implementiert werden. Dieser wird als Standalone-Client erstellt, was wie folgt bewerkstelligt wird:

1. Das Client-Projekt erstellen: File -> New Project…-> Categories: Java und Projects: Java Application -> Next -> Project Name: UsernameTokenServiceSOClient, Create Main Class: client.Main -> Finish.

Schritt 1: Erstellen des Client-Projekts

Abbildung 6: Schritt 1: Erstellen des Client-Projekts

2. Den Web Service Client erstellen (GlassFish muss laufen und UsernameTokenService muss deployed sein): Rechtsklick auf erstelltes Projekt -> New -> Other… -> Categories: Web Services und File Type: Web Service Client -> Next -> WSDL URL: http://localhost:8080/UsernameTokenService/UsernameTokenServiceService?wsdl -> Finish

Hinweis: Die URL zur WSDL-Datei ergibt sich nach folgendem Muster:

Transportprotokoll des Web Service-Bindings://Rechnername:GlassFish-Port/ContextRoot des Web Service-War-Files (Standard: Projektname)/X?wsdl

X entspricht dabei standardmäßig dem Namen der Implementierungsklasse des Web Services plus dem Suffix Service. In unserem Falle heißt die Implementierungsklasse UsernameTokenService. Samt Suffix wird daraus dann UsernameTokenServiceService. X kann allerdings auch frei gewählt werden. Hierfür muss ein Attribut der @WebService-Annotation der Implementierungsklasse genutzt werden. Durch die Angabe von @WebService(serviceName="ABCService") wird aus dem X-Teil der WSDL-URL ABCService. Gleichzeitig bewirkt die Angabe dieses Attributs, dass das name-Attribut des Service-Elements in der für den Web Service generierten WSDL-Datei auf ABCService gesetzt wird. Diese Regeln gelten nicht nur für die WSDL-URL, sondern auch für die Aufruf-URL des Web Services.

Schritt 2: Erstellen des Web Service-Clients

Abbildung 7: Schritt 2: Erstellen des Web Service-Clients

3. Einbinden der für den Aufruf des Web Services vom Web Service Client benötigten Bibliotheken: Rechtsklick auf erstelltes Projekt -> Properties -> auf linker Seite Libraries auswählen -> auf rechter Seite auf „Add JAR/Folder„ klicken um die Bibiliotheken webservices-rt.jar und webservices-tools.jar aus METRO_HOME/lib auszuwählen -> OK. (METRO_HOME entspricht dem Pfad zum Metro-Ordner der bei der Installation von Metro erzeugt wurde).

Schritt 3: Einbinden der benötigten Bibliotheken

Abbildung 8: Schritt 3: Einbinden der benötigten Bibliotheken

4. Anschließend müssen die Stub-Klassen zum Aufruf des Web Services generiert werden: Im Projekt den Ordner "Web Service References" expandieren -> Rechtsklick auf UsernameTokenServiceService -> Edit Web Service Attributes -> Haken bei Use development defaults (HINWEIS: Der Haken erscheint nicht, Bug in NetBeans, erfolgreich wenn nach Klick auf Finish im Ordner „Source Packages„ der Ordner META-INF erstellt wurde).

5. Unter „Source Packages“ die Datei META-INF\UsernameTokenServiceService.xml editieren. Der Benutzername und das Passwort sollen zur Laufzeit bestimmt werden, daher muss die statische Konfiguration entfernt werden (siehe Screenshot).

Schritt 5: Entfernen des sc:CallbackHandlerConfiguration-Elements (markierter Bereich).

Abbildung 9: Schritt 5: Entfernen des sc:CallbackHandlerConfiguration-Elements (markierter Bereich).

6. In UsernameTokenServiceService.xml muss jetzt die Konfiguration für den zu verwendenden Public Key des Servers angepasst werden. Dieser befindet sich in einem JKS-Keystore namens „client-truststore.jks„und wird über das location-Attribut ausgewählt, welches dem TrustStore-Element hinzuzufügen ist: <sc:TrustStore location="client-truststore.jks" …/> Die Änderungen an UsernameTokenServiceService.xml werden jetzt gespeichert: File -> Save all.

Der Public Key des Servers wird benötigt, um den zur Laufzeit erzeugten symmetrischen Schlüssel zur Verschlüsselung des UsernameTokens für den Server zu verschlüsseln. Der Server entschlüsselt dann mit seinem Private Key den symmetrischen Schlüssel und verwendet diesen zur Entschlüsselung des UsernameTokens. Anhand der Informationen im UsernameToken führt er dann die Zugriffskontrolle aus.

Verschlüsselungsoperationen mit WS-Security

Wenn man sich die Vorgehensweise bei der Verschlüsselung betrachtet, fällt einem auf, dass das UsernameToken auch direkt mit dem Public Key des Servers verschlüsselt werden könnte. WS-Security verbietet jedoch den Einsatz von Public Key-Verschlüsselung für andere Elemente. Nur symmetrische Schlüssel dürfen mittels Public Key-Kryptographie verschlüsselt werden. Diese Entscheidung beruht auf der Tatsache, dass die Laufzeit von Public Key-Algorithmen um ein Vielfaches schlechter ist als die von symmetrischen Algorithmen. Daher sollten diese nur zur Verschlüsselung von kleinen Datenmengen, wie eben einem Schlüssel, verwendet werden.

Schritt 6: Setzen des location-Attributs zur Auswahl des TrustStores.

Abbildung 10: Schritt 6: Setzen des location-Attributs zur Auswahl des TrustStores.

7. Minimieren Sie NetBeans und wechseln Sie mit dem Windows-Explorer oder ähnlichem in das Hauptverzeichnis des Client-Projektes auf Ihrer Festplatte. Kopieren Sie den src\META-INF-Ordner in den build\classes-Ordner. Kopieren Sie die Datei GLASSFISH_HOME\domains\domain1\config\cacerts.jks nach build\classes\META-INF und nennen Sie sie in „client-truststore.jks„ um. GLASSFISH_HOME bezeichnet das Wurzelverzeichnis ihrer GlassFish-Installation.

Nun muss der eigentliche Anwendungscode zum Aufruf des gesicherten Services geschrieben werden. Der Ablauf sieht wie folgt aus: in client.Main wird der Benutzername und das Passwort von der Kommandokonsole gelesen. Diese Daten werden dann in der Klasse security.UserPasswordInfo in dafür vorgesehenen statischen Variablen hinterlegt und sind somit für den JAX-WS-Handler security.UsernameTokenHandler abrufbar. Dieser Handler erzeugt dann aufgrund der Benutzerdaten das benötigte UsernameToken.

Schritt 7: Die wesentlichen Schritte der Client-Implementierung als Sequence Diagram.

Abbildung 11: Schritt 7: Die wesentlichen Schritte der Client-Implementierung als Sequence Diagram.

8. security.UserPasswordInfo implementieren:

package security;

public class UserPasswordInfo 
{
  private static String username;
  private static String password;
          
  public static String getPassword()
  {
    return password;
  }
          
  public static void setPassword(String password)
  {
    UserPasswordInfo.password = password;
  }
          
  public static String getUsername()
  {
    return username;
  }
          
  public static void setUsername(String username)
  {
    UserPasswordInfo.username = username;
  }
}

Beispiel 3: security.UserPasswordInfo

9. security.UsernameTokenHandler implementieren:

package security;

import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.*;
import java.util.*;
        
public class UsernameTokenHandler 
implements SOAPHandler<SOAPMessageContext>
{
  public Set<QName> getHeaders()
  {
    return null;
  }
        
  public boolean handleMessage(SOAPMessageContext context)
  {
    Boolean isOutbound = 
    (Boolean)context.get(context.MESSAGE_OUTBOUND_PROPERTY);
    if(isOutbound.booleanValue())
    {
      context.put(com.sun.xml.wss.XWSSConstants.USERNAME_PROPERTY,
      UserPasswordInfo.getUsername());
      context.put(com.sun.xml.wss.XWSSConstants.PASSWORD_PROPERTY,
      UserPasswordInfo.getPassword());
    }
    return true;
  }
        
  public boolean handleFault(SOAPMessageContext context)
  {
    return true;
  }
        
  public void close(MessageContext context)
  {
                
  }
}

Beispiel 4: security.UsernameTokenHandler:

Relevant ist nur die Implementierung der handleMessage-Methode. In ihr wird zunächst überprüft, ob der Handler für eine ausgehende Nachricht (also eine Nachricht hin zum Server) aufgerufen wird. Ist dies der Fall, werden die Benutzername- und Passwort-Werte des UsernameTokens gesetzt.

10. Jetzt muss noch client.Main implementiert werden (Siehe Beispiel 5):

Zu Beginn werden die für den Service-Aufruf zu verwendenden Benutzerdaten von der Kommandokonsole gelesen und in UserPasswordInfo hinterlegt. Dann werden die generierten Service Client-Klassen verwendet und der UsernameTokenHandler als Handler für Service-Aufrufe hinzugefügt. Es folgt der eigentliche Service-Aufruf. Das Setzen der System-Property bewirkt die Ausgabe der ausgetauschten SOAP-Nachrichten auf der Konsole. Für eine übersichtlichere Form eignet sich das Tool TCPMon [9].

11. Abschließend kann der Client gestartet werden: File -> Save All. Dann Rechtsklick auf Client-Projekt -> Build. Dann Rechtsklick auf client.Main und Run File wählen. Geben Sie den Benutzernamen und das Passwort des von Ihnen erstellten Benutzers ein.

In diesem Artikel wurden die benötigten Schritte aufgezeigt, um einen Web Service mittels NetBeans und GlassFish per Passwort-Authentifizierung zu schützen und einen dazugehörigen Client zu erstellen. Zunächst wurde der zu sichernde Web Service erstellt und mit dem Sicherheitsmechanismus Username Authentication with Symmetric Key geschützt, um die Authentifizierung per UsernameToken zu aktivieren. Dann wurde ein Benutzer zur Nutzung des Services unter Angabe von Benutzername und Passwort in GlassFish eingerichtet. Die Erstellung des zugehörigen Standalone-Clients benötigte im Vergleich zu den ersten beiden Schritten mehr Handarbeit. Nach dem Erstellen der Stub-Klassen war die erstellte Konfigurationsdatei anzupassen sowie zusammen mit dem benötigten Keystore-File an die richtige Stelle zu kopieren. Kern der Client-Anwendungslogik war das Setzen des abgefragten Benutzernamens und Passworts in einem JAX-WS-Handler.

package client;

import javax.xml.ws.*;
import javax.xml.ws.handler.*;
import java.util.*;
import java.io.*;
        
import security.*;
        
public class Main 
{
  public static void main(String[] args) 
  {
    try 
    { 
      System.out.println("Bitte geben Sie ihren Benutzernamen ein: ");
      BufferedReader reader = 
          new BufferedReader(new InputStreamReader(System.in));
      UserPasswordInfo.setUsername(reader.readLine());
      System.out.println("Bitte geben Sie ihr Passwort ein: ");
      UserPasswordInfo.setPassword(reader.readLine());
      reader.close();
      System.setProperty("com.sun.xml.ws.transport.http.client.
      HttpTransportPipe.dump", "true");
      server.UsernameTokenServiceService service = 
          new server.UsernameTokenServiceService();
      server.UsernameTokenService port = 
          service.getUsernameTokenServicePort();
      BindingProvider prov = (BindingProvider)port;
      List<Handler> handlerChain = 
          prov.getBinding().getHandlerChain();
      handlerChain.add(new UsernameTokenHandler());
      prov.getBinding().setHandlerChain(handlerChain);
      prov.getRequestContext().put(prov.ENDPOINT_ADDRESS_PROPERTY,
          "http://localhost:8080/UsernameTokenService/
           UsernameTokenServiceService");
      java.lang.String name = "Test";
      java.lang.String result = port.sayHello(name);
      System.out.println("Result = "+result);
    } 
    catch (Exception ex) 
    {
      ex.printStackTrace();
    }
  }
}

Beispiel 5: client.Main:

Die fertigen Projekte stehen unter [10] zum Download bereit. Dabei ist zu beachten, dass das Keystore-File des Client-Projektes wie im Artikel beschrieben zu ersetzen ist. Außerdem müssen im Client-Projekt die Pfade zu den benötigten Libraries an die lokale Installation angepasst werden.

Überblick auf die Lösung

Abbildung 12: Überblick auf die Lösung

Zum Geschaeftsbreich Competence Center
Schulung
Vielleicht interessiert Sie unsere Schulung zu Web Service Security?
Beratung
Oder unsere Beratung zu SOA?