Adds application authorization endpoint
All checks were successful
Build, Push and Run Container / build (push) Successful in 32s

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.
This commit is contained in:
2025-08-13 22:29:48 +02:00
parent 1b8fda32d9
commit 8c801c88ce
3 changed files with 71 additions and 7 deletions

View File

@@ -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

View File

@@ -31,7 +31,7 @@
<ItemGroup>
<None Update="Resources\Signature\public-key.pem">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

View File

@@ -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);
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);
return new Result<bool>();
//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();
}
}