Configures Tesla OpenID Connect authentication
All checks were successful
Build, Push and Run Container / build (push) Successful in 24s

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.
This commit is contained in:
2025-08-16 22:01:32 +02:00
parent a7ea7ff632
commit 31f823b51f
3 changed files with 105 additions and 24 deletions

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using ProofOfConcept.Models; using ProofOfConcept.Models;
using ProofOfConcept.Services; using ProofOfConcept.Services;
using ProofOfConcept.Utilities; using ProofOfConcept.Utilities;
@@ -25,29 +26,61 @@ builder.Services.AddHttpClient();
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddHealthChecks() builder.Services.AddHealthChecks()
.AddAsyncCheck("", cancellationToken => Task.FromResult(HealthCheckResult.Healthy()), ["ready"]); //TODO: Check tag .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; OnAuthorizationCodeReceived = ctx =>
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; {
}) if (ctx.TokenEndpointRequest is not null)
.AddCookie() ctx.TokenEndpointRequest.Parameters["audience"] = FleetApiAudience;
.AddOpenIdConnect(options =>
{ return Task.CompletedTask;
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
});
// Add own services // Add own services
builder.Services.AddSingleton<IMessageProcessor, MessageProcessor>(); builder.Services.AddSingleton<IMessageProcessor, MessageProcessor>();
@@ -80,8 +113,25 @@ if (app.Environment.IsDevelopment())
}); });
app.MapGet("/CheckRegisteredApplication", ([FromServices] TeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync()); app.MapGet("/CheckRegisteredApplication", ([FromServices] TeslaAuthenticatorService service) => service.CheckApplicationRegistrationAsync());
app.MapGet("/RegisterApplication", ([FromServices] TeslaAuthenticatorService service) => service.RegisterApplicationAsync()); 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("/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 //Map static assets

View File

@@ -1,4 +1,4 @@
namespace ProofOfConcept; namespace ProofOfConcept.Utilities;
public class Configurator public class Configurator
{ {

View File

@@ -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<OpenIdConnectConfiguration>
{
private readonly IConfigurationManager<OpenIdConnectConfiguration> _inner;
private readonly string _tokenEndpointOverride;
// No HttpClient/ServiceProvider needed — uses default retriever internally
public TeslaOIDCConfigurationManager(string metadataAddress, string tokenEndpointOverride)
{
_tokenEndpointOverride = tokenEndpointOverride;
_inner = new ConfigurationManager<OpenIdConnectConfiguration>(
metadataAddress,
new OpenIdConnectConfigurationRetriever());
}
public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken cancel)
{
var cfg = await _inner.GetConfigurationAsync(cancel).ConfigureAwait(false);
cfg.TokenEndpoint = _tokenEndpointOverride; // <-- required by Tesla
return cfg;
}
public void RequestRefresh() => _inner.RequestRefresh();
}