GitHub Sponsors Reference Implementation

Overview

The reference implementation of the SponsorLink specification leverages GitHub Sponsors to provide a simple and secure way for OSS projects on GitHub to properly attribute sponsorsing status to their users on their dev machine, to enable sponsorable libraries and tools to provide sponsor-specific features or benefits.

The reference implementation comprises two parts:

  • Issuer backend service for self-hosting by sponsorable accounts.
  • CLI tool for users to sync their sponsor manifest(s) with the backend service(s).

The primary goal of the reference implementation is to provide a simple way for sponsorables to issue sponsor manifests and for sponsors to sync them locally. The backend service is self-hosted by the sponsorable account, ensuring that no private data is shared with the SponsorLink developer.

The user-facing tool is implemented as a dotnet global tool which can be installed by running the following command:

1
dotnet tool install -g dotnet-sponsor

(or dotnet tool update -g dotnet-sponsor to update to the latest version).

On first run, the tool provides the usage terms, private policy and asks for consent to proceed.

The user subsequently runs sponsor sync [account]* to sync the manifest for the given account(s) for offline use while consuming sponsorable libraries.

Whenever run, the tool performs the following steps:

  1. If no accounts were provided, automatic discovery is offered, which involves using the the GitHub CLI API to determine sponsorable candidate accounts for the current user, which are:

    • All directly sponsored accounts
    • Publicly sponsored accounts by organizations the user is a member of
    • Sponsorables of repositories the user has contributed to, considered indirect sponsporships
  2. Each account is checked for a SponsorLink manifest at https://github.com/[account]/.github/raw/main/sponsorlink.jwt. This location is the same as the GitHub default community health files
  3. If found, the sponsorable manifest client_id and iss claims are used to authenticate with the sponsorable accountā€™s backend service and request the updated sponsor manifest.

Running sponsor sync [account]* will sync the manifest for specific account(s), and typically be much faster than the entire discovery + sync for all candidate accounts.

This implementation honors the recommended convention for manifest location and places them at ~/.sponsorlink/github/[sponsorable].jwt.

Authentication

In order to avoid sharing any data (either by the sponsorable as well as the sponsor) with the SponsorLink developer, the tool relies on self-hosting of the issuer backend by each sponsorable, as well as the use of a sponsorable-provided GitHub OAuth apps to authenticate the user and request the necessary permissions to issue the sponsor manifest. In order to identify this OAuth app, this implementation requires an additional claim in the sponsorable manifest:

client_id

A required standard client_id OAuth 2.0 client identifier claim provided in the sponsorable manifest for client authentication with the issuer through the GitHub OAuth app.

Example:

1
2
3
4
5
6
7
8
9
10
{
  "iss": "https://sponsorlink.devlooped.com/",
  "aud": "https://github.com/sponsors/devlooped",
  "client_id": "asdf1234",
  "sub_jwk": {
    "e": "AQAB",
    "kty": "RSA",
    "n": "5inhv8Q..."
  }
}

Authentication is performed using the device flow, where the sponsor gets a chance to review the requested permissions prior to granting access. The access token is then used to request the sponsor manifest from the sponsorable backend service.

The default browser is launched to authenticate and enter the device code that is automatically copied to the clipboard by the CLI extension.

To avoid having to authenticate every time for each sponsorable account, the tool caches the access token by client_id using the cross-platform credential store API provided by the Git Credential Manager.

For unattended use (i.e. in CI/CD), the tool can be provided with a --with-token option to read the token from the standard input, which can be piped from a secure store or environment variable, such as:

1
2
- name: šŸ” sync
  run: '${{ secrets.GH_TOKEN }}' | sponsor sync devlooped --tos --with-token

Telemetry

The dotnet-sponsor tool does not collect any telemetry by itself. Sponsor backend services may collect anonymous usage telemetry to improve your experience, however. Such telemetry may be associated by default with an opaque and random identifier of the tool installation that is not linked to any personal information and is sent as a custom header x-telemetry-id when invoking the backend service.

Telemetry data helps the backend team understand how its APIs are used by the tool so they can be improved. To opt out of associating the backend API invocations with your tool installation, set the SPONSOR_CLI_TELEMETRY_OPTOUT environment variable to 1 or true.

Sponsoring Checks

SponsorLink-enabled libraries and tools can use the previously synchronized sponsor manifest to check the sponsoring status of the user. These checks are entirely offline and do not require any network access. Even though the publicly available sponsorable manifest contains the public key to verify the signature, the libraries and tools themselves would typically embed this public key for purely offline verification.

This SponsorLink implementation does not dictate how a specific sponsorable library or tool integrates these checks, it only provides the manifest format and predictable location for the manifest.

Typical ways in which this might be integrated are:

  • An editor-only build warning that never causes a build failure or shows up in CLI builds
  • A message whenever a CLI (i.e. dotnet global tool) is run as a reminder
  • Additional functionality unlocked when sponsoring (i.e. improved intellisense), but without affecting CI/CLI builds (same code should compile with/without sponsoring).

Auto Sync

When running the sponsor sync command, the tool will ask whether to enable auto-sync. If enabled, tools and libraries checking the manifest can attempt to refresh an expired manifest in an exclusively non-interactive way, by using the cached access token to request a new manifest from the sponsorable backend service.

This unattended refresh is subject to the same consent and permissions as the initial sync, and it may not succeed (i.e. an interactive authentication to get new consent is needed). The user can disable auto-sync at any time by running sponsor sync --autosync=false.

By enabling autosync, you agree to allow other tools and libraries to automatically check for updates to expired sponsor manifests. This involves network access to the manifest issuer.

Tools and libraries can check for this user preference setting by reading the dotnetconfig-formatted file at ~/.sponsorlink/.netconfig.

1
2
[sponsorlink]
  autosync = [true|false]

Sponsorable Setup

Three steps are required to set up a GitHub account to issue sponsor manifests:

  1. Create a GitHub OAuth app
  2. Generate and upload a sponsorable manifest
  3. Self-host the issuer backend service

GitHub OAuth App

Follow the steps for creating an OAuth app and make sure you enable Device Flow for it. Make note of the Client ID as well as a new Client Secret you will create. Fill in the application name, icon, description and other details as needed. Your sponsors will see this information when they authenticate for the first time.

The authorization callback URL will be https://[app].azurewebsites.net/.auth/login/github/callback (unless you provide a custom domain for your Azure Functions app), where [app] is the name of your Azure Functions app where youā€™ll deploy the issuer backend service.

Sponsorable Manifest

Next you will need to create a valid sponsorable manifest and upload it to your GitHub accountā€™s community health files repo (.github repo root).

The sponsor tool provides a command to generate it, init:

1
2
3
4
5
6
7
8
9
10
11
12
13
> sponsor init --help
DESCRIPTION:
Initializes a sponsorable manifest and token

USAGE:
    sponsor init [OPTIONS]

OPTIONS:
    -h, --help                 Prints help information
    -i, --issuer               The base URL of the manifest issuer web app
    -a, --audience <VALUES>    The intended audience or supported sponsorship platforms, e.g. https://github.com/sponsors/curl
    -c, --clientId             The Client ID of the GitHub OAuth application created by the sponsorable account
    -k, --key                  Existing private key to use. By default, creates a new one

If you were initializing the sponsorable manifest for the curl GitHub account, you would run:

1
sponsor init -i https://curl.azurewebsites.net -a https://github.com/sponsors/curl -c [client_id]

This will generate a few files in the current directory:

  • curl.key: a newly created RSA 3072bits private key in PKCS#1 binary format
  • curl.key.jwk: same key in JWK format
  • curl.key.txt: same key in Base64-encoded format
  • curl.pub: corresponding public key in PKCS#1 binary format
  • curl.pub.jwk: same public key in JWK format
  • curl.pub.txt: same public key in Base64-encoded format
  • sponsorlink.jwt: the sponsorable manifest in JWT format

Place the sponsorlink.jwt file in the .github repo root of the main branch for the curl account. Move the other files to a secure location, as they are needed to sign the sponsor manifests. The Base64-encoded public and private keys are used by the issuer backend service next.

You can copy this real world SponsorLink manifest into jwt.io and inspect its contents.

Sponsorable Backend Self-Hosting

The reference implementation of the GitHub-based issuer backend service is an Azure Functions app that provides a REST API to issue sponsor manifests. The sync tool makes no additional assumptions about the backend other than:

  1. It provides a /me endpoint that accepts a Authorization: Bearer [token] header with an access token obtained by authenticating with the sponsorableā€™s GitHub OAuth app.
  2. It returns a signed sponsor manifest in the standard format with the required claims that can be verified against the public key provided in the sponsorlink.jwt sponsorable manifest.
  3. The returned manifest contains roles for the user, determined based on the token provided.

The /me is implemented to allow interactive browsing to the backend URL to test the authentication and configuration. Tools must specify an Accept: application/jwt header to get the sponsor manifest in JWT format, otherwise the response will be in JSON format with mostly the same information plus the GitHub authentication user claims.

For manifest validation, a tool or client can append the /jwt endpoint to the issuerā€™s base URL to get the the full sponsorable manifest in the standard format, from which the public key can be extracted and used to verify the signature of a cached sponsor manifest. Alternatively, the /jwk endpoint can be used to get the public key in JWK format (basically to get just the sub_jwk claim from the full sponsorable manifest).

To deploy and configure the backend:

  1. Fork the SponsorLink repository
  2. Create an Azure Functions app
  3. Setup deployment to the Azure Functions app from your forked repository
  4. Configure the following required application settings:
    • GitHub:Token: a GitHub token with permissions to read the sponsorable profile, emails, sponsorships and repositories
    • SponsorLink:Account: the sponsorable GitHub account name, unless itā€™s the same as the GitHub token owner
    • SponsorLink:PrivateKey: the Base64-encoded private key (the contents of curl.key.txt in the example above)

Finally, enable the GitHub identity provider under Settings > Authentication, providing the OAuth appā€™s Client ID and Client Secret. Make sure you set Allow unauthenticated requests and have Token store enabled.

At this point, go back to the GitHub OAuth app settings and and update the Authorization callback URL to https://[app].azurewebsites.net/.auth/login/github/callback.

The backendā€™s authentication and configuration can be tested manually by navigating to https://[app].azurewebsites.net/me, which would redirect to the GitHub OAuth app for authentication and upon returning to your issuer site, return the userā€™s profile and claims as JSON.

Conclusion

By requiring a GitHub OAuth app for each the sponsorable, the reference implementation avoids having a central authority for issuing manifests, which would require sharing private data (e.g. sponsorships and repository contributions) with the SponsorLink developer. In addition, this approach provides an explicit consent step where sponsors can decide to grant only the necessary permissions to the sponsorable account for manifest issuance.

The user, in turn, can conveniently revoke access at any time in their GitHub applications settings.