From 9d4135d6d39c916c88730783f9c217c00ad3b701 Mon Sep 17 00:00:00 2001 From: Denis-Cosmin Nutiu Date: Fri, 17 Apr 2020 17:04:13 +0300 Subject: [PATCH] NUC-33: Implement authentication handling for TelemetryPublisherFirestore --- NucuCar.Domain/Readme.md | 3 + .../Telemetry/TelemetryPublisherFirestore.cs | 90 +++++++++++++++++-- .../TelemetryPublisherFirestoreTest.cs | 7 ++ 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs diff --git a/NucuCar.Domain/Readme.md b/NucuCar.Domain/Readme.md index e72574b..8b0e828 100644 --- a/NucuCar.Domain/Readme.md +++ b/NucuCar.Domain/Readme.md @@ -72,6 +72,9 @@ The Telemetry:Publisher must be set to: Firestore Example connection string: `ProjectId=nucuhub;CollectionName=sensors-telemetry-test;Timeout=1000` +If you want to use Authentication you can do so by providing the following keys +in the connection string: WebApiEmail, WebApiPassword, WebApiKey. + ### Reader You will need use a firebase client or rest API. \ No newline at end of file diff --git a/NucuCar.Domain/Telemetry/TelemetryPublisherFirestore.cs b/NucuCar.Domain/Telemetry/TelemetryPublisherFirestore.cs index edcd916..6b09f27 100644 --- a/NucuCar.Domain/Telemetry/TelemetryPublisherFirestore.cs +++ b/NucuCar.Domain/Telemetry/TelemetryPublisherFirestore.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -19,6 +21,9 @@ namespace NucuCar.Domain.Telemetry /// The connection string has the following parameters: /// ProjectId (required) — The string for the Firestore project id. /// CollectionName (required) — The string for the Firestore collection name. + /// WebApiKey (optional) — The web api key of the firebase project. + /// WebApiEmail (optional) — An email to use when requesting id tokens. + /// WebApiPassword (optional) — The password to use when requesting id tokens. /// Timeout (optional) — The number in milliseconds in which to timeout if publishing fails. Default: 10000 /// /// @@ -27,6 +32,13 @@ namespace NucuCar.Domain.Telemetry private readonly HttpClient _httpClient; private readonly int _timeout; + private string _idToken; + + // Variables used for authentication + private readonly string _webEmail; + private readonly string _webPassword; + private readonly string _webApiKey; + public TelemetryPublisherFirestore(TelemetryPublisherBuilderOptions opts) : base(opts) { var options = ConnectionStringParser.Parse(opts.ConnectionString); @@ -44,7 +56,10 @@ namespace NucuCar.Domain.Telemetry $"Missing CollectionName!"); } - _timeout = int.Parse(options.GetValueOrDefault("Timeout", "10000")); + _timeout = int.Parse(options.GetValueOrDefault("Timeout", "10000") ?? "10000"); + _webApiKey = options.GetValueOrDefault("WebApiKey", null); + _webEmail = options.GetValueOrDefault("WebApiEmail", null); + _webPassword = options.GetValueOrDefault("WebApiPassword", null); var requestUrl = $"https://firestore.googleapis.com/v1/projects/{firestoreProjectId}/" + $"databases/(default)/documents/{firestoreCollection}/"; @@ -53,27 +68,90 @@ namespace NucuCar.Domain.Telemetry Logger?.LogInformation($"Initialized {nameof(TelemetryPublisherFirestore)}"); } + private async Task SetupAuthenticationHeaders() + { + // Make request + var requestUrl = $"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={_webApiKey}"; + var data = new Dictionary() + { + ["email"] = _webEmail, + ["password"] = _webPassword, + ["returnSecureToken"] = true + }; + // Handle response & setup headers + var cts = new CancellationTokenSource(); + try + { + cts.CancelAfter(_timeout); + var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); + var responseMessage = await _httpClient.PostAsync(requestUrl, content, cts.Token); + var responseJson = + JsonConvert.DeserializeObject(await responseMessage.Content.ReadAsStringAsync()); + if (responseMessage.StatusCode == HttpStatusCode.OK) + { + _idToken = responseJson.idToken; + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _idToken); + } + else + { + throw new HttpRequestException(responseJson.message ?? "unknown"); + } + } + catch (TaskCanceledException e) + { + Logger.LogWarning($"FireStore authenticate: Timeout or cancellation occured. Message {e.Message}.\n"); + } + catch (HttpRequestException e) + { + Logger?.LogError($"Failed to authenticate!\n{e.GetType().FullName}: {e.Message}"); + throw; + } + } + public override async Task PublishAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return; } - + var cts = new CancellationTokenSource(); - cts.CancelAfter(_timeout); try { + cts.CancelAfter(_timeout); var data = FirebaseRestTranslator.Translator.Translate(null, GetTelemetry()); var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); - await _httpClient.PostAsync("", content, cts.Token); - Logger?.LogInformation("Published data to Firestore!"); + var responseMessage = await _httpClient.PostAsync("", content, cts.Token); + var responseJson = + JsonConvert.DeserializeObject(await responseMessage.Content.ReadAsStringAsync()); + + switch (responseMessage.StatusCode) + { + case HttpStatusCode.OK: + Logger?.LogInformation("Published data to Firestore!"); + break; + case HttpStatusCode.Forbidden: + Logger.LogWarning("Failed to publish telemetry! Forbidden!"); + await SetupAuthenticationHeaders(); + break; + default: + throw new HttpRequestException(responseJson.message ?? "unknown error"); + } } - catch (Exception e) + catch (TaskCanceledException e) + { + Logger.LogWarning( + $"Firestore publish telemetry: Timeout or cancellation occured. Message {e.Message}.\n"); + } + catch (HttpRequestException e) { Logger?.LogError($"Failed to publish telemetry data!\n{e.GetType().FullName}: {e.Message}"); throw; } + finally + { + cts.Dispose(); + } } public override void Dispose() diff --git a/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs b/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs new file mode 100644 index 0000000..ef69901 --- /dev/null +++ b/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs @@ -0,0 +1,7 @@ +namespace NucuCar.UnitTests.NucuCar.Domain.Tests.Telemetry +{ + public class TelemetryPublisherFirestoreTest + { + // TODO after refactoring + } +} \ No newline at end of file