Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 151 additions & 15 deletions docs/implementing/restricted.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,169 @@
# 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",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For type : subscription, should also add

"expires": <date/time as UTC>

"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 |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider a require-reauth field, boolean false, after a time interval, or after a UTC timestamp

need a means of forcing a re-check for subscriptions, license expiry, or enforce reauth (oauth) as needed.

#### 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

On each release that has restricted artifacts, set `auth` to tell clients how to authenticate with the repository:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and when reauthentication must occur, if applicable.


```json
{
"auth": {
"type": "bearer",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"auth-expiry": "----",

"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.
9 changes: 6 additions & 3 deletions docs/restricted.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
110 changes: 110 additions & 0 deletions ext-auth.md
Original file line number Diff line number Diff line change
@@ -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 <token>
```

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 <base64(username:password)>
```

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.
12 changes: 12 additions & 0 deletions registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ 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

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) |
Loading