Skip to main content
Build the ASP.NET Core backend for a JTL Cloud App using .NET 8 and Microsoft.IdentityModel.Tokens for JWT verification. By the end, the backend runs locally and the frontend’s “Connecting to JTL Platform…” placeholder turns into a real connection. Stack: .NET 8, ASP.NET Core Web API, Microsoft.IdentityModel.Tokens for app token verification.

Prerequisites

You need:
  • ✅ A finished frontend from the Build the Frontend page, running locally on http://localhost:5173
  • ✅ .NET SDK 8.0 or higher. Run dotnet --version to check.

What you’re Building

During setup, your backend verifies that the app token from your frontend is valid and was issued by JTL for your app. To do this:
  • Your backend fetches the identity provider’s public keys (JWKS)
  • Verifies the token’s signature, issuer, and expiry against them
  • Confirms the token’s audience is your app, so a token minted for another app is rejected
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, authenticating with its own client credentials.

1. Set up the Project

Create a Backend folder alongside the existing frontend folder, then scaffold a Web API project inside it.
Your project structure now looks like this:
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

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:
The values are placeholders for now. You’ll get real values from the Partner Portal in the next page and update them with the same dotnet user-secrets set commands. Jtl:AppId is your app’s ID, which an app token’s audience must contain. Jtl:Issuer is the identity provider that issues app tokens. 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. Your backend uses it to make tenant-scoped calls to the JTL Cloud API. Create Backend/Services/JtlAuthService.cs:
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 method fetches a fresh token on each call. For higher-traffic backends you’d add caching. See Service Account Authentication.

5. Build the Token Verifier

The backend verifies the app token before trusting anything in it. Microsoft.IdentityModel.Tokens parses the identity provider’s key set, selects the key matching the token’s kid, and validates the signature, issuer, expiry, and audience in one pass. Create Backend/Services/AppTokenVerifier.cs:
ValidateToken performs four checks at once: the signature against the key whose kid matches the token header, the issuer, the expiry, and that the audience contains your app ID. Any failure throws. The urn:jtl:app_id claim is optional and is not covered by ValidateAudience, so it needs its own check. When the claim is absent the token is valid for your app, and when it is present it must equal your app ID. The key set is fetched on first use and held for the lifetime of the service. Public keys change infrequently, so refetching on every request adds latency for no benefit.

6. Build the Connect Tenant Controller

Now wire the verifier into a controller. The frontend’s shell layout sends the app token to the backend as a Bearer token and expects a claim with the tenant information including tenant ID, client ID, etc. Create Backend/Controllers/ConnectTenantController.cs:
The controller reads the app token from the Authorization 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 Tenant Mapping 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:
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. AppTokenVerifier is registered as transient, so its key set cache lives for one request. For production, register it as a singleton so the keys are fetched once per process. 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:
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:
You should see output like:
In a second terminal, send a request with a fake app token to confirm the route is reachable:
You should get back a 401 response with {"error":"Failed to verify app token"}. A real app token from the App Shell will follow the same path and succeed.

Common Issues

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.
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.
Verification checks the signature, issuer, expiry, and audience. The audience check is the one most often wrong: Jtl:AppId is the app ID shown in the Partner Portal under your app, not the client ID.If the audience is correct, confirm Jtl:Issuer matches the environment your app is registered in. A token issued by one environment does not verify against another’s keys.App tokens are valid for about an hour, so a token captured earlier during debugging will fail on expiry.
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.
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.

Next: Connect and Fetch Data

The backend verifies app 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:

Connect and Fetch Data

Register your app, install it in the JTL Hub, and fetch products from JTL-Wawi.