Files
Automatic-Parking/Source/ProofOfConcept/Program.cs
Szakáts Alpár Zsolt 890162394d
All checks were successful
Build, Push and Run Container / build (push) Successful in 23s
Configures Tesla OIDC endpoints dynamically
Configures the Tesla OIDC endpoints using the base URL
to dynamically set authorization, token, JWKS URI, end
session, and user info endpoints. This eliminates the need
for a URL override function and centralizes the endpoint
configuration logic.
2025-08-16 23:37:14 +02:00

200 lines
9.5 KiB
C#

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.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.AddHttpClient();
builder.Services.AddRazorPages();
builder.Services.AddHealthChecks()
.AddAsyncCheck("", cancellationToken => Task.FromResult(HealthCheckResult.Healthy()), ["ready"]); //TODO: Check tag
builder.Services.AddHttpContextAccessor();
builder.Services
.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(o =>
{
// Point directly at the third-party metadata
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.ConfigurationManager = new TeslaOIDCConfigurationManager(o.MetadataAddress);
// Standard OIDC web app settings
o.ResponseType = OpenIdConnectResponseType.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");
// ✅ 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<IMessageProcessor, MessageProcessor>();
builder.Services.AddTransient<ITeslaAuthenticatorService, TeslaAuthenticatorService>();
// Add hosted services
builder.Services.AddHostedService<MQTTServer>();
builder.Services.AddHostedService<MQTTClient>();
//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<Token>(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 = "/tokens" }));
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
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<string, string> headers = new Dictionary<string, string>();
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;
});
}
//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();