All checks were successful
Build, Push and Run Container / build (push) Successful in 24s
Adds multiple valid issuers to the authentication configuration. This ensures compatibility with different Tesla authentication endpoints, including fleet and regional variations.
194 lines
9.0 KiB
C#
194 lines
9.0 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;
|
|
|
|
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 =>
|
|
{
|
|
const string TeslaAuthority = "https://auth.tesla.com/oauth2/v3";
|
|
const string TeslaMetadataEndpoint = $"{TeslaAuthority}/.well-known/openid-configuration";
|
|
const string FleetAuthTokenEndpoint = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token";
|
|
const string FleetApiAudience = "https://fleet-api.prd.eu.vn.cloud.tesla.com";
|
|
|
|
// Let the middleware do discovery/JWKS (on demand), but override token endpoint
|
|
o.ConfigurationManager = new TeslaOIDCConfigurationManager(TeslaMetadataEndpoint, FleetAuthTokenEndpoint);
|
|
|
|
// Standard OIDC settings
|
|
o.Authority = TeslaAuthority; // discovery + /authorize
|
|
o.ClientId = "b2240ee4-332a-4252-91aa-bbcc24f78fdb";
|
|
o.ClientSecret = "ta-secret.YG+XSdlvr6Lv8U-x";
|
|
o.ResponseType = OpenIdConnectResponseType.Code;
|
|
o.UsePkce = true;
|
|
o.SaveTokens = true;
|
|
|
|
// This must match exactly what you register at Tesla
|
|
o.CallbackPath = new PathString("/token-exchange");
|
|
|
|
o.TokenValidationParameters.ValidIssuers = ["https://fleet-auth.tesla.com/oauth2/v3/nts", "https://auth.tesla.com/oauth2/v3", "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/nts"];
|
|
|
|
// Scopes you actually need
|
|
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 parameters
|
|
o.AdditionalAuthorizationParameters.Add("prompt_missing_scopes", "true");
|
|
o.AdditionalAuthorizationParameters.Add("require_requested_scopes", "true");
|
|
o.AdditionalAuthorizationParameters.Add("show_keypair_step", "true");
|
|
|
|
// If keys rotate during runtime, auto-refresh JWKS
|
|
o.RefreshOnIssuerKeyNotFound = true;
|
|
|
|
// Add Tesla's required audience to the token request
|
|
o.Events = new OpenIdConnectEvents
|
|
{
|
|
OnAuthorizationCodeReceived = ctx =>
|
|
{
|
|
if (ctx.TokenEndpointRequest is not null)
|
|
ctx.TokenEndpointRequest.Parameters["audience"] = FleetApiAudience;
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
});
|
|
|
|
// 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(); |