using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; 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 interface ITeslaAuthenticatorService { /// 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. Task> AcquirePartnerAuthenticationTokenAsync(); /// 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. Task> GetPartnerAuthenticationTokenAsync(); Task RegisterApplicationAsync(CancellationToken cancellationToken = default(CancellationToken)); Task> CheckApplicationRegistrationAsync(CancellationToken cancellationToken = default(CancellationToken)); string GetAplicationAuthorizationURL(); } public class TeslaAuthenticatorService : ITeslaAuthenticatorService { 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); try { //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); //Parse response using JsonDocument? doc = JsonDocument.Parse(responseBody); string publicKeyHex = doc.RootElement.GetProperty("response").GetProperty("public_key").GetString() ?? throw new JsonException("Public key not found in response"); //Public key bytes byte[] bytes = Convert.FromHexString(publicKeyHex); //Get bytes from PEM key string pem = await File.ReadAllTextAsync("Resources/Signature/public-key.pem", cancellationToken: cancellationToken);; string[] lines = pem.Split('\n') .Select(l => l.Trim()) .Where(l => !string.IsNullOrEmpty(l) && !l.StartsWith("-----")) .ToArray(); string base64 = string.Join("", lines); byte[] pemBytes = Convert.FromBase64String(base64); // Parse the PEM with ECDsa to get the raw Q.X||Q.Y using var ecdsa = ECDsa.Create(); ecdsa.ImportSubjectPublicKeyInfo(pemBytes, out _); ECParameters parameters = ecdsa.ExportParameters(false); byte[]? x = parameters.Q.X; byte[]? y = parameters.Q.Y; if (x is null || y is null) throw new CryptographicException("Invalid PEM file"); // Assemble into uncompressed SEC1 format byte[] pemKeyBytes = new byte[1 + x.Length + y.Length]; pemKeyBytes[0] = 0x04; // uncompressed marker Buffer.BlockCopy(x, 0, pemKeyBytes, 1, x.Length); Buffer.BlockCopy(y, 0, pemKeyBytes, 1 + x.Length, y.Length); // Compare bool match = bytes.SequenceEqual(pemKeyBytes); return Result.Success(match); } catch (Exception e) { logger.LogError(e, "Error while checking application registration"); return Result.Fail(e); } } public string GetAplicationAuthorizationURL() { //https://auth.tesla.com/oauth2/v3/authorize?&client_id=$CLIENT_ID&locale=en-US&prompt=login&redirect_uri=$REDIRECT_URI&response_type=code&scope=openid%20vehicle_device_data%20offline_access&state=$STATE StringBuilder sb = new StringBuilder(); sb.Append("https://auth.tesla.com/oauth2/v3/authorize?response_type=code"); sb.AppendFormat("&client_id={0}", this.configuration.ClientID); sb.AppendFormat("&redirect_uri={0}", "https://automatic-parking.app/token-exchange"); sb.AppendFormat("&scope=openid offline_access vehicle_device_data vehicle_location"); sb.AppendFormat("&state={0}", ""); sb.AppendFormat("&nonce={0}", ""); sb.AppendFormat("&prompt_missing_scopes=true"); sb.AppendFormat("&require_requested_scopes=true"); sb.AppendFormat("&show_keypair_step=true"); return sb.ToString(); } } 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; } = "automatic-parking.app"; }