diff --git a/Client.IntegrationTests/Client.IntegrationTests.csproj b/Client.IntegrationTests/Client.IntegrationTests.csproj
index 40f33367..4a73dd9e 100644
--- a/Client.IntegrationTests/Client.IntegrationTests.csproj
+++ b/Client.IntegrationTests/Client.IntegrationTests.csproj
@@ -32,6 +32,15 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
diff --git a/Client.IntegrationTests/OAuthIntegrationTest.cs b/Client.IntegrationTests/OAuthIntegrationTest.cs
new file mode 100644
index 00000000..20546b04
--- /dev/null
+++ b/Client.IntegrationTests/OAuthIntegrationTest.cs
@@ -0,0 +1,48 @@
+using System.Threading.Tasks;
+using Grpc.Core;
+using NUnit.Framework;
+
+namespace Client.IntegrationTests;
+
+[TestFixture]
+public class OAuthIntegrationTest
+{
+ private readonly ZeebeIntegrationTestHelper testHelper = ZeebeIntegrationTestHelper.Latest().WithIdentity();
+
+ [OneTimeSetUp]
+ public async Task Setup()
+ {
+ await testHelper.SetupIntegrationTest();
+ }
+
+ [OneTimeTearDown]
+ public async Task Stop()
+ {
+ await testHelper.TearDownIntegrationTest();
+ }
+
+ [Test]
+ public async Task ShouldSendRequestAndNotFailingWithAuthenticatedClient()
+ {
+ var authenticatedZeebeClient = testHelper.CreateAuthenticatedZeebeClient();
+ var topology = await authenticatedZeebeClient.TopologyRequest().Send();
+ var gatewayVersion = topology.GatewayVersion;
+ Assert.AreEqual(ZeebeIntegrationTestHelper.LatestVersion, gatewayVersion);
+
+ var topologyBrokers = topology.Brokers;
+ Assert.AreEqual(1, topologyBrokers.Count);
+
+ var topologyBroker = topologyBrokers[0];
+ Assert.AreEqual(0, topologyBroker.NodeId);
+ }
+
+ [Test]
+ public Task ShouldFailWithUnauthenticatedClient()
+ {
+ Assert.ThrowsAsync(code: async () =>
+ {
+ await testHelper.CreateZeebeClient().TopologyRequest().Send();
+ });
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/Client.IntegrationTests/Resources/Broker/chain.cert.pem b/Client.IntegrationTests/Resources/Broker/chain.cert.pem
new file mode 100644
index 00000000..e20e7f03
--- /dev/null
+++ b/Client.IntegrationTests/Resources/Broker/chain.cert.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDtDCCApygAwIBAgIUPlH7wI1Bq6F7k63ZNtSXR0DgjxwwDQYJKoZIhvcNAQEL
+BQAwVTELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0VuZ2xhbmQxEDAOBgNVBAoMB0Nh
+bXVuZGExDjAMBgNVBAsMBXplZWJlMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjAw
+NzA2MTA1MTM3WhgPMjEyMDA2MTIxMDUxMzdaMFUxCzAJBgNVBAYTAkRFMRAwDgYD
+VQQIDAdFbmdsYW5kMRAwDgYDVQQKDAdDYW11bmRhMQ4wDAYDVQQLDAV6ZWViZTES
+MBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA0ALY6dhCih7lbyOI79N1HTi7vapsw7DISr+btQgZArsPt/xnU2tTvCH7F7mJ
+OQiz1T5cUrNqTi9ZSkP6nDoGsFDZkQRZkJc+fF3OjIUnZ62OGyD7LV62tKisojx4
+ulNYW+a7oVu+hQtP2ht3Hsi30fgt9P1Nq+0c11BQzNQfFwo74hFQTVCbYHQf3uU7
+W08o0rYCCRIN+rJXUdsD1pm5snFmg7o3nQSUGFYpDHezoZZzL/d4a3YiPAG//YBH
+qT/GerhHwa5fK5PFPryns+oq+4PgaF8JB3qVQC6rJWhnmT32n0cQ4OxNTjX5+8JC
+jKH3rGkPDSXf3zJaAbU0GWNzEQIDAQABo3oweDAdBgNVHQ4EFgQUgtu9rM/iDT8J
+6FvuokdJ3E6gRc4wHwYDVR0jBBgwFoAUgtu9rM/iDT8J6FvuokdJ3E6gRc4wDwYD
+VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwFQYDVR0RBA4wDIcEfwAAAYcE
+AAAAADANBgkqhkiG9w0BAQsFAAOCAQEAkinfKsHPfDEoOsJ2ic4Bc8ynCV/Fm4GS
+huTOxnHiB5KqxN12s8MBd2zTpZNx2H4Pj32W9OUUWluyLvofueTYsarvUHY4TxkT
+z04aFFcK5D0lPGLy7eBPSsWmiovCTvjWixxgOiRxYo+t6/ttNvXsZ0PnAZypSbfb
+vAb7DeP3SXDEP+QnpIw0PpO3IaoYgilPSrfQV1n4fgFSOVa6545cUpJHINj264qF
+Si8d8c1YokLcJFHepUQzTRKOgCgds+e3496iJGhQbhIDw24dMVmefYOqDepTYVlw
+KxaCNRcTKljnM9QazGG8scqJaPjGDteDYBzh63+XbdPhjzWK8/yFIA==
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/Client.IntegrationTests/Resources/Broker/private.key.pem b/Client.IntegrationTests/Resources/Broker/private.key.pem
new file mode 100644
index 00000000..98598d98
--- /dev/null
+++ b/Client.IntegrationTests/Resources/Broker/private.key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQAtjp2EKKHuVv
+I4jv03UdOLu9qmzDsMhKv5u1CBkCuw+3/GdTa1O8IfsXuYk5CLPVPlxSs2pOL1lK
+Q/qcOgawUNmRBFmQlz58Xc6MhSdnrY4bIPstXra0qKyiPHi6U1hb5ruhW76FC0/a
+G3ceyLfR+C30/U2r7RzXUFDM1B8XCjviEVBNUJtgdB/e5TtbTyjStgIJEg36sldR
+2wPWmbmycWaDujedBJQYVikMd7OhlnMv93hrdiI8Ab/9gEepP8Z6uEfBrl8rk8U+
+vKez6ir7g+BoXwkHepVALqslaGeZPfafRxDg7E1ONfn7wkKMofesaQ8NJd/fMloB
+tTQZY3MRAgMBAAECggEAFnSoPB53mHebZzMb3mAinYP5aJFUao/UH5Wt1o5IPO46
+1S7vbKcChCXa+IW0Fa8l0tiHmPn7ePNNnWHXVTRCcphX1Hr2vFBHk5+A49SgG2Y2
+GCGoXA6EhN5MvLrwgZTrzggLq3C/EZfWCAK9Clq61XUIaRFLaEsRuQDXqDUiIhdr
+5FzoKy/94BhOpqRrF/RQUw8VtUtUSzHaUCBZiFTfBWSKKdYVLNMhu6fsdJOBiXTU
+vOpP/IRk4L8vqlZJqRQHu76OcUxd7OA6MsqFjQsYTNvR0y5xsV9T2syKze3cDUPj
+oO4fnu/Z2ZvD34/d2lzcjZWjQ5d6vSOYZN2DYBWV0QKBgQDuEySRojaQvPj1aCW5
+/mWmgtpCOgScWXgdHagsMWTMEpfqFIBRWeYKyW80eBjRyOHJ+JisHYWFxEXu0ZoX
+yqhBpHma0YPsH+Xm778KrbS74aYrW7inIGhYtUX0+n+YQ6q9aroFBq4erTJu/Ev6
+4PX1yJPWbHWA3WVi97NWBmmF7QKBgQDfrDtaU9IbIp/LA1HFA1e28iCwDh6S3Rsz
+E5K0mEZInfKdGESJfYuAaik0LCmwIaOJ/7fHBx1R7JGTYwt+FeROwBcC6n3qzhbD
+K4ptmmTbwzOebz2r1SjX1rMaBIfWkQbxYxecL/HzYSnYF/fEPjCyO8CjXIcum2N4
+V9eGFv59NQKBgH7knyIsdq7wujV7XFhlWuLEbfbMm7aGDXpfW0qqzRHkeyod4UL7
+Cp0HPomV1YzDaG1RXnamiYuB0NB40YwKzWGne9VkBM+vNMfBU28qpOFbZUlI6wPR
+Ryy4+d+YQLf0oSWypBGXvOjG4dG8EfdXPmHRldK9HmggGTEF24Vnh4kFAoGBAIuf
+adVy6X8C2BjUU6DV+1U6Q+lihvdKioYRu8x8GbOO1Tn3QiFJe2GH43yr7MID3aBx
+PnlBGa5gLGeCtlPYupHmGvc5Ba0jRNZEQb81V6xPZ9OIwUiYYUyKu3aMSXdJRLo+
+DyjyTOiOSJ6aJ5Ia+C7qWdAgHEqduTQQMXuEswvZAoGBAN5kC+RjAsEQ5myY7evz
+1R/3uk5uwUk/m1GadMCHoWJs7uLdwPa0ThC0Bt9yQxltWseGBj8ysKuWzoy2O11f
+gMqZF61NuZibVk+eFc2T9IgPkYXYBSRluafunqEgvclUnuCwUrfwEAN07bHfSuGD
+bNGJygSW2596II+xg/TRPhnF
+-----END PRIVATE KEY-----
diff --git a/Client.IntegrationTests/Resources/server.crt b/Client.IntegrationTests/Resources/server.crt
new file mode 100644
index 00000000..80563574
--- /dev/null
+++ b/Client.IntegrationTests/Resources/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSzCCAjMCFBDDfC44PsguTshkcc2CVESnk/UyMA0GCSqGSIb3DQEBCwUAMGIx
+CzAJBgNVBAYTAkRFMRAwDgYDVQQIDAdHZXJtYW55MQ8wDQYDVQQHDAZCZXJsaW4x
+ETAPBgNVBAoMCFplZWJlLmlvMQ4wDAYDVQQLDAVaZWViZTENMAsGA1UEAwwEemVs
+bDAeFw0xOTA5MDUwNjE5MjFaFw0yOTA5MDIwNjE5MjFaMGIxCzAJBgNVBAYTAkRF
+MRAwDgYDVQQIDAdHZXJtYW55MQ8wDQYDVQQHDAZCZXJsaW4xETAPBgNVBAoMCFpl
+ZWJlLmlvMQ4wDAYDVQQLDAVaZWViZTENMAsGA1UEAwwEemVsbDCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMncS2Y4ddY/7PNu2kW0DYdSvLcv5ONIec0V
+E0+Dgls7ElVZJxqOwbIDB8Q4sOT335xpY4rtFK6lUrh541fCkVdtajI+Bv1pzFU+
+9LxMPU5GD/uFScVFA2MJy9ps5L6naFYPF3ZkE3s3gb6APTFc5ou5xZTOZ5To0s/Z
+ay1RQ92inxjAyvArL9IZSryB6Xq4qyBLUm8wZYZaQ+arLQ8pBADapWkgMY8tKIlA
+b185hbb2nY1ns/zws+dsHA5NIk6p7yZ+D3/SXKL/0fDBcPKmgJ+3hkgBpajMtpuw
+rLoWyppQfDFGCODiV/Pd1KUrIpbrZorIlkpd3s11fUooqkrtSwMCAwEAATANBgkq
+hkiG9w0BAQsFAAOCAQEAmk8VLv7nzOGCvU0gwJI/Sa6JkGdiQ7Jz0cfQGWkqfN6D
+Yhw/pFlFelgrD5kGLG8jAGNT4kWiaqGtoFpUkHxADDojJZ63Tk8xUI/o9xFWZkEY
+B0pxL39ybzQyuhX/dqkxZdLiNzJV5GTiSCtuTW+N8+WO5CFzT52rxYWQlPP31R5n
+p4wzIVqbK/XEYqyyvZyQ5XrM9FIV/57OSXNp5kUXT9RX3HHjp6oaeKOYw6arpcrg
+y9LIvuGV4h48ougO0696CgupMgYONKvDI+avRVqxJX/wr0+u56dlQ9+0XhpxCl8G
+Sr53Syfz5AD3WMLZt03iO4IfW5MVnq5LRLhPCVqiQg==
+-----END CERTIFICATE-----
diff --git a/Client.IntegrationTests/ZeebeIntegrationTestHelper.cs b/Client.IntegrationTests/ZeebeIntegrationTestHelper.cs
index 1e8671f9..c48548cf 100644
--- a/Client.IntegrationTests/ZeebeIntegrationTestHelper.cs
+++ b/Client.IntegrationTests/ZeebeIntegrationTestHelper.cs
@@ -1,14 +1,17 @@
using System;
using System.IO;
-using System.Threading;
+using System.Net;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
-using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Images;
+using DotNet.Testcontainers.Networks;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using Zeebe.Client;
+using Zeebe.Client.Impl.Builder;
+using IContainer = DotNet.Testcontainers.Containers.IContainer;
namespace Client.IntegrationTests
{
@@ -17,17 +20,26 @@ public class ZeebeIntegrationTestHelper
public const string LatestVersion = "8.3.0";
private const ushort ZeebePort = 26500;
+ private const ushort KeycloakPort = 8080;
+ private const ushort IdentityPort = 8084;
- private IContainer container;
+
+ private IContainer zeebeContainer;
private IZeebeClient client;
private readonly string version;
+ private readonly string audience;
+ private bool withIdentity;
private int count = 1;
public readonly ILoggerFactory LoggerFactory;
+ private IContainer postgresContainer;
+ private IContainer keycloakContainer;
+ private IContainer identityContainer;
private ZeebeIntegrationTestHelper(string version)
{
this.version = version;
+ audience = Guid.NewGuid().ToString();
LoggerFactory = new NLogLoggerFactory();
}
@@ -47,13 +59,47 @@ public static ZeebeIntegrationTestHelper OfVersion(string version)
return new ZeebeIntegrationTestHelper(version);
}
+ public ZeebeIntegrationTestHelper WithIdentity()
+ {
+ withIdentity = true;
+ return this;
+ }
+
public async Task SetupIntegrationTest()
{
TestcontainersSettings.Logger = LoggerFactory.CreateLogger();
- container = CreateZeebeContainer();
- await container.StartAsync();
- client = CreateZeebeClient();
+ if (withIdentity)
+ {
+ var network = new NetworkBuilder()
+ .WithName(Guid.NewGuid().ToString("D"))
+ .Build();
+
+ postgresContainer = CreatePostgresContainer(network);
+ await postgresContainer.StartAsync();
+ keycloakContainer = CreateKeyCloakContainer(network);
+ await keycloakContainer.StartAsync();
+
+ identityContainer = CreateIdentityContainer(network);
+ await identityContainer.StartAsync();
+ zeebeContainer = CreateZeebeContainer(network);
+ }
+ else
+ {
+ zeebeContainer = CreateZeebeContainer();
+ }
+
+ await zeebeContainer.StartAsync();
+
+ if (withIdentity)
+ {
+ client = CreateAuthenticatedZeebeClient();
+ }
+ else
+ {
+ client = CreateZeebeClient();
+ }
+
await AwaitBrokerReadiness();
return client;
}
@@ -62,24 +108,122 @@ public async Task TearDownIntegrationTest()
{
client.Dispose();
client = null;
- await container.StopAsync();
- container = null;
+ if (withIdentity)
+ {
+ await postgresContainer.StopAsync();
+ postgresContainer = null;
+ await keycloakContainer.StopAsync();
+ keycloakContainer = null;
+ await identityContainer.StopAsync();
+ identityContainer = null;
+ }
+
+ await zeebeContainer.StopAsync();
+ zeebeContainer = null;
}
- private IContainer CreateZeebeContainer()
+ private IContainer CreateZeebeContainer(INetwork network = null)
{
- return new ContainerBuilder()
+ var containerBuilder = new ContainerBuilder()
.WithImage(new DockerImage("camunda", "zeebe", version))
.WithPortBinding(ZeebePort, true)
- .WithEnvironment("ZEEBE_BROKER_CLUSTER_PARTITIONSCOUNT", count.ToString())
+ .WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
+ .WithEnvironment("ZEEBE_BROKER_CLUSTER_PARTITIONSCOUNT", count.ToString());
+
+ if (withIdentity)
+ {
+ containerBuilder = containerBuilder.WithEnvironment("ZEEBE_BROKER_GATEWAY_SECURITY_AUTHENTICATION_MODE",
+ "identity")
+ .WithEnvironment(
+ "ZEEBE_BROKER_GATEWAY_SECURITY_AUTHENTICATION_IDENTITY_ISSUERBACKENDURL",
+ "http://integration-keycloak:8080/auth/realms/camunda-platform")
+ .WithEnvironment("ZEEBE_BROKER_GATEWAY_SECURITY_AUTHENTICATION_IDENTITY_AUDIENCE",
+ "zeebe-api")
+ .WithEnvironment("ZEEBE_BROKER_GATEWAY_SECURITY_ENABLED", "true")
+ .WithEnvironment("ZEEBE_BROKER_GATEWAY_SECURITY_CERTIFICATECHAINPATH", "/security/chain.cert.pem")
+ .WithEnvironment("ZEEBE_BROKER_GATEWAY_SECURITY_PRIVATEKEYPATH", "/security/private.key.pem")
+ .WithResourceMapping(new DirectoryInfo("./Resources/Broker"), "/security")
+ .WithNetwork(network);
+ }
+
+ containerBuilder = containerBuilder.WithAutoRemove(true);
+ return containerBuilder.Build();
+ }
+
+ private IContainer CreatePostgresContainer(INetwork network)
+ {
+ var containerBuilder = new ContainerBuilder()
+ .WithImage("postgres")
+ .WithName("integration-postgres")
+ .WithPortBinding(5432, true)
+ .WithEnvironment("POSTGRES_DB", "bitnami_keycloak")
+ .WithEnvironment("POSTGRES_USER", "bn_keycloak")
+ .WithEnvironment("POSTGRES_PASSWORD", "#3]O?4RGj)DE7Z!9SA5")
+ .WithNetwork(network)
.WithAutoRemove(true)
- .Build();
+ .WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432));
+
+ return containerBuilder.Build();
}
- private IZeebeClient CreateZeebeClient()
+ private IContainer CreateIdentityContainer(INetwork network)
+ {
+ var containerBuilder = new ContainerBuilder()
+ .WithImage(new DockerImage("camunda", "identity", version)) // identity and zeebe will have the same version
+ .WithName("integration-identity")
+ .WithPortBinding(IdentityPort, true)
+ .WithEnvironment("SERVER_PORT", IdentityPort.ToString())
+ .WithEnvironment("IDENTITY_RETRY_DELAY_SECONDS", "30")
+ .WithEnvironment("KEYCLOAK_URL", "http://integration-keycloak:8080/auth")
+ .WithEnvironment("IDENTITY_AUTH_PROVIDER_BACKEND_URL",
+ "http://integration-keycloak:8080/auth/realms/camunda-platform")
+ .WithEnvironment("IDENTITY_DATABASE_HOST", "integration-postgres")
+ .WithEnvironment("IDENTITY_DATABASE_PORT", "5432")
+ .WithEnvironment("IDENTITY_DATABASE_NAME", "bitnami_keycloak")
+ .WithEnvironment("IDENTITY_DATABASE_USERNAME", "bn_keycloak")
+ .WithEnvironment("IDENTITY_DATABASE_PASSWORD", "#3]O?4RGj)DE7Z!9SA5")
+ .WithEnvironment("KEYCLOAK_INIT_ZEEBE_NAME", "zeebe")
+ .WithEnvironment("KEYCLOAK_CLIENTS_0_NAME", "zeebe")
+ .WithEnvironment("KEYCLOAK_CLIENTS_0_ID", "zeebe")
+ .WithEnvironment("KEYCLOAK_CLIENTS_0_SECRET", "sddh123865WUS)(1%!")
+ .WithEnvironment("KEYCLOAK_CLIENTS_0_TYPE", "M2M")
+ .WithEnvironment("KEYCLOAK_CLIENTS_0_PERMISSIONS_0_RESOURCE_SERVER_ID", "zeebe-api")
+ .WithEnvironment("KEYCLOAK_CLIENTS_0_PERMISSIONS_0_DEFINITION", "write:*")
+ .WithEnvironment("RESOURCE_PERMISSIONS_ENABLED", "false")
+ .WithAutoRemove(true)
+ .WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
+ .WithNetwork(network);
+
+
+ return containerBuilder.Build();
+ }
+
+ private IContainer CreateKeyCloakContainer(INetwork network)
+ {
+ var containerBuilder = new ContainerBuilder()
+ .WithImage(new DockerImage("bitnami", "keycloak", "21.1.2"))
+ .WithName("integration-keycloak")
+ .WithPortBinding("8080", true)
+ .WithEnvironment("KEYCLOAK_HTTP_RELATIVE_PATH", "/auth")
+ .WithEnvironment("KEYCLOAK_DATABASE_HOST", "integration-postgres")
+ .WithEnvironment("KEYCLOAK_DATABASE_PASSWORD", "#3]O?4RGj)DE7Z!9SA5")
+ .WithEnvironment("KEYCLOAK_ADMIN_USER", "admin")
+ .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin")
+ .WithNetwork(network)
+ .WithAutoRemove(true)
+ .WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
+ request.ForPort(8080).ForPath("/auth").ForStatusCode(HttpStatusCode.OK)));
+
+
+ return containerBuilder.Build();
+ }
+
+ public IZeebeClient CreateZeebeClient()
{
var loggerFactory = LoggerFactory;
- var host = container.Hostname + ":" + container.GetMappedPublicPort(ZeebePort);
+ var host = zeebeContainer.Hostname + ":" + zeebeContainer.GetMappedPublicPort(ZeebePort);
return ZeebeClient.Builder()
.UseLoggerFactory(loggerFactory)
@@ -88,9 +232,27 @@ private IZeebeClient CreateZeebeClient()
.Build();
}
+ public IZeebeClient CreateAuthenticatedZeebeClient()
+ {
+ var loggerFactory = LoggerFactory;
+ var host = zeebeContainer.Hostname + ":" + zeebeContainer.GetMappedPublicPort(ZeebePort);
+
+ return ZeebeClient.Builder()
+ .UseLoggerFactory(loggerFactory)
+ .UseGatewayAddress(host)
+ .UseTransportEncryption()
+ .AllowUntrustedCertificates()
+ .UseAccessTokenSupplier(
+ new CamundaCloudTokenProviderBuilder()
+ .UseAuthServer($"http://{keycloakContainer.Hostname}:{keycloakContainer.GetMappedPublicPort(KeycloakPort)}/auth/realms/camunda-platform/protocol/openid-connect/token")
+ .UseClientId("zeebe")
+ .UseClientSecret("sddh123865WUS)(1%!")
+ .UseAudience(audience).Build()).Build();
+ }
+
private async Task AwaitBrokerReadiness()
{
- var zeebeClient = (ZeebeClient) client;
+ var zeebeClient = withIdentity ? (ZeebeClient)CreateAuthenticatedZeebeClient() : (ZeebeClient)client;
await zeebeClient.Connect();
var topologyErrorLogger = LoggerFactory.CreateLogger();
var ready = false;
@@ -101,7 +263,7 @@ private async Task AwaitBrokerReadiness()
{
try
{
- var topology = await client.TopologyRequest().Send(TimeSpan.FromSeconds(1));
+ var topology = await zeebeClient.TopologyRequest().Send(TimeSpan.FromSeconds(1));
ready = topology.Brokers[0].Partitions.Count >= count;
topologyErrorLogger.LogInformation("Requested topology [retries {Retries}], got '{Topology}'", retries, topology);
}
diff --git a/Client.UnitTests/CamundaCloudTokenProviderTest.cs b/Client.UnitTests/CamundaCloudTokenProviderTest.cs
deleted file mode 100644
index ee16cc5b..00000000
--- a/Client.UnitTests/CamundaCloudTokenProviderTest.cs
+++ /dev/null
@@ -1,320 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using NLog;
-using NUnit.Framework;
-using Zeebe.Client.Impl.Builder;
-
-namespace Zeebe.Client
-{
- [TestFixture]
- public class CamundaCloudTokenProviderTest
- {
- private HttpMessageHandlerStub MessageHandlerStub { get; set; }
- private CamundaCloudTokenProvider TokenProvider { get; set; }
- private string TokenStoragePath { get; set; }
- private static long ExpiresIn { get; set; }
- private static string Token { get; set; }
-
- private static string _requestUri;
- private static string _clientId;
- private static string _clientSecret;
- private static string _audience;
-
- [SetUp]
- public void Init()
- {
- _requestUri = "https://local.de";
- _clientId = "ID";
- _clientSecret = "SECRET";
- _audience = "AUDIENCE";
- TokenProvider = new CamundaCloudTokenProviderBuilder()
- .UseAuthServer(_requestUri)
- .UseClientId(_clientId)
- .UseClientSecret(_clientSecret)
- .UseAudience(_audience)
- .Build();
-
- MessageHandlerStub = new HttpMessageHandlerStub();
- TokenProvider.SetHttpMessageHandler(MessageHandlerStub);
- TokenStoragePath = Path.GetTempPath() + ".zeebe/";
- TokenProvider.TokenStoragePath = TokenStoragePath;
- ExpiresIn = 3600;
- Token = "REQUESTED_TOKEN";
- }
-
- [TearDown]
- public void CleanUp()
- {
- Directory.Delete(TokenStoragePath, true);
- TokenProvider.Dispose();
- }
-
- private class HttpMessageHandlerStub : HttpMessageHandler
- {
- public int RequestCount { get; set; }
- private bool _disposed = false;
-
- protected override async Task SendAsync(HttpRequestMessage request,
- CancellationToken cancellationToken)
- {
- CheckDisposed();
- Assert.AreEqual(request.RequestUri, _requestUri);
- var content = await request.Content.ReadAsStringAsync();
- var jsonObject = JObject.Parse(content);
- Assert.AreEqual((string)jsonObject["client_id"], _clientId);
- Assert.AreEqual((string)jsonObject["client_secret"], _clientSecret);
- Assert.AreEqual((string)jsonObject["audience"], _audience);
-
- RequestCount++;
- var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent(@"{
- ""access_token"":""" + Token + @""",
- ""token_type"":""bearer"",
- ""expires_in"": " + ExpiresIn + @",
- ""refresh_token"":""IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk"",
- ""scope"":""create""}"),
- };
-
- return responseMessage;
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- _disposed = true;
- }
-
- private void CheckDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException("HttpMessageHandlerStub");
- }
- }
- }
-
- [Test]
- public async Task ShouldRequestCredentials()
- {
- // given
-
- // when
- var token = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("REQUESTED_TOKEN", token);
- Assert.AreEqual(1, MessageHandlerStub.RequestCount);
- }
-
- [Test]
- public async Task ShouldStoreCredentials()
- {
- // given
-
- // when
- var token = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("REQUESTED_TOKEN", token);
- var files = Directory.GetFiles(TokenStoragePath);
- Assert.AreEqual(1, files.Length);
- var tokenFile = files[0];
- var content = File.ReadAllText(tokenFile);
- var credentials = JsonConvert.DeserializeObject>(content);
- Assert.AreEqual(credentials["AUDIENCE"].Token, token);
- }
-
- [Test]
- public async Task ShouldStoreMultipleCredentials()
- {
- // given
- await TokenProvider.GetAccessTokenForRequestAsync();
- var otherProvider = new CamundaCloudTokenProviderBuilder()
- .UseAuthServer(_requestUri)
- .UseClientId(_clientId = "OTHERID")
- .UseClientSecret(_clientSecret = "OTHERSECRET")
- .UseAudience(_audience = "OTHER_AUDIENCE")
- .Build();
- otherProvider.SetHttpMessageHandler(MessageHandlerStub);
- otherProvider.TokenStoragePath = TokenStoragePath;
- Token = "OTHER_TOKEN";
-
- // when
- var token = await otherProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("OTHER_TOKEN", token);
- var files = Directory.GetFiles(TokenStoragePath);
- Assert.AreEqual(1, files.Length);
- var tokenFile = files[0];
- var content = File.ReadAllText(tokenFile);
- var credentials = JsonConvert.DeserializeObject>(content);
-
- Assert.AreEqual(credentials.Count, 2);
- Assert.AreEqual(token, credentials["OTHER_AUDIENCE"].Token);
- Assert.AreEqual("REQUESTED_TOKEN", credentials["AUDIENCE"].Token);
- }
-
- [Test]
- public async Task ShouldGetTokenFromInMemory()
- {
- // given
- await TokenProvider.GetAccessTokenForRequestAsync();
- var files = Directory.GetFiles(TokenStoragePath);
- var tokenFile = files[0];
- File.WriteAllText(tokenFile, "FILE_TOKEN");
-
- // when
- var token = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("REQUESTED_TOKEN", token);
- Assert.AreEqual(1, MessageHandlerStub.RequestCount);
- }
-
- [Test]
- public async Task ShouldExpireInOneSecond()
- {
- // given
- ExpiresIn = 1;
- var firstToken = await TokenProvider.GetAccessTokenForRequestAsync();
- var files = Directory.GetFiles(TokenStoragePath);
- var tokenFile = files[0];
- File.WriteAllText(tokenFile, "FILE_TOKEN");
-
- // when
- Token = "NEW_TOKEN";
- var secondToken = await TokenProvider.GetAccessTokenForRequestAsync();
- Thread.Sleep(1_000);
- var thirdToken = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("REQUESTED_TOKEN", firstToken);
- Assert.AreEqual(secondToken, firstToken);
- Assert.AreEqual("NEW_TOKEN", thirdToken);
- Assert.AreEqual(2, MessageHandlerStub.RequestCount);
- }
-
- [Test]
- public async Task ShouldRequestNewTokenWhenExpired()
- {
- // given
- ExpiresIn = 0;
- var firstToken = await TokenProvider.GetAccessTokenForRequestAsync();
- var files = Directory.GetFiles(TokenStoragePath);
- var tokenFile = files[0];
- File.WriteAllText(tokenFile, "FILE_TOKEN");
-
- // when
- Token = "SECOND_TOKEN";
- var secondToken = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("REQUESTED_TOKEN", firstToken);
- Assert.AreNotEqual(secondToken, firstToken);
- Assert.AreEqual("SECOND_TOKEN", secondToken);
- Assert.AreEqual(2, MessageHandlerStub.RequestCount);
- }
-
- [Test]
- public async Task ShouldUseCachedFile()
- {
- // given
- Token = "STORED_TOKEN";
- await TokenProvider.GetAccessTokenForRequestAsync();
- // re-init the TokenProvider
- Init();
-
- // when
- var token = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("STORED_TOKEN", token);
- Assert.AreEqual(0, MessageHandlerStub.RequestCount);
- }
-
- [Test]
- public async Task ShouldNotUseCachedFileForOtherAudience()
- {
- // given
- Token = "STORED_TOKEN";
- await TokenProvider.GetAccessTokenForRequestAsync();
- var otherProvider = new CamundaCloudTokenProviderBuilder()
- .UseAuthServer(_requestUri)
- .UseClientId(_clientId = "OTHERID")
- .UseClientSecret(_clientSecret = "OTHERSECRET")
- .UseAudience(_audience = "OTHER_AUDIENCE")
- .Build();
- otherProvider.SetHttpMessageHandler(MessageHandlerStub);
- otherProvider.TokenStoragePath = TokenStoragePath;
- Token = "OTHER_TOKEN";
-
- // when
- var token = await otherProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("OTHER_TOKEN", token);
- }
-
- [Test]
- public async Task ShouldRequestWhenCachedFileExpired()
- {
- // given
- ExpiresIn = 0;
- Token = "STORED_TOKEN";
- await TokenProvider.GetAccessTokenForRequestAsync();
- // re-init the TokenProvider
- Init();
-
- // when
- var token = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("REQUESTED_TOKEN", token);
- Assert.AreEqual(1, MessageHandlerStub.RequestCount);
- }
-
- [Test]
- public async Task ShouldUseCachedFileAndAfterwardsInMemory()
- {
- // given
- Token = "STORED_TOKEN";
- await TokenProvider.GetAccessTokenForRequestAsync();
- // re-init the TokenProvider
- Init();
-
- // when
- await TokenProvider.GetAccessTokenForRequestAsync();
- var files = Directory.GetFiles(TokenStoragePath);
- var tokenFile = files[0];
- File.WriteAllText(tokenFile, "FILE_TOKEN");
- var token = await TokenProvider.GetAccessTokenForRequestAsync();
-
- // then
- Assert.AreEqual("STORED_TOKEN", token);
- Assert.AreEqual(0, MessageHandlerStub.RequestCount);
- }
-
- [Test]
- public async Task ShouldNotThrowObjectDisposedExceptionWhenTokenExpires()
- {
- // given
- ExpiresIn = 0;
- await TokenProvider.GetAccessTokenForRequestAsync();
-
- // when
- Assert.DoesNotThrowAsync(async () => await TokenProvider.GetAccessTokenForRequestAsync());
-
- // then
- Assert.AreEqual(2, MessageHandlerStub.RequestCount);
- }
- }
-}
\ No newline at end of file
diff --git a/Client.UnitTests/Impl/Builder/CamundaCloudTokenProviderTest.cs b/Client.UnitTests/Impl/Builder/CamundaCloudTokenProviderTest.cs
new file mode 100644
index 00000000..0f743c13
--- /dev/null
+++ b/Client.UnitTests/Impl/Builder/CamundaCloudTokenProviderTest.cs
@@ -0,0 +1,128 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+
+namespace Zeebe.Client.Impl.Builder
+{
+ [TestFixture]
+ public class CamundaCloudTokenProviderTest
+ {
+ private HttpMessageHandlerStub MessageHandlerStub { get; set; }
+ private CamundaCloudTokenProvider TokenProvider { get; set; }
+ private string TokenStoragePath { get; set; }
+ private static long ExpiresIn { get; set; }
+ private static string Token { get; set; }
+
+ private static string _requestUri;
+ private static string _clientId;
+ private static string _clientSecret;
+ private static string _audience;
+
+ [SetUp]
+ public void Init()
+ {
+ _requestUri = "https://local.de";
+ _clientId = "ID";
+ _clientSecret = "SECRET";
+ _audience = "AUDIENCE";
+ TokenStoragePath = Path.GetTempPath() + ".zeebe/";
+ TokenProvider = new CamundaCloudTokenProviderBuilder()
+ .UseAuthServer(_requestUri)
+ .UseClientId(_clientId)
+ .UseClientSecret(_clientSecret)
+ .UseAudience(_audience)
+ .UsePath(TokenStoragePath)
+ .Build();
+
+ MessageHandlerStub = new HttpMessageHandlerStub();
+ TokenProvider.SetHttpMessageHandler(MessageHandlerStub);
+ ExpiresIn = 3600;
+ Token = "REQUESTED_TOKEN";
+ }
+
+ [TearDown]
+ public void CleanUp()
+ {
+ Directory.Delete(TokenStoragePath, true);
+ TokenProvider.Dispose();
+ }
+
+ private class HttpMessageHandlerStub : HttpMessageHandler
+ {
+ public int RequestCount { get; set; }
+ private bool _disposed = false;
+
+ protected override async Task SendAsync(HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ Assert.AreEqual(request.RequestUri, _requestUri);
+ var content = await request.Content.ReadAsStringAsync();
+ var queryString = HttpUtility.ParseQueryString(content);
+ Assert.AreEqual((string)queryString["client_id"], _clientId);
+ Assert.AreEqual((string)queryString["client_secret"], _clientSecret);
+ Assert.AreEqual((string)queryString["audience"], _audience);
+
+ RequestCount++;
+ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(@"{
+ ""access_token"":""" + Token + @""",
+ ""token_type"":""bearer"",
+ ""expires_in"": " + ExpiresIn + @",
+ ""refresh_token"":""IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk"",
+ ""scope"":""create""}"),
+ };
+
+ return responseMessage;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _disposed = true;
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException("HttpMessageHandlerStub");
+ }
+ }
+ }
+
+ [Test]
+ public async Task ShouldRequestCredentials()
+ {
+ // given
+
+ // when
+ var token = await TokenProvider.GetAccessTokenForRequestAsync();
+
+ // then
+ Assert.AreEqual("REQUESTED_TOKEN", token);
+ Assert.AreEqual(1, MessageHandlerStub.RequestCount);
+ }
+
+ [Test]
+ public async Task ShouldNotThrowObjectDisposedExceptionWhenTokenExpires()
+ {
+ // given
+ ExpiresIn = 0;
+ await TokenProvider.GetAccessTokenForRequestAsync();
+
+ // when
+ Assert.DoesNotThrowAsync(async () => await TokenProvider.GetAccessTokenForRequestAsync());
+
+ // then
+ Assert.AreEqual(2, MessageHandlerStub.RequestCount);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Client.UnitTests/Impl/Misc/PersistedAccessTokenCacheTest.cs b/Client.UnitTests/Impl/Misc/PersistedAccessTokenCacheTest.cs
new file mode 100644
index 00000000..d27d8391
--- /dev/null
+++ b/Client.UnitTests/Impl/Misc/PersistedAccessTokenCacheTest.cs
@@ -0,0 +1,212 @@
+//
+// Copyright (c) 2021 camunda services GmbH (info@camunda.com)
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using NUnit.Framework;
+
+namespace Zeebe.Client.Impl.Misc;
+
+[TestFixture]
+public class PersistedAccessTokenCacheTest
+{
+ private string tempPath;
+
+ [SetUp]
+ public void Init()
+ {
+ tempPath = Path.GetTempPath() + ".zeebe/";
+ Directory.CreateDirectory(tempPath);
+ }
+
+ [TearDown]
+ public void CleanUp()
+ {
+ Directory.Delete(tempPath, true);
+ }
+
+ [Test]
+ public async Task ShouldGetToken()
+ {
+ // given
+ var accessTokenCache = new PersistedAccessTokenCache(Path.Combine(tempPath, TestContext.CurrentContext.Test.Name),
+ () =>
+ Task.FromResult(new AccessToken("token", DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeMilliseconds())));
+
+ // when
+ var token = await accessTokenCache.Get("test");
+
+ // then
+ Assert.AreEqual("token", token);
+ }
+
+ [Test]
+ public async Task ShouldCacheToken()
+ {
+ // given
+ int fetchCounter = 0;
+ var accessTokenCache = new PersistedAccessTokenCache(Path.Combine(tempPath, TestContext.CurrentContext.Test.Name),
+ () => Task.FromResult(new AccessToken("token-" + fetchCounter++, DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeMilliseconds())));
+
+ // when
+ await accessTokenCache.Get("test");
+ var token = await accessTokenCache.Get("test");
+
+ // then
+ Assert.AreEqual("token-0", token);
+ Assert.AreEqual(1, fetchCounter);
+ }
+
+ [Test]
+ public async Task ShouldCacheTokenForDifferentAudience()
+ {
+ // given
+ int fetchCounter = 0;
+ var accessTokenCache = new PersistedAccessTokenCache(Path.Combine(tempPath, TestContext.CurrentContext.Test.Name),
+ () => Task.FromResult(new AccessToken("token-" + fetchCounter++, DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeMilliseconds())));
+
+ // when
+ var firstToken = await accessTokenCache.Get("first");
+ var secondToken = await accessTokenCache.Get("second");
+
+ // then
+ Assert.AreEqual("token-0", firstToken);
+ Assert.AreEqual("token-1", secondToken);
+ Assert.AreEqual(2, fetchCounter);
+ }
+
+ [Test]
+ public async Task ShouldResolveNewTokenAfterExpiry()
+ {
+ // given
+ int fetchCounter = 0;
+ var accessTokenCache = new PersistedAccessTokenCache(Path.Combine(tempPath, TestContext.CurrentContext.Test.Name),
+ () => Task.FromResult(new AccessToken("token-" + fetchCounter++, DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeMilliseconds())));
+
+ // when
+ await accessTokenCache.Get("test");
+ var token = await accessTokenCache.Get("test");
+
+ // then
+ Assert.AreEqual("token-1", token);
+ Assert.AreEqual(2, fetchCounter);
+ }
+
+
+ [Test]
+ public async Task ShouldReflectTokenOnDiskAfterExpiry()
+ {
+ // given
+ var audience = "test";
+ int fetchCounter = 0;
+ var path = Path.Combine(tempPath, TestContext.CurrentContext.Test.Name);
+ var accessTokenCache = new PersistedAccessTokenCache(path,
+ () => Task.FromResult(new AccessToken("token-" + fetchCounter++, DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeMilliseconds())));
+ var firstToken = await accessTokenCache.Get(audience);
+
+ var credentials = await File.ReadAllTextAsync(Directory.GetFiles(path)[0]);
+ Assert.That(credentials, Does.Contain(firstToken));
+ Assert.That(credentials, Does.Contain(audience));
+
+ // when
+ var secondToken = await accessTokenCache.Get(audience);
+
+ // then
+ Assert.AreNotEqual(secondToken, firstToken);
+ Assert.AreEqual("token-1", secondToken);
+ Assert.AreEqual(2, fetchCounter);
+
+ credentials = await File.ReadAllTextAsync(Directory.GetFiles(path)[0]);
+ Assert.That(credentials, Does.Contain(secondToken));
+ Assert.That(credentials, Does.Contain(audience));
+ }
+
+ [Test]
+ public async Task ShouldPersistTokenToDisk()
+ {
+ // given
+ var audience = "test";
+ int fetchCounter = 0;
+ var path = Path.Combine(tempPath, TestContext.CurrentContext.Test.Name);
+ var accessTokenCache = new PersistedAccessTokenCache(path,
+ () => Task.FromResult(new AccessToken("token-" + fetchCounter++, DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeMilliseconds())));
+
+ // when
+ var token = await accessTokenCache.Get(audience);
+
+ // then
+ var fileNames = Directory.GetFiles(path);
+ Assert.AreEqual(1, fileNames.Length);
+ var content = await File.ReadAllTextAsync(fileNames[0]);
+ Assert.That(content, Does.Contain(token));
+ Assert.That(content, Does.Contain(audience));
+ }
+
+
+ [Test]
+ public async Task ShouldPersistMultipleTokenToDisk()
+ {
+ // given
+ int fetchCounter = 0;
+ var path = Path.Combine(tempPath, TestContext.CurrentContext.Test.Name);
+ var accessTokenCache = new PersistedAccessTokenCache(path,
+ () => Task.FromResult(new AccessToken("token-" + fetchCounter++, DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeMilliseconds())));
+
+ // when
+ var firstToken = await accessTokenCache.Get("first");
+ var secondToken = await accessTokenCache.Get("second");
+
+ // then
+ Assert.AreEqual("token-0", firstToken);
+ Assert.AreEqual("token-1", secondToken);
+ Assert.AreEqual(2, fetchCounter);
+
+ var fileNames = Directory.GetFiles(path);
+ Assert.AreEqual(1, fileNames.Length);
+ var content = await File.ReadAllTextAsync(fileNames[0]);
+ Assert.That(content, Does.Contain(firstToken));
+ Assert.That(content, Does.Contain("first"));
+
+
+ Assert.That(content, Does.Contain(secondToken));
+ Assert.That(content, Does.Contain("second"));
+ }
+
+ [Test]
+ public async Task ShouldFetchNewTokenWhenPersistTokenGotLost()
+ {
+ // given
+ var audience = "test";
+ int fetchCounter = 0;
+ var path = Path.Combine(tempPath, TestContext.CurrentContext.Test.Name);
+ var accessTokenCache = new PersistedAccessTokenCache(path,
+ () => Task.FromResult(new AccessToken("token-" + fetchCounter++, DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeMilliseconds())));
+ await accessTokenCache.Get(audience);
+ File.Delete(Directory.GetFiles(path)[0]);
+
+ // when
+ var token = await accessTokenCache.Get(audience);
+
+ // then
+ Assert.AreEqual("token-1", token);
+ Assert.AreEqual(2, fetchCounter);
+ var fileNames = Directory.GetFiles(path);
+ Assert.AreEqual(1, fileNames.Length);
+ var content = await File.ReadAllTextAsync(fileNames[0]);
+ Assert.That(content, Does.Contain(token));
+ Assert.That(content, Does.Contain(audience));
+ }
+}
\ No newline at end of file
diff --git a/Client/Api/Builder/ICamundaCloudTokenProviderBuilder.cs b/Client/Api/Builder/ICamundaCloudTokenProviderBuilder.cs
index 968d30e4..d623b237 100644
--- a/Client/Api/Builder/ICamundaCloudTokenProviderBuilder.cs
+++ b/Client/Api/Builder/ICamundaCloudTokenProviderBuilder.cs
@@ -58,6 +58,15 @@ public interface ICamundaCloudTokenProviderBuilderStep4
public interface ICamundaCloudTokenProviderBuilderFinalStep
{
+
+ ///
+ /// Use given path to store credentials on disk.
+ ///
+ /// Per default credentials are stored in the home directory.
+ /// The path were to store the credentials.
+ /// The final step in building a CamundaCloudTokenProvider.
+ ICamundaCloudTokenProviderBuilderFinalStep UsePath(string path);
+
///
/// Builds the CamundaCloudTokenProvider, which can be used by the ZeebeClient to
/// communicate with the Camunda Cloud.
diff --git a/Client/Impl/Builder/CamundaCloudTokenProvider.cs b/Client/Impl/Builder/CamundaCloudTokenProvider.cs
index b83d82e0..d30352f7 100644
--- a/Client/Impl/Builder/CamundaCloudTokenProvider.cs
+++ b/Client/Impl/Builder/CamundaCloudTokenProvider.cs
@@ -1,25 +1,17 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using Zeebe.Client.Api.Builder;
+using Zeebe.Client.Impl.Misc;
namespace Zeebe.Client.Impl.Builder
{
public class CamundaCloudTokenProvider : IAccessTokenSupplier, IDisposable
{
- private const string JsonContent =
- "{{\"client_id\":\"{0}\",\"client_secret\":\"{1}\",\"audience\":\"{2}\",\"grant_type\":\"client_credentials\"}}";
-
- private const string ZeebeCloudTokenFileName = "credentials";
-
private static readonly string ZeebeRootPath =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".zeebe");
@@ -30,24 +22,23 @@ public class CamundaCloudTokenProvider : IAccessTokenSupplier, IDisposable
private readonly string audience;
private HttpClient httpClient;
private HttpMessageHandler httpMessageHandler;
+ private readonly PersistedAccessTokenCache persistedAccessTokenCache;
internal CamundaCloudTokenProvider(
string authServer,
string clientId,
string clientSecret,
string audience,
- ILogger logger = null)
+ string path = null,
+ ILoggerFactory loggerFactory = null)
{
- this.logger = logger;
+ persistedAccessTokenCache = new PersistedAccessTokenCache(path ?? ZeebeRootPath, FetchAccessToken, loggerFactory?.CreateLogger());
+ this.logger = loggerFactory?.CreateLogger();
this.authServer = authServer;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.audience = audience;
-
- // default client handler
httpClient = new HttpClient(new HttpClientHandler(), disposeHandler: false);
- TokenStoragePath = ZeebeRootPath;
- Credentials = new Dictionary();
}
public static CamundaCloudTokenProviderBuilder Builder()
@@ -55,136 +46,55 @@ public static CamundaCloudTokenProviderBuilder Builder()
return new CamundaCloudTokenProviderBuilder();
}
- public string TokenStoragePath { get; set; }
- private string TokenFileName => TokenStoragePath + Path.DirectorySeparatorChar + ZeebeCloudTokenFileName;
- private Dictionary Credentials { get; set; }
-
- public Task GetAccessTokenForRequestAsync(
- string authUri = null,
- CancellationToken cancellationToken = default(CancellationToken))
- {
- // check in memory
- AccessToken currentAccessToken;
- if (Credentials.TryGetValue(audience, out currentAccessToken))
- {
- logger?.LogTrace("Use in memory access token.");
- return GetValidToken(currentAccessToken);
- }
-
- // check if token file exists
- var tokenFileName = TokenFileName;
- var existToken = File.Exists(tokenFileName);
- if (existToken)
- {
- logger?.LogTrace("Read cached access token from {tokenFileName}", tokenFileName);
- // read token
- var content = File.ReadAllText(tokenFileName);
- Credentials = JsonConvert.DeserializeObject>(content);
- if (Credentials.TryGetValue(audience, out currentAccessToken))
- {
- logger?.LogTrace("Found access token in credentials file.");
- return GetValidToken(currentAccessToken);
- }
- }
-
- // request token
- return RequestAccessTokenAsync();
- }
-
internal void SetHttpMessageHandler(HttpMessageHandler handler)
{
httpMessageHandler = handler;
httpClient = new HttpClient(handler);
}
- private Task GetValidToken(AccessToken currentAccessToken)
- {
- var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
- var dueDate = currentAccessToken.DueDate;
- if (now < dueDate)
- {
- // still valid
- return Task.FromResult(currentAccessToken.Token);
- }
-
- logger?.LogTrace("Access token is no longer valid (now: {now} > dueTime: {dueTime}), request new one.", now, dueDate);
- return RequestAccessTokenAsync();
- }
-
- // Requesting the token is similar to this:
- // curl --request POST \
- // --url https://login.cloud.[ultrawombat.com | camunda.io]/oauth/token \
- // --header 'content-type: application/json' \
- // --data '{"client_id":"${clientId}","client_secret":"${clientSecret}","audience":"${audience}","grant_type":"client_credentials"}'
-
- // Code expects the following result:
- //
- // {
- // "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
- // "token_type":"bearer",
- // "expires_in":3600,
- // "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
- // "scope":"create"
- // }
- //
- // Defined here https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
-
- private async Task RequestAccessTokenAsync()
+ private async Task FetchAccessToken()
{
- var directoryInfo = Directory.CreateDirectory(TokenStoragePath);
- if (!directoryInfo.Exists)
- {
- throw new IOException("Expected to create '~/.zeebe/' directory, but failed to do so.");
- }
-
- var tokenFileName = TokenFileName;
- var json = string.Format(JsonContent, clientId, clientSecret, audience);
-
- using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
- {
- var httpResponseMessage = await httpClient.PostAsync(authServer, content);
-
- var result = await httpResponseMessage.Content.ReadAsStringAsync();
- var token = ToAccessToken(result);
- logger?.LogDebug("Received access token for {audience}, will backup at {path}.", audience, tokenFileName);
- Credentials[audience] = token;
- WriteCredentials();
-
- return token.Token;
- }
- }
-
- private void WriteCredentials()
- {
- File.WriteAllText(TokenFileName, JsonConvert.SerializeObject(Credentials));
- }
-
- private static AccessToken ToAccessToken(string result)
- {
- var jsonResult = JObject.Parse(result);
- var accessToken = (string)jsonResult["access_token"];
-
- var expiresInMilliSeconds = (long)jsonResult["expires_in"] * 1_000L;
- var dueDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + expiresInMilliSeconds;
- var token = new AccessToken(accessToken, dueDate);
+ // Requesting the token is similar to this:
+ // curl -X POST https://login.cloud.ultrawombat.com/oauth/token \
+ // -H "Content-Type: application/x-www-form-urlencoded" \
+ // -d "client_id=213131&client_secret=12-23~oU.321&audience=zeebe.ultrawombat.com&grant_type=client_credentials"
+ //
+ // alternative is json
+ // curl --request POST \
+ // --url https://login.cloud.[ultrawombat.com | camunda.io]/oauth/token \
+ // --header 'content-type: application/json' \
+ // --data '{"client_id":"${clientId}","client_secret":"${clientSecret}","audience":"${audience}","grant_type":"client_credentials"}'
+
+ var formContent = BuildRequestAccessTokenContent();
+ var httpResponseMessage = await httpClient.PostAsync(authServer, formContent);
+
+ // Code expects the following result:
+ //
+ // {
+ // "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
+ // "token_type":"bearer",
+ // "expires_in":3600,
+ // "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
+ // "scope":"create"
+ // }
+ //
+ // Defined here https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
+ var result = await httpResponseMessage.Content.ReadAsStringAsync();
+ var token = AccessToken.FromJson(result);
+ logger?.LogDebug("Received access token for {Audience}", audience);
return token;
}
- public class AccessToken
+ private FormUrlEncodedContent BuildRequestAccessTokenContent()
{
- public string Token { get; set; }
- public long DueDate { get; set; }
-
- public AccessToken(string token, long dueDate)
- {
- Token = token;
- DueDate = dueDate;
- }
-
- public override string ToString()
+ var formContent = new FormUrlEncodedContent(new[]
{
- return $"{nameof(Token)}: {Token}, {nameof(DueDate)}: {DueDate}";
- }
+ new KeyValuePair("client_id", clientId),
+ new KeyValuePair("client_secret", clientSecret),
+ new KeyValuePair("audience", audience),
+ new KeyValuePair("grant_type", "client_credentials")
+ });
+ return formContent;
}
public void Dispose()
@@ -192,5 +102,11 @@ public void Dispose()
httpClient.Dispose();
httpMessageHandler.Dispose();
}
+
+ public async Task GetAccessTokenForRequestAsync(string authUri = null,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return await persistedAccessTokenCache.Get(audience);
+ }
}
}
\ No newline at end of file
diff --git a/Client/Impl/Builder/CamundaCloudTokenProviderBuilder.cs b/Client/Impl/Builder/CamundaCloudTokenProviderBuilder.cs
index 5a56c4cc..e8bbaa7b 100644
--- a/Client/Impl/Builder/CamundaCloudTokenProviderBuilder.cs
+++ b/Client/Impl/Builder/CamundaCloudTokenProviderBuilder.cs
@@ -18,6 +18,7 @@ public class CamundaCloudTokenProviderBuilder :
private string authServer = "https://login.cloud.camunda.io/oauth/token";
private string clientId;
private string clientSecret;
+ private string path;
///
public ICamundaCloudTokenProviderBuilder UseLoggerFactory(ILoggerFactory loggerFactory)
@@ -74,6 +75,17 @@ public ICamundaCloudTokenProviderBuilderFinalStep UseAudience(string audience)
return this;
}
+ public ICamundaCloudTokenProviderBuilderFinalStep UsePath(string path)
+ {
+ if (path == null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ this.path = path;
+ return this;
+ }
+
///
public CamundaCloudTokenProvider Build()
{
@@ -82,7 +94,8 @@ public CamundaCloudTokenProvider Build()
clientId,
clientSecret,
audience,
- loggerFactory?.CreateLogger());
+ path,
+ loggerFactory);
}
}
}
diff --git a/Client/Impl/Misc/AccessToken.cs b/Client/Impl/Misc/AccessToken.cs
new file mode 100644
index 00000000..8046ff15
--- /dev/null
+++ b/Client/Impl/Misc/AccessToken.cs
@@ -0,0 +1,35 @@
+using System;
+using Newtonsoft.Json.Linq;
+
+namespace Zeebe.Client.Impl.Misc;
+
+///
+/// AccessToken, which consist of an token and a dueDate (expiryDate).
+///
+public class AccessToken
+{
+ public string Token { get; set; }
+ public long DueDate { get; set; }
+
+ public AccessToken(string token, long dueDate)
+ {
+ Token = token;
+ DueDate = dueDate;
+ }
+
+ public override string ToString()
+ {
+ return $"{nameof(Token)}: {Token}, {nameof(DueDate)}: {DueDate}";
+ }
+
+ public static AccessToken FromJson(string result)
+ {
+ var jsonResult = JObject.Parse(result);
+ var accessToken = (string)jsonResult["access_token"];
+
+ var expiresInMilliSeconds = (long)jsonResult["expires_in"] * 1_000L;
+ var dueDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + expiresInMilliSeconds;
+ var token = new AccessToken(accessToken, dueDate);
+ return token;
+ }
+}
\ No newline at end of file
diff --git a/Client/Impl/Misc/IAccessTokenCache.cs b/Client/Impl/Misc/IAccessTokenCache.cs
new file mode 100644
index 00000000..0b3e2f1f
--- /dev/null
+++ b/Client/Impl/Misc/IAccessTokenCache.cs
@@ -0,0 +1,43 @@
+//
+// Copyright (c) 2021 camunda services GmbH (info@camunda.com)
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Threading.Tasks;
+
+namespace Zeebe.Client.Impl.Misc;
+
+///
+/// cache, which allows to cache tokens per audience.
+///
+public interface IAccessTokenCache
+{
+ ///
+ /// A valid token, which is related to the given audience.
+ ///
+ ///
+ /// The token may be cached, or new resolved if there was no token corresponding
+ /// to the audience stored yet, or if the token has been expired.
+ /// The audience which corresponds to the token.
+ /// A valid token for the audience.
+ Task Get(string audience);
+
+ ///
+ /// An asynchronous access token resolver, which is used to fill the cache, when
+ /// token can't be found.
+ ///
+ ///
+ /// Resolver should be given to the cache, on creation time.
+ /// The new access token.
+ public delegate Task AccessTokenResolverAsync();
+}
\ No newline at end of file
diff --git a/Client/Impl/Misc/PersistedAccessTokenCache.cs b/Client/Impl/Misc/PersistedAccessTokenCache.cs
new file mode 100644
index 00000000..add57200
--- /dev/null
+++ b/Client/Impl/Misc/PersistedAccessTokenCache.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Zeebe.Client.Api.Builder;
+
+namespace Zeebe.Client.Impl.Misc;
+
+public class PersistedAccessTokenCache : IAccessTokenCache
+{
+ private static string ZeebeTokenFileName => "credentials";
+ private Dictionary CachedCredentials { get; set; }
+
+ // private static readonly string ZeebeRootPath =
+ // Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".zeebe");
+
+ private readonly ILogger logger;
+ private readonly IAccessTokenCache.AccessTokenResolverAsync accessTokenFetcherAsync;
+
+ private readonly string tokenStoragePath;
+ private string TokenFileName => Path.Combine(tokenStoragePath, ZeebeTokenFileName);
+
+ public PersistedAccessTokenCache(string path, IAccessTokenCache.AccessTokenResolverAsync fetcherAsync, ILogger logger = null)
+ {
+ var directoryInfo = Directory.CreateDirectory(path);
+ if (!directoryInfo.Exists)
+ {
+ throw new IOException("Expected to create '~/.zeebe/' directory, but failed to do so.");
+ }
+
+ tokenStoragePath = path;
+ this.logger = logger;
+ accessTokenFetcherAsync = fetcherAsync;
+ CachedCredentials = new Dictionary();
+ }
+
+ public async Task Get(string audience)
+ {
+ // check in memory
+ if (CachedCredentials.TryGetValue(audience, out var currentAccessToken))
+ {
+ logger?.LogTrace("Use in memory access token");
+ return await GetValidToken(audience, currentAccessToken);
+ }
+
+ // check if token file exists
+ var useCachedFileToken = File.Exists(TokenFileName);
+ if (useCachedFileToken)
+ {
+ logger?.LogTrace("Read cached access token from {TokenFileName}", TokenFileName);
+ // read token
+ var content = await File.ReadAllTextAsync(TokenFileName);
+ CachedCredentials = JsonConvert.DeserializeObject>(content);
+ if (CachedCredentials.TryGetValue(audience, out currentAccessToken))
+ {
+ logger?.LogTrace("Found access token in credentials file");
+ return await GetValidToken(audience, currentAccessToken);
+ }
+ }
+
+ // fetch new token
+ var newAccessToken = await FetchNewAccessToken(audience);
+ return newAccessToken.Token;
+ }
+
+ private async Task GetValidToken(string audience, AccessToken currentAccessToken)
+ {
+ var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ var dueDate = currentAccessToken.DueDate;
+ if (now < dueDate)
+ {
+ // still valid
+ return currentAccessToken.Token;
+ }
+
+ logger?.LogTrace("Access token is no longer valid (now: {Now} > dueTime: {DueTime}), request new one", now,
+ dueDate);
+ var newAccessToken = await FetchNewAccessToken(audience);
+ return newAccessToken.Token;
+ }
+
+ private async Task FetchNewAccessToken(string audience)
+ {
+ var newAccessToken = await accessTokenFetcherAsync();
+ CachedCredentials[audience] = newAccessToken;
+ WriteCredentials();
+ return newAccessToken;
+ }
+
+ private void WriteCredentials()
+ {
+ File.WriteAllText(TokenFileName, JsonConvert.SerializeObject(CachedCredentials));
+ }
+}
\ No newline at end of file
diff --git a/global.json b/global.json
index 34fe4d43..37704f06 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "8.0.100",
+ "version": "7.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
-}
\ No newline at end of file
+}