Turn your WordPress site into a fully featured OpenID Connect (OIDC) identity provider.
Keystone OIDC is a WordPress plugin that exposes standard OIDC / OAuth 2.0 endpoints so that other applications — dashboards, CLIs, mobile apps, or any OIDC-aware tool — can authenticate users against your existing WordPress user database.
No external identity provider or third-party service is required.
- Features
- Requirements
- Installation
- Quick Start
- Admin UI walkthrough
- How it works
- OIDC Endpoint reference
- Scopes & Claims
- Releases & CI pipeline
- Troubleshooting
- Contributing
- License
| ✅ | OIDC Authorization Code Flow (RFC 6749 / OpenID Connect Core 1.0) |
| ✅ | PKCE – S256 and plain code-challenge methods (RFC 7636) |
| ✅ | RS256 JWT signed access tokens and ID tokens |
| ✅ | Refresh tokens with single-use rotation |
| ✅ | OIDC Discovery document (/wenisch-tech/keystone-oidc/.well-known/openid-configuration) for zero-config clients |
| ✅ | JWKS endpoint so clients can verify tokens without any shared secret |
| ✅ | Multi-client management – create and manage as many clients as needed |
| ✅ | Client secret reset – secrets shown only once, stored hashed |
| ✅ | Consent screen – users explicitly approve each application |
| ✅ | Signing key rotation from the admin panel |
| ✅ | Automatic cleanup of expired auth codes and revoked tokens |
| Requirement | Minimum version |
|---|---|
| WordPress | 5.6 |
| PHP | 7.4 |
| PHP extension | openssl |
| PHP extension | json |
Pretty permalinks must be enabled in WordPress (Settings → Permalinks). The plugin uses custom rewrite rules that do not work with the plain ?p=123 permalink structure.
- Download the latest
keystone-oidc-x.y.z.zipfrom the Releases page. - In your WordPress admin go to Plugins → Add New → Upload Plugin.
- Choose the downloaded ZIP and click Install Now.
- Click Activate Plugin.
- Download and unzip the release archive.
- Copy the
keystone-oidcfolder towp-content/plugins/. - Activate the plugin in Plugins → Installed Plugins.
- Three database tables are created:
{prefix}oidc_clients,{prefix}oidc_auth_codes,{prefix}oidc_tokens. - A 2048-bit RSA key pair is generated and stored in WordPress options (private key encrypted via WordPress option storage; never exposed through any UI).
- Rewrite rules for all OIDC endpoints are flushed automatically.
After activating the plugin you only need to do two things:
1. Create a client
Go to OIDC Provider → Add Client in the WordPress admin sidebar.
Fill in:
- Application Name – a friendly label (e.g. "My Dashboard")
- Redirect URIs – the callback URL(s) your application will use (one per line, e.g.
https://dashboard.example.com/auth/callback) - Allowed Scopes – tick
profileand/oremailif your app needs user info beyond the basic identity
Click Create Client. You will be shown the Client ID and Client Secret once – copy them now.
2. Configure your application
Point your OIDC client library at the discovery URL:
https://your-wordpress-site.example.com/wenisch-tech/keystone-oidc/.well-known/openid-configuration
Most OIDC libraries (e.g. openid-client for Node.js, python-jose, Keycloak adapters, Dex, etc.) will read this document and configure all endpoints automatically.
Provide the Client ID and Client Secret you copied in step 1, set the redirect URI to match what you registered, and request the openid scope (plus profile and/or email as needed).
That's it – your users can now sign in to external applications using their WordPress credentials.
OIDC Provider → Clients
Displays all registered clients in a table with their name, client ID, allowed scopes, and creation date. The discovery URL is shown at the top of the page for convenience.
From the list you can click Edit to open a client's detail page.
Add a client: OIDC Provider → Add Client
Edit a client: click Edit on the client list, or navigate to OIDC Provider → Clients and click a client name.
The edit screen has three sections:
Client Credentials (edit view only)
- Shows the Client ID with a copy button.
- Shows the discovery URL.
- Contains a Reset Secret button. Clicking it generates a new secret and invalidates the previous one. The new secret is displayed once on the following page.
Configuration form
- Application Name – displayed on the consent screen to users.
- Redirect URIs – the allowed callback URLs after authorization. One URI per line. Must be an exact match (no wildcard).
- Allowed Scopes – which scopes the client may request.
openidis always included.
Danger Zone
- Permanently Delete Client and all its associated tokens.
OIDC Provider → Settings
Displays read-only information about the server:
- Issuer URL (the WordPress site URL)
- All endpoint URLs ready to copy
- Current signing key ID (
kid) - Plugin version
Rotate Signing Keys – generates a new RSA-2048 key pair. All previously issued access tokens and ID tokens will immediately become invalid. Use this after a suspected key compromise.
Client App WordPress (OIDC Provider) User
│ │ │
│── GET /wenisch-tech/keystone-oidc/oauth/authorize ──── │ │
│ ?response_type=code │ │
│ &client_id=… │ │
│ &redirect_uri=… │ │
│ &scope=openid profile │ │
│ &state=… │ │
│ │── redirect to wp-login ──── │
│ │ (if not logged in) │
│ │ │
│ │◄──── user logs in ──────── │
│ │ │
│ │── show consent screen ───── │
│ │ │
│ │◄── user clicks Allow ────── │
│ │ │
│◄── redirect to redirect_uri │ │
│ ?code=…&state=… │ │
│ │ │
│── POST /wenisch-tech/keystone-oidc/oauth/token ─────── │ │
│ grant_type=authorization_code │
│ code=… redirect_uri=… │ │
│ client_id=… client_secret=│ │
│ │ │
│◄── access_token + id_token │ │
│ + refresh_token ──────── │ │
│ │ │
│── GET /wenisch-tech/keystone-oidc/oauth/userinfo ────── │ │
│ Authorization: Bearer … │ │
│ │ │
│◄── { sub, name, email, … } │ │
- The client redirects the browser to
/wenisch-tech/keystone-oidc/oauth/authorizewith the standard parameters. - If the user is not logged in, WordPress's own login flow handles authentication and then redirects back to the authorize endpoint.
- A consent screen lists the scopes being requested and asks the user to Allow or Deny.
- On approval, a short-lived authorization code (valid 10 minutes, single-use) is issued and the browser is redirected to the client's
redirect_uri. - The client's back-end exchanges the code for tokens by calling
/wenisch-tech/keystone-oidc/oauth/token(server-to-server). - The plugin returns an access token (RS256 JWT), an ID token (RS256 JWT), and a refresh token.
- The client can call
/wenisch-tech/keystone-oidc/oauth/userinfoat any time with the Bearer access token to retrieve up-to-date user claims.
| Token | Lifetime |
|---|---|
| Authorization code | 10 minutes |
| Access token (JWT) | 1 hour |
| ID token (JWT) | 1 hour |
| Refresh token | 30 days |
Refresh tokens use single-use rotation: each time a refresh token is used, it is revoked and a new one is issued alongside a fresh access token.
Proof Key for Code Exchange protects public clients (e.g. SPAs, mobile apps) that cannot safely store a client secret.
When the authorization request includes a code_challenge, the plugin stores it alongside the auth code. The token endpoint then validates the code_verifier before issuing tokens. Both S256 (SHA-256 hash, preferred) and plain methods are supported.
On activation the plugin generates an RSA-2048 key pair using PHP's openssl extension. The private key is stored in the WordPress options table. The public key is published through the JWKS endpoint (/wenisch-tech/keystone-oidc/oauth/jwks) so that any client or resource server can verify tokens independently without trusting the plugin directly.
The key ID (kid) in each JWT header lets clients efficiently look up the correct key from the JWKS when multiple keys are present during rotation.
Client secrets are never stored in plaintext. When a secret is created or reset, the plugin:
- Generates a cryptographically random 64-character hex string.
- Hashes it with WordPress's
wp_hash_password()(bcrypt). - Stores only the hash in the database.
- Displays the plaintext secret once in the admin UI via a short-lived (5-minute) transient.
After the page is loaded once, the plaintext is discarded and cannot be recovered — only reset.
All URLs are relative to your WordPress site root (e.g. https://example.com).
| Endpoint | Method | Path |
|---|---|---|
| Discovery | GET | /wenisch-tech/keystone-oidc/.well-known/openid-configuration |
| Authorization | GET / POST | /wenisch-tech/keystone-oidc/oauth/authorize |
| Token | POST | /wenisch-tech/keystone-oidc/oauth/token |
| UserInfo | GET | /wenisch-tech/keystone-oidc/oauth/userinfo |
| JWKS | GET | /wenisch-tech/keystone-oidc/oauth/jwks |
Returns the OIDC Discovery document as JSON. Point your OIDC client library at this URL for automatic configuration.
{
"issuer": "https://example.com",
"authorization_endpoint": "https://example.com/wenisch-tech/keystone-oidc/oauth/authorize",
"token_endpoint": "https://example.com/wenisch-tech/keystone-oidc/oauth/token",
"userinfo_endpoint": "https://example.com/wenisch-tech/keystone-oidc/oauth/userinfo",
"jwks_uri": "https://example.com/wenisch-tech/keystone-oidc/oauth/jwks",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
"code_challenge_methods_supported": ["S256", "plain"]
}Starts the authorization flow. Required parameters:
| Parameter | Required | Description |
|---|---|---|
response_type |
✅ | Must be code |
client_id |
✅ | The client's ID |
redirect_uri |
✅ | Must exactly match a registered URI |
scope |
✅ | Space-separated; must include openid |
state |
recommended | Opaque value echoed back to prevent CSRF |
nonce |
recommended | Included in the ID token to prevent replay |
code_challenge |
PKCE | Base64url-encoded challenge |
code_challenge_method |
PKCE | S256 or plain |
Exchanges an authorization code or refresh token for tokens.
Client authentication – choose one:
- HTTP Basic Authentication:
Authorization: Basic base64(client_id:client_secret) - POST body parameters:
client_id+client_secret
Authorization Code grant (grant_type=authorization_code):
| Parameter | Required | Description |
|---|---|---|
grant_type |
✅ | authorization_code |
code |
✅ | Authorization code from the authorize endpoint |
redirect_uri |
✅ | Must match the value used in the authorize request |
client_id |
✅ | |
code_verifier |
PKCE | Required if code_challenge was used |
Refresh Token grant (grant_type=refresh_token):
| Parameter | Required | Description |
|---|---|---|
grant_type |
✅ | refresh_token |
refresh_token |
✅ | The refresh token |
client_id |
✅ |
Response:
{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "<JWT>",
"refresh_token": "<opaque string>",
"scope": "openid profile email"
}Returns claims for the authenticated user. Requires a valid Bearer token.
Authorization: Bearer <access_token>
Returns the public RSA key in JWKS format. Use this to verify RS256-signed JWTs without contacting the token endpoint.
| Scope | Claims included |
|---|---|
openid |
sub, iss, aud, iat, exp, jti |
profile |
name, given_name, family_name, preferred_username |
email |
email, email_verified |
The sub claim is always the WordPress user's numeric ID (as a string), ensuring stable, durable identity.
Every push to main automatically bumps the version (patch by default, following Conventional Commits) via the CI workflow which:
- Computes the next semver tag and generates a changelog.
- Patches the
Version:header inkeystone-oidc.phpandStable tag:inreadme.txt. - Creates a
keystone-oidc-x.y.z.ziparchive withkeystone-oidc/as the root folder (the layout WordPress expects). - Publishes a GitHub Release with the ZIP attached as a downloadable asset.
No manual tagging is required — just push to main and the pipeline handles everything.
The resulting ZIP can be uploaded directly via Plugins → Add New → Upload Plugin in WordPress.
- Make sure pretty permalinks are enabled (
Settings → Permalinks). Save the permalinks page once after activating the plugin to flush rewrite rules. - If you recently activated the plugin and are still seeing 404s, visit
Settings → Permalinksand click Save Changes to force a flush.
The redirect URI in the authorization request must be an exact match (including scheme, host, path, and any trailing slash) with one of the URIs registered for the client. Check for trailing slashes and http vs https differences.
- The
client_iddoes not exist. Double-check the value you configured in your application. - If you recently deleted and recreated a client, update the client ID in your application.
The public key in the JWKS may have changed since the token was issued (e.g. after a key rotation). Fetch the JWKS again to get the current public key. Most well-behaved OIDC clients do this automatically when they encounter an unknown kid.
Ensure the openssl PHP extension is installed and enabled. On most Linux distributions:
php -m | grep opensslIf missing, install it (e.g. sudo apt-get install php-openssl on Debian/Ubuntu) and restart your web server.
This can happen if WordPress's wp_login_url() filters the redirect URL. Ensure the allowed_redirect_hosts WordPress filter includes your site's domain, or that no security plugin is aggressively blocking redirect parameters.
- Fork the repository and create a feature branch.
- Make your changes following the WordPress Coding Standards.
- Open a pull request against
main.
To create a development release, push a tag:
git tag v0.0.1-dev
git push origin v0.0.1-devThis plugin is licensed under the GNU General Public License v2 or later, the same license as WordPress itself.
