# wpe-auth-core

Shared PHP library that handles every part of the OAuth 2.0 / `oauth-ext`
interaction for WP Engine plugins: PKCE bootstrap, RFC 7591 Dynamic Client
Registration, Ed25519 `private_key_jwt` assertion, token caching, OAuth
discovery, and a thin authenticated HTTP client.

## Purpose

- Implement Connect-to-WPE end-to-end so individual plugins do not duplicate
  OAuth code.
- Cache and refresh access tokens transparently.
- Discover the active environment's `oauth-ext`, AI Gateway, and console URLs
  from a single source of truth.

## Distribution

The library lives at `lib/php/wpe-auth-core/` and is bundled into each
consuming plugin under `vendor-lib/auth/`. A loader runs on the
`plugins_loaded` action at priority `-999` and compares the
`WPE_AUTH_CORE_VERSION` constant across every bundled copy. The highest
version wins; older copies short-circuit and never define their classes.
This guarantees a single, deterministic `WpeAuthCore` instance across all
active WP Engine plugins.

## Components

| Class / file | Responsibility |
|---|---|
| `WpeAuthCore` | Singleton facade. `instance()`, `connect()`, `token()`, `register()`, `gateway_url()`, `console_url()`, `catalog_url()`, `environments()`. |
| `WpeAuthCoreConnect` | Handles the PKCE callback URL when the user returns from `oauth-ext`. |
| `WpeAuthCoreRegister` | Exchanges the PKCE code for a bootstrap token, generates an Ed25519 keypair, calls the `oauth-ext` DCR endpoint, mints the first `client_credentials` access token via `private_key_jwt`, and notifies the site-registration catalog (served in-process by ai-gateway). |
| `WpeAuthCoreToken` | Caches access tokens via WordPress transients with a 300-second refresh buffer. `get_token()` returns a still-valid token, refreshing if needed. On copy-detection (`wpe_auth_site_copied`) the credentials are wiped and the human-readable message is written to the persistent `wpe_auth_copy_detected` option (cleared on re-registration). `enforce_valid_registration()` exposes a public, no-network entry point for re-running the credential validity checks without minting a token. |
| `WpeAuthCoreCrypto` | Ed25519 keypair generation and JWT assertion signing. |
| `WpeAuthCoreClient` | `wp_remote_request` wrapper that injects the `Authorization` header and retries once on HTTP 401 or 403. |
| `WpeAuthCoreDiscovery` | Fetches `.well-known/oauth-authorization-server` from `oauth-ext` to resolve the token, DCR, and authorize endpoints. |
| `WpeAuthCorePkce` | Generates the PKCE code verifier, challenge, and `state` parameter. |

## Environments

`WpeAuthCore::environments()` returns a map keyed by `Production`, `Staging`,
and `Development`. Each entry exposes:

- `oauth-ext` base URL
- AI Gateway URL
- Console URL

The active environment is read from the `wpe_auth_active_environment`
WordPress option; fresh installs default to `Production`. Switching
environments via the WPE Debug admin-bar modal updates this option, which in
turn changes every URL the library resolves.

## OAuth resource and scope

| Constant | Value |
|---|---|
| `OAUTH_EXT_RESOURCE` | `urn:wpengine:registration` |
| `OAUTH_EXT_SCOPE` | `urn:wpengine:wordpress` |
| `OAUTH_EXT_SCOPE_LEGACY` | `wpengine:wordpress` |

Both `OAUTH_EXT_RESOURCE` and `OAUTH_EXT_SCOPE` are passed to `oauth-ext` on
every `client_credentials` call. `oauth-ext` currently requires both as
duplicate form fields — see the inline comments in
`class-wpe-auth-core-register.php` and `class-wpe-auth-core-token.php` for the
explanation.

The scope is an RFC 8141 URN —
`urn:<namespace-identifier>:<namespace-specific-string>` — matching the shape
of the resource indicator and of the Okta admin scope
(`urn:wpengine:accounts:rw`). It was renamed from the pre-URN
`wpengine:wordpress` in library version 1.6.0.

### Requesting the scope the client actually holds

A client is only ever *granted* the scopes its `oauth-ext` registration record
allows. Renaming the constant changed what the plugin **requests**; it could not
change what any already-registered client is **granted**. A site that registered
before 1.6.0 holds a client registered for `wpengine:wordpress`, so requesting
`urn:wpengine:wordpress` for it comes back either as `invalid_scope` or — since
`oauth-ext` narrows a request to the record — as a token carrying no site scope
at all. ai-gateway then rejects that token with `403 insufficient scope` for its
whole lifetime, which is why re-registering (a fresh DCR under the new name) was
the only way to recover. Accepting both spellings at the gateway does not help
here: the token contains neither.

So the requested value follows the client, not the library version.
`Token::CLIENT_SCOPE_OPTION` (`wpe_auth_client_scope`) records which spelling
this site's client is registered for:

- written at registration time from `OAUTH_EXT_SCOPE`;
- learned on the first successful mint for clients that predate the option — the
  canonical spelling is tried first, and a token that comes back without a site
  scope (or an `invalid_scope` rejection) falls back to `OAUTH_EXT_SCOPE_LEGACY`
  and records it, so later refreshes cost one request;
- cleared with the other per-client credentials by `clear_registration()`, so a
  re-registered site is not pinned to its predecessor's scope.

Only a scope-shaped failure falls back. Any other error (network, signature,
revoked client) is returned unmasked, and a token carrying no site scope under
either spelling is still returned — it authenticates the same non-gateway calls
as before — with the condition logged for support.

Removing `OAUTH_EXT_SCOPE_LEGACY` requires every client registered under it to
be gone (re-registered, or migrated in `oauth-ext`), not merely every plugin
updated.

## Public exports

The wpe-auth plugin defines two global helpers backed by this library:

| Function | Behaviour |
|---|---|
| `wpe_auth_get_token()` | Returns a cached access token, or `WP_Error`. Refreshes when within the refresh buffer. |
| `wpe_auth_api_request($url, $args)` | Authenticated `wp_remote_request` wrapper; retries once on 401 / 403 after refreshing the token. |

These are convenience wrappers around `WpeAuthCore::instance()->token()` and
`WpeAuthCore::instance()->client()` respectively.

## Versioning

| Concern | Mechanism |
|---|---|
| Source of truth | `lib/php/wpe-auth-core/VERSION` (semver). |
| Constant | `WPE_AUTH_CORE_VERSION` defined from the `VERSION` file. |
| Selection | Loader at `plugins_loaded@-999` picks the highest version across all bundled copies. |

When releasing a change, bump `VERSION`, rebuild any bundled copies in
plugins, and verify the loader continues to select the new version at runtime.

## Used by

- [wpe-hub](../../../wpe-hub/docs/README.md)
- [wpe-ai-playground](../../../wpe-ai-playground/docs/README.md)
- [wpe-ai-connector](../../../wpe-ai-connector/docs/README.md)
