Skip to content

feat(oauth-server): dynamic client registration and the api-key:create scope - #327

Merged
roncodes merged 2 commits into
release/v1.6.74from
feat/oauth-dynamic-registration
Oct 27, 2026
Merged

roncodes merged 2 commits into
release/v1.6.74from
feat/oauth-dynamic-registration

Conversation

@roncodes

@roncodes roncodes commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Lets software installed on many sites connect with "Continue with Fleetbase". The first user is the Fleetbase for WooCommerce plugin: every store runs on its own domain, and the OAuth server only accepts exact, pre-registered redirect URIs, so one shared client cannot work.

Dynamic client registration (RFC 7591)

  • POST /oauth/register (throttle 10/min), advertised as registration_endpoint in discovery.
  • Only software listed under oauth-server.registration.software may register, and only with the scopes listed there. Today that is fleetbase-woocommerce with organization:read and api-key:create. The request supplies the software id and exactly one redirect URI; nothing else is taken from it.
  • The redirect must be https, or http on loopback (then the client is a development client). No fragments, no custom schemes.
  • Clients are public (PKCE only), never official, and named after the redirect host, for example "Fleetbase for WooCommerce (shop.example.com)". That name is what the consent screen and Connected apps show, beside the redirect host the consent screen already displays.
  • Registering again with the same software and redirect URI returns the same client (oauth_clients.registration_key). A client an admin disabled is refused. oauth-server:prune deletes registered clients that never got a grant after unused_client_hours (24).
  • OAUTH_SERVER_REGISTRATION_ENABLED=false turns it off and removes it from discovery.

api-key:create scope

  • POST /oauth/v1/api-key creates a live API key named after the app in the token's organization, or returns the one that app created before. Keys are tracked by the new api_credentials.oauth_client_id, never by name, so a hand-made key with the same name is never handed out. The console sandbox header cannot turn it into a test key.
  • When an app's last grant in an organization is revoked (Connected apps, admin revoke, or disabling the app), the keys it minted there expire. An integration's connection therefore ends when the merchant removes it in Connected apps.

Installations of one shared app (for the Shopify app: one official client, one key per shop)

  • POST /oauth/v1/api-key takes an optional installation (string, at most 190 characters, no control characters; blank means none). With it, the key is found or minted by (company, client, installation) and named "{app} ({installation})". Without it, behaviour is unchanged.
  • DELETE /oauth/v1/api-key, same middleware and optional installation, expires only the calling app's key for that installation in the token's company (the app-wide key when omitted). 204 and idempotent; the grant and other installations' keys are untouched.
  • Every grant revocation path (Connected apps, admin revoke, disabling the app, refresh-token replay, the membership re-check) goes through OAuthGrant::revoke() or the bulk disable path, and both expire the app's keys once no grant is left, installation keys included.

Migration 2026_10_27_000001_add_oauth_registration_columns: oauth_clients.software_id, oauth_clients.registration_key (unique), api_credentials.oauth_client_id, api_credentials.oauth_installation.

No console change is needed: the consent screen already shows the app name and redirect host.

Test plan

  • New OAuthServerRegistrationTest (28 tests, including installations A and B getting different keys, repeat mints returning the same key, DELETE A leaving B, DELETE without installation leaving installation keys, grant revoke expiring all of them, and invalid installations refused): registration, idempotency, loopback development clients, refusals, disabled registration and clients, discovery, PKCE-only sign-in limited to the registered redirect, key create and reuse, ignoring same-name keys, scope required, expiry on the last grant and on disabling the app, pruning. The real migration runs in the test setup.
  • Full suite: 2432 tests pass. php-cs-fixer and the test date-drift check are clean.
  • Local stack: discovery shows registration_endpoint and api-key:create; registering the sandbox store twice returns the same client; an unknown software id gets invalid_client_metadata; /oauth/authorize hands the registered redirect to the console consent screen and refuses a foreign redirect on Fleetbase.
  • End to end with the WooCommerce plugin's "Connect with Fleetbase" against the local stack.

…e scope

Software installed on many sites, such as the Fleetbase for WooCommerce plugin,
cannot share one registered redirect URI, so each install now registers its own
public PKCE client at POST /oauth/register (RFC 7591).

- Only software listed under oauth-server.registration.software may register,
  with the scopes listed there; the request supplies the software id and exactly
  one redirect URI. Clients are never official and are named after the
  redirect host, so the consent screen and Connected apps show the site.
  Registering again returns the same client; disabled clients are refused;
  clients that never get a grant are pruned after a day. Advertised as
  registration_endpoint in discovery; OAUTH_SERVER_REGISTRATION_ENABLED turns
  it off.
- api-key:create scope and POST /oauth/v1/api-key: creates or returns the live
  API key an app minted in the token's organization, tracked by the new
  api_credentials.oauth_client_id rather than by name.
- Revoking an app's last grant in an organization (Connected apps, admin
  revoke, disabling the app) expires the keys it minted there.
@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ae7740e) to head (4200ce8).

Additional details and impacted files
@@                 Coverage Diff                  @@
##             release/v1.6.74      #327    +/-   ##
====================================================
  Coverage             100.00%   100.00%            
- Complexity              9562      9600    +38     
====================================================
  Files                    518       519     +1     
  Lines                  29685     29841   +156     
====================================================
+ Hits                   29685     29841   +156     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…i-key

For an app that serves many installations from one client, such as the Shopify
app (one official client, one key per shop):

- POST /oauth/v1/api-key takes an optional installation (string, at most 190
  characters). With it, the key is found or minted by (company, client,
  installation) and named "{app} ({installation})", stored in the new
  api_credentials.oauth_installation. Without it nothing changes.
- DELETE /oauth/v1/api-key, same scope and optional installation, expires only
  the calling app's key for that installation in the token's company (the
  app-wide key when installation is omitted). 204, idempotent; the grant and
  other installations' keys are untouched.
- Revoking the app's last grant in the organization still expires all of its
  keys, installation keys included.
@roncodes
roncodes merged commit 64245ec into release/v1.6.74 Oct 27, 2026
5 checks passed
@roncodes
roncodes deleted the feat/oauth-dynamic-registration branch October 27, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant