# Offline License Validation in C#: Verifying Signed Tokens Without a Server

*Originally published on the [PermitCore blog](https://permitcore.dev/blog/offline-license-validation-csharp).*

A factory-floor terminal, a secure workstation with no internet, a laptop that's offline more often than not: apps like these can't call a license server on every launch. The answer is a signed offline token that your app verifies entirely on its own. Here's how that looks in C#, using the real verification method from PermitCore's .NET SDK.

## The token format

An offline token is a plain string with three dot-separated parts:

```
pc_offline_v1.<base64url(payload_json)>.<base64url(signature)>
```

The prefix identifies the format, so anything malformed is rejected immediately. The payload is a small JSON object (license ID, device ID, expiry, and a key ID `kid` naming the signing key), and the signature is ECDSA P-256 with SHA-256 over the raw bytes of the base64url-encoded payload. The issuer holds the private signing key; your application only ever needs the trusted public key.

## The verification code

This is a static method with zero dependencies, so it's easy to audit, or even copy into your own project:

```csharp
public static OfflineTokenResult VerifyOfflineToken(string token, string publicKeyBase64)
{
    var parts = token?.Split('.') ?? [];
    if (parts.Length != 3 || parts[0] != "pc_offline_v1")
        return new OfflineTokenResult { IsValid = false, Message = "Malformed token." };

    try
    {
        var payloadBytes = Encoding.UTF8.GetBytes(parts[1]);
        var sig          = FromBase64Url(parts[2]);

        using var ecdsa = ECDsa.Create();
        ecdsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKeyBase64), out _);
        if (!ecdsa.VerifyData(payloadBytes, sig, HashAlgorithmName.SHA256))
            return new OfflineTokenResult { IsValid = false, Message = "Invalid signature." };

        var json    = Encoding.UTF8.GetString(FromBase64Url(parts[1]));
        var payload = JsonSerializer.Deserialize<OfflineTokenPayload>(json);

        if (payload.ExpiresAt < DateTime.UtcNow)
            return new OfflineTokenResult { IsValid = false, Message = "Token expired.", Payload = payload };

        return new OfflineTokenResult { IsValid = true, Message = "Valid.", Payload = payload };
    }
    catch { return new OfflineTokenResult { IsValid = false, Message = "Invalid or corrupt token." }; }
}
```

The order matters: the signature is checked before the expiry date is trusted. Never make a security decision based on data you haven't verified yet. The method matches what the server does when it signs a token, so there's no drift between what gets issued and what gets accepted.

## The full offline flow

Most apps use two higher-level calls built on that primitive:

```csharp
// trustedTenantPublicKey comes from your application configuration.
using var client = new PermitCoreClient(new PermitCoreOptions {
    BaseUrl = apiUrl, TrustedPublicKeys = new[] { trustedTenantPublicKey },
    CacheDirectory = appCacheDirectory, DeviceId = deviceId
});

// First run: verify and retain the signed token.
var result = client.ActivateOffline(token, deviceId, productId);

// Every later launch, with no network call at all
var cached = client.ValidateOffline(deviceId, productId);
if (!cached.IsValid) { /* show activation screen */ }
```

`ActivateOffline` also checks that the token's device ID matches the machine calling it, so a correctly signed token issued for one device is rejected when copied onto another. `ValidateOffline` reads the original signed token and re-verifies its signature, expiry, device and (optionally) product against your configured trust keys.

## What the signature gives you

Because the contents are signed, nobody can edit the cached JSON on disk to push out the expiry date or swap the device ID: any change breaks the signature. How long a token stays valid between online checks is configured per license with Offline Grace Days, and for store subscriptions offline access is also bounded by the paid-through date plus a configurable payment grace (14 days by default). A successful online check refreshes the signed cache after renewal.

The full guide, including the related video tutorial, is on the [PermitCore blog](https://permitcore.dev/blog/offline-license-validation-csharp), and the [offline setup docs](https://permitcore.dev/docs/getting-started) walk through configuring trusted keys.

How do you handle licensing for apps that spend most of their life offline? 🙂

