> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jtl-software.com/llms.txt
> Use this file to discover all available pages before exploring further.

# C#

> Build the ASP.NET Core backend for your JTL Cloud App. Verify session tokens, fetch access tokens, and proxy requests to the JTL Cloud API.

Build the ASP.NET Core backend for a JTL Cloud App using .NET 8 and `NSec.Cryptography` for Ed25519 signature verification. By the end, the backend runs locally and the frontend's "Waiting for backend" placeholder turns into a real connection.

**Stack:** .NET 8, ASP.NET Core Web API, `NSec.Cryptography` for JWT verification.

## Prerequisites

You need:

* ✅ **A finished frontend** from the [Build the Frontend](/get-started/quick-start/from-scratch/frontend) page, running locally on `http://localhost:5173`
* ✅ **[.NET SDK](https://dotnet.microsoft.com/download) 8.0 or higher**. Run `dotnet --version` to check.

## What you're Building

During setup, your backend verifies that the session token from your frontend is valid and was issued by JTL.

To do this:

* Your backend authenticates with JTL using its client credentials
* Fetches JTL's public keys (JWKS)
* And uses them to verify the session token's signature

```mermaid theme={null}
sequenceDiagram
    participant FE as Your Frontend
    participant BE as Your Backend (ASP.NET Core)
    participant Auth as JTL Auth
    participant API as JTL Cloud API

    FE->>BE: GET /api/connect-tenant (X-Session-ID: token)
    BE->>Auth: POST /oauth2/token (client credentials)
    Auth-->>BE: Access token (JWT)
    BE->>API: GET /.well-known/jwks.json (with access token)
    API-->>BE: Public keys (JWKS)
    BE->>BE: Verify session token signature
    BE-->>FE: Verified tenant info
```

Once verified, the token tells your backend which tenant (merchant) and user is using your app. Your backend can then make tenant-scoped requests to the JTL Cloud API on their behalf.

## 1. Set up the Project

Create a `Backend` folder alongside the existing `frontend` folder, then scaffold a Web API project inside it.

```bash theme={null}
cd my-jtl-app
dotnet new webapi -n Backend
```

Your project structure now looks like this:

```
my-jtl-app/
├── frontend/      # From the previous page
└── Backend/       # New
```

The `dotnet new webapi` template includes a sample `WeatherForecast` controller and model. You can leave these in place or delete them. They won't affect anything you build in this guide.

## 2. Install Packages

```bash theme={null}
cd Backend
dotnet add package NSec.Cryptography
```

| Package             | Purpose                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| `NSec.Cryptography` | Cryptographic primitives for Ed25519 signature verification, used to validate JTL session tokens |

## 3. Set up Environment Variables

ASP.NET Core uses `appsettings.json` for configuration. For local secrets, use the .NET user-secrets tool so credentials never end up in source control.

From the `Backend/` directory:

```bash theme={null}
dotnet user-secrets init
dotnet user-secrets set "Jtl:ClientId" "your-client-id-here"
dotnet user-secrets set "Jtl:ClientSecret" "your-client-secret-here"
```

The values are placeholders for now. You'll get real values from the [Partner Portal](https://partner.jtl-cloud.com/) in the next page and update them with the same `dotnet user-secrets set` commands.

For production, swap user-secrets for environment variables, Azure Key Vault, or whichever secrets manager fits your hosting platform.

## 4. Build the Auth Service

The first piece of the backend is a service that authenticates with JTL using your client credentials and returns an access token. This token has two uses: fetching the public keys for session token verification, and making tenant-scoped calls to the JTL Cloud API.

Create `Backend/Services/JtlAuthService.cs`:

```csharp theme={null}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace Backend.Services;

public class JtlAuthService
{
    private readonly HttpClient _http;
    private readonly IConfiguration _config;

    public JtlAuthService(HttpClient http, IConfiguration config)
    {
        _http = http;
        _config = config;
    }

    public string ApiBaseUrl => "https://api.jtl-cloud.com";
    public string AuthEndpoint => "https://auth.jtl-cloud.com/oauth2/token";

    public async Task<string> GetJwtAsync()
    {
        var clientId = _config["Jtl:ClientId"];
        var clientSecret = _config["Jtl:ClientSecret"];

        if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
        {
            throw new InvalidOperationException(
                "Jtl:ClientId and Jtl:ClientSecret must be configured.");
        }

        var basic = Convert.ToBase64String(
            Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"));

        var request = new HttpRequestMessage(HttpMethod.Post, AuthEndpoint);
        request.Headers.Authorization = new AuthenticationHeaderValue("Basic", basic);
        request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
        });

        var response = await _http.SendAsync(request);
        var body = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException(
                $"Failed to fetch JWT ({(int)response.StatusCode}): {body}");
        }

        using var doc = JsonDocument.Parse(body);
        return doc.RootElement.GetProperty("access_token").GetString()!;
    }
}
```

The service reads its credentials from `IConfiguration` (which user-secrets feeds into automatically), encodes them as Basic auth, and sends a `client_credentials` grant request to JTL's auth endpoint. The token is short-lived, so the method fetches a fresh one on each call. For higher-traffic backends you'd add caching.

## 5. Build the Session Verifier

With an access token in hand, the backend can fetch the public keys it needs to verify session tokens. JTL signs session tokens with `Ed25519`.

Create `Backend/Services/SessionVerifier.cs`:

```csharp theme={null}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using NSec.Cryptography;
 
namespace Backend.Services;
 
public record SessionTokenPayload(
    long Exp,
    string UserId,
    string TenantId,
    string? TenantSlug);
 
public class SessionVerifier
{
    private readonly HttpClient _http;
    private readonly JtlAuthService _auth;
    private readonly ILogger<SessionVerifier> _logger;
 
    public SessionVerifier(
        HttpClient http,
        JtlAuthService auth,
        ILogger<SessionVerifier> logger)
    {
        _http = http;
        _auth = auth;
        _logger = logger;
    }
 
    public async Task<SessionTokenPayload> VerifyAsync(string sessionToken)
    {
        var jwt = await _auth.GetJwtAsync();
 
        var request = new HttpRequestMessage(
            HttpMethod.Get,
            $"{_auth.ApiBaseUrl}/account/.well-known/jwks.json");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
 
        var response = await _http.SendAsync(request);
        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException(
                $"Failed to fetch JWKS ({(int)response.StatusCode})");
        }
 
        var jwksJson = await response.Content.ReadAsStringAsync();
        var jwks = JsonSerializer.Deserialize<JsonElement>(jwksJson);
 
        // Select the signing key by its JWK properties (use=sig, alg=EdDSA)
        JsonElement? signingKey = null;
        foreach (var k in jwks.GetProperty("keys").EnumerateArray())
        {
            var use = k.TryGetProperty("use", out var u) ? u.GetString() : null;
            var alg = k.TryGetProperty("alg", out var a) ? a.GetString() : null;
            if (use == "sig" && alg == "EdDSA")
            {
                signingKey = k;
                break;
            }
        }
 
        if (signingKey is null)
            throw new InvalidOperationException("No EdDSA signing key found in JWKS");
 
        // JTL session tokens are signed with Ed25519 (OKP)
        var publicKeyBytes = Base64UrlDecode(signingKey.Value.GetProperty("x").GetString()!);
 
        return VerifyAndDecode(sessionToken, publicKeyBytes);
    }
 
    private SessionTokenPayload VerifyAndDecode(string token, byte[] publicKeyBytes)
    {
        var parts = token.Split('.');
        if (parts.Length != 3)
        {
            throw new ArgumentException(
                "Invalid JWT format. Expected 3 dot-separated parts.");
        }
 
        var signedData = Encoding.ASCII.GetBytes($"{parts[0]}.{parts[1]}");
        var signature = Base64UrlDecode(parts[2]);
 
        var algorithm = SignatureAlgorithm.Ed25519;
        var publicKey = PublicKey.Import(
            algorithm, publicKeyBytes, KeyBlobFormat.RawPublicKey);
 
        if (!algorithm.Verify(publicKey, signedData, signature))
        {
            _logger.LogError("JWT signature verification failed");
            throw new UnauthorizedAccessException("Invalid token signature.");
        }
 
        var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(parts[1]));
        var payload = JsonSerializer.Deserialize<JsonElement>(payloadJson);
 
        var exp = payload.GetProperty("exp").GetInt64();
        if (DateTimeOffset.FromUnixTimeSeconds(exp) < DateTimeOffset.UtcNow)
        {
            throw new UnauthorizedAccessException("Token has expired.");
        }
 
        return new SessionTokenPayload(
            Exp: exp,
            UserId: payload.GetProperty("userId").GetString()!,
            TenantId: payload.GetProperty("tenantId").GetString()!,
            TenantSlug: payload.TryGetProperty("tenantSlug", out var slug)
                ? slug.GetString()
                : null);
    }
 
    private static byte[] Base64UrlDecode(string input)
    {
        var padded = input.Replace('-', '+').Replace('_', '/');
        padded += (padded.Length % 4) switch
        {
            2 => "==",
            3 => "=",
            _ => ""
        };
        return Convert.FromBase64String(padded);
    }
}
```

There are two things going on inside this service. The public `VerifyAsync` method fetches the JWKS document and pulls out the first public key. The private `VerifyAndDecode` method splits the JWT into its three parts (header, payload, signature), reconstructs the signed data, and asks `NSec` to verify the signature against the public key. If the signature is valid and the token isn't expired, it returns the decoded payload as a strongly-typed record.

## 6. Build the Connect Tenant Controller

Now wire the verifier into a controller. The frontend's shell layout sends the session token to `/api/connect-tenant` and expects a tenant ID back.

Create `Backend/Controllers/ConnectTenantController.cs`:

```csharp theme={null}
using Backend.Services;
using Microsoft.AspNetCore.Mvc;

namespace Backend.Controllers;

[ApiController]
[Route("api/connect-tenant")]
public class ConnectTenantController : ControllerBase
{
    private readonly SessionVerifier _verifier;
    private readonly ILogger<ConnectTenantController> _logger;

    public ConnectTenantController(
        SessionVerifier verifier,
        ILogger<ConnectTenantController> logger)
    {
        _verifier = verifier;
        _logger = logger;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var sessionToken = Request.Headers["X-Session-ID"].FirstOrDefault();

        if (string.IsNullOrWhiteSpace(sessionToken))
        {
            return BadRequest(new { error = "Session token is required" });
        }

        try
        {
            var payload = await _verifier.VerifyAsync(sessionToken);

            // In a production app, you'd store the tenant connection in your database here
            _logger.LogInformation("Tenant connected: {TenantId}", payload.TenantId);

            return Ok(new
            {
                success = true,
                tenantId = payload.TenantId,
                userId = payload.UserId,
                tenantSlug = payload.TenantSlug,
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Connection failed");
            return Unauthorized(new { error = "Failed to verify session token" });
        }
    }
}
```

The controller reads the session token from the `X-Session-ID` request header, hands it to the verifier, and returns the tenant details on success or a 401 on failure. The `[ApiController]` attribute handles model binding and validation automatically.

See the [Tenant Mapping](/guides/cloud-apps/authentication-login#tenant-mapping) section for more on managing tenants in production.

## 7. Wire up Services and CORS

ASP.NET Core needs to know about the services it should inject and the controllers it should expose. Replace the contents of `Backend/Program.cs`:

```csharp theme={null}
using Backend.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddHttpClient<JtlAuthService>();
builder.Services.AddHttpClient<SessionVerifier>();

// Allow the Vite dev server to call the API during development
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins("http://localhost:5173")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

app.UseCors();
app.MapControllers();

app.Run();
```

`AddHttpClient<TService>` does two things at once: it registers the service in the DI container and gives each instance its own `HttpClient` from the built-in factory.

The CORS policy allows requests from the Vite dev server on port 5173. The dev proxy in `vite.config.ts` already routes frontend `fetch('/api/...')` calls to this backend, but the CORS middleware is a useful safety net during development.

## 8. Configure the Backend Port

By default, `dotnet new webapi` listens on a random port chosen at startup, but the frontend's Vite dev proxy expects the backend on `http://localhost:5273`. Pin the port in `Backend/Properties/launchSettings.json` by replacing the `applicationUrl` value in the `http` profile:

```json theme={null}
{
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": false,
      "applicationUrl": "http://localhost:5273",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```

You can leave the other profiles (`https`, `IIS Express`) in place. The `dotnet run` command picks the `http` profile by default during development.

## 9. Run the Backend

Start the backend:

```bash theme={null}
dotnet run
```

You should see output like:

```
Now listening on: http://localhost:5273
Application started. Press Ctrl+C to shut down.
```

In a second terminal, send a request with a fake session token to confirm the route is reachable:

```bash theme={null}
curl http://localhost:5273/api/connect-tenant \
  -H "X-Session-ID: fake-token"
```

You should get back a `401` response with `{"error":"Failed to verify session token"}`. A real session token from the App Shell will follow the same path and succeed.

## Common Issues

<AccordionGroup>
  <Accordion title="'Jtl:ClientId and Jtl:ClientSecret must be configured'">
    This error means the backend started but couldn't find your credentials in configuration. The most common cause is that user-secrets weren't initialised, or were set in a different directory than the project expects.

    User-secrets are scoped to a specific project, identified by the `UserSecretsId` GUID in the `.csproj` file. Running `dotnet user-secrets list` from inside the `Backend/` directory will show whether the secrets are present for this project. If the list comes back empty, running `dotnet user-secrets init` followed by the two `dotnet user-secrets set` commands from earlier in the guide will populate them.

    It's also worth confirming that you ran the user-secrets commands from the `Backend/` directory and not the project root. The tool resolves the target project based on the current working directory.
  </Accordion>

  <Accordion title="'Failed to fetch JWT (401)' from the auth endpoint">
    A 401 from JTL's auth endpoint means the credentials being sent aren't recognised. Until you've registered your app in the Partner Portal, this is the expected response, since the placeholder values from earlier aren't real credentials. The next page in this guide walks through registration and provides the real values.

    If you've already registered the app and are still seeing this error, the most likely causes are typos in the user-secrets values and forgetting to restart the backend after updating them. ASP.NET Core reads configuration once at startup, so changes to user-secrets take effect only on the next `dotnet run`. The `dotnet user-secrets list` command is the fastest way to confirm the stored values match what the Partner Portal showed you.
  </Accordion>

  <Accordion title="CORS error in the browser console">
    A CORS error usually means the request reached the backend but the browser blocked the response because the origin didn't match what the backend allows. The CORS policy in `Program.cs` is configured to allow `http://localhost:5173`, which matches the default Vite dev server port.

    If the Vite dev server is running on a different port (for example, because port 5173 was already in use and Vite picked 5174 instead), the browser will see a mismatch. Updating the `WithOrigins` value in `Program.cs` to match the actual Vite port, or freeing up port 5173, should resolve it.

    Worth noting: when the Vite dev proxy is forwarding `/api/*` requests, the browser shouldn't actually see CORS at all, since the requests look same-origin from the browser's perspective. CORS errors usually appear when something is calling the backend directly on `localhost:5273` instead of going through the proxy.
  </Accordion>

  <Accordion title="Backend starts on a different port than 5273">
    If `dotnet run` reports a different port (for example, 5000 or a random high number), the `launchSettings.json` change from earlier in the guide either hasn't been saved or isn't being picked up.

    Confirming that `Backend/Properties/launchSettings.json` contains `"applicationUrl": "http://localhost:5273"` in the `http` profile, then stopping and restarting `dotnet run`, should pin the port. If `dotnet run` is using a different profile than expected, passing `--launch-profile http` explicitly will force it to use the right one.

    An alternative if you'd rather not edit `launchSettings.json` is to set the `ASPNETCORE_URLS` environment variable: `ASPNETCORE_URLS=http://localhost:5273 dotnet run`. This works without any config file changes.
  </Accordion>
</AccordionGroup>

## Next: Connect and Fetch Data

The backend verifies session tokens and is ready to call the JTL Cloud API. The remaining work is registering your app with JTL to get real credentials, installing the app in the JTL Hub, and pulling product data from JTL-Wawi:

<CardGroup cols={2}>
  <Card title="Connect and Fetch Data" icon="link" href="/get-started/quick-start/from-scratch/connect-and-fetch">
    Register your app, install it in the JTL Hub, and fetch products from JTL-Wawi.
  </Card>
</CardGroup>
