@crabnebula/better-auth-license
v0.0.16
Published
Better Auth plugin for license key validation and management.
Downloads
1,250
Readme
Better Auth License Plugin
This plugin adds device-bound licensing to Better Auth, enabling cryptographically secure activation flows and offline-verifiable JWT validation for desktop, mobile, and native applications.
It implements a hardened licensing pipeline based on ephemeral nonces, rotating JWK signature keys, and mandatory device-side public-key encryption.
The server issues, validates, and revokes licenses tied to an individual device keypair, ensuring tokens cannot be replayed, copied, or reused on other machines.
Installation
# Using npm
npm install better-auth @crabnebula/better-auth-license
# Using yarn
yarn add better-auth @crabnebula/better-auth-license
# Using pnpm
pnpm add better-auth @crabnebula/better-auth-license
# Using bun
bun add better-auth @crabnebula/better-auth-licenseOverview
The plugin implements:
- Device-bound activation using a one-time nonce
- Mandatory device public key for all encrypted responses
- All server -> client responses delivered as JWE encrypted with the device public key
- Issuing a new nonce invalidates previous JWTs
- Revocation and expiration enforcement on every request
- Optional offline JWT verification using the
/license/jwkspublic keys
Basic Usage
Add the Plugin to Better Auth
// auth.ts
import { betterAuth } from "better-auth";
import { license } from "@crabnebula/better-auth-license";
export const auth = betterAuth({
baseURL: "https://license.yourapp.com",
plugins: [
license({
middleware: {
header: "X-License",
matcher: req => req.path === "/get-session",
},
jwt: {
expirationTime: "1d",
issuer: "yourapp",
},
nonce: {
expirationTime: "5m",
},
}),
],
});Endpoints
| Endpoint | Method | Description |
|-----------|---------|-------------|
| /license/nonce | POST | Requests a one-time nonce to start activation |
| /license/exchange | POST | Exchanges nonce + device ID for a signed JWT |
| /license/list | GET | Lists all user-owned licenses |
| /license/revoke | POST | Revokes a specific license |
| /license/jwks | GET | List the server JWKS used to verify JWT signatures and encryptions |
All responses from /license/nonce and /license/exchange are JWE payloads encrypted with the provided device public key.
Activation Flow
Fetch server JWKS
Client retrieves the encryption and signature public keys:
GET /license/jwksResponse:
{
"keys": [
{ "use": "enc", "alg": "ECDH-ES+A256KW", ... },
{ "use": "sig", "alg": "EdDSA", ... }
]
}Client extracts the enc key and uses it for encrypting payloads.
Generate device keypair
Client creates an X25519 keypair:
privateKey (stored locally)
publicKey (sent to server)Store both keys in a secure local vault (Keychain/Keyring/Windows DPAPI).
Request nonce
Client encrypts the JSON payload using the server enc JWK, producing JWE_STRING:
{
"licenseKey": "XXXX-XXXX-XXXX",
"devicePublicKey": "<base64url(public JWK)>"
}Server expects :
POST /license/nonce
{
"payload": "<compact JWE string>"
}Server returns:
{
"encrypted": true,
"payload": "<JWE encrypted with devicePublicKey>"
}Client decrypts using the device private key. The decrypted response contains:
{ "nonce": "<value>" }Exchange nonce for JWT
Client again encrypts:
{
"licenseKey": "XXXX-XXXX-XXXX",
"devicePublicKey": "<base64url(JSON(public JWK))>",
"nonce": "<value-received-previously>"
}Server expects :
POST /license/exchange
{
"payload": "<compact JWE string>"
}Server returns:
{
"encrypted": true,
"payload": "<JWE encrypted with devicePublicKey>"
}Client decrypts with private key:
{
"success": true,
"jwt": "eyJhbGciOiJFZERTQSJ9....",
"license": { "key": "XXXX", "expiresAt": 1761843168 },
"device": { "publicKey": "<public JWK>" }
}Using License Tokens With Middleware
When the middleware is enabled, the server automatically validates license JWTs for matching routes.
Example configuration:
license({
middleware: {
header: "X-License-Token",
matcher: req => req.path === "/get-session",
},
})With this configuration:
/get-sessionaccepts a license JWT and resolves the user who owns the license- The token is the JWT returned from
/license/exchange
Example Request
GET /get-session
X-License-Token: eyJhbGciOiJFZERTQSJ9...If the token is valid:
{
"user": {
"id": "2",
"email": "[email protected]",
...
}
}If invalid, expired, or revoked:
{
"error": {
"status": 401,
"code": "invalid_license_token"
}
}This allows a device-bound license JWT to authenticate a route without traditional user login.
Official Clients
Tauri
A first-party Tauri plugin is available on crates.io:
It provides:
- Local X25519 keypair generation
- Secure private-key storage using OS keyrings
- Automatic JWE encryption/decryption for all requests
- Local + remote JWT validation
- Built-in device-bound activation flow
This is the recommended integration path for desktop applications built with Tauri.
Security Model
- Nonces are single-use and invalidate previous tokens.
- Server -> client messages are always encrypted with the device public key.
- JWTs are signed with rotating JWKs
- A license cannot be reused on another device without invalidation
- Offline verification using
/license/jwkspublic key set - Middleware isolates license JWTs from user sessions
License
MIT or MIT/Apache 2.0 where applicable.
