From 8c801c88cea9d0b91327aab897e7b95db7a31f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szak=C3=A1ts=20Alp=C3=A1r=20Zsolt?= Date: Wed, 13 Aug 2025 22:29:48 +0200 Subject: [PATCH] Adds application authorization endpoint Implements the /Authorize endpoint to redirect users to the Tesla authentication page. This allows users to grant the application permission to access their Tesla account data. Updates the public key resource to be copied on build, ensuring it is always available at runtime. Adds logic to validate the application registration by comparing the public key retrieved from the Tesla API with the public key stored locally. --- Source/ProofOfConcept/Program.cs | 1 + Source/ProofOfConcept/ProofOfConcept.csproj | 2 +- .../Services/TeslaAuthenticatorService.cs | 75 +++++++++++++++++-- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/Source/ProofOfConcept/Program.cs b/Source/ProofOfConcept/Program.cs index 6aae7a5..aad6a6f 100644 --- a/Source/ProofOfConcept/Program.cs +++ b/Source/ProofOfConcept/Program.cs @@ -51,6 +51,7 @@ 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())); } //Map static assets diff --git a/Source/ProofOfConcept/ProofOfConcept.csproj b/Source/ProofOfConcept/ProofOfConcept.csproj index 5e55dab..d7a56a2 100644 --- a/Source/ProofOfConcept/ProofOfConcept.csproj +++ b/Source/ProofOfConcept/ProofOfConcept.csproj @@ -31,7 +31,7 @@ - Never + PreserveNewest diff --git a/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs b/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs index faecc32..81b4224 100644 --- a/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs +++ b/Source/ProofOfConcept/Services/TeslaAuthenticatorService.cs @@ -1,4 +1,6 @@ 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; @@ -220,12 +222,73 @@ public class TeslaAuthenticatorService 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(); + 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}"); + sb.AppendFormat("&scope=openid offline_access vehicle_device_data vehicle_location"); + sb.AppendFormat("&state=1234567890"); + sb.AppendFormat("&nonce=1234567890"); + sb.AppendFormat("&prompt_missing_scopes=true"); + sb.AppendFormat("&require_requested_scopes=true"); + sb.AppendFormat("&show_keypair_step=true"); + return sb.ToString(); } }