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,28 +26,60 @@ 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 =>
{ {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}) })
.AddCookie() .AddCookie()
.AddOpenIdConnect(options => .AddOpenIdConnect(o =>
{ {
options.Authority = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3"; // Tesla auth const string TeslaAuthority = "https://auth.tesla.com/oauth2/v3";
options.ClientId = "b2240ee4-332a-4252-91aa-bbcc24f78fdb"; const string TeslaMetadataEndpoint = $"{TeslaAuthority}/.well-known/openid-configuration";
options.ClientSecret = "ta-secret.YG+XSdlvr6Lv8U-x"; const string FleetAuthTokenEndpoint = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token";
options.ResponseType = "code"; const string FleetApiAudience = "https://fleet-api.prd.eu.vn.cloud.tesla.com";
options.SaveTokens = true; // access_token, refresh_token in auth ticket
options.CallbackPath = new PathString("/token-exchange"); // Let the middleware do discovery/JWKS (on demand), but override token endpoint
options.Scope.Add("openid"); o.ConfigurationManager = new TeslaOIDCConfigurationManager(TeslaMetadataEndpoint, FleetAuthTokenEndpoint);
options.Scope.Add("offline_access");
options.Scope.Add("vehicle_device_data"); // Standard OIDC settings
options.Scope.Add("vehicle_location"); o.Authority = TeslaAuthority; // discovery + /authorize
options.AdditionalAuthorizationParameters.Add("prompt_missing_scopes", "true"); o.ClientId = "b2240ee4-332a-4252-91aa-bbcc24f78fdb";
options.AdditionalAuthorizationParameters.Add("require_requested_scopes", "true"); o.ClientSecret = "ta-secret.YG+XSdlvr6Lv8U-x";
options.AdditionalAuthorizationParameters.Add("show_keypair_step", "true"); o.ResponseType = OpenIdConnectResponseType.Code;
// PKCE, state, nonce are handled automatically 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
{
OnAuthorizationCodeReceived = ctx =>
{
if (ctx.TokenEndpointRequest is not null)
ctx.TokenEndpointRequest.Parameters["audience"] = FleetApiAudience;
return Task.CompletedTask;
}
};
}); });
// Add own services // Add own services
@@ -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();
}