Secure OIDC Login
WordPress OIDC authentication plugin with PKCE, auto-discovery, environment variable support, and automatic user provisioning
by Not Glossy · github.com/notglossy/secure-oidc-login · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/notglossy/secure-oidc-login/archive/refs/heads/main.zipA secure OpenID Connect (OIDC) authentication plugin for WordPress that allows users to authenticate using any OIDC-compliant identity provider (IdP).
Features
- Standard OIDC Support: Works with any OIDC-compliant identity provider (Keycloak, Okta, Auth0, Azure AD, Google, etc.)
- Auto-Discovery: Automatically configure endpoints using the IdP's discovery URL
- PKCE Support: Implements Proof Key for Code Exchange for enhanced security
- User Provisioning: Automatically create WordPress users on first login
- Claim Mapping: Configurable mapping of OIDC claims to WordPress user fields
- Email Domain Filtering: Restrict authentication to specific email domains for multi-tenant scenarios
- Single Logout: Optional logout from IdP when logging out of WordPress
- Secure by Default: Uses state parameter for CSRF protection and validates all tokens
- Rate Limiting: IP-based rate limiting protects against brute force and DoS attacks
- SSRF Protection: All HTTP requests use WordPress's safe remote functions to prevent server-side request forgery
Requirements
- WordPress 5.8 or higher
- PHP 8.1 or higher
- HTTPS enabled (required for secure authentication)
- Composer (for development and building)
Installation
Option 1: Install Pre-built Package
- Download the latest release zip file
- In WordPress admin, go to Plugins > Add New > Upload Plugin
- Choose the downloaded zip file and click Install Now
- Click Activate Plugin
- Go to Settings > OIDC Auth to configure the plugin
Option 2: Install from Source
- Clone this repository or download the source files
- Run
./package.shto build the plugin package (see Building section below) - Upload the generated zip file from the
build/directory - Follow steps 2-5 from Option 1
Option 3: Manual Installation
- Download or clone the plugin source files
- Install production dependencies:
composer install --no-dev - Upload the
secure-oidc-loginfolder to/wp-content/plugins/ - Activate the plugin through the 'Plugins' menu in WordPress
- Go to Settings > OIDC Auth to configure the plugin
Building the Plugin
This plugin uses Composer for dependency management. To create a distributable package:
Prerequisites
- PHP 8.1 or higher
- Composer
zipcommand-line utility
Build Instructions
- Clone or download this repository
- Run the packaging script:
./package.sh
The script will:
- Install production dependencies via Composer
- Copy all necessary plugin files to a build directory
- Exclude development files (tests, configs, etc.)
- Create a zip file in the
build/directory namedsecure-oidc-login-{version}.zip - Restore development dependencies
The generated zip file is ready for distribution or installation on any WordPress site.
What's Included in the Package
- Plugin PHP files (
secure-oidc-login.php,includes/) - Production dependencies (
vendor/with firebase/php-jwt) - Documentation (
README.md)
What's Excluded from the Package
- Development dependencies (PHPStan, PHPCS, etc.)
- Configuration files (phpstan.neon, phpcs.xml, composer.json)
- IDE settings (.vscode/, .editorconfig)
- Git repository (.git/)
- Build artifacts and cache files
Configuration
Identity Provider Setup
Before configuring the plugin, you need to register your WordPress site as a client/application in your identity provider:
-
Redirect URI: Use the callback URL shown on the plugin settings page:
https://your-wordpress-site.com/?oidc_callback=1 -
Grant Type: Authorization Code
-
Scopes: At minimum, request
openid email profile
Plugin Settings
Navigate to Settings > OIDC Auth in your WordPress admin panel.
Identity Provider Settings
| Setting | Description | Required |
|---|---|---|
| Discovery URL | Your IdP's .well-known/openid-configuration URL. Click "Discover" to auto-populate endpoints. |
No |
| Client ID | The client ID from your IdP | Yes |
| Client Secret | The client secret from your IdP (for confidential clients) | No |
| Token Endpoint Auth Method | How client credentials are sent to the token endpoint: client_secret_basic (Authorization header, default) or client_secret_post (POST body) |
No |
| Authorization Endpoint | URL for the authorization endpoint | Yes |
| Token Endpoint | URL for the token endpoint | Yes |
| Userinfo Endpoint | URL for the userinfo endpoint | No |
| End Session Endpoint | URL for logout/end session | No |
| Issuer | Expected issuer value for token validation | Yes |
| Scope | OAuth scopes to request (default: openid email profile) |
No |
| Max Authentication Age | Maximum seconds since the user last authenticated at the IdP (max_age request parameter). When set, the ID token auth_time claim is required and verified. 0 disables. |
No |
| Prompt | OIDC prompt parameter: provider default, login (always re-prompt for credentials), consent, or select_account |
No |
Login Settings
| Setting | Description |
|---|---|
| Login Button Text | Text displayed on the SSO login button |
| Enable Single Logout | When enabled, logging out of WordPress also logs out of the IdP |
| Enable Back-Channel Logout | End WordPress sessions when the IdP reports a logout (OIDC Back-Channel Logout 1.0). Register <site>/?rest_route=/secure-oidc-login/v1/backchannel-logout (shown on the settings page) as the back-channel logout URI at your IdP. |
| Remember Users | Keep users logged in with WordPress's persistent 14-day cookie (default). Disable to use a session cookie that expires when the browser closes, aligning the WordPress session more closely with the IdP session. Also filterable via secure_oidc_login_remember_user. |
Back-Channel Logout
When enabled, the IdP can terminate WordPress sessions directly (server-to-server) the moment the user's IdP session ends — logout at the IdP, an admin-forced logout, or a session timeout. Without it, WordPress sessions survive until the auth cookie expires.
The endpoint validates the signed logout token per OIDC Back-Channel Logout 1.0 §2.6 (signature against the JWKS, iss, aud, events, sub/sid, no nonce) with single-use jti replay protection, then destroys all WordPress sessions for the matched user and clears their stored tokens.
Login Hints
A login link can pre-fill the IdP's identifier field by adding login_hint to the initiation URL:
https://example.com/wp-login.php?oidc_login=1&login_hint=user@example.com
Developers can add IdP-specific authorization request parameters with the secure_oidc_login_auth_params filter; security-critical parameters (state, nonce, PKCE, redirect URI) cannot be overridden.
User Settings
| Setting | Description |
|---|---|
| Create Users | Automatically create WordPress users for new OIDC users |
| Default Role | WordPress role assigned to new users |
| Require Verified Email | Require the identity provider to verify email addresses before linking/creating accounts. Enabled by default for security. Disable only for trusted IdPs. |
| Allowed Email Domains | Comma-separated list of allowed email domains (e.g., example.com,subsidiary.com). Leave empty to allow all domains. Supports wildcards like *.example.com for subdomains. |
| Username Claim | OIDC claim to use for WordPress username (default: preferred_username) |
| Email Claim | OIDC claim for email address (default: email) |
| First Name Claim | OIDC claim for first name (default: given_name) |
| Last Name Claim | OIDC claim for last name (default: family_name) |
Email Domain Filtering
Email domain filtering provides an additional layer of access control for multi-tenant scenarios where multiple organizations share the same identity provider.
Use Cases
- Subsidiary Control: Allow only employees from specific subsidiaries (e.g.,
example.com,subsidiary.com) - Vendor Access: Restrict vendor portal access to approved vendor domains
- Department Isolation: Limit access to specific departments using subdomain wildcards (e.g.,
*.hr.example.com) - Multi-tenant IdP: Filter users when multiple tenants authenticate through the same IdP
Configuration
Allow all domains (default): Leave the "Allowed Email Domains" field empty.
Single domain:
example.com
Only users with @example.com email addresses can authenticate.
Multiple domains:
example.com,subsidiary.com,partner.org
Users from any of these domains can authenticate.
Wildcard subdomains:
*.example.com
Matches both the base domain (user@example.com) and all subdomains (user@dept.example.com, user@hr.subsidiary.example.com, etc.).
Mixed configuration:
example.com,*.subsidiary.com,partner.org
Allows @example.com, any subdomain of subsidiary.com, and @partner.org.
Behavior
- Empty configuration: No filtering (all domains allowed)
- Case-insensitive:
Example.COMandexample.comare treated identically - Validation timing: Checked after email verification but before user lookup/creation
- Error message: Users from blocked domains see: "Your email domain (blocked.com) is not authorized to access this site. Please contact your administrator."
Security Considerations
- Domain filtering is an additional layer beyond email verification, not a replacement
- Validation happens server-side and cannot be bypassed
- Wildcards only work at the subdomain level (
*.example.com), not TLD level (example.*) - Use the
SECURE_OIDC_ALLOWED_EMAIL_DOMAINSenvironment variable to lock down domains in production without database access
Using Environment Variables
For enhanced security in production environments, you can override sensitive settings using environment variables instead of storing them in the WordPress database. This is particularly useful for containerized deployments and follows 12-factor app methodology.
Supported Environment Variables
Authentication Credentials:
SECURE_OIDC_CLIENT_ID- Overrides the Client ID settingSECURE_OIDC_CLIENT_SECRET- Overrides the Client Secret setting
Discovery:
SECURE_OIDC_DISCOVERY_URL- Pre-populates the Discovery URL field
OIDC Endpoints:
SECURE_OIDC_AUTHORIZATION_ENDPOINT- Overrides the Authorization EndpointSECURE_OIDC_TOKEN_ENDPOINT- Overrides the Token EndpointSECURE_OIDC_USERINFO_ENDPOINT- Overrides the Userinfo EndpointSECURE_OIDC_END_SESSION_ENDPOINT- Overrides the End Session (logout) EndpointSECURE_OIDC_JWKS_URI- Overrides the JWKS URI for token verification
Client Authentication:
SECURE_OIDC_TOKEN_ENDPOINT_AUTH_METHOD- Overrides the token endpoint auth method (client_secret_basicorclient_secret_post)
Token Validation:
SECURE_OIDC_ISSUER- Overrides the expected Issuer value for JWT validation
Access Control:
SECURE_OIDC_ALLOWED_EMAIL_DOMAINS- Overrides the Allowed Email Domains setting (comma-separated list)
Rate Limiting:
SECURE_OIDC_RATE_LIMIT_ATTEMPTS- Maximum attempts before lockout (1-100, default: 10)SECURE_OIDC_RATE_LIMIT_WINDOW- Time window in seconds (60-3600, default: 300)SECURE_OIDC_RATE_LIMIT_LOCKOUT- Lockout duration in seconds (60-86400, default: 900)
JWT Validation:
SECURE_OIDC_JWT_LEEWAY- Clock skew tolerance in seconds (1-600, default: 15)
HTTP Timeouts:
SECURE_OIDC_HTTP_TIMEOUT- Timeout in seconds for IdP HTTP requests (token exchange, JWKS, userinfo, discovery/refresh) (5-30, default: 10)
Authentication Flow:
SECURE_OIDC_STATE_TTL- State/nonce parameter expiration in seconds (60-600, default: 300)
Setting Environment Variables
On your server:
export SECURE_OIDC_CLIENT_ID="your-client-id"
export SECURE_OIDC_CLIENT_SECRET="your-client-secret"
export SECURE_OIDC_DISCOVERY_URL="https://your-idp.com/.well-known/openid-configuration"
export SECURE_OIDC_ALLOWED_EMAIL_DOMAINS="example.com,subsidiary.com"
Using .env file (with a WordPress .env loader):
SECURE_OIDC_CLIENT_ID=your-client-id
SECURE_OIDC_CLIENT_SECRET=your-client-secret
SECURE_OIDC_DISCOVERY_URL=https://your-idp.com/.well-known/openid-configuration
SECURE_OIDC_ALLOWED_EMAIL_DOMAINS=example.com,subsidiary.com
Using Docker (docker-compose.yml):
services:
wordpress:
environment:
SECURE_OIDC_CLIENT_ID: "your-client-id"
SECURE_OIDC_CLIENT_SECRET: "your-client-secret"
SECURE_OIDC_DISCOVERY_URL: "https://your-idp.com/.well-known/openid-configuration"
SECURE_OIDC_ALLOWED_EMAIL_DOMAINS: "example.com,subsidiary.com"
Using Kubernetes ConfigMap/Secret:
apiVersion: v1
kind: Secret
metadata:
name: oidc-credentials
type: Opaque
stringData:
SECURE_OIDC_CLIENT_ID: your-client-id
SECURE_OIDC_CLIENT_SECRET: your-client-secret
Admin UI Behavior
When environment variables are set:
- Client ID and Client Secret fields: Become disabled (read-only) with a notice indicating the environment variable is in use. The database value is preserved but ignored.
- Discovery URL field: Pre-populated with the environment variable value but remains editable, allowing you to modify it before clicking "Discover" if needed.
Advantages of Using Environment Variables
- Enhanced Security: Secrets are not stored in the WordPress database
- Container-Friendly: Compatible with Docker, Kubernetes, and other containerized deployments
- Infrastructure as Code: Works seamlessly with secrets management systems (AWS Secrets Manager, HashiCorp Vault, etc.)
- 12-Factor Compliance: Follows best practices for modern web applications
- Easy Rotation: Update credentials without modifying database or code
- Backward Compatible: Existing installations continue to work unchanged; removing environment variables automatically falls back to database values
Rate Limiting
The plugin includes built-in rate limiting to protect against brute force attacks and denial of service. Rate limiting is enabled by default with sensible limits.
Default Configuration
| Setting | Default | Description |
|---|---|---|
| Attempts | 10 | Maximum login/callback attempts before lockout |
| Window | 5 minutes | Time window for counting attempts |
| Lockout | 15 minutes | Duration of lockout after exceeding limit |
Customizing Rate Limits
Configure via environment variables:
# Allow 5 attempts per 2 minutes, with 30-minute lockout
export SECURE_OIDC_RATE_LIMIT_ATTEMPTS=5
export SECURE_OIDC_RATE_LIMIT_WINDOW=120
export SECURE_OIDC_RATE_LIMIT_LOCKOUT=1800
Rate Limited Endpoints
- Login initiation (
?oidc_login=1) - Prevents state exhaustion attacks - Callback handler (
?oidc_callback=1) - Prevents callback flooding - Discovery REST endpoint (
/wp-json/secure-oidc-login/v1/discover) - Prevents endpoint abuse
Rate limits are tracked per IP address and automatically clear on successful authentication.
Reverse Proxy / Load Balancer Configuration
If your WordPress site runs behind a reverse proxy (nginx, CloudFlare, AWS ALB, etc.), you need to enable proxy header trust so rate limiting uses the correct client IP:
// wp-config.php
define( 'SECURE_OIDC_TRUST_PROXY_HEADERS', true );
Or via environment variable:
SECURE_OIDC_TRUST_PROXY_HEADERS=true
When enabled, the plugin checks these headers in order:
X-Real-IPX-Forwarded-For(first IP in the list)Client-IP
Security Warning: Only enable this if your server is actually behind a trusted proxy. These headers can be spoofed by attackers if requests reach your server directly.
Important Security Considerations
- Verify your proxy configuration: Ensure your reverse proxy is stripping or overwriting client-provided
X-Forwarded-Forheaders before adding the real client IP - Disable in development: Leave
SECURE_OIDC_TRUST_PROXY_HEADERSundefined (defaults to false) for local development - Spoofing risk: If clients can connect directly to your WordPress server (bypassing the proxy), attackers can spoof these headers to bypass rate limiting
- Testing: After enabling, verify rate limiting works correctly by testing with multiple IPs behind your proxy
Provider-Specific Configuration
Keycloak
- Create a new client in your Keycloak realm
- Set Client Protocol to
openid-connect - Set Access Type to
confidentialorpublic - Add your WordPress callback URL to Valid Redirect URIs
- Discovery URL:
https://your-keycloak.com/realms/your-realm/.well-known/openid-configuration
Azure AD / Entra ID
- Register a new application in Azure Portal
- Add a Web platform with your callback URL
- Create a client secret (for confidential clients)
- Discovery URL:
https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration
Okta
- Create a new OIDC Web Application
- Set the Sign-in redirect URI to your callback URL
- Discovery URL:
https://your-domain.okta.com/.well-known/openid-configuration
Auth0
- Create a new Regular Web Application
- Add your callback URL to Allowed Callback URLs
- Discovery URL:
https://your-domain.auth0.com/.well-known/openid-configuration
- Create OAuth 2.0 credentials in Google Cloud Console
- Add your callback URL to Authorized redirect URIs
- Discovery URL:
https://accounts.google.com/.well-known/openid-configuration
Hooks and Filters
Actions
// Fired when a new user is created via OIDC
do_action('secure_oidc_login_user_created', $user_id, $claims);
// Fired when an existing user is updated via OIDC
do_action('secure_oidc_login_user_updated', $user_id, $claims);
Example: Assign Role Based on Claims
add_action('secure_oidc_login_user_created', function($user_id, $claims) {
// Check for admin group in claims
if (isset($claims['groups']) && in_array('admins', $claims['groups'])) {
$user = get_user_by('ID', $user_id);
$user->set_role('administrator');
}
}, 10, 2);
Security Considerations
- Always use HTTPS in production
- The plugin implements PKCE (Proof Key for Code Exchange) for enhanced security
- State parameter is used to prevent CSRF attacks
- JWT verification is handled by the firebase/php-jwt library
- Tokens are validated for issuer, audience, and expiration
- Email verification is required by default before linking/creating accounts - disable only for trusted IdPs
- Client secrets are stored in the WordPress database (consider using environment variables for sensitive deployments)
- Environment variables can be used to keep credentials out of the database entirely
- Rate limiting protects all authentication endpoints against brute force and DoS attacks
- SSRF protection via
wp_safe_remote_get/postblocks requests to internal IPs and non-standard ports - For intranet identity providers, use the
http_request_host_is_externalWordPress filter - Object-cache note: rate limiting uses transients, which a persistent object cache (memcached, some Redis configs) may evict under memory pressure before their expiry. On cache-backed sites, rate limiting is best-effort — signature verification, nonce binding, and PKCE do not depend on it. The back-channel logout jti replay cache is stored as database options and is not affected by cache eviction.
Troubleshooting
Common Issues
-
"Invalid state parameter": This usually means the authentication took too long (>5 minutes by default) or cookies are not being preserved. Check your browser's cookie settings. You can increase the timeout using
SECURE_OIDC_STATE_TTL(60-600 seconds). -
"Token exchange failed": Verify your client ID and secret are correct, and that the callback URL matches exactly what's configured in your IdP.
-
"User does not exist": Enable "Create Users" in the plugin settings, or manually create the user in WordPress first.
-
"Your email domain is not authorized": The user's email domain is not in the allowed list. Check the "Allowed Email Domains" setting or
SECURE_OIDC_ALLOWED_EMAIL_DOMAINSenvironment variable. Wildcards like*.example.comcan be used to allow all subdomains. -
Login button not appearing: Ensure Client ID and Authorization Endpoint are configured.
-
"Too many login attempts": You've been rate limited. Wait for the lockout period to expire (default: 15 minutes) or adjust rate limit settings via environment variables.
-
"Discovery URL was blocked for security reasons": The IdP URL points to a private IP or uses a non-standard port. For intranet IdPs, add a filter:
add_filter('http_request_host_is_external', function($external, $host) { if ($host === 'idp.internal.company.com') { return true; } return $external; }, 10, 2);
Debug Mode
To enable debug logging, add this to your wp-config.php:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
Then check /wp-content/debug.log for OIDC-related messages.
Dependencies
This plugin uses the following open-source libraries:
- firebase/php-jwt - JWT verification and validation
License
GPL v2 or later
Contributing
Contributions are welcome! Please submit pull requests or open issues on the project repository.
Changelog
See CHANGELOG.md for a detailed history of changes.