This commit is contained in:
153
Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs
Normal file
153
Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
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<TeslaAuthenticatorService> 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<TeslaAuthenticatorService> logger, IOptions<TeslaAuthenticatorServiceConfiguration> 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.
|
||||
/// <return>Returns a string containing the authentication token received from the API.</return>
|
||||
/// <exception cref="HttpRequestException">Thrown when the HTTP request to the API fails.</exception>
|
||||
public async Task<Token> 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<string, string> formData = new Dictionary<string, string>
|
||||
{
|
||||
{ "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<Token>(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.
|
||||
/// <return>Returns a Token object containing the authentication token and its expiration details.</return>
|
||||
/// <exception cref="HttpRequestException">Thrown when the HTTP request to acquire a new partner token fails.</exception>
|
||||
/// <exception cref="Exception">Thrown for any unexpected errors during the token retrieval or caching process.</exception>
|
||||
public async Task<Token> 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<Result> 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<Result<bool>> 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<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user