IT Security – Customized Security mit IdentityServer

27. August 2019

Im ersten Teil dieses Themenschwerpunktes wurde eine grundlegende Problematik in der Softwareentwicklung aufgezeigt: Informatiker sind nicht notwendigerweise IT-Security-Experten und IT Security ist kein nebenläufiger Aspekt der Software-Entwicklung, den man mehr oder weniger dem Zufall überlassen kann. In diesem zweiten Teil soll IdentityServer vorgestellt werden, ein Framework, mit dem wir bei SDX im Einsatz für den Kunden gute Erfahrungen gemacht haben.

IdentityServer

IdentityServer ist ein von Dominick Baier and Brock Allen gegründetes und weitergepflegtes OpenSource-Projekt, das im Wesentlichen aus einem Framework für die Authentifizierung und Autorisierung mittels OAuth 2.0  und OpenID für ASP.NET-Core-Anwendungen besteht. Ein charakteristisches Merkmal von IdentityServer ist, dass es nicht nach dem Werkzeugkastenprinzip funktioniert, sondern bereits funktionierende Workflows bereitstellt, in die man gezielt eingreifen kann, um den jeweiligen Business-Anforderungen gerecht zu werden. Das zugrundeliegende Prinzip ist die konsequente Nutzung der in ASP.NET Core üblichen Dependency Injection.

Ein typischer Workflow sieht wie folgt aus:

  • Ein Benutzer möchte mittels einer URL auf eine geschützte Ressource zugreifen
  • Der Server, der die Ressource verwaltet (im Folgenden „Ressourcenserver“ genannt), stellt fest, dass der Benutzer nicht authentifiziert ist, also seine Identität noch nicht nachgewiesen hat.
  • Der Benutzer wird auf eine Login-Seite umgeleitet, die sich entweder auf dem Server befinden kann oder von einem externen Anbieter (z. B. Microsoft, Google oder einem separaten, unternehmenseigenen Server) bereitgestellt wird.
  • Wenn der Benutzer sich erfolgreich eingeloggt hat, erstellt der Server für ihn ein Token (üblicherweise im JWT-Format), das nicht nur für die Authentifizierung des Benutzers genutzt werden kann, sondern weitere Informationen zum Benutzer enthält.
  • Die Sicherheit des Tokens gegen Fälschungen basiert auf einer digitalen Signatur mit einem kryptografischen Schlüssel, deren Authentizität und Integrität der Ressourcenserver mittels eines Zertifikats überprüfen kann.
  • Der Ressourcenserver kann nun anhand der Informationen aus dem Token entscheiden, ob der Benutzer die für den Zugriff auf die Ressource gewünschten Anforderungen erfüllt, also dafür autorisiert ist.

Der Inhalt eines JWT-Tokens könnte folgende Gestalt haben:

{
  "nbf": 1557136178,
  "exp": 1557143378,
  "iss": "https://my-product-site-site.net",
  "aud": [
    "https://my-product-site-site.net/resources",
    "my-product"
  ],
  "amr": [
    "pwd"
  ],
  "client_id": "my-product-client",
  "sub": "0fd314a1-2c33-492a-827d-b540afa0e86e",
  "auth_time": 1557136178,
  "idp": "local",
  "role": [
    "user",
    "operator"
  ],
  "given_name": "Annalena",
  "family_name": "Schmidt",
  "email": "annalena.schmidt@my-product-site-site.net",
  "email_verified": true,
  "scope": [
    "openid",
    "offline_access",
    "customer_service"
  ]
}

Claims und Role-Based Security

Für fast alle diese Informationen gibt es standardisierte Claim-Arten. Das „sub“-Claim enthält z. B. typischerweise eine technische Benutzer-ID und „email_verified“ gibt an, dass der Benutzer die hinterlegte Emailadresse bestätigt hat. Besonders nützlich sind auch die im „role“-Claim enthaltenen Werte. Im Beispiel hat der Benutzer die Rollen „user“ und „operator“. Dieser Umstand lässt sich nutzen, um Endpunkte auf dem Server auf einfache Weise abzusichern.

Die auf dem Ressourcen-Server aufgerufene Komponente könnte in ASP.NET Core z. B. so aussehen:

[Authorize]
public IActionResult Index()
{
	// ...
}

[Authorize(Roles="operator, admin")]
public IActionResult GetCustomerInfo()
{
	// ...
}

[Authorize(Roles="admin")]
public IActionResult DeleteCustomer()
{
	// ...
}

Bei entsprechender Konfiguration des Servers reicht dieses Authorize-Attribut bereits aus, um den Zugriff auf den jeweiligen Endpunkt zu beschränken. Auf „Index()“ dürfen alle authentifizierten Benutzer unabhängig von ihrer Rolle zugreifen, während für den Zugriff auf „GetCustomerInfo()“ zusätzlich die Rolle „operator“ und/oder „admin“ erforderlich ist. „DeleteCustomer“ ist nur für Benutzer mit der Rolle „admin“ aufrufbar. Im Fehlerfall liefert der Server den HTTP-Statuscode 401 („Unauthorized“) zurück.

Selbstverständlich benötigt man für eine solche einfache rollenbasierende Autorisierung kein zusätzliches Framework wie IdentityServer. Was aber wäre, wenn als zusätzliche Business-Anforderung dazu käme, dass bestimmte Benutzer „GetCustomerInfo()“ und andere Methoden, die sensible Kundendaten zurückliefern, nur innerhalb ihrer offiziellen Arbeitszeit abrufen können sollen?

Customizing

In solchen Situationen erweist es sich von Vorteil, wenn man keinen in sich geschlossenen Authentifizierungs- und Autorisierungsserver verwendet, sondern die volle Kontrolle über die entsprechenden Prozesse hat.

In der Regel wird man eine Datenbank oder ein wie auch immer aufgebautes Verzeichnis haben, in der der Benutzer mit der technischen ID „0fd314a1-2c33-492a-827d-b540afa0e86e“ (Annalena Schmitt) mit den Rollen „user“ und „operator“ verknüpft ist.

Um das beschriebene Ziel zu erreichen, könnte man den Rollennamen ändern (z. B. von „operator“ in „limited-op“), ohne die „Authorize“-Attribute anzupassen. Dies würde zunächst dazu führen, dass Annalena Schmitt (vgl. Inhalt des beispielhaften JWT-Tokens) und andere Operatoren, die nicht zusätzlich die Rolle „admin“ haben, gar nicht mehr auf Methoden wie „GetCustomerInfo()“ zugreifen können. Im zweiten Schritt würde man die IdentityServer-Infrastruktur auf Seiten des Authentifizierungs- und Autorisierungsservers mittels Dependency Injection verändern bzw. erweitern, z. B. in der Datei „Startup.cs“ mit der Zeile:

services.AddScoped<IUserClaimsPrincipalFactory<IdentityExpressUser>, CustomUserClaimsPrincipalFactory>();

Die neue Komponente „CustomUserClaimsPrincipalFactory” könnte von der IdentityServer-Komponente abgeleitet worden sein, die sie später ersetzt (verkürzte Darstellung):

public class CustomUserClaimsPrincipalFactory : IdentityExpressUserClaimsPrincipalFactory&lt;IdentityExpressUser, IdentityExpressRole&gt;
{
    protected override async Task&lt;ClaimsIdentity&gt; GenerateClaimsAsync(IdentityExpressUser user)
    {
        var claimsIdentity = await base.GenerateClaimsAsync(user);
        var claims = user.Claims.ToList();
        var limitedOperatorRole = user.Claims.FirstOrDefault(x =&gt; x.ClaimType == JwtClaimTypes.Role &amp;&amp; x.ClaimValue == "limited-op");

        if (limitedOperatorRole != null &amp;&amp; DetermineIsActiveOperator(user))
        {
            claimsIdentity.AddClaim(new Claim(JwtClaimTypes.Role, "operator"));
        }

        return claimsIdentity;
    }

    private bool DetermineIsActiveOperator(IdentityExpressUser user)
    {
       // Implementation Details ...
    }
}

In der Methode „DetermineIsActiveOperator“ würde dann ermittelt werden, ob die Authentifizierung innerhalb der Arbeitszeiten des Operators stattfindet. In diesem Fall würde die Rolle „operator“ dann dynamisch hinzugefügt. Weitere denkbare Anpassungen wären das Verbergen des „limited-op“-Role-Claims, sofern man dieses nicht für Log-Zwecke erhalten möchte, sowie eine geeignete Verkürzung der Lebensdauer des Tokens, da die Arbeitszeiten bei diesem Ansatz nur im Moment der Authentifizierung berücksichtigt werden würden.

Wie man erkennen kann, sind solche Anpassungen mit wenigen Code-Zeilen möglich, ohne dass in den restlichen Authentifizierungs- und Autorisierungsworkflow eingegriffen wird. Die Herausforderung für einen Software-Entwickler besteht hauptsächlich darin, die richtige Stelle in der IdentityServer-Infrastruktur zu identifizieren, nicht aber den Workflow in allen Feinheiten verstehen zu müssen.

Trotzdem kann natürlich eine kritische Masse an business-spezifischen Anforderungen erreicht werden, ab der es sinnvoll ist, einen IT-Security-Experten hinzuziehen. Gleiches gilt für die Authentifizierung und Autorisierung in heterogenen Umgebungen mit voneinander abweichenden Sicherheitsanforderungen, z. B. Workflows, die gleichzeitig von einer mobilen App und einer Web-Applikation genutzt werden.

Fazit

IdentityServer bietet eine standardisierte, ständig weiterentwickelte Basis für die Abbildung von benutzerspezifischen Authentifizierungs- und Autorisierungsprozessen, die so mit schlüsselfertigen Identity-Provider-Lösungen nicht abgedeckt werden könnten. Statt den gesamten Prozess in Eigenregie neu zu entwickeln, was das Risiko von Sicherheitslücken birgt, müssen nur einzelne Komponenten des vorgegebenen Prozesses erweitert oder ersetzt werden.

 

autor Torben Graefe

Torben Graefe

Principal eXpert