using System.Net.Http.Headers; using System.Text.Json; 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); // 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 string json = await response.Content.ReadAsStringAsync(); // Deserialize Token? result = JsonSerializer.Deserialize(json); if (result is null) throw new FormatException($"Could not deserialize token response: {json}"); // Return the token response return result; } /// 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 token; } //Acquire token token = await AcquirePartnerAuthenticationTokenAsync(); //Save to cache this.memoryCache.Set(Keys.TeslaPartnerToken, token, token.Expires.Subtract(this.configuration.MemoryCacheDelta)); this.logger.LogInformation("Partner authentication token provided provided by acquiring from API"); return token; } 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(); Token partnerToken = await GetPartnerAuthenticationTokenAsync(); //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"); return new Result(success); } public async Task> CheckApplicationRegistrationAsync(CancellationToken cancellationToken = default(CancellationToken)) { this.logger.LogTrace("Checking application registration..."); //Create client and get partner token HttpClient httpClient = httpClientFactory.CreateClient(); Token partnerToken = await GetPartnerAuthenticationTokenAsync(); //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", 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"; }