using System.Text.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using ProofOfConcept.Models; using ProofOfConcept.Services; using ProofOfConcept.Utilities; Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; var builder = WebApplication.CreateSlimBuilder(args); // Load static web assets manifest (referenced libs + your wwwroot) builder.WebHost.UseStaticWebAssets(); // builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); }); // Add services builder.Services.AddOpenApi(); builder.Services.AddMediator(); builder.Services.AddMemoryCache(); builder.Services.AddHybridCache(); builder.Services.AddRazorPages(); builder.Services.AddHealthChecks() .AddAsyncCheck("", cancellationToken => Task.FromResult(HealthCheckResult.Healthy()), ["ready"]); //TODO: Check tag builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpClient().AddHttpClient("InsecureClient") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }); builder.Services .AddAuthentication(o => { o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(o => { // Point directly at the third-party metadata // Metadata is wrong... it sets non-existing uris like: "jwks_uri": "https://fleet-auth.tesla.com/oauth2/v3/discovery/thirdparty/keys" // o.MetadataAddress = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/thirdparty/.well-known/openid-configuration"; // // // === Use Fleet-Auth third-party OIDC config === // o.Authority = "https://fleet-auth.tesla.com/oauth2/v3/nts"; // // o.Configuration ??= new OpenIdConnectConfiguration(); // o.Configuration.AuthorizationEndpoint = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/authorize"; // o.Configuration.TokenEndpoint = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token"; // o.Configuration.JwksUri = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/discovery/thirdparty/keys"; // o.Configuration.EndSessionEndpoint = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/logout"; // o.Configuration.UserInfoEndpoint = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/userinfo"; // // o.Configuration.TokenEndpointAuthMethodsSupported.Clear(); // o.Configuration.TokenEndpointAuthMethodsSupported.Add("client_secret_post"); // // o.Configuration.ResponseModesSupported.Clear(); // o.Configuration.ResponseModesSupported.Add("query"); // // o.Configuration.GrantTypesSupported.Clear(); // o.Configuration.GrantTypesSupported.Add("authorization_code"); // // o.Configuration.SubjectTypesSupported.Clear(); // o.Configuration.SubjectTypesSupported.Add("public"); // // o.Configuration.ScopesSupported.Clear(); // o.Configuration.ScopesSupported.Add("openid"); // o.Configuration.ScopesSupported.Add("email"); // o.Configuration.ScopesSupported.Add("profile"); // o.Configuration.ScopesSupported.Add("metadata"); // // o.Configuration.IdTokenSigningAlgValuesSupported.Clear(); // o.Configuration.IdTokenSigningAlgValuesSupported.Add("RS256"); // // o.Configuration.TokenEndpointAuthSigningAlgValuesSupported.Clear(); // o.Configuration.TokenEndpointAuthSigningAlgValuesSupported.Add("RS256"); // // o.Configuration.ClaimsSupported.Clear(); // o.Configuration.ClaimsSupported.Add("iss"); // o.Configuration.ClaimsSupported.Add("iat"); // o.Configuration.ClaimsSupported.Add("exp"); // o.Configuration.ClaimsSupported.Add("nonce"); // o.Configuration.ClaimsSupported.Add("sub"); // o.Configuration.ClaimsSupported.Add("aud"); o.ConfigurationManager = new TeslaOIDCConfigurationManager("https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/thirdparty/.well-known/openid-configuration"); // Standard OIDC web app settings o.ResponseType = "code"; o.UsePkce = true; o.SaveTokens = true; o.ClientId = "b2240ee4-332a-4252-91aa-bbcc24f78fdb"; o.ClientSecret = "ta-secret.YG+XSdlvr6Lv8U-x"; // Must exactly match what you registered in Tesla portal o.CallbackPath = new PathString("/token-exchange"); // Set scopes o.Scope.Clear(); o.Scope.Add("openid"); o.Scope.Add("offline_access"); o.Scope.Add("vehicle_device_data"); o.Scope.Add("vehicle_location"); // Optional Tesla flags o.AdditionalAuthorizationParameters.Add("require_requested_scopes", "true"); o.AdditionalAuthorizationParameters.Add("show_keypair_step", "true"); o.AdditionalAuthorizationParameters.Add("prompt_missing_scopes", "true"); o.TokenValidationParameters.ValidateIssuer = true; o.TokenValidationParameters.ValidIssuer = "https://fleet-auth.tesla.com/oauth2/v3/nts"; // ✅ Add the Fleet API audience to the token POST const string FleetApiAudience = "https://fleet-api.prd.eu.vn.cloud.tesla.com"; // set your region base o.Events = new OpenIdConnectEvents { OnAuthorizationCodeReceived = ctx => { ctx.TokenEndpointRequest.Parameters["audience"] = FleetApiAudience; return Task.CompletedTask; } }; // Auto-refresh keys if Tesla rotates JWKS o.RefreshOnIssuerKeyNotFound = true; }); // Add own services builder.Services.AddSingleton(); builder.Services.AddTransient(); // Add hosted services builder.Services.AddHostedService(); builder.Services.AddHostedService(); //Build app WebApplication app = builder.Build(); ForwardedHeadersOptions forwardedHeadersOptions = new ForwardedHeadersOptions() { ForwardedHeaders = ForwardedHeaders.All }; forwardedHeadersOptions.KnownNetworks.Clear(); forwardedHeadersOptions.KnownProxies.Clear(); app.UseForwardedHeaders(forwardedHeadersOptions); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapGet("/GetPartnerAuthenticationToken", ([FromServices] TeslaAuthenticatorService service) => service.GetPartnerAuthenticationTokenAsync()); app.MapGet("/PartnerToken", ([FromQueryAttribute] string json, [FromServices] IMemoryCache memoryCache) => { var serializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; Token? token = JsonSerializer.Deserialize(json, serializerOptions); if (token is not null) memoryCache.Set(Keys.TeslaPartnerToken, token, token.Expires.Subtract(TimeSpan.FromSeconds(5))); return JsonSerializer.Serialize(token, new JsonSerializerOptions() { WriteIndented = true }); }); app.MapGet("/CheckRegisteredApplication", ([FromServices] TeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync()); app.MapGet("/RegisterApplication", ([FromServices] TeslaAuthenticatorService service) => service.RegisterApplicationAsync()); app.MapGet("/Authorize", async (IHttpContextAccessor contextAccessor) => await (contextAccessor.HttpContext!).ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/Tesla" })); app.MapGet("/KeyPairing", () => Results.Redirect("https://tesla.com/_ak/developer-domain.com")); app.MapGet("/Tokens", async (IHttpContextAccessor httpContextAccessor) => { var ctx = httpContextAccessor.HttpContext; var accessToken = await ctx.GetTokenAsync("access_token"); var idToken = await ctx.GetTokenAsync("id_token"); var refreshToken = await ctx.GetTokenAsync("refresh_token"); var expiresAtRaw = await ctx.GetTokenAsync("expires_at"); // ISO 8601 string return JsonSerializer.Serialize(new { AccessToken = accessToken, IDToken = idToken, RefreshToken = refreshToken, ExpiresAtRaw = expiresAtRaw }); }); app.MapGet("DebugProxy", (IHttpContextAccessor httpContextAccessor) => { var ctx = httpContextAccessor.HttpContext!; var request = ctx.Request; Dictionary headers = new Dictionary(); headers.Add("Host", request.Host.Value ?? ""); headers.Add("Scheme", request.Scheme); headers.Add("Method", request.Method); headers.Add("Path", request.Path.Value ?? ""); headers.Add("QueryString", request.QueryString.Value ?? ""); headers.Add("RemoteIpAddress", ctx.Connection.RemoteIpAddress?.ToString() ?? ""); headers.Add("RemotePort", ctx.Connection.RemotePort.ToString()); headers.Add("LocalIpAddress", ctx.Connection.LocalIpAddress?.ToString() ?? ""); headers.Add("LocalPort", ctx.Connection.LocalPort.ToString()); headers.Add("IsHttps", request.IsHttps.ToString()); headers.Add("X-Forwarded-For", request.Headers["X-Forwarded-For"].ToString()); headers.Add("X-Forwarded-Proto", request.Headers["X-Forwarded-Proto"].ToString()); headers.Add("X-Forwarded-Host", request.Headers["X-Forwarded-Host"].ToString()); headers.Add("X-Forwarded-Port", request.Headers["X-Forwarded-Port"].ToString()); headers.Add("X-Forwarded-Prefix", request.Headers["X-Forwarded-Prefix"].ToString()); headers.Add("X-Forwarded-Server", request.Headers["X-Forwarded-Server"].ToString()); headers.Add("X-Forwarded-Path", request.Headers["X-Forwarded-Path"].ToString()); headers.Add("X-Forwarded-PathBase", request.Headers["X-Forwarded-PathBase"].ToString()); headers.Add("X-Forwarded-Query", request.Headers["X-Forwarded-Query"].ToString()); headers.Add("X-Forwarded-Query-String", request.Headers["X-Forwarded-Query-String"].ToString()); headers.Add("Connection", request.Headers["Connection"].ToString()); headers.Add("Accept", request.Headers["Accept"].ToString()); headers.Add("Accept-Encoding", request.Headers["Accept-Encoding"].ToString()); headers.Add("Accept-Language", request.Headers["Accept-Language"].ToString()); headers.Add("Cache-Control", request.Headers["Cache-Control"].ToString()); headers.Add("Content-Length", request.Headers["Content-Length"].ToString()); headers.Add("Content-Type", request.Headers["Content-Type"].ToString()); headers.Add("Cookie", request.Headers["Cookie"].ToString()); headers.Add("Pragma", request.Headers["Pragma"].ToString()); headers.Add("Referer", request.Headers["Referer"].ToString()); String json = JsonSerializer.Serialize(headers, new JsonSerializerOptions() { WriteIndented = true }); return json; }); app.MapGet("/Tesla", async ([FromServices] ILogger logger, [FromServices] IHttpContextAccessor httpContextAccessor, [FromServices] IHttpClientFactory httpClientFactory) => { HttpContext? context = httpContextAccessor.HttpContext; if (context is null) return Results.BadRequest(); string? access_token = await context.GetTokenAsync("access_token"); string? refresh_token = await context.GetTokenAsync("refresh_token"); logger.LogCritical("User has access_token: {access_token} and refresh_token: {refresh_token}", access_token, refresh_token); if (String.IsNullOrEmpty(access_token)) return Results.LocalRedirect("/Authorize"); HttpClient client = httpClientFactory.CreateClient("InsecureClient"); client.BaseAddress = new Uri("https://tesla_command_proxy"); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {access_token}"); //Get cars VehiclesEnvelope? vehiclesEnvelope = await client.GetFromJsonAsync("/api/1/vehicles"); string[] vins = vehiclesEnvelope?.Response.Select(x => x.Vin ?? "").Where(v => !String.IsNullOrWhiteSpace(v)).ToArray() ?? Array.Empty(); logger.LogCritical("User has access to {count} cars: {vins}", vins.Length, String.Join(", ", vins)); if (vins.Length == 0) return Results.Ok("No cars found"); //Get CA from validate server file string fileContent = await File.ReadAllTextAsync("Resources/validate_server.json"); ValidationModel? vm = JsonSerializer.Deserialize(fileContent); TelemetryConfigRequest configRequest = new TelemetryConfigRequest() { Vins = new List(vins), Config = new TelemetryConfig() { Hostname = "tesla-connector.automatic-parking.app", Port = 443, CertificateAuthority = vm?.CA ?? "EMPTY", Fields = new Dictionary() { { "Gear", new TelemetryFieldConfig() { IntervalSeconds = 60 } }, { "Locked", new TelemetryFieldConfig() { IntervalSeconds = 60 } }, { "DriverSeatOccupied", new TelemetryFieldConfig() { IntervalSeconds = 60 } }, { "GpsState", new TelemetryFieldConfig() { IntervalSeconds = 60 } }, { "Location", new TelemetryFieldConfig() { IntervalSeconds = 60 } }, } } }; logger.LogInformation("Config request: {configRequest}", JsonSerializer.Serialize(configRequest, new JsonSerializerOptions() { WriteIndented = true })); client.BaseAddress = new Uri("https://fleet-api.prd.eu.vn.cloud.tesla.com"); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {access_token}"); client.DefaultRequestHeaders.Add("X-Tesla-User-Agent", "Tesla-Connector/1.0.0"); HttpResponseMessage response = await client.PostAsJsonAsync("/api/1/vehicles/fleet_telemetry_config", configRequest); return Results.Ok(response.Content.ReadAsStringAsync()); }); } //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();