From a7ea7ff6320a528187129530ef9c45957874a34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szak=C3=A1ts=20Alp=C3=A1r=20Zsolt?= Date: Sat, 16 Aug 2025 20:40:27 +0200 Subject: [PATCH] Add authorization and key-pairing --- Source/ProofOfConcept/Configurator.cs | 19 ++++++++++ Source/ProofOfConcept/Program.cs | 36 +++++++++++++++++-- Source/ProofOfConcept/ProofOfConcept.csproj | 1 + .../Services/TeslaAuthenticatorService.cs | 31 +++++++++++++--- 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 Source/ProofOfConcept/Configurator.cs diff --git a/Source/ProofOfConcept/Configurator.cs b/Source/ProofOfConcept/Configurator.cs new file mode 100644 index 0000000..3e8ce39 --- /dev/null +++ b/Source/ProofOfConcept/Configurator.cs @@ -0,0 +1,19 @@ +namespace ProofOfConcept; + +public class Configurator +{ + private readonly IHostEnvironment environment; + private readonly IConfiguration configuration; + + public Configurator(IHostEnvironment environment, IConfiguration configuration) + { + this.environment = environment; + this.configuration = configuration; + } + + +} + +public static class ConfiguratorExtensions +{ +} \ No newline at end of file diff --git a/Source/ProofOfConcept/Program.cs b/Source/ProofOfConcept/Program.cs index aad6a6f..0c8d9f1 100644 --- a/Source/ProofOfConcept/Program.cs +++ b/Source/ProofOfConcept/Program.cs @@ -1,6 +1,10 @@ using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Diagnostics.HealthChecks; using ProofOfConcept.Models; using ProofOfConcept.Services; using ProofOfConcept.Utilities; @@ -19,10 +23,35 @@ builder.Services.AddMemoryCache(); builder.Services.AddHybridCache(); builder.Services.AddHttpClient(); builder.Services.AddRazorPages(); +builder.Services.AddHealthChecks() + .AddAsyncCheck("", cancellationToken => Task.FromResult(HealthCheckResult.Healthy()), ["ready"]); //TODO: Check tag +builder.Services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(options => + { + options.Authority = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3"; // Tesla auth + options.ClientId = "b2240ee4-332a-4252-91aa-bbcc24f78fdb"; + options.ClientSecret = "ta-secret.YG+XSdlvr6Lv8U-x"; + options.ResponseType = "code"; + options.SaveTokens = true; // access_token, refresh_token in auth ticket + options.CallbackPath = new PathString("/token-exchange"); + options.Scope.Add("openid"); + options.Scope.Add("offline_access"); + options.Scope.Add("vehicle_device_data"); + options.Scope.Add("vehicle_location"); + options.AdditionalAuthorizationParameters.Add("prompt_missing_scopes", "true"); + options.AdditionalAuthorizationParameters.Add("require_requested_scopes", "true"); + options.AdditionalAuthorizationParameters.Add("show_keypair_step", "true"); + // PKCE, state, nonce are handled automatically + }); // Add own services builder.Services.AddSingleton(); -builder.Services.AddTransient(); +builder.Services.AddTransient(); // Add hosted services builder.Services.AddHostedService(); @@ -51,12 +80,15 @@ if (app.Environment.IsDevelopment()) }); app.MapGet("/CheckRegisteredApplication", ([FromServices] TeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync()); app.MapGet("/RegisterApplication", ([FromServices] TeslaAuthenticatorService service) => service.RegisterApplicationAsync()); - app.MapGet("/Authorize", ([FromServices] TeslaAuthenticatorService service) => new RedirectResult(service.GetAplicationAuthorizationURL())); + app.MapGet("/Authorize", (async context => await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }))); + app.MapGet("/KeyPairing", () => Results.Redirect("https://tesla.com/_ak/developer-domain.com")); } //Map static assets app.MapStaticAssets(); +//TODO: Build a middleware that responds with 503 if the public key is not registered at Tesla + app.MapRazorPages(); app.Run(); \ No newline at end of file diff --git a/Source/ProofOfConcept/ProofOfConcept.csproj b/Source/ProofOfConcept/ProofOfConcept.csproj index d7a56a2..a216424 100644 --- a/Source/ProofOfConcept/ProofOfConcept.csproj +++ b/Source/ProofOfConcept/ProofOfConcept.csproj @@ -16,6 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs b/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs index 81b4224..0ff1d86 100644 --- a/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs +++ b/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs @@ -11,7 +11,30 @@ using SzakatsA.Result; namespace ProofOfConcept.Services; -public class TeslaAuthenticatorService +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; @@ -281,10 +304,10 @@ public class TeslaAuthenticatorService 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}"); + 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=1234567890"); - sb.AppendFormat("&nonce=1234567890"); + 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");