• Noser.com
facebook
linkedin
twitter
youtube
  • NOSERmobile
    • Android
    • HTML 5
    • Hybrid Apps
    • iOS
    • Windows Phone
  • NOSERembedded
    • Medizintechnik
  • NOSERprojektmanagement
  • NOSERtesting
  • NOSERlifecycle
    • .NET Allgemein
    • Application Lifecylce Management
    • Apps
    • Architektur
    • ASP.NET
    • Azure
    • Cleancode
    • Cloud
    • Silverlight
    • Visual Studio / Team Foundation Server
    • Windows 8
    • Windows Presentation Foundation
  • NOSERinnovation
    • Big Data
    • Cloud
    • IoT
    • Operations Research
    • Augmented Reality
    • RFID, NFC, Bluetooth LE

gRPC Tutorial Teil 7: JWT Client Credentials mit Rollen und IdentityServer4

28. Februar 2020
Erik Stroeken
1
C#, gRPC, identityserver4

Im letzten Blog wurde der einfachste OAuth2-Flow, der Client Credential Flow, anhand eines Beispiels unter Verwendung eines gRPC-Clients / -Servers mit einem minimalistischen Token-Server erläutert. In diesem Blog wird der Token Server implementiert mit IdentityServer4. Es wird das Public-Key-Verschlüsselungsverfahren verwendet und rollenbasierte Autorisierung.

Der Beispielcode kann hier als ZIP-Datei heruntergeladen werden.

Ablauf

Das Konstrukt involviert drei Teilnehmer: den Client, den Identity Server und den Ressourcen-Server.

Der Ablauf ist wie folgt:

  1. Der Client holt sich beim Discovery Endpoint des Identity Server den Token Endpoint.
  2. Mit seiner ClientId, dem Secret und dem Namen des gewünschten APIs fragt der Client ein JWT Token beim Token Endpoint des Identity Servers an.
  3. Der Identity Server prüft die ClientId, das Client Secret und ob der Client mit der gewünschten API kommunizieren darf. Der Identity Server definiert die Autorisierungsrolle des Clients und packt diese in das Token.
  4. Der Client speichert das Token und sendet es bei jeder Anfrage am Ressourcen-Server mit (im Header).
  5. Der Ressourcen-Server validiert das Token und kontrolliert die Signatur. Wenn das Token gültig ist, wird die Rolle aus dem Token gelesen und kontrolliert, ob der Client berechtigt ist, die spezifische Anfrage zu machen.

Die ganze Kommunikation verläuft über eine gesicherte Verbindung, weil der Besitzer des Tokens direkt auf die Ressource zugreifen kann.

Identity Server 4

Identity Server 4 ist ein OpenID Connect- und OAuth 2.0-Framework für .NET, .NET Core und ASP.NET Core, um JWT Bearer Tokens zu generieren. Identity Server 4 ist eine Implementierung der OAuth 2.0-Spezifikation und unterstützt die Standard OAuth 2.0 ‘Flows’ (siehe https://blog.noser.com/grpc-tutorial-teil-6-jwt-client-credentials). Die Webseiten des Identity Server 4 sind sehr ausführlich und beinhalten einfache Beispiele, unter anderen vom Client Credential Flow (https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html).

Beispiel: Identity-Server

Das Identity-Server Projekt im Beispiel heisst RpcJwtClientCredentialsRolesIdentityServer und referenziert das NuGet Paket IdentityServer4 (version 3.0.1).

Das Projekt benutzt den Kestrel-Server mit gesicherter Verbindung wie beschrieben in ‘gRPC Tutorial Teil 4: HTTP/2 über HTTPS’.

Die relevanten Parameter in appsettings.json sehen wie folgt aus:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
…
"ApiResource": {
    "Name": "DemoService",
    "DisplayName": "gRPC DemoService API"
},
"ClientUser": {
    "ClientId": "ClientUser",
    "ClientSecret": "D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967",
    "AllowedScopes": "DemoService",
    "Role": "User"
},
"ClientAdmin": {
    "ClientId": "ClientAdmin",
    "ClientSecret": "V4YH2Q8C968XC52S5EFWD7VDX3TBEU9ZNY8EE6GE2F35ATM5XSS3PD6C6PEA9DZM",
    "AllowedScopes": "DemoService",
    "Role": "Admin"
},
"Certificate": {
    "File": "server.pfx",
    "Password": "P@ssw0rd"
}
…

In der Methode ConfigureServices() in Startup.cs wird der Identity-Server konfiguriert.

Als erstes wird ein Array erstellt mit den APIs, hier nur DemoService:

C#
1
2
3
4
5
IEnumerable<ApiResource> apis = new ApiResource[]
{
    new ApiResource(Configuration["ApiResource:Name"],
        Configuration["ApiResource:DisplayName"])
};

Dann wird ein Array mit zwei Clients definiert, einen mit User- und einen mit Admin-Rechten:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
IEnumerable<Client> clients = new Client[]
{
    // Client User
    new Client
    {
        ClientId = Configuration["ClientUser:ClientId"],
        AllowedGrantTypes = GrantTypes.ClientCredentials,
        ClientSecrets =
        {
            new Secret(Configuration["ClientUser:ClientSecret"].Sha256())
        },
        AllowedScopes = { Configuration["ClientUser:AllowedScopes"] },
        Claims = new List<Claim>
        {
            new Claim(JwtClaimTypes.Role, Configuration["ClientUser:Role"])
        }
        ,ClientClaimsPrefix = null
    },
 
    // Client Admin
    new Client
    {
        ClientId = Configuration["ClientAdmin:ClientId"],
        AllowedGrantTypes = GrantTypes.ClientCredentials,
        ClientSecrets =
        {
            new Secret(Configuration["ClientAdmin:ClientSecret"].Sha256())
        },
        AllowedScopes = { Configuration["ClientAdmin:AllowedScopes"] },
        Claims = new List<Claim>
        {
            new Claim(JwtClaimTypes.Role, Configuration["ClientAdmin:Role"])
        }
        ,ClientClaimsPrefix = null
    }
};

Wenn ein Client ein Token anfragt, dann sendet dieser seine ClientId, Secret und gewünschte API mit. Die ClientId wird benutzt, um in den ‘clients’-Array die Daten aufzusuchen. Das ClientSecret wird verglichen und es wird kontrolliert, ob der Client auf die gewünschte API zugreifen darf. Die definierte Rolle wird im Token verpackt.

Der Identity-Server hört in diesem Projekt auf https://localhost:5001 (eingestellt in ‘launchSettings.json’). Man kann unter der URL https://localhost:5001/.well-known/openid-configuration ein ‘discovery’-Dokument beim Server abfragen, worin sich die URL des Token-Endpoints befindet (token_endpoint: “https://localhost:5001/connect/token“).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
issuer: "https://localhost:5001",
jwks_uri: "https://localhost:5001/.well-known/openid-configuration/jwks",
authorization_endpoint: "https://localhost:5001/connect/authorize",
token_endpoint: "https://localhost:5001/connect/token",
userinfo_endpoint: "https://localhost:5001/connect/userinfo",
end_session_endpoint: "https://localhost:5001/connect/endsession",
check_session_iframe: "https://localhost:5001/connect/checksession",
revocation_endpoint: "https://localhost:5001/connect/revocation",
introspection_endpoint: "https://localhost:5001/connect/introspect",
device_authorization_endpoint: "https://localhost:5001/connect/deviceauthorization",
frontchannel_logout_supported: true,
frontchannel_logout_session_supported: true,
backchannel_logout_supported: true,
backchannel_logout_session_supported: true,
scopes_supported:
[
"DemoService",
"offline_access"
],
claims_supported: [ ],
grant_types_supported:
[
"authorization_code",
"client_credentials",
…

Beispiel: Client

Das Client-Projekt im Beispiel heisst RpcJwtClientCredentialsRolesClient. Die relevanten Parameter in appsettings.json sehen wie folgt aus:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"IdentityServer": {
    "Authority": "https://localhost:5001"
},
"ClientUser": {
    "ClientId": "ClientUser",
    "ClientSecret": "D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967",
    "Audience": "DemoService"
},
"ClientAdmin": {
    "ClientId": "ClientAdmin",
    "ClientSecret": "V4YH2Q8C968XC52S5EFWD7VDX3TBEU9ZNY8EE6GE2F35ATM5XSS3PD6C6PEA9DZM",
    "Audience": "DemoService"
},
"Service": {
    "CustomerId": 1,
    "DelayInterval": 1000,
    "ServiceUrl": "https://localhost:5002"
}

In Worker.cs ist eine Methode ‘GetToken()’ definiert. Als erstes holt sich diese Methode das ‘Discovery’-Dokument beim Identity-Server:

C#
1
2
3
4
5
6
7
using var identityClient = new HttpClient();
var discover = await identityClient.GetDiscoveryDocumentAsync(_config["IdentityServer:Authority"]).ConfigureAwait(false);
if (discover.IsError)
{
    _logger.LogError(discover.Error);
    return false;
}

Wenn das gelungen ist, extrahiert der Client den Token-Endpoint und sendet einen Request für ein Token ab:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ClientCredentialsTokenRequest tokenRequest = new ClientCredentialsTokenRequest()
{
    Address = discover.TokenEndpoint,
    ClientId = _config["ClientAdmin:ClientId"],
    ClientSecret = _config["ClientAdmin:ClientSecret"],
    Scope = _config["ClientAdmin:Audience"]
};
var tokenResponse = await identityClient.RequestClientCredentialsTokenAsync(tokenRequest).ConfigureAwait(false);
if (tokenResponse.IsError)
{
    _logger.LogError(tokenResponse.Error);
    return false;
}
_logger.LogInformation(tokenResponse.Json.ToString());
_token = tokenResponse.AccessToken;

Das JWT-Token wird gespeichert und sieht für ClientAdmin wie folgt aus:

Noser Engineering AG - gRPC with IdentityServer4 JWT-Token

gRPC with IdentityServer4 JWT-Token

gRPC with IdentityServer4 JWT-TokenDer gRPC-Client wird wie üblich erstellt, nur wird bei jeder Anfrage das Token im Header mitgesendet:

C#
1
2
3
4
5
6
7
8
9
10
11
NameMessage nameMessage = new NameMessage
{
    Name = $"{Assembly.GetExecutingAssembly().FullName}"
};
string tokenValue = $"Bearer {_token}";
var metadata = new Metadata()
{
    { "Authorization", tokenValue }
};
CallOptions callOptions = new CallOptions(metadata);
var result = await _client.ExchangeNamesAsync(nameMessage, callOptions);

Beispiel: Ressourcen-Server

Das Client-Projekt im Beispiel heisst RpcJwtClientCredentialsRolesServer. Das Projekt benutzt ebenfalls den Kestrel-Server mit gesicherter Verbindung wie beschrieben in ‘gRPC Tutorial Teil 4: HTTP/2 über HTTPS’.

Die relevanten Parameter in appsettings.json sehen wie folgt aus:

C#
1
2
3
4
5
6
7
8
"JwtBearerAuth": {
    "Audience": "DemoService",
    "Authority": "https://localhost:5001"
},
"Certificate": {
    "File": "server.pfx",
    "Password": "P@ssw0rd"
}

Die JwtBearer Authentication wird in Startup.cs wie folgt konfiguriert. Der Code ist genau gleich für gRpc wie für einen REST-Server.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(cfg =>
        {
            cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; // "Bearer"
            cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.Authority = Configuration["JwtBearerAuth:Authority"];
            options.Audience = Configuration["JwtBearerAuth:Audience"];
            options.Events = new JwtBearerEvents()
            {
                OnAuthenticationFailed = c =>
                {
                    _logger.LogDebug($"Exception: {c.Exception}");
                    return Task.CompletedTask;
                }
            };
        });
    services.AddAuthorization();
    services.AddControllers();
    services.AddGrpc(opt => { opt.EnableDetailedErrors = true; });
}
 
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
    _logger = loggerFactory.CreateLogger("JwtBearerAuthentication");
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<DemoServiceRpc>();
        endpoints.MapControllers();
    });
}

Nun kann man wie üblich die Methoden mit dem Authorize-Attribute schützen.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DemoServiceRpc : DemoService.DemoServiceBase
{
    [Authorize(Roles = "User,Admin")]
    public override Task<NameMessage> ExchangeNames(NameMessage request, ServerCallContext context)
    {
        NameMessage result = new NameMessage
        {
            Name = "Hello from Server"
        };
        return Task.FromResult(result);
    }
 
    [Authorize(Roles = "Admin")]
    public override Task<ResultMessage> ChangeSettings(SettingsMessage request, ServerCallContext context)
    {
        ResultMessage result = new ResultMessage()
        {
            Success = true
        };
        return Task.FromResult(result);
    }
}

Wenn der Client ein JWT-Token für ClientUser gefragt hat und damit auf die Methode ‘ChangeSettings()’ zugreift, dann gibt es folgende Ausnahme:

Noser Engineering AG - gRPC permission denied

gRPC permission denied

Fazit

Dieser Blog hat gezeigt, wie Client Credential Flow mit asymmetrischer Verschlüsselung, Rollen und IdentityServer4 verwendet werden kann. Der IdentityServer4 implementiert ebenfalls die kompliziertere OAuth2 Flows, welche ausserhalb den Scope dieser Blog-Serie fallen.

gRPC bietet noch ein paar weitere fortgeschrittenen Funktionen wie Server Side Reflection, um die Schnittstelle des Servers abzufragen (WSDL wird nicht unterstützt) oder Custom Attributes, welche sowieso schon in der C#-Sprache existieren. Bis jetzt ist es den Autor nicht gelungen, einen Client zu implementieren, welcher erfolgreich die gesamte Schnittstelle mit Server Side Reflection abfragen konnte. Der gRPC-Community Support hat bis jetzt keine Lösungen für diese Probleme gebracht.

Damit endet diese Blog-Serie. Meine persönliche Schlussfolgerung ist, dass gRPC WCF nicht vollständig ersetzt (keine Named-Pipes, Server Notifications nur über Streams möglich) und bin enttäuscht, dass Microsoft WCF in .NET Core nicht mehr weiterführt.

← Voriger Post

gRPC Tutorial Teil 6: JWT Client Credentials

23. Januar 2020
Erik Stroeken
3
client credential flow, gRPC, identity server, jwt, oauth

In diesem Blog wird erläutert, wie die Authentifizierung und Autorisierung mit JSON Web Token (JWT) funktioniert. Der einfachste OAuth2-Flow, der Client Credential Flow, wird anhand eines Beispiels mit einem gRPC-Client / Server und einem minimalistischen Tokenserver erläutert.

JSON Web Token (JWT)

JSON Web Token (JWT) ist ein auf JSON basiertes und nach RFC 7519 genormtes Access-Token und wird zur Authentifizierung und Autorisierung eines Benutzers verwendet. Die Informationen im Token können überprüft und als vertrauenswürdig eingestuft werden, da sie digital signiert sind. JWTs können symmetrisch, mit einem geheimen Schlüssel (‘HS256’: HMAC mit SHA-256), oder asymmetrisch mit einem Public- / Private-Schlüsselpaar mit RSA (‘RS256’: RSA Signatur mit SHA-256) signiert werden (‘RS256’).

Man sollte sich bewusst sein, dass ein JWT nicht verschlüsselt ist und daher immer über eine sichere Verbindung gesendet werden sollte.

Ein JWT besteht aus drei Teilen: Header, Payload und Signatur. Alle drei Teile sind Base64 kodiert und durch einen Punkt getrennt.

Header.Payload.Signatur

Beispiel eines JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Dekodiert vom Base64 Format ergibt das Beispiel oben:

{“alg”:”HS256″,”typ”:”JWT”}.{“sub”:”1234567890″,”name”:”John Doe”,”iat”:1516239022}.[Binäre Signatur]

 

JWT Header

Der Header hat üblicherweise nur zwei Felder, wovon das Feld ‘typ’  auf ‘JWT’ gesetzt ist. Das Feld ‘alg’ beschreibt, welche Verschlüsselungsmethode für die Signierung zum Einsatz kommt: ‘HS256’ für symmetrische Verschlüsselung oder ‘RS256’ (RSA mit SHA-256) für asymmetrische Verschlüsselung.
Beispiel:

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}

JWT Payload

Die Payload-Sektion enthält die Claims, d.h. die zu übermittelnden Informationen.
Beispiel:

1
2
3
4
5
6
7
{
  "iat": 1579623173,
  "jti": "de84fbc9-e47e-43f2-82ec-f03b05416450",
  "exp": 1579624973,
  "iss": "https://localhost:5001",
  "aud": "DemoService"
}

Einige oft verwendete Claims:

iss Issuer der Aussteller des Tokens
sub Subject definiert für wen oder was die Claims getätigt werden
aud Audience die Zieldomäne, für die das Token ausgestellt wurde, bei uns die API
exp Expiration Time Das Ablaufdatum des Tokens in Unixzeit, also der Anzahl der Sekunden seit 1970-01-01T00:00:00Z (‘Epoch time’)
nbf Not Before Die Unixzeit, ab der das Token gültig ist
iat Issued At Die Unixzeit, zu der das Token ausgestellt wurde
jti JWT ID Eindeutige ID die Mehrfachverwendung des Tokens verhindert

 

JWT Signatur

Die Signatur wird dadurch erzeugt, dass Header und Payload im Base64 kodierten und durch einen Punkt getrennten Format mit der spezifizierten Hashmethode gehashed wird:

1
2
var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
var hash = HMACSHA256(encodedString, secret);

Online JWT validieren/dekodieren: jwt.io

Die Seite jwt.io stellt den Inhalt eines JWT dar, das man einfach im linken Fenster (‘Encoded’) einfügen kann. Wenn man den Schlüssel besitzt, kann man diesen im Feld ‘Decoded / VERIFY SIGNATURE’ einfügen. Die Seite zeigt dann, ob die Signatur gültig ist.

Noser Engineering AG - gRPC - Client Credential Flow - jwt.io

jwt.io

JWT generieren und validieren/dekodieren

Mit dem NuGet-Paket ‘Microsoft.IdentityModels.Tokens.JWT’ kann man JWT generieren und validieren/dekodieren.

Beispiel:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public string CreateJwtToken()
{
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(DateTime.Now).ToString(), ClaimValueTypes.Integer64),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    };
 
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967"));
    var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    var jwtSecurityToken = new JwtSecurityToken(
        "https://localhost:5001",
        "DemoService",
        claims,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: signingCredentials);
    return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
 
public ClaimsPrincipal ValidateDecodeJwtToken(string token)
{
    var validationParams = new TokenValidationParameters()
    {
        ValidIssuer = "https://localhost:5001",
        ValidAudience = "DemoService",
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes("D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967"))
    };
    var handler = new JwtSecurityTokenHandler();
    SecurityToken securityToken;
   return handler.ValidateToken(token, validationParams, out securityToken);
}

OAuth 2.0

OAuth 2.0 ist ein offenes Protokoll, welches eine standardisierte, sichere API-Autorisierung für Desktop-, Web- und Mobile-Anwendungen erlaubt. Die erste Version OAuth 1.0 wurde in 2007 veröffentlicht, die aktuelle Version OAuth 2.0 in 2010.

Der Ressourcenbesitzer kann mit Hilfe dieses Protokolls einer Anwendung (Client) den Zugriff auf seine Daten zulassen (Autorisierung), die von einem anderen Dienst (Resourcen-Server) bereitgestellt werden, ohne geheime Details seiner Zugangsberechtigung (Authentifizierung) dem Client preiszugeben. Der Ressourcenbesitzer kann so Dritten gestatten, in seinem Namen einen Dienst zu benutzen. Typischerweise wird dabei die Übermittlung von Passwörtern an Dritte vermieden.

OAuth 2.0 ist zu vergleichen mit einem Parkservice: der Ressourcenbesitzer gibt das Auto (die Ressource) an den Parkservice (Client) weiter. Er gibt dem Parkservice den Parkservice-Schlüssel. Moderne amerikanische Autos haben zwei Arten von Schlüsseln: einen Hauptschlüssel und einen Parkservice-Schlüssel mit eingeschränkten Möglichkeiten (z. B. eingeschränkte Kilometer, Kofferraum kann nicht geöffnet werden). In der Computerwelt ist der Hauptschlüssel Ihr Benutzername mit Kennwort (mit vollem Zugriff). Bei OAuth2 geht es darum, Clients einen Schlüssel für Ihre Ressourcen mit beschränktem Zugriff (das JWT-Token) zu geben, ohne den Hauptschlüssel herausgeben zu müssen.

In OAuth 2.0 gibt es 4 Arten von ‘Flows’. Die einfachste Art, der Client Credential Flow, wird in diesem Blog behandelt.

Client Credential Flow

Beim Client Credential Flow meldet der Client sich beim Identity-Server mit seinem ‘Secret’ (z.B. Benutzername und Passwort) an. Der Identity-Server authentifiziert den Benutzer und gibt ein JWT zurück. Dieses JWT enthält u.a. Zugriffsberechtigungen (z.B. Resourcen und Rolle) und ist signiert mit einem symmetrischen Key (‘HS256’) oder dem privaten Schlüssel eines Public-/Private-Key Paares (‘RS256’).

Der Client sendet jetzt bei jeder Resourcen-Server-Anfrage das JWT mit; entweder im HTTP-Header oder in der URL. Der Resourcen-Server validiert das Token mit Hilfe der Signatur und dem privaten symmetrischen Schlüssel (ist beim Resourcen-Server bekannt). Bei einer RS256 Signatur holt der Resourcen-Server über die Issuer-URL im JWT beim Identity-Server den Public-Key zur Validierung ab. Wenn die Validierung des JWTs in Ordnung ist, darf der Client auf die Resourcen zugreifen.

Beispiel: Client Credential Flow

Mit diesem Beispiel ‘RpcJwtClientCredentials’ wird die einfachste Art von OAuth 2.0, der Client Credential Flow, demonstriert. Der Quellcode kann hier als ZIP-Datei heruntergeladen werden.

Weil das JWT nicht verschlüsselt ist, muss die Kommunikation zwingend über eine gesicherte Verbindung laufen. Die Projekte im Beispiel benutzen den Kestrel-Server mit gesicherter Verbindung wie beschrieben in ‘gRPC Tutorial Teil 4: HTTP/2 über HTTPS’.

Obwohl dieses Beispiel mit einem gRPC-Server arbeitet, kann der gleiche Code auch zur Absicherung eines REST-Servers verwendet werden.

Die Solution hat 3 Teilnehmer:

  1. Der Client, welcher auf die Resourcen zugreifen möchte
  2. Der Identity-Server, der im Namen des Resourcenbesitzers ein JWT generiert
  3. Der Resourcen-Server, welcher die Resourcen bereitstellt.

 

Identity-Server

Im Identity-Server wird mit CreateToken() der Benutzername und das Passwort validiert. Wenn der Benutzer bekannt ist und das Passwort stimmt, wird ein JWT erstellt und zurückgegeben.

Hier ist der Code der IdentityController-Klasse:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[Route("~/", Name = "default")]
[ApiController]
public class IdentityController : ControllerBase
{
    private readonly IServerUtil _serverUtil;
    private readonly ILogger<IdentityController> _logger;
 
    public IdentityController(
        ILogger<IdentityController> logger,
        IServerUtil serverUtil)
    {
        _serverUtil = serverUtil;
        _logger = logger;
    }
 
    [HttpGet]
    public string CreateToken(string username, string password)
    {
        string token = null;
        if (!_serverUtil.IsUserValid(username, password))
        {
            _logger.LogError($"Invalid User ('{username}') or password supplied.");
        }
        else
        {
            token = _serverUtil.CreateJwtToken(username, password);
            _logger.LogInformation($"Generated toke for User '{username}'. Token {token}");
        }
        return token;
    }
}

Die Datei appsettings.json enthält die Werte zur Generierung des JWTs und zur Verschlüsselung der Verbindung (‘Certificate’, siehe ‘gRPC Tutorial Teil 4: HTTP/2 über HTTPS’).

C#
1
2
3
4
5
6
7
8
9
10
11
12
{
…
    "JwtToken": {
        "Issuer": "https://localhost:5001",
        "Key": "D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967",
        "Audience": "DemoService"
    },
    "Certificate": {
        "File": "server.pfx",
        "Password": "P@ssw0rd"
    }
}

Die Schnittstelle IServerUtil definiert die Methoden zur Validierung und JWT Generierung:

C#
1
2
3
4
5
public interface IServerUtil
{
    string CreateJwtToken(string username, string password);
    bool IsUserValid(string username, string password);
}

Hier die Implementierung:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ServerUtil : IServerUtil
{
    private readonly IConfiguration _config;
    private readonly ILogger<ServerUtil> _logger;
 
    public ServerUtil(IConfiguration config, ILogger<ServerUtil> logger)
    {
        _config = config;
        _logger = logger;
    }
 
    public string CreateJwtToken(string username, string password)
    {
        string token = null;
        try
        {
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.UniqueName, username)
            };
 
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JwtToken:Key"]));
            var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwtSecurityToken = new JwtSecurityToken(
                _config["JwtToken:Issuer"],
                _config["JwtToken:Audience"],
                claims,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: signingCredentials);
            token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
        }
        catch (Exception e)
        {
            _logger.LogError(e.ToString());
        }
        return token;
    }
 
    public bool IsUserValid(string username, string password)
    {
        // Dummy implementation: validate user credentials here
        return username=="Demo" && password=="P@ssw0rd";
    }
}

Die Validierung der User Credentials passiert in IsUserValid() und ist nicht implementiert. Nur der Benutzer ‘Demo’ mit Passwort ‘P@ssw0rd’ ist berechtigt, auf den Resourcen-Server zuzugreifen.

Das JWT wird mit der Funktionalität aus dem NuGet ‘System.IdentityModel.Tokens.Jwt’ für die Resource ‘DemoService’ erstellt (‘Audience’). Für Demozwecke werden ein paar Claims im Token festgelegt, welche durch den Resourcen-Server validiert werden können. Signiert wird mit dem symmetrischen Schlüssel ‘Key’ definiert in appsettings.json.

In Startup.cs wird die Implementation von IServerUtil am IoC-Container hinzugefügt:

C#
1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IServerUtil, ServerUtil>();
    services.AddControllers();
}

Nach dem Starten des Identity-Servers kann folgendermassen ein JWT abgeholt werden:

Noser Engineering AG - gRPC - Client Credential Flow - Abrufen JWT beim IdentityServer

Abrufen JWT beim IdentityServer

Der Output vom Identity-Server sieht folgendermassen aus:

Noser Engineering AG - gRPC - Client Credential Flow - IdentityServer Ausgabe

IdentityServer Ausgabe

Wenn man jetzt in jwt.io das JWT zusammen mit dem privaten Schlüssel einfügt, wird folgendes angezeigt:

Noser Engineering AG - gRPC - Client Credential Flow - JWT vom IdentityServer

JWT vom IdentityServer

Resourcen-Server

Im Resourcen-Server wird der Service wie bei REST mit dem Attribut [Authorize] abgesichert.

In DemoServiceRpc.cs befindet sich die Implementation des gRPC Demo-Service:

C#
1
2
3
4
5
6
7
8
9
10
11
12
[Authorize]
public class DemoServiceRpc : DemoService.DemoServiceBase
{
    public override Task<NameMessage> ExchangeNames(NameMessage request, ServerCallContext context)
    {
        NameMessage result = new NameMessage
        {
            Name = "Hello from Server"
        };
        return Task.FromResult(result);
    }
}

Damit die Signatur und die einzelnen Claims kontrolliert werden können, muss der Resourcen-Server den symmetrischen Schlüssel und die zu erwarteten Inhalte der Claims kennen.

Diese werden in appsettings.json definiert. Auch hier wieder die Sektion ‘Certificate’ zum Konfigurieren des Kestrel-Servers für eine gesicherte Verbindung.

Der relevante Teil von appsettings.json:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information",
            "grpc": "Debug",
            "JwtBearerAuthentication": "Debug"
        }
    },
    "AllowedHosts": "*",
    "JwtBearerAuth": {
        "Audience": "DemoService",
        "Issuer": "https://localhost:5001",
        "Key": "D9B31A4E97C40EC47D758490C801FFCDD39BCE3EF5E9C978E4FF9D34FA0F1967"
    },
    "Certificate": {
        "File": "server.pfx",
        "Password": "P@ssw0rd"
    }
}

Die Magie wird in Startup.cs konfiguriert. Mit AddAuthentication wird JWT-Bearer Authentication konfiguriert durch Zuweisen von ‘Bearer’ an DefaultAuthenticateScheme und DefaultChallengeScheme. Die Signatur wird validiert mit dem symmetrischen Schlüssel (‘IssuerSigningKey’) und die Claims ‘Issuer’ und ‘Audience’ werden verglichen mit den Werten wie in appsettings.json konfiguriert.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
 
    public IConfiguration Configuration { get; }
 
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(cfg =>
            {
                // Simply holds "Bearer"
                cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = Configuration["JwtBearerAuth:Audience"],
                    ValidIssuer = Configuration["JwtBearerAuth:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtBearerAuth:Key"]))
                };
                options.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        _logger.LogDebug($"Exception: {c.Exception}");
                        return Task.CompletedTask;
                    }
                };
            });
        services.AddAuthorization();
        services.AddControllers();
        services.AddGrpc(opt => { opt.EnableDetailedErrors = true; });
    }
 
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger("JwtBearerAuthentication");
…
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<DemoServiceRpc>();
            endpoints.MapControllers();
        });
    }
}

Wenn die Validierung des JWTs fehlschlägt werden standardmässig keine weiteren Informationen ausgegeben. Durch Installieren eines Eventhandlers auf ‘OnAuthenticationFailed()’ in der Sektion ‘options.Events’, kann man schnell die Ursache der Validierungsprobleme ausfindig machen.

Der Client

Bevor der gRPC-Client erstellt wird, wird ein JWT Token beim Identity-Server angefragt. Der Code befindet sich in worker.cs:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private async Task<bool> GetToken()
{
    if (string.IsNullOrEmpty(_token) == false)
    {
        return true;
    }
    try
    {
        using var identityClient = new HttpClient();
        string identityUri = $"{_config["IdentityServer:Authority"]}?username={_config["Client:Username"]}&password={_config["Client:Password"]}";
        _token = await identityClient.GetStringAsync(identityUri).ConfigureAwait(false);
    }
    catch (Exception e)
    {
        _logger.LogCritical(e, "Exception while getting JWT");
        return false;
    }
    return true;
}

Das JWT wird bei jeder Anfrage im Header mitgesendet. Dazu wird das HTTP-Headerfeld ‘Authorization’ auf ‘Bearer [JWT]’ gesetzt.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
_logger.LogInformation("Calling 'ExchangeNames()' at: {time}", DateTimeOffset.Now);
NameMessage nameMessage = new NameMessage
{
    Name = $"{Assembly.GetExecutingAssembly().FullName}"
};
string tokenValue = $"Bearer {_token}";
var metadata = new Metadata()
{
    { "Authorization", tokenValue }
};
CallOptions callOptions = new CallOptions(metadata);
var result = await _client.ExchangeNamesAsync(nameMessage, callOptions);
_logger.LogInformation($"Server answered with {result.Name}");
...

Wenn beispielsweise der Inhalt des JWT unterwegs manipuliert wird, generiert der Client folgende Ausgabe:

Noser Engineering AG - gRPC - Client Credential Flow - Unauthenticated Client

Unauthenticated Client

Dank des OnAuthenticationFailed Handlers im Resourcen-Server findet man jetzt einfach den Grund des Problems heraus:

Noser Engineering AG - gRPC - Client Credential Flow - Unauthenticated Client Server-Ausgabe

Unauthenticated Client Server-Ausgabe

Fazit

Obwohl der einfachste OAuth2 Flow, der Client Credential Flow, mit symmetrischem Schlüssel verwendet wird, zeigt dieser Blog, dass die Absicherung mit JWT nicht ganz trivial ist. Im nächsten Blog wird der IdentityServer4 verwendet, immer noch mit Client Credential Flow, aber mit Public-/Private-Key Authentifizierung und Rollen.

← Voriger Post
Nächster Post →
1234Next ›Last »

Tag Cloud

.NET android AngularJs app Arduino ASP.Net automated testing Azure C# C++ Cloud continuous integration Elm Embedded gRPC HTML5 Internet of Things IoT Java Javascript linux M2M Matlab OWASP Projektmanagement protobuf Raspberry Pi Reactive Programming REST Scrum Security Softwarequalität SPA Testen testing Testmanagement Teststrategie Visual Studio WebAPI windows windows phone 8 WPF Xamarin Xamarin.Android Xamarin.Forms

Archive

Current Posts

  • Das Büro im Kopf – Arbeiten im VR Home Office
  • D3.js: Chord-Diagramm Teil 2 – Benutzerdefinierte Sortierung und Kurvenformen
  • Azure Übersicht Teil 3: Cosmos DB
  • Ubuntu Core oder Azure Sphere? – Plattformen für das IoT
  • Mach mehr aus Animationen in Xamarin.Forms mit SkiaSharp

Last Comments

  • Noser Blog D3.js: Chord-Diagramm Teil 2 - Benutzerdefinierte Sortierung und Kurvenformen - Noser Blog bei D3.js: Chord-Diagramm Teil 1 – Von den Daten zum Diagramm
  • Noser Blog Azure Übersicht Teil 2: SQL Datenspeicher - Noser Blog bei Azure Übersicht Teil 3: Cosmos DB
  • Noser Blog Azure Übersicht Teil 3: Cosmos DB - Noser Blog bei Azure Übersicht Teil 2: SQL Datenspeicher
  • carmine bei Solid Prinzip
  • Helmut Max Kleiner bei In 6 Schritten zur sicheren Software

Popular Posts

Xamarin.Android Code Obfuscation

6 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 1 - Aufbruch

5 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 2 - Kein Picknick

4 Comments

Contact us

  1. Name *
    * Please enter your name
  2. Email *
    * Please enter a valid email address
  3. Message *
    * Please enter message
© 2013 NOSER ENGINEERING AG. All rights reserved. Datenschutz | Cookie-Richtlinie