From 31f823b51ff1c7b16e1dc4f8e6ccddd10b3c5510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szak=C3=A1ts=20Alp=C3=A1r=20Zsolt?= Date: Sat, 16 Aug 2025 22:01:32 +0200 Subject: [PATCH] Configures Tesla OpenID Connect authentication Implements authentication against the Tesla Fleet API using OpenID Connect. Uses a custom OIDC configuration manager to override the token endpoint. Configures authentication services and adds required scopes and parameters. Adds endpoints for application registration and token retrieval during development. --- Source/ProofOfConcept/Program.cs | 96 ++++++++++++++----- .../{ => Utilities}/Configurator.cs | 2 +- .../TeslaOIDCConfigurationManager.cs | 31 ++++++ 3 files changed, 105 insertions(+), 24 deletions(-) rename Source/ProofOfConcept/{ => Utilities}/Configurator.cs (91%) create mode 100644 Source/ProofOfConcept/Utilities/TeslaOIDCConfigurationManager.cs diff --git a/Source/ProofOfConcept/Program.cs b/Source/ProofOfConcept/Program.cs index 0c8d9f1..6547960 100644 --- a/Source/ProofOfConcept/Program.cs +++ b/Source/ProofOfConcept/Program.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect; 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; @@ -25,29 +26,61 @@ builder.Services.AddHttpClient(); builder.Services.AddRazorPages(); builder.Services.AddHealthChecks() .AddAsyncCheck("", cancellationToken => Task.FromResult(HealthCheckResult.Healthy()), ["ready"]); //TODO: Check tag -builder.Services.AddAuthentication(options => + +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("https://automatic-parking.app/token-exchange"); + + // 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 { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; - }) - .AddCookie() - .AddOpenIdConnect(options => - { - options.Authority = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3"; // Tesla auth - options.ClientId = "b2240ee4-332a-4252-91aa-bbcc24f78fdb"; - options.ClientSecret = "ta-secret.YG+XSdlvr6Lv8U-x"; - options.ResponseType = "code"; - options.SaveTokens = true; // access_token, refresh_token in auth ticket - options.CallbackPath = new PathString("/token-exchange"); - options.Scope.Add("openid"); - options.Scope.Add("offline_access"); - options.Scope.Add("vehicle_device_data"); - options.Scope.Add("vehicle_location"); - options.AdditionalAuthorizationParameters.Add("prompt_missing_scopes", "true"); - options.AdditionalAuthorizationParameters.Add("require_requested_scopes", "true"); - options.AdditionalAuthorizationParameters.Add("show_keypair_step", "true"); - // PKCE, state, nonce are handled automatically - }); + OnAuthorizationCodeReceived = ctx => + { + if (ctx.TokenEndpointRequest is not null) + ctx.TokenEndpointRequest.Parameters["audience"] = FleetApiAudience; + + return Task.CompletedTask; + } + }; +}); // Add own services builder.Services.AddSingleton(); @@ -80,8 +113,25 @@ if (app.Environment.IsDevelopment()) }); app.MapGet("/CheckRegisteredApplication", ([FromServices] TeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync()); app.MapGet("/RegisterApplication", ([FromServices] TeslaAuthenticatorService service) => service.RegisterApplicationAsync()); - app.MapGet("/Authorize", (async context => await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }))); + 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 + }); + }); } //Map static assets diff --git a/Source/ProofOfConcept/Configurator.cs b/Source/ProofOfConcept/Utilities/Configurator.cs similarity index 91% rename from Source/ProofOfConcept/Configurator.cs rename to Source/ProofOfConcept/Utilities/Configurator.cs index 3e8ce39..3121295 100644 --- a/Source/ProofOfConcept/Configurator.cs +++ b/Source/ProofOfConcept/Utilities/Configurator.cs @@ -1,4 +1,4 @@ -namespace ProofOfConcept; +namespace ProofOfConcept.Utilities; public class Configurator { diff --git a/Source/ProofOfConcept/Utilities/TeslaOIDCConfigurationManager.cs b/Source/ProofOfConcept/Utilities/TeslaOIDCConfigurationManager.cs new file mode 100644 index 0000000..4c3f4c2 --- /dev/null +++ b/Source/ProofOfConcept/Utilities/TeslaOIDCConfigurationManager.cs @@ -0,0 +1,31 @@ +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace ProofOfConcept.Utilities; + +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +public sealed class TeslaOIDCConfigurationManager : IConfigurationManager +{ + private readonly IConfigurationManager _inner; + private readonly string _tokenEndpointOverride; + + // No HttpClient/ServiceProvider needed — uses default retriever internally + public TeslaOIDCConfigurationManager(string metadataAddress, string tokenEndpointOverride) + { + _tokenEndpointOverride = tokenEndpointOverride; + _inner = new ConfigurationManager( + metadataAddress, + new OpenIdConnectConfigurationRetriever()); + } + + public async Task GetConfigurationAsync(CancellationToken cancel) + { + var cfg = await _inner.GetConfigurationAsync(cancel).ConfigureAwait(false); + cfg.TokenEndpoint = _tokenEndpointOverride; // <-- required by Tesla + return cfg; + } + + public void RequestRefresh() => _inner.RequestRefresh(); +} \ No newline at end of file