From d456f05dc66e6a52836c7712ce481b30cbdc71a3 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Sat, 21 Mar 2026 11:23:53 +0100 Subject: [PATCH 1/4] Separate authentication and entitlements, add federated DRM The existing `auth` property on Release Documents conflated authentication (how to present credentials) with authorization (whether a user may access a package). This change separates the two concerns and adds a federated entitlement verification protocol: - Narrow `auth` on releases to repository authentication only - Add `entitlements` property to Metadata Documents for vendor-controlled access policy (subscription, purchase, license-key, free-registration) - Add `FairEntitlementService` DID Document service type as trust anchor - Define entitlement verification protocol with JWT-based proofs - Write the ext-auth.md authentication methods extension (bearer, basic, oauth2) - Add entitlement types to the registry - Update implementation guide and docs Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joost de Valk --- docs/implementing/restricted.md | 136 ++++++++++++++++++++++++++++---- docs/restricted.md | 9 ++- ext-auth.md | 110 ++++++++++++++++++++++++++ registry.md | 10 +++ specification.md | 130 +++++++++++++++++++++++++++--- 5 files changed, 364 insertions(+), 31 deletions(-) diff --git a/docs/implementing/restricted.md b/docs/implementing/restricted.md index a193612..7fafbee 100644 --- a/docs/implementing/restricted.md +++ b/docs/implementing/restricted.md @@ -1,33 +1,139 @@ # Restricted Packages -FAIR builds the concept of "restricted" packages right into the protocol. These are packages which require some form of authentication, such as a token or a username and password. +FAIR builds the concept of "restricted" packages right into the protocol. These are packages which require some form of entitlement, such as a subscription, purchase, or license key. In the WP ecosystem, many types of restricted packages are available, including privately-published plugins and premium plugins. FAIR builds support for these into the protocol. +FAIR separates two distinct concerns: -## Indicating a restricted package +- **Authentication** (`auth` on releases) — How to present credentials to the repository's HTTP server. This is the mechanism (bearer token, basic auth, OAuth2). +- **Entitlements** (`entitlements` on metadata) — What a user needs to be allowed to access the package. This is the policy, controlled by the vendor. -To indicate a restricted package, your package metadata can specify an `auth` property, indicating that the package is only available for authorized users. +This separation means that vendors control access to their packages regardless of which repository serves them, and users keep their entitlements when packages move between repositories. -In the FAIR plugin, two types of authentication are supported: -* `bearer` - This type indicates that a bearer token (such as an API key) is required. -* `basic` - This type indicates that a username and password is required. +## Setting up entitlements -The `hint` property can be provided to provide human-readable text indicating why authentication is required, and `hint_url` provides a way to link users to more information or a purchase page. +### 1. Add an entitlement service to your DID Document -For example, a premium plugin could provide the following: +Register a `FairEntitlementService` in your DID Document pointing to your license/entitlement server: ```json { - "auth": { - "hint": "Example Plugin requires an active subscription. Visit the link to purchase it, or enter your token.", - "hint_url": "https://plugin.example.com/buy", - "type": "bearer" - } + "service": [ + { + "id": "#fairpm_repo", + "serviceEndpoint": "https://repo.example.com/packages/1234", + "type": "FairPackageManagementRepo" + }, + { + "id": "#fairpm_entitlements", + "serviceEndpoint": "https://licenses.example.com", + "type": "FairEntitlementService" + } + ] } ``` -The FAIR plugin would then display the following UI: +This is the trust anchor — because you control your DID, you control where entitlement checks go, even if you change repositories. -... + +### 2. Add entitlements to your package metadata + +In your package metadata, specify the `entitlements` property: + +```json +{ + "entitlements": { + "service": "https://licenses.example.com/verify", + "type": "subscription", + "hint": "Example Plugin requires an active Pro subscription.", + "hint_url": "https://example.com/pricing" + } +} +``` + +The `service` URL must be under the `FairEntitlementService` URL in your DID Document. Clients validate this to prevent rogue repositories from redirecting entitlement checks. + +Available entitlement types: + +| Type | Use case | +| ------------------- | -------------------------------------------------- | +| `subscription` | Premium plugins/themes with recurring billing | +| `purchase` | One-time purchase plugins/themes | +| `license-key` | Software with traditional license key activation | +| `free-registration` | Free plugins that require vendor registration | + + +### 3. Set up repository authentication + +On each release that has restricted artifacts, set `auth` to tell clients how to authenticate with the repository: + +```json +{ + "auth": { + "type": "bearer", + "hint": "Your entitlement token will be used automatically.", + "hint_url": "https://example.com/help/installation" + } +} +``` + +When a package has both `entitlements` and `auth`, the client flow is: + +1. Client verifies the user's entitlement with the vendor's service +2. The entitlement service returns a signed JWT (entitlement proof) +3. Client presents the JWT as a bearer token to the repository +4. Repository validates the JWT and serves the artifact + +Mark individual artifacts as restricted using `requires-auth`: + +```json +{ + "artifacts": { + "package": { + "url": "https://repo.example.com/packages/1234/download/2.1.0", + "requires-auth": true, + "signature": "...", + "checksum": "sha256:..." + }, + "banner": { + "url": "https://repo.example.com/packages/1234/banner.png", + "content-type": "image/png" + } + } +} +``` + +In this example, the package binary requires authentication (and therefore entitlement verification), but the banner image is publicly accessible. + + +## How it works end-to-end + +When a user wants to install a restricted package: + +1. **Client resolves the DID** and finds both `FairPackageManagementRepo` and `FairEntitlementService` services. + +2. **Client fetches metadata** from the repository and sees the `entitlements` property. It validates that the entitlement service URL matches the DID Document. + +3. **Client displays the requirement** to the user: "This package requires an active Pro subscription. [Learn more](https://example.com/pricing)" + +4. **User provides credentials** (API key, license key, etc.). + +5. **Client contacts the entitlement service** with the user's credentials and the package DID. The service verifies the entitlement and returns a signed JWT proof. + +6. **Client downloads the artifact** from the repository, presenting the JWT as a bearer token. The repository validates the JWT signature and expiration. + +7. **Client verifies the package signature** against the DID Document's signing keys, as with any package. + + +## Why this separation matters + +Because entitlements are tied to the vendor's DID (not the repository), they survive repository changes. If a vendor moves from Repository A to Repository B: + +- The `FairEntitlementService` in the DID Document stays the same +- The entitlement service URL in the metadata stays the same +- Users' entitlements continue to work — the JWT proofs are validated against the vendor's entitlement service, not the repository +- The new repository just needs to accept the same JWT proofs + +This also means aggregators and caches can enforce the same access controls by validating the same JWTs. diff --git a/docs/restricted.md b/docs/restricted.md index bba4f80..a8e82f3 100644 --- a/docs/restricted.md +++ b/docs/restricted.md @@ -2,8 +2,11 @@ FAIR builds the concept of "restricted" packages right into the protocol. -In the WP ecosystem, many types of restricted packages are available, including privately-published themes and premium plugins. FAIR builds support for these into the protocol. The FAIR plugin also displays information you specify directly in the installation UI: +In the WP ecosystem, many types of restricted packages are available, including privately-published themes and premium plugins. FAIR builds support for these into the protocol using a two-layer system: -... +- **Entitlements** — The vendor controls who can access the package through their own entitlement service. This works regardless of which repository hosts the package. +- **Authentication** — The repository controls how credentials are presented when downloading artifacts. -The reference FAIR repository does not implement restricted packages, but custom repositories may implement it. +This separation means your customers keep their entitlements even if you change repositories, and aggregators can enforce the same access controls. + +For implementation details, see the [implementing restricted packages](./implementing/restricted.md) guide. diff --git a/ext-auth.md b/ext-auth.md index e69de29..0f5329a 100644 --- a/ext-auth.md +++ b/ext-auth.md @@ -0,0 +1,110 @@ +# FAIR Authentication Methods + +This extension defines the standard authentication methods for the FAIR Package Management Protocol. + +Authentication methods specify **how** a client presents credentials to a repository when accessing artifacts that require authentication (as indicated by the `auth` property on a [Release Document](./specification.md#property-auth) or the `requires-auth` property on an artifact). + +For access control and entitlement verification (determining **whether** a user is authorized), see [Entitlements](./specification.md#entitlements) and [Entitlement Verification](./specification.md#entitlement-verification) in the core specification. + + +## Methods + + +### bearer + +The `bearer` method indicates that the client must present a bearer token in the `Authorization` header. + +``` +Authorization: Bearer +``` + +The token may be an API key, an [entitlement proof](./specification.md#entitlement-proof) JWT, or any other opaque token accepted by the repository. + +The `auth` object for this method uses only the common properties (`type`, `hint`, `hint_url`). No additional properties are defined. + +Example: + +```json +{ + "auth": { + "type": "bearer", + "hint": "Enter your repository API key to download this artifact.", + "hint_url": "https://repo.example.com/account/api-keys" + } +} +``` + + +### basic + +The `basic` method indicates that the client must present a username and password using HTTP Basic authentication. + +``` +Authorization: Basic +``` + +The credentials MUST be encoded as specified in [RFC 7617][rfc7617]. + +The `auth` object for this method uses only the common properties (`type`, `hint`, `hint_url`). No additional properties are defined. + +Example: + +```json +{ + "auth": { + "type": "basic", + "hint": "Use your repository account credentials.", + "hint_url": "https://repo.example.com/register" + } +} +``` + +[rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617 + + +### oauth2 + +The `oauth2` method indicates that the client must authenticate using an [OAuth 2.0][rfc6749] authorization flow. + +The `auth` object for this method defines the following additional properties: + +* `authorization_url` (required) - A URL string. The OAuth 2.0 authorization endpoint. +* `token_url` (required) - A URL string. The OAuth 2.0 token endpoint. +* `scopes` (optional) - A list of strings. The OAuth 2.0 scopes required for access. If omitted, the client SHOULD request no specific scopes. + +Clients SHOULD support the Authorization Code flow as defined in [RFC 6749, Section 4.1][rfc6749-s4.1]. Clients MAY support additional flows as appropriate. + +Example: + +```json +{ + "auth": { + "type": "oauth2", + "authorization_url": "https://repo.example.com/oauth/authorize", + "token_url": "https://repo.example.com/oauth/token", + "scopes": ["packages:read"], + "hint": "Sign in with your repository account to download.", + "hint_url": "https://repo.example.com/register" + } +} +``` + +[rfc6749]: https://datatracker.ietf.org/doc/html/rfc6749 +[rfc6749-s4.1]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1 + + +## Combining Authentication with Entitlements + +When a package has both `entitlements` (on the metadata) and `auth` (on a release), the two work together: + +1. The client verifies the user's entitlement by contacting the vendor's entitlement service, as described in [Entitlement Verification](./specification.md#entitlement-verification). +2. The entitlement service returns an [entitlement proof](./specification.md#entitlement-proof) JWT. +3. The client presents the entitlement proof as a bearer token to the repository when downloading artifacts. + +In this flow, the repository's `auth` type is typically `bearer`, and the token is the entitlement proof JWT. The repository validates the JWT signature and expiration before serving the artifact. + +This separation allows: +- **Vendors** to control who can access their packages (via the entitlement service). +- **Repositories** to enforce access without needing their own authorization logic (by validating JWTs). +- **Aggregators and caches** to enforce the same access controls (by accepting the same JWTs). +- **Users** to move between repositories without losing their entitlements. diff --git a/registry.md b/registry.md index 1d507dc..f7b4720 100644 --- a/registry.md +++ b/registry.md @@ -26,3 +26,13 @@ This document is a registry for the known extensions to the FAIR Package Managem | `bearer` | [FAIR Authentication Methods](./ext-auth.md) | FAIR Working Group | | `basic` | [FAIR Authentication Methods](./ext-auth.md) | FAIR Working Group | | `oauth2` | [FAIR Authentication Methods](./ext-auth.md) | FAIR Working Group | + + +## Entitlement Types + +| Type | Description | +| ------------------- | -------------------------------------------------------------- | +| `subscription` | Active subscription required to access the package. | +| `purchase` | One-time purchase required to access the package. | +| `license-key` | Valid license key required to access the package. | +| `free-registration` | Free to use, but requires registration with the vendor. | diff --git a/specification.md b/specification.md index 1147e70..6cedc45 100644 --- a/specification.md +++ b/specification.md @@ -69,6 +69,11 @@ The FAIR system focusses on three key flows: V Client selects which package release to install | + (If entitlements required) + | + V + Client verifies entitlement with vendor's entitlement service + | V Client downloads package binary | @@ -76,7 +81,7 @@ The FAIR system focusses on three key flows: Client verifies package binary signature matches signing key ``` -The Installation flow has five discrete steps: +The Installation flow has up to six discrete steps: 1. The client resolves the specified DID to a DID Document. The Document contains information on which repository is valid for the DID. @@ -84,9 +89,11 @@ The Installation flow has five discrete steps: 3. The client selects one of the available package releases to download and install. This may be presented as a choice to the user, or the client may select this automatically (for example, the latest release). -4. The client downloads the package binary or binaries for the selected release. +4. If the metadata specifies [entitlements](#entitlements), the client verifies the user's entitlement with the vendor's [entitlement service](#entitlement-verification). If the user is not entitled, the client SHOULD display the entitlement hint and hint URL to the user and MUST NOT proceed with the download. -5. The client verifies the signature for the downloaded binary file(s) against the valid signing keys specified in the DID Document. +5. The client downloads the package binary or binaries for the selected release. + +6. The client verifies the signature for the downloaded binary file(s) against the valid signing keys specified in the DID Document. Once these steps have been completed, the client can treat the package as verified and perform additional client-specific steps such as installation. @@ -109,6 +116,11 @@ For package binaries, clients may use a local (caching) mirror for any signed as V Client selects which package release to install | + (If entitlements required) + | + V + Client verifies entitlement with vendor's entitlement service + | V Client downloads package binary | @@ -158,7 +170,9 @@ Valid documents MUST contain a service of type `FairPackageManagementRepo` with The repo URL SHOULD point to a valid [Metadata Document](#metadata-document) available via the HTTP protocol. Clients SHOULD ensure they have robust error handling if this URL is invalid, such as if the server is unavailable. -Valid documents SHOULD NOT contain multiple services without the `FairPackageManagementRepo` type unless specified by an extension to this specification. Clients which assume a single repository MUST use the first service with the matching type in the [set][ordered-set]. +Valid documents SHOULD NOT contain multiple services with the `FairPackageManagementRepo` type unless specified by an extension to this specification. Clients which assume a single repository MUST use the first service with the matching type in the [set][ordered-set]. + +Valid documents MAY contain a service of type `FairEntitlementService` with a valid URL. This service specifies the base URL of the vendor's entitlement verification service, as described in [Entitlement Verification](#entitlement-verification). If a package's metadata specifies [entitlements](#entitlements), clients MUST verify that the entitlement service URL in the metadata is under the `FairEntitlementService` URL from the DID Document. If the DID Document does not contain a `FairEntitlementService`, or the URLs do not match, clients MUST reject the entitlement configuration and SHOULD display a warning to the user. Valid documents MUST contain one or more verification methods in the `verificationMethod` property. Valid verification methods MUST have the type `Multibase`, and MUST use an ID where the non-fragment parts of the URL match the DID, and where the fragment part starts with `fair_`. @@ -178,6 +192,11 @@ For example, the following document is considered a valid DID document: "id": "#fairpm_repo", "serviceEndpoint": "https://example.fair.pm/packages/1234", "type": "FairPackageManagementRepo" + }, + { + "id": "#fairpm_entitlements", + "serviceEndpoint": "https://licenses.example.com", + "type": "FairEntitlementService" } ], "verificationMethod": [ @@ -191,6 +210,8 @@ For example, the following document is considered a valid DID document: } ``` +The `FairEntitlementService` is optional. Packages which do not require entitlements do not need this service in their DID Document. + [ordered-set]: https://infra.spec.whatwg.org/#ordered-set @@ -214,8 +235,9 @@ The following properties are defined for the metadata document: | name | no | A string. | | description | no | A string. | | keywords | no | A list of strings. | -| sections | no | A map that conforms to the rules of [sections](#property-sections) | -| _links | no | [HAL links][hal], with [defined relationships](#links-metadata) | +| sections | no | A map that conforms to the rules of [sections](#property-sections) | +| entitlements | no | An object that conforms to the rules of [entitlements](#entitlements) | +| _links | no | [HAL links][hal], with [defined relationships](#links-metadata) | The properties of the metadata document have the following semantic meanings and constraints. @@ -368,6 +390,27 @@ Other keys MAY be specified, and their meaning MAY be defined within extensions Clients SHOULD ignore any section which does not have an explicit semantic meaning specified. +### entitlements + + + +The `entitlements` property specifies the vendor's access requirements for the package. Unlike `auth` on releases (which describes how to authenticate with the repository), entitlements describe what a user needs to be allowed to access the package, as controlled by the vendor. + +This property MUST be a valid object, represented as a JSON Object, with the following properties: + +* `service` (required) - A URL string. The URL of the vendor's entitlement verification endpoint, as described in [Entitlement Verification](#entitlement-verification). Clients MUST validate that this URL is under the `FairEntitlementService` URL specified in the [DID Document](#did-document). If the URLs do not match, clients MUST reject the entitlement configuration. +* `type` (required) - A string. The type of entitlement required. This SHOULD be a type defined in the [entitlement type registry][entitlement-registry]. Custom or non-standard types SHOULD be prefixed with `x-` to indicate they are non-standard. +* `hint` (optional) - A string. A human-readable hint to display to the user explaining what is required to access the package. The hint SHOULD be written in plain text, and clients MUST escape any special characters for the applicable formatting context (such as HTML). The hint SHOULD NOT exceed 140 characters. Clients MAY truncate the hint if it exceeds this limit. +* `hint_url` (optional) - A URL string. A URL for more information about the access requirements, such as a purchase page or subscription signup. +* `scope` (optional) - A string or list of strings. Specifies what the entitlement controls. The value `"artifacts"` (the default) indicates that only artifact downloads require entitlement verification. The value `"metadata"` indicates that all access including metadata requires entitlement verification. A list of strings indicates that only the specified artifact types require entitlement verification. + +Clients MUST clearly communicate entitlement requirements to users before initiating any authentication or purchase flow. + +Clients which do not recognise the entitlement type SHOULD display the `hint` and `hint_url` to the user. Vendors SHOULD include the `hint` and `hint_url` properties. + +[entitlement-registry]: ./registry.md#entitlement-types + + ### _links @@ -552,7 +595,7 @@ This property matches the format of the [`requires` property](#property-requires -The `auth` property specifies authentication requirements to access the package. +The `auth` property specifies the authentication mechanism required to access artifacts from the repository. This property describes **how** to authenticate with the repository's HTTP server, not **whether** a user is authorized to access the package. For access control and entitlement requirements, see [entitlements](#entitlements) on the Metadata Document. This property MUST be a valid object, conforming to the authentication method being used. The `type` property of this object indicates the authentication method being used. The authentication method SHOULD be a method defined in the [authentication registry][auth-registry]. @@ -561,16 +604,16 @@ Custom or non-standard methods SHOULD be prefixed with `x-` to indicate they are Common properties of the object are defined as: * `type` (required) - The authentication method being used. -* `hint` (optional) - A human-readable hint to the authentication method. -* `hint_url` (optional) - A URL for more information about the required authentication. +* `hint` (optional) - A human-readable hint indicating how to obtain repository credentials. +* `hint_url` (optional) - A URL where the user can obtain repository credentials or find more information. Extensions MAY specify additional properties which are type-specific. Clients which do not recognise the method being used SHOULD display the `hint` and `hint_url` to the user. Repositories SHOULD include the `hint` and `hint_url` properties. -Authentication may be used for limited-access packages, such as those requiring purchase, and clients SHOULD display the `hint` and `hint_url` to the user to ensure they understand why access is limited. +Access to individual artifacts may be limited on a per-artifact basis using the `requires-auth` property on the artifact. The presence of this flag indicates clients must authenticate with the repository in order to access the artifact. -Access to individual artifacts may be limited on a per-artifact basis using the `requires-auth` property on the artifact. The presence of this flag indicates clients must authenticate in order to access the artifact. +When a package has both `auth` (on the release) and `entitlements` (on the metadata), the client SHOULD first verify the user's entitlement, then use the resulting [entitlement proof](#entitlement-proof) as the bearer token for repository authentication. This allows repositories to enforce entitlements without needing their own authorization logic. The properties of this object have the following semantic meanings and constraints. @@ -588,7 +631,7 @@ Custom or non-standard methods SHOULD be prefixed with `x-` to indicate they are #### hint -The `hint` property specifies a hint to display to the user to indicate how they can authenticate and why it is required. +The `hint` property specifies a hint to display to the user to indicate how they can obtain repository credentials. This property MUST be a string. @@ -599,7 +642,7 @@ The hint SHOULD be written in plain text, and clients MUST escape any special ch #### hint_url -The `hint_url` property specifies a URL which provides more information about the authentication requirements for the package. +The `hint_url` property specifies a URL where the user can obtain repository credentials or find more information about the repository's authentication requirements. This property MUST be a string with a valid URL to a HTTP document. @@ -700,6 +743,67 @@ If the `privacy` property is not specified, clients SHOULD block installation of Repository Documents may have links to other resources, using the [HAL specification][hal], as provided in the `_links` property. +## Entitlement Verification + + + +Entitlement verification allows vendors to control access to their packages independently of which repository serves them. This provides a federated access control mechanism where the vendor maintains authority over who can access their packages, even as packages move between repositories, are mirrored by aggregators, or are served by caches. + +The entitlement verification protocol has three components: + +1. The **entitlement service**, operated by the vendor, which verifies whether a user is entitled to access a package. +2. The **verification request**, made by the client to the entitlement service. +3. The **entitlement proof**, returned by the entitlement service, which the client can present to repositories and caches. + + +### Verification Request + +To verify a user's entitlement, the client sends an HTTP `POST` request to the entitlement service URL specified in the metadata's `entitlements.service` property. + +The request body MUST be a JSON object with the following properties: + +* `package_did` (required) - The DID of the package being accessed. +* `version` (optional) - The version of the release being accessed. If omitted, the entitlement service SHOULD verify access to the package as a whole. + +The client MUST include the user's credentials in the `Authorization` header, using an authentication method supported by the entitlement service. The specific method depends on the entitlement type; for example, a `license-key` entitlement might use a bearer token containing the license key. + +The entitlement service MUST respond with one of the following HTTP status codes: + +* `200 OK` - The user is entitled. The response body MUST contain an [Entitlement Proof](#entitlement-proof). +* `401 Unauthorized` - The credentials are invalid or missing. The response body SHOULD contain `hint` and `hint_url` properties to guide the user. +* `402 Payment Required` - The user needs to purchase or subscribe. The response body SHOULD contain `hint` and `hint_url` properties to guide the user. +* `403 Forbidden` - The user's entitlement has been revoked. The response body SHOULD contain `hint` and `hint_url` properties explaining why. + + +### Entitlement Proof + + + +An entitlement proof is a [JSON Web Token (JWT)][jwt] that attests to a user's entitlement to access a package. The proof is issued by the entitlement service and can be presented to repositories and caches as a bearer token to access restricted artifacts. + +The JWT payload MUST contain the following claims: + +* `sub` - The authenticated user or client identifier. +* `package_did` - The DID of the package this proof is valid for. +* `iss` - The entitlement service URL that issued the proof. +* `iat` - The time at which the proof was issued, as a Unix timestamp. +* `exp` - The time at which the proof expires, as a Unix timestamp. This value SHOULD NOT be more than 24 hours after `iat`. + +The JWT payload MAY contain the following claims: + +* `version` - The version this proof is valid for. If omitted, the proof is valid for all versions. +* `scope` - A list of artifact types this proof grants access to. If omitted, the proof grants access to all artifact types. + +The JWT MUST be signed using a key that can be verified through the entitlement service. Entitlement services SHOULD publish their signing keys via a [JSON Web Key Set (JWKS)][jwks] endpoint at `/.well-known/jwks.json` relative to the service URL. + +Repositories and caches which accept entitlement proofs MUST verify the JWT signature and expiration before granting access. Repositories SHOULD accept entitlement proofs as bearer tokens in the `Authorization` header. + +Clients MAY cache entitlement proofs for the duration of their validity (until `exp`), and SHOULD reuse cached proofs for subsequent requests to the same package. + +[jwt]: https://datatracker.ietf.org/doc/html/rfc7519 +[jwks]: https://datatracker.ietf.org/doc/html/rfc7517 + + ## Caching Clients MAY choose to use an external cache of package data instead of fetching it directly from the repository. In particular, external caches may be used for artifacts such as the main installable package, to reduce bandwidth costs and latency. From a19a38919a9655f9a8274fe3cc67b8d388e6e914 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Sat, 21 Mar 2026 13:10:34 +0100 Subject: [PATCH 2/4] Fix test data to conform to JSON schema - Change `provides` from empty array `[]` to empty object `{}` in both test data files (spec requires provides to be a map/object) - Add a security contact to did_plc_m5tfrwxd3btacxlstcvop2ib.json (spec requires at least one security contact) Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joost de Valk --- .../did_plc_afjf7gsjzsqmgc7dlhb553mv.json | 60 +++++++++---------- .../did_plc_m5tfrwxd3btacxlstcvop2ib.json | 10 +++- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/test-data/did_plc_afjf7gsjzsqmgc7dlhb553mv.json b/test-data/did_plc_afjf7gsjzsqmgc7dlhb553mv.json index 7bf7e08..2c032e2 100644 --- a/test-data/did_plc_afjf7gsjzsqmgc7dlhb553mv.json +++ b/test-data/did_plc_afjf7gsjzsqmgc7dlhb553mv.json @@ -40,7 +40,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -83,7 +83,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -126,7 +126,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -169,7 +169,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -212,7 +212,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -255,7 +255,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -298,7 +298,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -341,7 +341,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -384,7 +384,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -427,7 +427,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -470,7 +470,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -513,7 +513,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -556,7 +556,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -599,7 +599,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -642,7 +642,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -685,7 +685,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -728,7 +728,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -771,7 +771,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -814,7 +814,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -857,7 +857,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -900,7 +900,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -943,7 +943,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -986,7 +986,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -1029,7 +1029,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -1072,7 +1072,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -1115,7 +1115,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -1158,7 +1158,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -1201,7 +1201,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -1244,7 +1244,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { @@ -1287,7 +1287,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "banner": [ { diff --git a/test-data/did_plc_m5tfrwxd3btacxlstcvop2ib.json b/test-data/did_plc_m5tfrwxd3btacxlstcvop2ib.json index a228d32..60874b5 100644 --- a/test-data/did_plc_m5tfrwxd3btacxlstcvop2ib.json +++ b/test-data/did_plc_m5tfrwxd3btacxlstcvop2ib.json @@ -13,7 +13,11 @@ } ], "license": "GPL v3 or later", - "security": [], + "security": [ + { + "url": "https://github.com/ProgressPlanner/pp-glossary/security/advisories" + } + ], "keywords": [ "glossary", "definitions", @@ -37,7 +41,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "icon": [ { @@ -66,7 +70,7 @@ "suggests": { "env:wp": ">=6.9.3" }, - "provides": [], + "provides": {}, "artifacts": { "icon": [ { From 049943d3947286ffe5f4180a0d982b1fae89e471 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Sat, 21 Mar 2026 15:44:56 +0100 Subject: [PATCH 3/4] Increase description character limit from 140 to 250 140 characters is too short for a meaningful package description. 250 characters allows 1-2 sentences while still keeping it concise. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joost de Valk --- schemas/metadata.schema.json | 4 ++-- specification.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/schemas/metadata.schema.json b/schemas/metadata.schema.json index 9b00928..a9790e2 100644 --- a/schemas/metadata.schema.json +++ b/schemas/metadata.schema.json @@ -91,8 +91,8 @@ }, "description": { "type": "string", - "description": "A short description of the package. Should not exceed 140 characters.", - "maxLength": 140 + "description": "A short description of the package. Should not exceed 250 characters.", + "maxLength": 250 }, "keywords": { "type": "array", diff --git a/specification.md b/specification.md index 6cedc45..80fddaa 100644 --- a/specification.md +++ b/specification.md @@ -359,7 +359,7 @@ The `description` property specifies a short description of the package, which t The description MUST be a string. -The description SHOULD be written in plain text, and clients MUST escape any special characters for the applicable formatting context (such as HTML). The description SHOULD NOT exceed 140 characters. Clients MAY truncate the description if it exceeds this limit. +The description SHOULD be written in plain text, and clients MUST escape any special characters for the applicable formatting context (such as HTML). The description SHOULD NOT exceed 250 characters. Clients MAY truncate the description if it exceeds this limit. ### keywords From a3730e6e6d72d40970398293195a8e0af7e67ff3 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Fri, 29 May 2026 08:49:12 +0200 Subject: [PATCH 4/4] Address review: entitlement re-auth and proof lifetime - Add require-reauth to entitlements object (vendor opt-in to disable proof caching) - Document re-verification rules: cached proofs expire via exp, are discarded on repo 401, refresh on 403 - Remove the 24h cap on proof exp; expiry is at the entitlement service's discretion - Document typical proof lifetimes per entitlement type in the registry - Walk through expiry strategy in docs/implementing/restricted.md Addresses toderash review on PR #66. --- docs/implementing/restricted.md | 30 ++++++++++++++++++++++++++++++ registry.md | 14 ++++++++------ specification.md | 20 ++++++++++++++++++-- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/docs/implementing/restricted.md b/docs/implementing/restricted.md index 7fafbee..08cd247 100644 --- a/docs/implementing/restricted.md +++ b/docs/implementing/restricted.md @@ -64,6 +64,36 @@ Available entitlement types: | `license-key` | Software with traditional license key activation | | `free-registration` | Free plugins that require vendor registration | +#### When entitlements expire and how often clients re-check + +The protocol is deliberately agnostic about how an entitlement expires. The vendor's entitlement service controls re-verification timing through the `exp` claim on the JWT proof it issues — not through fields in the metadata. This keeps the metadata stable across renewals, plan changes, and authentication-method changes. + +In practice: + +- For a **monthly subscription**, issue proofs valid for a few days to a billing cycle (e.g., 1–30 days). When the subscription renews out of band, the next refresh will simply succeed and the client keeps going. +- For a **one-time purchase**, issue long-lived proofs (months). Use the `401`/`403` revocation flow if a refund or chargeback occurs. +- For a **license-key** entitlement, set `exp` to the licence's own expiry. A 365-day licence becomes a proof with `exp = now + 365 days`. +- For **free-registration**, issue long-lived proofs (weeks to a year). Clients refresh on demand if the vendor revokes. +- For **high-security plugins** where you want a re-check at least every shift, issue short proofs (e.g., 8 hours). + +Clients are required to cache proofs until `exp` and to refresh on `401`/`403`. This means you do **not** need to perform a fresh entitlement check on every page load — once a client has a valid cached proof, it reuses it until expiry or until the repository rejects it. + +If you need to force clients to skip caching and re-verify on every install/update — for example, for strict per-seat licence enforcement — set `require-reauth: true` on the entitlements object: + +```json +{ + "entitlements": { + "service": "https://licenses.example.com/verify", + "type": "license-key", + "require-reauth": true, + "hint": "Each install verifies your seat allocation in real time.", + "hint_url": "https://example.com/seats" + } +} +``` + +Use `require-reauth: true` sparingly: it disables proof caching and adds a verification round-trip to every protected action. + ### 3. Set up repository authentication diff --git a/registry.md b/registry.md index f7b4720..c8d8a5d 100644 --- a/registry.md +++ b/registry.md @@ -30,9 +30,11 @@ This document is a registry for the known extensions to the FAIR Package Managem ## Entitlement Types -| Type | Description | -| ------------------- | -------------------------------------------------------------- | -| `subscription` | Active subscription required to access the package. | -| `purchase` | One-time purchase required to access the package. | -| `license-key` | Valid license key required to access the package. | -| `free-registration` | Free to use, but requires registration with the vendor. | +The "Typical proof lifetime" column is non-normative guidance for vendors choosing the JWT `exp` claim on [entitlement proofs](./specification.md#entitlement-proof). Vendors MAY use any lifetime appropriate to their entitlement model; the spec does not impose a maximum. + +| Type | Description | Typical proof lifetime | +| ------------------- | ------------------------------------------------------- | ----------------------------------------------- | +| `subscription` | Active subscription required to access the package. | Days to one billing cycle (e.g., 1–30 days) | +| `purchase` | One-time purchase required to access the package. | Long-lived (months); refresh on revocation only | +| `license-key` | Valid license key required to access the package. | Up to the licence's own expiry | +| `free-registration` | Free to use, but requires registration with the vendor. | Long-lived (weeks to a year) | diff --git a/specification.md b/specification.md index 80fddaa..a6d6b1c 100644 --- a/specification.md +++ b/specification.md @@ -403,6 +403,9 @@ This property MUST be a valid object, represented as a JSON Object, with the fol * `hint` (optional) - A string. A human-readable hint to display to the user explaining what is required to access the package. The hint SHOULD be written in plain text, and clients MUST escape any special characters for the applicable formatting context (such as HTML). The hint SHOULD NOT exceed 140 characters. Clients MAY truncate the hint if it exceeds this limit. * `hint_url` (optional) - A URL string. A URL for more information about the access requirements, such as a purchase page or subscription signup. * `scope` (optional) - A string or list of strings. Specifies what the entitlement controls. The value `"artifacts"` (the default) indicates that only artifact downloads require entitlement verification. The value `"metadata"` indicates that all access including metadata requires entitlement verification. A list of strings indicates that only the specified artifact types require entitlement verification. +* `require-reauth` (optional) - A boolean. When `true`, clients MUST NOT cache [entitlement proofs](#entitlement-proof) for this package and MUST run a fresh [verification request](#verification-request) before each protected action (install, update, or artifact download). When `false` or omitted, clients MAY cache proofs up to the proof's `exp` claim. This property is a vendor-side override of the default caching behaviour; the proof's `exp` claim remains the authoritative re-verification deadline for cached proofs. + +Re-verification deadlines (when a cached proof becomes stale and the client must obtain a new one) are carried by the proof itself, in the JWT `exp` claim, not in the metadata. This keeps the metadata agnostic about authentication method and entitlement model: a `subscription` proof can be valid for 30 days, a high-security `license-key` proof for 8 hours, and a `free-registration` proof for a year, with the vendor's entitlement service choosing the appropriate value at issuance. See [Entitlement Proof](#entitlement-proof) for the full caching and re-verification rules. Clients MUST clearly communicate entitlement requirements to users before initiating any authentication or purchase flow. @@ -787,7 +790,7 @@ The JWT payload MUST contain the following claims: * `package_did` - The DID of the package this proof is valid for. * `iss` - The entitlement service URL that issued the proof. * `iat` - The time at which the proof was issued, as a Unix timestamp. -* `exp` - The time at which the proof expires, as a Unix timestamp. This value SHOULD NOT be more than 24 hours after `iat`. +* `exp` - The time at which the proof expires, as a Unix timestamp. The entitlement service chooses this value to balance revocation latency against verification cost; there is no protocol-defined maximum. As a guideline, a short value (for example, 8 hours) is appropriate when entitlements may be revoked at any moment, a longer value (for example, 30 days) is appropriate for typical subscription billing cycles, and a value aligned with the underlying licence or registration expiry is appropriate for `license-key` and `free-registration` entitlements. See the [entitlement type registry][entitlement-registry] for typical guidance per type. The JWT payload MAY contain the following claims: @@ -798,7 +801,20 @@ The JWT MUST be signed using a key that can be verified through the entitlement Repositories and caches which accept entitlement proofs MUST verify the JWT signature and expiration before granting access. Repositories SHOULD accept entitlement proofs as bearer tokens in the `Authorization` header. -Clients MAY cache entitlement proofs for the duration of their validity (until `exp`), and SHOULD reuse cached proofs for subsequent requests to the same package. + +### Caching and Re-verification + +Clients MAY cache entitlement proofs for the duration of their validity (until `exp`), and SHOULD reuse cached proofs for subsequent requests to the same package. This caching exists specifically to avoid running a verification request on every user interaction; clients SHOULD NOT verify entitlement on every page load or list refresh when a non-expired proof is available. + +When the [entitlements](#entitlements) property has `require-reauth` set to `true`, clients MUST NOT cache proofs across protected actions, even if `exp` has not yet been reached. + +Clients MUST treat a cached proof as no longer usable when any of the following occur, and MUST run a fresh [verification request](#verification-request) before retrying the protected action: + +1. The proof's `exp` claim is in the past or within the client's configured clock-skew margin. +2. A repository or cache responds with `401 Unauthorized` while the client is presenting the proof. This indicates the repository considers the proof revoked or otherwise no longer valid; the client MUST discard the cached proof. +3. The entitlement service responds to a refresh attempt with `403 Forbidden`. The client MUST discard the cached proof and SHOULD surface the returned `hint` and `hint_url` to the user. + +If the refreshed verification request returns `401 Unauthorized` or `402 Payment Required`, clients SHOULD prompt the user to supply or update credentials before retrying. [jwt]: https://datatracker.ietf.org/doc/html/rfc7519 [jwks]: https://datatracker.ietf.org/doc/html/rfc7517