using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using ProofOfConcept.Models; using ProofOfConcept.Utilities; using SzakatsA.Result; namespace ProofOfConcept.Services; public class TeslaAuthenticatorService { private readonly ILogger logger; private readonly TeslaAuthenticatorServiceConfiguration configuration; private readonly IHttpClientFactory httpClientFactory; private readonly IMemoryCache memoryCache; const string authEndpointURL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token"; const string euBaseURL = "https://fleet-api.prd.eu.vn.cloud.tesla.com"; public TeslaAuthenticatorService(ILogger logger, IOptions options, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache) { this.logger = logger; this.httpClientFactory = httpClientFactory; this.memoryCache = memoryCache; this.configuration = options.Value; } /// Asynchronously retrieves an authentication token from the Tesla partner API. /// This method sends a POST request with client credentials to obtain an /// OAuth2 access token that grants access to Tesla partner services. /// Returns a string containing the authentication token received from the API. /// Thrown when the HTTP request to the API fails. public async Task> AcquirePartnerAuthenticationTokenAsync() { this.logger.LogTrace("Acquiring authentication token from Tesla partner API..."); using HttpClient client = httpClientFactory.CreateClient(); // URL to request the token const string audience = euBaseURL; // Prepare the form-urlencoded body Dictionary formData = new Dictionary { { "grant_type", "client_credentials" }, { "client_id", this.configuration.ClientID }, { "client_secret", this.configuration.ClientSecret }, { "scope", "openid offline_access vehicle_device_data vehicle_location" }, { "audience", audience } }; FormUrlEncodedContent content = new FormUrlEncodedContent(formData); string json = ""; try { // Send POST request HttpResponseMessage response = await client.PostAsync(authEndpointURL, content); this.logger.LogTrace("Response status code: {0}", response.StatusCode); // Throw if not successful response.EnsureSuccessStatusCode(); // Read the response body as a string json = await response.Content.ReadAsStringAsync(); } catch (HttpRequestException httpRequestException) { this.logger.LogError(httpRequestException, "HTTP Request error while acquiring partner authentication token from Tesla API"); return new Result(httpRequestException); } catch (Exception e) { this.logger.LogError(e, "Unexpected communication error while acquiring partner authentication token from Tesla API"); return new Result(e); } try { // Check if the response is empty if (String.IsNullOrWhiteSpace(json)) throw new JsonException("Response could not be deserialized (empty)."); // Deserializer options: {"access_token":"999 random chars including some special characters","expires_in":28800,"token_type":"Bearer"} JsonSerializerOptions serializerOptions = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; // Deserialize the JSON response Token? result = JsonSerializer.Deserialize(json, serializerOptions); if (result is null) throw new JsonException("Response could not be deserialized (null)."); this.logger.LogInformation("Authentication token acquired from Tesla partner API"); return new Result(result); } catch (JsonException jsonException) { this.logger.LogError(jsonException, "JSON deserialization error while acquiring partner authentication token from Tesla API"); return new Result(jsonException); } catch (Exception e) { this.logger.LogError(e, "Unexpected serialization error while acquiring partner authentication token from Tesla API"); return new Result(e); } } /// Retrieves a cached partner authentication token or acquires a new one if not available. /// This method first attempts to fetch the token from the memory cache. If the token is not found /// or has expired, it invokes the method to retrieve a new token from the Tesla partner API and stores it in the cache. /// The cached token is set to expire slightly earlier than its actual expiration time to avoid unexpected token expiry during usage. /// Returns a Token object containing the authentication token and its expiration details. /// Thrown when the HTTP request to acquire a new partner token fails. /// Thrown for any unexpected errors during the token retrieval or caching process. public async Task> GetPartnerAuthenticationTokenAsync() { this.logger.LogTrace("Getting partner authentication token..."); //Token is available in cache if (this.memoryCache.TryGetValue(Keys.TeslaPartnerToken, out Token? token) && token is not null) { this.logger.LogInformation("Partner authentication token provided from cache"); return Result.Success(token); } //Acquire token Result aquirationResult = await AcquirePartnerAuthenticationTokenAsync(); //Save to cache if (aquirationResult.IsSuccessful) { token = aquirationResult.Value; this.memoryCache.Set(Keys.TeslaPartnerToken, token, token.Expires.Subtract(this.configuration.MemoryCacheDelta)); this.logger.LogInformation("Partner authentication token provided by acquiring from API"); } else this.logger.LogError("Failed to get partner authentication token"); return aquirationResult; } public async Task RegisterApplicationAsync(CancellationToken cancellationToken = default(CancellationToken)) { this.logger.LogTrace("Registering application to Tesla fleet API..."); //Create client and get partner token HttpClient httpClient = httpClientFactory.CreateClient(); Result partnerTokenResult = await GetPartnerAuthenticationTokenAsync(); if (!partnerTokenResult.IsSuccessful) return Result.Fail(partnerTokenResult.Exception ?? new Exception("Partner authentication token not found")); //Set partner token Token partnerToken = partnerTokenResult.Value; //Set headers httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerToken.AccessToken); //Prepare body var postBody = new { domain = this.configuration.Domain }; //Send request HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{euBaseURL}/api/1/partner_accounts", postBody, cancellationToken); bool success = response.IsSuccessStatusCode; logger.LogInformation("Application registration result: {Success}", success ? "Success" : "Failed"); //Load response from server string json = await response.Content.ReadAsStringAsync(cancellationToken); this.logger.LogTrace("Registration response from server: {Response}", json); //{"isSuccessful":true,"isFailed":false,"exceptions":[],"exception":null} //Parse JSON JsonNode? root = JsonNode.Parse(json); bool isSuccessful = root?["isSuccessful"]?.GetValue() ?? false; bool isFailed = root?["isFailed"]?.GetValue() ?? false; if (success && isSuccessful && !isFailed) { logger.LogInformation("Application registered successfully"); return Result.Success(); } else { logger.LogError("Application registration failed"); return Result.Fail(new AggregateException(new Exception(root?["exception"]?.ToJsonString()), new Exception(root?["exceptions"]?.ToJsonString()))); } } public async Task> CheckApplicationRegistrationAsync(CancellationToken cancellationToken = default(CancellationToken)) { this.logger.LogTrace("Checking application registration..."); //Create client and get partner token HttpClient httpClient = httpClientFactory.CreateClient(); Result partnerTokenResult = await GetPartnerAuthenticationTokenAsync(); if (!partnerTokenResult.IsSuccessful) return Result.Fail(partnerTokenResult.Exception ?? new Exception("Partner authentication token not found")); //Set partner token Token partnerToken = partnerTokenResult.Value; //Set headers httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerToken.AccessToken); //Send request HttpResponseMessage response = await httpClient.GetAsync($"{euBaseURL}/api/1/partner_accounts/public_key?domain={this.configuration.Domain}", cancellationToken); string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); logger.LogInformation("Application registration result: {Result}", responseBody); return new Result(); } } public class TeslaAuthenticatorServiceConfiguration { public string ClientID { get; set; } = "b2240ee4-332a-4252-91aa-bbcc24f78fdb"; public string ClientSecret { get; set; } = "ta-secret.YG+XSdlvr6Lv8U-x"; public TimeSpan MemoryCacheDelta { get; set; } = TimeSpan.FromSeconds(5); public string Domain { get; set; } = "tesla-connector.automatic-parking.app"; }