Compare commits

...

5 Commits

1935 changed files with 331078 additions and 35742 deletions

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="phpunit" version="^9.5.21" installed="9.5.21" location="./tools/phpunit" copy="true"/>
<phar name="phpcs" version="^3.7.1" installed="3.7.1" location="./tools/phpcs" copy="true"/>
<phar name="phpcbf" version="^3.7.1" installed="3.7.1" location="./tools/phpcbf" copy="true"/>
<phar name="behat/behat" version="^3.13.0" installed="3.13.0" location="./tools/behat" copy="true"/>
<phar name="wp-cli/wp-cli" version="^2.9.0" installed="2.9.0" location="./tools/wp-cli" copy="true"/>
</phive>

View File

@ -1,7 +0,0 @@
Copyright <YEAR> <COPYRIGHT HOLDER>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,95 +0,0 @@
# authLDAP
[![Join the chat at https://gitter.im/heiglandreas/authLdap](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/heiglandreas/authLdap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Use your existing LDAP as authentication-backend for your wordpress!
[![Build Status](https://github.com/heiglandreas/authLdap/actions/workflows/tests.yml/badge.svg)](https://github.com/heiglandreas/authLdap/actions/workflows/tests.yml)
[![WordPress Stats](https://img.shields.io/wordpress/plugin/dt/authldap.svg)](https://wordpress.org/plugins/authldap/stats/)
[![WordPress Version](https://img.shields.io/wordpress/plugin/v/authldap.svg)](https://wordpress.org/plugins/authldap/)
[![WordPress testet](https://img.shields.io/wordpress/v/authldap.svg)](https://wordpress.org/plugins/authldap/)
[![Code Climate](https://codeclimate.com/github/heiglandreas/authLdap/badges/gpa.svg)](https://codeclimate.com/github/heiglandreas/authLdap)
[![codecov](https://codecov.io/gh/heiglandreas/authLdap/branch/master/graph/badge.svg?token=AYAhEeWtRQ)](https://codecov.io/gh/heiglandreas/authLdap)
So what are the differences to other Wordpress-LDAP-Authentication-Plugins?
* **Flexible**: You are totaly free in which LDAP-backend to use. Due to the extensive configuration you can
freely decide how to do the authentication of your users. It simply depends on your
filters
* **Independent**: As soon as a user logs in, it is added/updated to the Wordpress' user-database
to allow wordpress to always use the correct data. You only have to administer your users once.
* **Failsafe**: Due to the users being created in Wordpress' User-database they can
also log in when the LDAP-backend currently is gone.
* **Role-Aware**: You can map Wordpress' roles to values of an existing LDAP-attribute.
## How does the plugin work?
Well, as a matter of fact it is rather simple. The plugin verifies, that the user
seeking authentification can bind to the LDAP using the provided password.
If that is so, the user is either created or updated in the wordpress-user-database.
This update includes the provided password (so the wordpress can authenticate users
even without the LDAP), the users name according to the authLDAP-preferences and
the status of the user depending on the groups-settings of the authLDAP-preferences
Writing this plugin would not have been as easy as it has been, without the
wonderfull plugin of Alistair Young from http://www.weblogs.uhi.ac.uk/sm00ay/?p=45
## Configuration
### Usage Settings
* **Enable Authentication via LDAP** Whether you want to enable authLdap for login or not
* **debug authLdap** When you have problems with authentication via LDAP you can enable a debugging mode here.
* **Save entered Password** Decide whether passwords will be cached in your wordpress-installation. **Attention:** Without the cache your users will not be able to log into your site when your LDAP is down!
### Server Settings
* **LDAP Uri** This is the URI where your ldap-backend can be reached. More information are actually on the Configuration page
* **Filter** This is the real McCoy! The filter you define here specifies how a user will be found. Before applying the filter a %s will be replaced with the given username. This means, when a user logs in using foobar as username the following happens:
* **uid=%1$s** check for any LDAP-Entry that has an attribute uid with value foobar
* **(&(objectclass=posixAccount)(|(uid=%1$s)(mail=%1$s)))** check for any LDAP-Entry that has an attribute objectclass with value posixAccout and either a UID- or a mail-attribute with value foobar
This filter is rather powerfull if used wisely.
### Creating Users
* **Name-Attribute** Which Attribute from the LDAP contains the Full or the First name of the user trying to log in. This defaults to name
* **Second Name Attribute** If the above Name-Attribute only contains the First Name of the user you can here specify an Attribute that contains the second name. This field is empty by default
* **User-ID Attribute** This field will be used as login-name for wordpress. Please give the Attribute, that is used to identify the user. This should be the same as you used in the above Filter-Option. This field defaults to uid
* **Mail Attribute** Which Attribute holds the eMail-Address of the user? If more than one eMail-Address are stored in the LDAP, only the first given is used. This field defaults to mail
* **Web-Attribute** If your users have a personal page (URI) stored in the LDAP, it can be provided here. This field is empty by default
### User-Groups for Roles
* **Group-Attribute** This is the attribute that defines the Group-ID that can be matched against the Groups defined further down This field defaults to gidNumber.
* **Group-Filter** Here you can add the filter for selecting groups for the currentlly logged in user The Filter should contain the string %s which will be replaced by the login-name of the currently logged in
## FAQ
<dl>
<dt>Can I change a users password with this plugin?</dt>
<dd>Short Answer: <strong>No</strong>!<br>Long Answer: As the users credentials are not
only used for a wordpress-site when you authenticate against an LDAP but for
many other services also chances are great that there is a centralized place
where password-changes shall be made. We'll later allow inclusion of a link
to such a place but currently it's not available. And as password-hashing and
where to store it requires deeper insight into the LDAP-Server then most users
have and admins are willing to give, password changes are out of scope of this
plugin. If you know exactyl what you do, you might want to have a look at
<a href="https://github.com/heiglandreas/authLdap/issues/54#issuecomment-125851029">
issue 54</a>
wherer a way of adding it is described!
</dd>
<dt>Can I add a user to the LDAP when she creates a user-account on wordpress?</dt>
<dd>Short Answer: <strong>No</strong>!<br>Long Answer: Even though that is technically possible
it's not in the scope of this plugin. As creating a user in an LDAP often involves
an administrative process that has already been implemented in your departments
administration it doesn't make sense to rebuild that - in most cases highly
individual - process in this plugin. If you know exactly what you do, have a look at
<a href="https://github.com/heiglandreas/authLdap/issues/65">issue 65</a>
where <a href="https://github.com/wtfiwtz">wtfiwtz</a> shows how to implement that feature.
</dd>
</dl>

View File

@ -1,18 +0,0 @@
# Security-Policy
## Supported Versions
| Version | Supported |
| ------- |--------------------|
| 2.x | :white_check_mark: |
| 1.x | :x: |
## Reporting a Vulnerability
* Check our security.txt file for details on how to contact us
* Contact us before publicly disclosing the issue anywhere else
This plugin is developed as OpenSource under the MIT licence.
There is no money earned from it. Therefore we are not able to
provide any bug-bounties whatsoever. You will be mentioned in the
release notes of a fix-release though.

View File

@ -1,13 +0,0 @@
.row {
overflow: hidden;
padding-top: 10px;
}
.element {
float: right;
text-align: left;
}
.authldap-options input[type=text] {
width: 100%;
}

View File

@ -1,946 +0,0 @@
<?php
/*
Plugin Name: AuthLDAP
Plugin URI: https://github.com/heiglandreas/authLdap
Description: This plugin allows you to use your existing LDAP as authentication base for WordPress
Version: 2.6.2
Author: Andreas Heigl <andreas@heigl.org>
Author URI: http://andreas.heigl.org
License: MIT
License URI: https://opensource.org/licenses/MIT
*/
// phpcs:disable PSR1.Files.SideEffects
use Org_Heigl\AuthLdap\LdapList;
use Org_Heigl\AuthLdap\LdapUri;
use Org_Heigl\AuthLdap\Manager\Ldap;
use Org_Heigl\AuthLdap\UserRoleHandler;
use Org_Heigl\AuthLdap\Wrapper\LdapFactory;
require_once __DIR__ . '/src/Wrapper/LdapInterface.php';
require_once __DIR__ . '/src/Exception/Error.php';
require_once __DIR__ . '/src/Exception/InvalidLdapUri.php';
require_once __DIR__ . '/src/Exception/Error.php';
require_once __DIR__ . '/src/Exception/InvalidLdapUri.php';
require_once __DIR__ . '/src/Exception/MissingValidLdapConnection.php';
require_once __DIR__ . '/src/Exception/SearchUnsuccessfull.php';
require_once __DIR__ . '/src/Manager/Ldap.php';
require_once __DIR__ . '/src/Wrapper/Ldap.php';
require_once __DIR__ . '/src/Wrapper/LdapFactory.php';
require_once __DIR__ . '/src/LdapList.php';
require_once __DIR__ . '/src/LdapUri.php';
require_once __DIR__ . '/src/UserRoleHandler.php';
function authLdap_debug($message)
{
if (authLdap_get_option('Debug')) {
error_log('[AuthLDAP] ' . $message, 0);
}
}
function authLdap_addmenu()
{
if (!is_multisite()) {
add_options_page(
'AuthLDAP',
'AuthLDAP',
'manage_options',
basename(__FILE__),
'authLdap_options_panel'
);
} else {
add_submenu_page(
'settings.php',
'AuthLDAP',
'AuthLDAP',
'manage_options',
'authldap',
'authLdap_options_panel'
);
}
}
function authLdap_get_post($name, $default = '')
{
return isset($_POST[$name]) ? $_POST[$name] : $default;
}
function authLdap_options_panel()
{
// inclusde style sheet
wp_enqueue_style('authLdap-style', plugin_dir_url(__FILE__) . 'authLdap.css');
if (($_SERVER['REQUEST_METHOD'] == 'POST') && array_key_exists('ldapOptionsSave', $_POST)) {
if (!isset($_POST['authLdapNonce'])) {
die("Go away!");
}
if (!wp_verify_nonce($_POST['authLdapNonce'], 'authLdapNonce')) {
die("Go away!");
}
$new_options = [
'Enabled' => authLdap_get_post('authLDAPAuth', false),
'CachePW' => authLdap_get_post('authLDAPCachePW', false),
'URI' => authLdap_get_post('authLDAPURI'),
'URISeparator' => authLdap_get_post('authLDAPURISeparator'),
'StartTLS' => authLdap_get_post('authLDAPStartTLS', false),
'Filter' => authLdap_get_post('authLDAPFilter'),
'NameAttr' => authLdap_get_post('authLDAPNameAttr'),
'SecName' => authLdap_get_post('authLDAPSecName'),
'UidAttr' => authLdap_get_post('authLDAPUidAttr'),
'MailAttr' => authLdap_get_post('authLDAPMailAttr'),
'WebAttr' => authLdap_get_post('authLDAPWebAttr'),
'Groups' => authLdap_get_post('authLDAPGroups', []),
'GroupSeparator' => authLdap_get_post('authLDAPGroupSeparator', ','),
'Debug' => authLdap_get_post('authLDAPDebug', false),
'GroupBase' => authLdap_get_post('authLDAPGroupBase'),
'GroupAttr' => authLdap_get_post('authLDAPGroupAttr'),
'GroupFilter' => authLdap_get_post('authLDAPGroupFilter'),
'DefaultRole' => authLdap_get_post('authLDAPDefaultRole'),
'GroupEnable' => authLdap_get_post('authLDAPGroupEnable', false),
'GroupOverUser' => authLdap_get_post('authLDAPGroupOverUser', false),
'DoNotOverwriteNonLdapUsers' => authLdap_get_post('authLDAPDoNotOverwriteNonLdapUsers', false),
'UserRead' => authLdap_get_post('authLDAPUseUserAccount', false),
];
if (authLdap_set_options($new_options)) {
echo "<div class='updated'><p>Saved Options!</p></div>";
} else {
echo "<div class='error'><p>Could not save Options!</p></div>";
}
}
// Do some initialization for the admin-view
$authLDAP = authLdap_get_option('Enabled');
$authLDAPCachePW = authLdap_get_option('CachePW');
$authLDAPURI = authLdap_get_option('URI');
$authLDAPURISeparator = authLdap_get_option('URISeparator');
$authLDAPStartTLS = authLdap_get_option('StartTLS');
$authLDAPFilter = authLdap_get_option('Filter');
$authLDAPNameAttr = authLdap_get_option('NameAttr');
$authLDAPSecName = authLdap_get_option('SecName');
$authLDAPMailAttr = authLdap_get_option('MailAttr');
$authLDAPUidAttr = authLdap_get_option('UidAttr');
$authLDAPWebAttr = authLdap_get_option('WebAttr');
$authLDAPGroups = authLdap_get_option('Groups');
$authLDAPGroupSeparator = authLdap_get_option('GroupSeparator');
$authLDAPDebug = authLdap_get_option('Debug');
$authLDAPGroupBase = authLdap_get_option('GroupBase');
$authLDAPGroupAttr = authLdap_get_option('GroupAttr');
$authLDAPGroupFilter = authLdap_get_option('GroupFilter');
$authLDAPDefaultRole = authLdap_get_option('DefaultRole');
$authLDAPGroupEnable = authLdap_get_option('GroupEnable');
$authLDAPGroupOverUser = authLdap_get_option('GroupOverUser');
$authLDAPDoNotOverwriteNonLdapUsers = authLdap_get_option('DoNotOverwriteNonLdapUsers');
$authLDAPUseUserAccount = authLdap_get_option('UserRead');
$tChecked = ($authLDAP) ? ' checked="checked"' : '';
$tDebugChecked = ($authLDAPDebug) ? ' checked="checked"' : '';
$tPWChecked = ($authLDAPCachePW) ? ' checked="checked"' : '';
$tGroupChecked = ($authLDAPGroupEnable) ? ' checked="checked"' : '';
$tGroupOverUserChecked = ($authLDAPGroupOverUser) ? ' checked="checked"' : '';
$tStartTLSChecked = ($authLDAPStartTLS) ? ' checked="checked"' : '';
$tDoNotOverwriteNonLdapUsers = ($authLDAPDoNotOverwriteNonLdapUsers) ? ' checked="checked"' : '';
$tUserRead = ($authLDAPUseUserAccount) ? ' checked="checked"' : '';
$roles = new WP_Roles();
$action = $_SERVER['REQUEST_URI'];
if (!extension_loaded('ldap')) {
echo '<div class="warning">The LDAP-Extension is not available on your '
. 'WebServer. Therefore Everything you can alter here does not '
. 'make any sense!</div>';
}
include dirname(__FILE__) . '/view/admin.phtml';
}
/**
* get a LDAP server object
*
* throws exception if there is a problem connecting
*
* @conf boolean authLDAPDebug true, if debugging should be turned on
* @conf string authLDAPURI LDAP server URI
*
* @return Org_Heigl\AuthLdap\LdapList LDAP server object
*/
function authLdap_get_server()
{
static $_ldapserver = null;
if (is_null($_ldapserver)) {
$authLDAPDebug = authLdap_get_option('Debug');
$authLDAPURI = explode(
authLdap_get_option('URISeparator', ' '),
authLdap_get_option('URI')
);
$authLDAPStartTLS = authLdap_get_option('StartTLS');
//$authLDAPURI = 'ldap:/foo:bar@server/trallala';
authLdap_debug('connect to LDAP server');
require_once dirname(__FILE__) . '/src/LdapList.php';
$_ldapserver = new LdapList();
foreach ($authLDAPURI as $uri) {
$_ldapserver->addLdap(new Ldap(
new LdapFactory(),
LdapUri::fromString($uri),
$authLDAPStartTLS
));
}
}
return $_ldapserver;
}
/**
* This method authenticates a user using either the LDAP or, if LDAP is not
* available, the local database
*
* For this we store the hashed passwords in the WP_Database to ensure working
* conditions even without an LDAP-Connection
*
* @param null|WP_User|WP_Error
* @param string $username
* @param string $password
* @param boolean $already_md5
* @return boolean true, if login was successfull or false, if it wasn't
* @conf boolean authLDAP true, if authLDAP should be used, false if not. Defaults to false
* @conf string authLDAPFilter LDAP filter to use to find correct user, defaults to '(uid=%s)'
* @conf string authLDAPNameAttr LDAP attribute containing user (display) name, defaults to 'name'
* @conf string authLDAPSecName LDAP attribute containing second name, defaults to ''
* @conf string authLDAPMailAttr LDAP attribute containing user e-mail, defaults to 'mail'
* @conf string authLDAPUidAttr LDAP attribute containing user id (the username we log on with), defaults to 'uid'
* @conf string authLDAPWebAttr LDAP attribute containing user website, defaults to ''
* @conf string authLDAPDefaultRole default role for authenticated user, defaults to ''
* @conf boolean authLDAPGroupEnable true, if we try to map LDAP groups to Wordpress roles
* @conf boolean authLDAPGroupOverUser true, if LDAP Groups have precedence over existing user roles
*/
function authLdap_login($user, $username, $password, $already_md5 = false)
{
// don't do anything when authLDAP is disabled
if (!authLdap_get_option('Enabled')) {
authLdap_debug(
'LDAP disabled in AuthLDAP plugin options (use the first option in the AuthLDAP options to enable it)'
);
return $user;
}
// If the user has already been authenticated (only in that case we get a
// WP_User-Object as $user) we skip LDAP-authentication and simply return
// the existing user-object
if ($user instanceof WP_User) {
authLdap_debug(sprintf(
'User %s has already been authenticated - skipping LDAP-Authentication',
$user->get('nickname')
));
return $user;
}
authLdap_debug("User '$username' logging in");
if ($username == 'admin') {
authLdap_debug('Doing nothing for possible local user admin');
return $user;
}
global $wpdb, $error;
try {
$authLDAP = authLdap_get_option('Enabled');
$authLDAPFilter = authLdap_get_option('Filter');
$authLDAPNameAttr = authLdap_get_option('NameAttr');
$authLDAPSecName = authLdap_get_option('SecName');
$authLDAPMailAttr = authLdap_get_option('MailAttr');
$authLDAPUidAttr = authLdap_get_option('UidAttr');
$authLDAPWebAttr = authLdap_get_option('WebAttr');
$authLDAPDefaultRole = authLdap_get_option('DefaultRole');
$authLDAPGroupEnable = filter_var(authLdap_get_option('GroupEnable'), FILTER_VALIDATE_BOOLEAN);
$authLDAPGroupOverUser = filter_var(authLdap_get_option('GroupOverUser'), FILTER_VALIDATE_BOOLEAN);
$authLDAPUseUserAccount = authLdap_get_option('UserRead');
if (!$username) {
authLdap_debug('Username not supplied: return false');
return false;
}
if (!$password) {
authLdap_debug('Password not supplied: return false');
$error = __('<strong>Error</strong>: The password field is empty.');
return false;
}
// First check for valid values and set appropriate defaults
if (!$authLDAPFilter) {
$authLDAPFilter = '(uid=%s)';
}
if (!$authLDAPNameAttr) {
$authLDAPNameAttr = 'name';
}
if (!$authLDAPMailAttr) {
$authLDAPMailAttr = 'mail';
}
if (!$authLDAPUidAttr) {
$authLDAPUidAttr = 'uid';
}
// If already_md5 is TRUE, then we're getting the user/password from the cookie. As we don't want
// to store LDAP passwords in any
// form, we've already replaced the password with the hashed username and LDAP_COOKIE_MARKER
if ($already_md5) {
if ($password == md5($username) . md5($ldapCookieMarker)) {
authLdap_debug('cookie authentication');
return true;
}
}
// Remove slashes as noted on https://github.com/heiglandreas/authLdap/issues/108
$password = stripslashes_deep($password);
// No cookie, so have to authenticate them via LDAP
$result = false;
try {
authLdap_debug('about to do LDAP authentication');
$result = authLdap_get_server()->Authenticate($username, $password, $authLDAPFilter);
} catch (Exception $e) {
authLdap_debug('LDAP authentication failed with exception: ' . $e->getMessage());
return false;
}
// Make optional querying from the admin account #213
if (!authLdap_get_option('UserRead')) {
// Rebind with the default credentials after the user has been loged in
// Otherwise the credentials of the user trying to login will be used
// This fixes #55
authLdap_get_server()->bind();
}
if (true !== $result) {
authLdap_debug('LDAP authentication failed');
// TODO what to return? WP_User object, true, false, even an WP_Error object...
// all seem to fall back to normal wp user authentication
return;
}
authLdap_debug('LDAP authentication successful');
$attributes = array_values(
array_filter(
apply_filters(
'authLdap_filter_attributes',
[
$authLDAPNameAttr,
$authLDAPSecName,
$authLDAPMailAttr,
$authLDAPWebAttr,
$authLDAPUidAttr,
]
)
)
);
try {
$attribs = authLdap_get_server()->search(
sprintf($authLDAPFilter, $username),
$attributes
);
// First get all the relevant group informations so we can see if
// whether have been changes in group association of the user
if (!isset($attribs[0]['dn'])) {
authLdap_debug('could not get user attributes from LDAP');
throw new UnexpectedValueException('dn has not been returned');
}
if (!isset($attribs[0][strtolower($authLDAPUidAttr)][0])) {
authLdap_debug('could not get user attributes from LDAP');
throw new UnexpectedValueException('The user-ID attribute has not been returned');
}
$dn = $attribs[0]['dn'];
$realuid = $attribs[0][strtolower($authLDAPUidAttr)][0];
} catch (Exception $e) {
authLdap_debug('Exception getting LDAP user: ' . $e->getMessage());
return false;
}
$uid = authLdap_get_uid($realuid);
// This fixes #172
if (true == authLdap_get_option('DoNotOverwriteNonLdapUsers', false)) {
if (!get_user_meta($uid, 'authLDAP')) {
return null;
}
}
$roles = [];
// we only need this if either LDAP groups are disabled or
// if the WordPress role of the user overrides LDAP groups
if ($authLDAPGroupEnable === false || $authLDAPGroupOverUser === false) {
$userRoles = authLdap_user_role($uid);
if ($userRoles !== []) {
$roles = array_merge($roles, $userRoles);
}
// TODO, this needs to be revised, it seems, like authldap is taking only the first role
// even if in WP there are assigned multiple.
}
// do LDAP group mapping if needed
// (if LDAP groups override wordpress user role, $role is still empty)
if ((empty($roles) || $authLDAPGroupOverUser === true) && $authLDAPGroupEnable === true) {
$mappedRoles = authLdap_groupmap($realuid, $dn);
if ($mappedRoles !== []) {
$roles = $mappedRoles;
authLdap_debug('role from group mapping: ' . json_encode($roles));
}
}
// if we don't have a role yet, use default role
if (empty($roles) && !empty($authLDAPDefaultRole)) {
authLdap_debug('no role yet, set default role');
$roles[] = $authLDAPDefaultRole;
}
if (empty($roles)) {
// Sorry, but you are not in any group that is allowed access
trigger_error('no group found');
authLdap_debug('user is not in any group that is allowed access');
return false;
} else {
$wp_roles = new WP_Roles();
// not sure if this is needed, but it can't hurt
// Get rid of unexisting roles.
foreach ($roles as $k => $v) {
if (!$wp_roles->is_role($v)) {
unset($k);
}
}
// check if single role or an empty array provided
if (empty($roles)) {
trigger_error('no group found');
authLdap_debug('role is invalid');
return false;
}
}
// from here on, the user has access!
// now, lets update some user details
$user_info = [];
$user_info['user_login'] = $realuid;
$user_info['user_email'] = '';
$user_info['user_nicename'] = '';
// first name
if (isset($attribs[0][strtolower((string) $authLDAPNameAttr)][0])) {
$user_info['first_name'] = $attribs[0][strtolower((string) $authLDAPNameAttr)][0];
}
// last name
if (isset($attribs[0][strtolower((string) $authLDAPSecName)][0])) {
$user_info['last_name'] = $attribs[0][strtolower((string) $authLDAPSecName)][0];
}
// mail address
if (isset($attribs[0][strtolower((string) $authLDAPMailAttr)][0])) {
$user_info['user_email'] = $attribs[0][strtolower((string) $authLDAPMailAttr)][0];
}
// website
if (isset($attribs[0][strtolower((string) $authLDAPWebAttr)][0])) {
$user_info['user_url'] = $attribs[0][strtolower((string) $authLDAPWebAttr)][0];
}
// display name, nickname, nicename
if (array_key_exists('first_name', $user_info)) {
$user_info['display_name'] = $user_info['first_name'];
$user_info['nickname'] = $user_info['first_name'];
$user_info['user_nicename'] = sanitize_title_with_dashes($user_info['first_name']);
if (array_key_exists('last_name', $user_info)) {
$user_info['display_name'] .= ' ' . $user_info['last_name'];
$user_info['nickname'] .= ' ' . $user_info['last_name'];
$user_info['user_nicename'] .= '_' . sanitize_title_with_dashes($user_info['last_name']);
}
}
$user_info['user_nicename'] = substr($user_info['user_nicename'], 0, 50);
// optionally store the password into the wordpress database
if (authLdap_get_option('CachePW')) {
// Password will be hashed inside wp_update_user or wp_insert_user
$user_info['user_pass'] = $password;
} else {
// clear the password
$user_info['user_pass'] = '';
}
// add uid if user exists
if ($uid) {
// found user in the database
authLdap_debug('The LDAP user has an entry in the WP-Database');
$user_info['ID'] = $uid;
unset($user_info['display_name'], $user_info['nickname']);
$userid = wp_update_user($user_info);
} else {
// new wordpress account will be created
authLdap_debug('The LDAP user does not have an entry in the WP-Database, a new WP account will be created');
$userid = wp_insert_user($user_info);
}
// if the user exists, wp_insert_user will update the existing user record
if (is_wp_error($userid)) {
authLdap_debug('Error creating user : ' . $userid->get_error_message());
trigger_error('Error creating user: ' . $userid->get_error_message());
return $userid;
}
// Update user roles.
$user = new \WP_User($userid);
/**
* Add hook for custom User-Role assignment
*
* @param WP_User $user This user-object will be returned. Can be modified as necessary in the actions.
* @param array $roles
*/
do_action('authldap_user_roles', $user, $roles);
/**
* Add hook for custom updates
*
* @param int $userid User ID.
* @param array $attribs [0] Attributes retrieved from LDAP for the user.
*/
do_action('authLdap_login_successful', $userid, $attribs[0]);
authLdap_debug('user id = ' . $userid);
// flag the user as an ldap user so we can hide the password fields in the user profile
update_user_meta($userid, 'authLDAP', true);
// return a user object upon positive authorization
return $user;
} catch (Exception $e) {
authLdap_debug($e->getMessage() . '. Exception thrown in line ' . $e->getLine());
trigger_error($e->getMessage() . '. Exception thrown in line ' . $e->getLine());
}
}
/**
* Get user's user id
*
* Returns null if username not found
*
* @param string $username username
* @param string user id, null if not found
*/
function authLdap_get_uid($username)
{
global $wpdb;
// find out whether the user is already present in the database
$uid = $wpdb->get_var(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->users} WHERE user_login = %s",
$username
)
);
if ($uid) {
authLdap_debug("Existing user, uid = {$uid}");
return $uid;
} else {
return null;
}
}
/**
* Get the user's current role
*
* Returns empty string if not found.
*
* @param int $uid wordpress user id
* @return array roles, empty if none found
*/
function authLdap_user_role($uid)
{
global $wpdb, $wp_roles;
if (!$uid) {
return [];
}
/** @var array<string, bool> $usercapabilities */
$usercapabilities = get_user_meta($uid, "{$wpdb->prefix}capabilities", true);
if (!is_array($usercapabilities)) {
return [];
}
/** @var array<string, array{name: string, capabilities: array<mixed>} $editable_roles */
$editable_roles = $wp_roles->roles;
// By using this approach we are now using the order of the roles from the WP_Roles object
// and not from the capabilities any more.
$userroles = array_keys(array_intersect_key($editable_roles, $usercapabilities));
authLdap_debug(sprintf("Existing user's roles: %s", implode(', ', $userroles)));
return $userroles;
}
/**
* Get LDAP groups for user and map to role
*
* @param string $username
* @param string $dn
* @return array role, empty array if no mapping found, first or all role(s) found otherwise
* @conf array authLDAPGroups, associative array, role => ldap_group
* @conf string authLDAPGroupBase, base dn to look up groups
* @conf string authLDAPGroupAttr, ldap attribute that holds name of group
* @conf string authLDAPGroupFilter, LDAP filter to find groups. can contain %s and %dn% placeholders
*/
function authLdap_groupmap($username, $dn)
{
$authLDAPGroups = authLdap_sort_roles_by_capabilities(
authLdap_get_option('Groups')
);
$authLDAPGroupBase = authLdap_get_option('GroupBase');
$authLDAPGroupAttr = authLdap_get_option('GroupAttr');
$authLDAPGroupFilter = authLdap_get_option('GroupFilter');
$authLDAPGroupSeparator = authLdap_get_option('GroupSeparator');
if (!$authLDAPGroupAttr) {
$authLDAPGroupAttr = 'gidNumber';
}
if (!$authLDAPGroupFilter) {
$authLDAPGroupFilter = '(&(objectClass=posixGroup)(memberUid=%s))';
}
if (!$authLDAPGroupSeparator) {
$authLDAPGroupSeparator = ',';
}
if (!is_array($authLDAPGroups) || count(array_filter(array_values($authLDAPGroups))) == 0) {
authLdap_debug('No group names defined');
return [];
}
try {
// To allow searches based on the DN instead of the uid, we replace the
// string %dn% with the users DN.
$authLDAPGroupFilter = str_replace(
'%dn%',
ldap_escape($dn, '', LDAP_ESCAPE_FILTER),
$authLDAPGroupFilter
);
authLdap_debug('Group Filter: ' . json_encode($authLDAPGroupFilter));
authLdap_debug('Group Base: ' . $authLDAPGroupBase);
$groups = authLdap_get_server()->search(
sprintf($authLDAPGroupFilter, ldap_escape($username, '', LDAP_ESCAPE_FILTER)),
[$authLDAPGroupAttr],
$authLDAPGroupBase
);
} catch (Exception $e) {
authLdap_debug('Exception getting LDAP group attributes: ' . $e->getMessage());
return [];
}
$grp = [];
for ($i = 0; $i < $groups ['count']; $i++) {
if ($authLDAPGroupAttr == "dn") {
$grp[] = $groups[$i]['dn'];
} else {
for ($k = 0; $k < $groups[$i][strtolower($authLDAPGroupAttr)]['count']; $k++) {
$grp[] = $groups[$i][strtolower($authLDAPGroupAttr)][$k];
}
}
}
authLdap_debug('LDAP groups: ' . json_encode($grp));
// Check whether the user is member of one of the groups that are
// allowed acces to the blog. If the user is not member of one of
// The groups throw her out! ;-)
$roles = [];
foreach ($authLDAPGroups as $key => $val) {
$currentGroup = explode($authLDAPGroupSeparator, $val);
// Remove whitespaces around the group-ID
$currentGroup = array_map('trim', $currentGroup);
if (0 < count(array_intersect($currentGroup, $grp))) {
$roles[] = $key;
}
}
// Default: If the user is member of more than one group only the first one
// will be taken into account!
// This filter allows you to return multiple user roles. WordPress
// supports this functionality, but not natively via UI from Users
// overview (you need to use a plugin). However, it's still widely used,
// for example, by WooCommerce, etc. Use if you know what you're doing.
if (apply_filters('authLdap_allow_multiple_roles', false) === false && count($roles) > 1) {
$roles = array_slice($roles, 0, 1);
}
authLdap_debug("Roles from LDAP group: " . json_encode($roles));
return $roles;
}
/**
* This function disables the password-change fields in the users preferences.
*
* It does not make sense to authenticate via LDAP and then allow the user to
* change the password only in the wordpress database. And changing the password
* LDAP-wide can not be the scope of Wordpress!
*
* Whether the user is an LDAP-User or not is determined using the authLDAP-Flag
* of the users meta-informations
*
* @return false, if the user whose prefs are viewed is an LDAP-User, true if
* he isn't
* @conf boolean authLDAP
*/
function authLdap_show_password_fields($return, $user)
{
if (!$user) {
return true;
}
if (get_user_meta($user->ID, 'authLDAP')) {
return false;
}
return $return;
}
/**
* This function disables the password reset for a user.
*
* It does not make sense to authenticate via LDAP and then allow the user to
* reset the password only in the wordpress database. And changing the password
* LDAP-wide can not be the scope of Wordpress!
*
* Whether the user is an LDAP-User or not is determined using the authLDAP-Flag
* of the users meta-informations
*
* @author chaplina (https://github.com/chaplina)
* @conf boolean authLDAP
* @return false, if the user is an LDAP-User, true if he isn't
*/
function authLdap_allow_password_reset($return, $userid)
{
if (!(isset($userid))) {
return true;
}
if (get_user_meta($userid, 'authLDAP')) {
return false;
}
return $return;
}
/**
* Sort the given roles by number of capabilities
*
* @param array $roles
*
* @return array
*/
function authLdap_sort_roles_by_capabilities($roles)
{
global $wpdb;
$myRoles = get_option($wpdb->get_blog_prefix() . 'user_roles');
authLdap_debug(print_r($roles, true));
uasort($myRoles, 'authLdap_sortByCapabilitycount');
$return = [];
foreach ($myRoles as $key => $role) {
if (isset($roles[$key])) {
$return[$key] = $roles[$key];
}
}
authLdap_debug(print_r($return, true));
return $return;
}
/**
* Sort according to the number of capabilities
*
* @param $a
* @param $b
*/
function authLdap_sortByCapabilitycount($a, $b)
{
if (count($a['capabilities']) > count($b['capabilities'])) {
return -1;
}
if (count($a['capabilities']) < count($b['capabilities'])) {
return 1;
}
return 0;
}
/**
* Load AuthLDAP Options
*
* Sets and stores defaults if options are not up to date
*/
function authLdap_load_options($reload = false)
{
static $options = null;
// the current version for options
$option_version_plugin = 1;
$optionFunction = 'get_option';
if (is_multisite()) {
$optionFunction = 'get_site_option';
}
if (is_null($options) || $reload) {
$options = $optionFunction('authLDAPOptions', []);
}
// check if option version has changed (or if it's there at all)
if (!isset($options['Version']) || ($options['Version'] != $option_version_plugin)) {
// defaults for all options
$options_default = [
'Enabled' => false,
'CachePW' => false,
'URI' => '',
'URISeparator' => ' ',
'Filter' => '', // '(uid=%s)'
'NameAttr' => '', // 'name'
'SecName' => '',
'UidAttr' => '', // 'uid'
'MailAttr' => '', // 'mail'
'WebAttr' => '',
'Groups' => [],
'Debug' => false,
'GroupAttr' => '', // 'gidNumber'
'GroupFilter' => '', // '(&(objectClass=posixGroup)(memberUid=%s))'
'DefaultRole' => '',
'GroupEnable' => true,
'GroupOverUser' => true,
'Version' => $option_version_plugin,
'DoNotOverwriteNonLdapUsers' => false,
];
// check if we got a version
if (!isset($options['Version'])) {
// we just changed to the new option format
// read old options, then delete them
$old_option_new_option = [
'authLDAP' => 'Enabled',
'authLDAPCachePW' => 'CachePW',
'authLDAPURI' => 'URI',
'authLDAPFilter' => 'Filter',
'authLDAPNameAttr' => 'NameAttr',
'authLDAPSecName' => 'SecName',
'authLDAPUidAttr' => 'UidAttr',
'authLDAPMailAttr' => 'MailAttr',
'authLDAPWebAttr' => 'WebAttr',
'authLDAPGroups' => 'Groups',
'authLDAPDebug' => 'Debug',
'authLDAPGroupAttr' => 'GroupAttr',
'authLDAPGroupFilter' => 'GroupFilter',
'authLDAPDefaultRole' => 'DefaultRole',
'authLDAPGroupEnable' => 'GroupEnable',
'authLDAPGroupOverUser' => 'GroupOverUser',
];
foreach ($old_option_new_option as $old_option => $new_option) {
$value = get_option($old_option, null);
if (!is_null($value)) {
$options[$new_option] = $value;
}
delete_option($old_option);
}
delete_option('authLDAPCookieMarker');
delete_option('authLDAPCookierMarker');
}
// set default for all options that are missing
foreach ($options_default as $key => $default) {
if (!isset($options[$key])) {
$options[$key] = $default;
}
}
// set new version and save
$options['Version'] = $option_version_plugin;
update_option('authLDAPOptions', $options);
}
return $options;
}
/**
* Get an individual option
*/
function authLdap_get_option($optionname, $default = null)
{
$options = authLdap_load_options();
if (isset($options[$optionname]) && $options[$optionname]) {
return $options[$optionname];
}
if (null !== $default) {
return $default;
}
//authLdap_debug('option name invalid: ' . $optionname);
return null;
}
/**
* Set new options
*/
function authLdap_set_options($new_options = [])
{
// initialize the options with what we currently have
$options = authLdap_load_options();
// set the new options supplied
foreach ($new_options as $key => $value) {
$options[$key] = $value;
}
// store options
$optionFunction = 'update_option';
if (is_multisite()) {
$optionFunction = 'update_site_option';
}
if ($optionFunction('authLDAPOptions', $options)) {
// reload the option cache
authLdap_load_options(true);
return true;
}
// could not set options
return false;
}
/**
* Do not send an email after changing the password or the email of the user!
*
* @param boolean $result The initial resturn value
* @param array $user The old userdata
* @param array $newUserData The changed userdata
*
* @return bool
*/
function authLdap_send_change_email($result, $user, $newUserData)
{
if (get_user_meta($user['ID'], 'authLDAP')) {
return false;
}
return $result;
}
$hook = is_multisite() ? 'network_' : '';
add_action($hook . 'admin_menu', 'authLdap_addmenu');
add_filter('show_password_fields', 'authLdap_show_password_fields', 10, 2);
add_filter('allow_password_reset', 'authLdap_allow_password_reset', 10, 2);
add_filter('authenticate', 'authLdap_login', 10, 3);
/** This only works from WP 4.3.0 on */
add_filter('send_password_change_email', 'authLdap_send_change_email', 10, 3);
add_filter('send_email_change_email', 'authLdap_send_change_email', 10, 3);
$handler = new UserRoleHandler();
add_action('authldap_user_roles', [$handler, 'addRolesToUser'], 10, 2);

View File

@ -1 +0,0 @@
default:

View File

@ -1,308 +0,0 @@
<?php
declare(strict_types=1);
use Behat\Behat\Tester\Exception\PendingException;
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Response;
use Org_Heigl\AuthLdap\OptionFactory;
use Org_Heigl\AuthLdap\Options;
use Webmozart\Assert\Assert;
class FeatureContext implements Context
{
private ?Response $res = null;
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
exec('wp --allow-root core install --url=localhost --title=Example --admin_user=localadmin --admin_password=P@ssw0rd --admin_email=info@example.com');
exec('wp --allow-root plugin activate authldap');
}
/**
* @Given a default configuration
*/
public function aDefaultConfiguration()
{
$options = new Options();
$options->set(Options::URI, 'ldap://cn=admin,dc=example,dc=org:insecure@openldap:389/dc=example,dc=org');
$options->set(Options::ENABLED, true);
$options->set(Options::FILTER, 'uid=%1$s');
$options->set(Options::DEFAULT_ROLE, 'subscriber');
$options->set(Options::DEBUG, true);
$options->set(Options::NAME_ATTR, 'cn');
exec(sprintf(
'wp --allow-root option update --format=json authLDAPOptions \'%1$s\'',
json_encode($options->toArray())
));
}
/**
* @Given configuration value :arg1 is set to :arg2
*/
public function configurationValueIsSetTo($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root option patch update authLDAPOptions %1$s %2$s --format=json',
$arg1,
"'" . json_encode($arg2) . "'"
));
}
/**
* @Given an LDAP user :arg1 with name :arg2, password :arg3 and email :arg4 exists
*/
public function anLdapUserWithNamePasswordAndEmailExists($arg1, $arg2, $arg3, $arg4)
{
exec(sprintf(
'ldapadd -x -H %1$s -D "%2$s" -w %3$s <<LDIF
%4$s
LDIF',
'ldap://openldap',
'cn=admin,dc=example,dc=org',
'insecure',
<<<LDIF
dn: uid=$arg1,dc=example,dc=org
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
objectClass: simpleSecurityObject
uid: $arg1
cn: $arg2
sn: $arg2
userPassword: $arg3
mail: $arg4
LDIF
));
exec(sprintf(
'ldappasswd -H ldap://openldap:389 -x -D "uid=admin,dc=example,dc=org" -w "%3$s" -s "%2$s" "uid=%1$s,dc=example,dc=org"',
$arg1,
$arg3,
'insecure'
));
}
/**
* @Given an LDAP group :arg1 exists
*/
public function anLdapGroupExists($arg1)
{
exec(sprintf(
'ldapadd -x -H %1$s -D "%2$s" -w %3$s <<LDIF
%4$s
LDIF',
'ldap://openldap',
'cn=admin,dc=example,dc=org',
'insecure',
<<<LDIF
dn: cn=$arg1,dc=example,dc=org
objectClass: groupOfUniqueNames
cn: $arg1
uniqueMember: cn=admin,dc=example,dc=org
LDIF
));
}
/**
* @Given a WordPress user :arg1 with name :arg2 and email :arg3 exists
*/
public function aWordpressUserWithNameAndEmailExists($arg1, $arg2, $arg3)
{
exec(sprintf(
'wp --allow-root user create %1$s %3$s --display_name=%2$s --porcelain',
$arg1,
$arg2,
$arg3
));
}
/**
* @Given a WordPress role :arg1 exists
*/
public function aWordpressRoleExists($arg1)
{
exec(sprintf(
'wp --allow-root role create %1$s %1$s',
$arg1,
));
}
/**
* @Given WordPress user :arg1 has role :arg2
*/
public function wordpressUserHasRole($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root user add-role %1$s %2$s',
$arg1,
$arg2
));
}
/**
* @When LDAP user :arg1 logs in with password :arg2
*/
public function ldapUserLogsInWithPassword($arg1, $arg2)
{
// curl -i 'http://localhost/wp-login.php' -X POST -H 'Cookie: wordpress_test_cookie=test' --data-raw 'log=localadmin&pwd=P%40ssw0rd'
$client = new Client();
$this->res = $client->post('http://wp/wp-login.php', [
'cookies' => CookieJar::fromArray([
'wordpress_test_cookie' => 'test',
'XDEBUG_SESSION' => 'PHPSTORM',
], 'http://wp'),
'form_params' => [
'log' => $arg1,
'pwd' => $arg2,
],
'allow_redirects' => false
]);
}
/**
* @Then the login suceeds
*/
public function theLoginSuceeds()
{
Assert::isInstanceOf($this->res, Response::class);
Assert::eq( $this->res->getStatusCode(), 302);
Assert::startsWith($this->res->getHeader('Location')[0], 'http://localhost/wp-admin');
}
/**
* @Then a new WordPress user :arg1 was created with name :arg2 and email :arg3
*/
public function aNewWordpressUserWasCreatedWithNameAndEmail($arg1, $arg2, $arg3)
{
exec(sprintf(
'wp --allow-root user get %1$s --format=json 2> /dev/null',
$arg1,
), $output, $result);
Assert::eq(0, $result);
$user = json_decode($output[0], true);
Assert::eq($user['user_email'], $arg3);
Assert::eq($user['display_name'], $arg2);
Assert::greaterThan(
new DateTimeImmutable($user['user_registered']),
(new DateTimeImmutable())->sub(new DateInterval('PT1M')),
);
}
/**
* @Then the WordPress user :arg1 is member of role :arg2
*/
public function theWordpressUserIsMemberOfRole($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root user get %1$s --format=json 2> /dev/null',
$arg1,
), $output, $result);
Assert::eq(0, $result);
$user = json_decode($output[0], true);
$roles = array_map(function($item): string {
return trim($item);
}, explode(',', $user['roles']));
Assert::inArray($arg2, $roles);
}
/**
* @Given LDAP user :arg1 is member of LDAP group :arg2
*/
public function ldapUserIsMemberOfLdapGroup($arg1, $arg2)
{
exec(sprintf(
'ldapmodify -x -H %1$s -D "%2$s" -w %3$s 2>&1 <<LDIF
%4$s
LDIF',
'ldap://openldap',
'cn=admin,dc=example,dc=org',
'insecure',
<<<LDIF
dn: cn=$arg2,dc=example,dc=org
changetype: modify
add: uniqueMember
uniqueMember: uid=$arg1,dc=example,dc=org
LDIF
));
}
/**
* @Given a WordPress user :arg1 does not exist
*/
public function aWordpressUserDoesNotExist($arg1)
{
exec(sprintf(
'wp --allow-root user delete --yes %1$s',
$arg1,
));
}
/**
* @Given configuration value :arg1 is set to :arg2 and :arg3
*/
public function configurationValueIsSetToAnd($arg1, $arg2, $arg3)
{
$roles = [];
foreach ([$arg2, $arg3] as $arg) {
$access = explode('=', $arg);
$roles[$access[0]] = $access[1];
}
exec(sprintf(
'echo %2$s | wp --allow-root option patch update authLDAPOptions %1$s --format=json',
$arg1,
"'" . json_encode($roles) . "'"
), $result);
}
/**
* @Then the WordPress user :arg1 is not member of role :arg2
*/
public function theWordpressUserIsNotMemberOfRole($arg1, $arg2)
{
exec(sprintf(
'wp --allow-root user get %1$s --format=json 2> /dev/null',
$arg1,
), $output, $result);
Assert::eq(0, $result);
$user = json_decode($output[0], true);
$roles = array_map(function($item): string {
return trim($item);
}, explode(',', $user['roles']));
Assert::false(in_array($arg2, $roles));
}
/**
* @Given LDAP user :arg1 is not member of LDAP group :arg2
*/
public function ldapUserIsNotMemberOfLdapGroup($arg1, $arg2)
{
exec(sprintf(
'ldapmodify -x -H %1$s -D "%2$s" -w %3$s 2>&1 <<LDIF
%4$s
LDIF',
'ldap://openldap',
'cn=admin,dc=example,dc=org',
'insecure',
<<<LDIF
dn: cn=$arg2,dc=example,dc=org
changetype: modify
delete: uniqueMember
uniqueMember: uid=$arg1,dc=example,dc=org
LDIF
)); }
}

View File

@ -1,84 +0,0 @@
Feature: Log in without group assignment
Scenario: Login without group assignment with
Given a default configuration
And configuration value "GroupEnable" is set to "false"
And configuration value "DefaultRole" is set to "subscriber"
And an LDAP user "ldapuser" with name "LDAP User", password "P@ssw0rd" and email "ldapuser@example.com" exists
And an LDAP group "ldapgroup" exists
And LDAP user "ldapuser" is member of LDAP group "ldapgroup"
And a WordPress user "wordpressuser" with name "WordPress_User" and email "wordpressuser@example.com" exists
And a WordPress role "wordpressrole" exists
And WordPress user "wordpressuser" has role "wordpressrole"
And a WordPress user "ldapuser" does not exist
When LDAP user "ldapuser" logs in with password "P@ssw0rd"
Then the login suceeds
And a new WordPress user "ldapuser" was created with name "LDAP User" and email "ldapuser@example.com"
And the WordPress user "ldapuser" is member of role "subscriber"
Scenario: Login with group assignment to multiple groups where only first wordpress group is used
Given a default configuration
And configuration value "GroupEnable" is set to "true"
And configuration value "DefaultRole" is set to "subscriber"
And configuration value "Groups" is set to "administrator=ldapgroup" and "editor=ldapgroup"
And configuration value "GroupAttr" is set to "cn"
And configuration value "GroupFilter" is set to "uniquemember=%dn%"
And configuration value "GroupOverUser" is set to "true"
And an LDAP user "ldapuser" with name "LDAP User", password "P@ssw0rd" and email "ldapuser@example.com" exists
And an LDAP group "ldapgroup" exists
And LDAP user "ldapuser" is member of LDAP group "ldapgroup"
And a WordPress user "wordpressuser" with name "WordPress_User" and email "wordpressuser@example.com" exists
And a WordPress role "wordpressrole" exists
And WordPress user "wordpressuser" has role "wordpressrole"
And a WordPress user "ldapuser" does not exist
When LDAP user "ldapuser" logs in with password "P@ssw0rd"
Then the login suceeds
And a new WordPress user "ldapuser" was created with name "LDAP User" and email "ldapuser@example.com"
And the WordPress user "ldapuser" is member of role "administrator"
And the WordPress user "ldapuser" is not member of role "editor"
And the WordPress user "ldapuser" is not member of role "subscriber"
Scenario: Second Login with group assignment to multiple groups where only first wordpress group is used.
Given a default configuration
And configuration value "GroupEnable" is set to "true"
And configuration value "DefaultRole" is set to "subscriber"
And configuration value "Groups" is set to "administrator=ldapgroup" and "editor=ldapgroup"
And configuration value "GroupAttr" is set to "cn"
And configuration value "GroupFilter" is set to "uniquemember=%dn%"
And configuration value "GroupOverUser" is set to "false"
And an LDAP user "ldapuser" with name "LDAP User", password "P@ssw0rd" and email "ldapuser@example.com" exists
And an LDAP group "ldapgroup" exists
And LDAP user "ldapuser" is member of LDAP group "ldapgroup"
And a WordPress user "wordpressuser" with name "WordPress_User" and email "wordpressuser@example.com" exists
And a WordPress role "wordpressrole" exists
And WordPress user "wordpressuser" has role "wordpressrole"
And a WordPress user "ldapuser" does not exist
And LDAP user "ldapuser" logs in with password "P@ssw0rd"
And WordPress user "ldapuser" has role "wordpressrole"
And the WordPress user "ldapuser" is member of role "wordpressrole"
When LDAP user "ldapuser" logs in with password "P@ssw0rd"
Then the login suceeds
And the WordPress user "ldapuser" is member of role "administrator"
And the WordPress user "ldapuser" is member of role "wordpressrole"
And the WordPress user "ldapuser" is not member of role "editor"
And the WordPress user "ldapuser" is not member of role "subscriber"
Scenario: Second Login with group assignment that changes between first and second login
Given a default configuration
And configuration value "GroupEnable" is set to "true"
And configuration value "DefaultRole" is set to "subscriber"
And configuration value "Groups" is set to "administrator=ldapgroup1" and "editor=ldapgroup2"
And configuration value "GroupAttr" is set to "cn"
And configuration value "GroupFilter" is set to "uniquemember=%dn%"
And configuration value "GroupOverUser" is set to "true"
And an LDAP user "ldapuser" with name "LDAP User", password "P@ssw0rd" and email "ldapuser@example.com" exists
And an LDAP group "ldapgroup1" exists
And an LDAP group "ldapgroup2" exists
And LDAP user "ldapuser" is member of LDAP group "ldapgroup1"
And LDAP user "ldapuser" logs in with password "P@ssw0rd"
And LDAP user "ldapuser" is member of LDAP group "ldapgroup2"
And LDAP user "ldapuser" is not member of LDAP group "ldapgroup1"
When LDAP user "ldapuser" logs in with password "P@ssw0rd"
Then the login suceeds
And the WordPress user "ldapuser" is member of role "editor"
And the WordPress user "ldapuser" is not member of role "administrator"
And the WordPress user "ldapuser" is not member of role "subscriber"

View File

@ -1,22 +0,0 @@
<?xml version="1.0"?>
<ruleset name="Custom Standard" namespace="MyProject\CS\Standard">
<description>authLdap codestyle</description>
<file>./src</file>
<file>./authLdap.php</file>
<file>./tests</file>
<arg name="colors"/>
<arg value="sp"/>
<autoload>./vendor/autoload.php</autoload>
<rule ref="PSR12">
<exclude name="Generic.WhiteSpace.DisallowTabIndent"/>
</rule>
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="tabIndent" value="true"/>
</properties>
</rule>
</ruleset>

View File

@ -1,169 +0,0 @@
=== authLdap ===
Contributors: heiglandreas
Tags: ldap, auth, authentication, active directory, AD, openLDAP, Open Directory
Requires at least: 2.5.0
Tested up to: 6.5.0
Requires PHP: 7.4
Stable tag: trunk
License: MIT
License URI: https://opensource.org/licenses/MIT
Use your existing LDAP flexible as authentication backend for WordPress
== Description ==
Use your existing LDAP as authentication-backend for your wordpress!
So what are the differences to other Wordpress-LDAP-Authentication-Plugins?
* Flexible: You are totaly free in which LDAP-backend to use. Due to the extensive configuration you can freely decide how to do the authentication of your users. It simply depends on your filters
* Independent: As soon as a user logs in, it is added/updated to the Wordpress' user-database to allow wordpress to always use the correct data. You only have to administer your users once.
* Failsafe: Due to the users being created in Wordpress' User-database they can also log in when the LDAP-backend currently is gone.
* Role-Aware: You can map Wordpress' roles to values of an existing LDAP-attribute.
For more Information on the configuration have a look at https://github.com/heiglandreas/authLdap
== Installation ==
1. Upload the extracted folder `authLdap` to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Configure the Plugin via the 'authLdap'-Configuration-Page.
== Frequently Asked Questions ==
= Where can I find more Informations about the plugin? =
Go to https://github.com/heiglandreas/authLdap
= Where can I report issues with the plugin? =
Please use the issuetracker at https://github.com/heiglandreas/authLdap/issues
= Where can I report sensitive security issues with the plugin? =
In essence: Report a security vulnerability at https://github.com/heiglandreas/authLdap/security/advisories/new
Please see https://github.com/heiglandreas/authLdap/blob/master/SECURITY.md for more details
== Changelog ==
= 2.6.2 =
* Fix issue with Groups not being updated on existing accounts (see https://github.com/heiglandreas/authLdap/issues/250 for details)
= 2.6.0 =
* Fix reducing assigned WordPress roles to single role on login when WordPress roles shall be kept
* Add Behavioural testing and first 3 scenarios
= 2.5.9 =
* Adds information about security-contacts
* Addresses CVE-2023-41655
= 2.5.8 =
* Fix regression from 2.5.7
= 2.5.7 =
* Fix regressions from 2.5.4
* Fix CI system
= 2.5.4 =
* Update Tested up to
= 2.5.3 =
* Fix issue with broken role-assignement in combination with WooCommerce
* Fix spelling issue
* Allow DN as role-definition
= 2.5.0 =
* Ignore the order of capabilities to tell the role. In addition the filter `editable_roles` can be used to limit the roles
= 2.4.11 =
* Fix issue with running on PHP8.1
= 2.4.9 =
* Improve group-assignement UI
= 2.4.8 =
* Make textfields in settings-page wider
= 2.4.7 =
* Replace deprecated function
* Fix undefined index
* Add filter for retrieving other params at login (authLdap_filter_attributes)
* Add do_action after successfull login (authLdap_login_successful)
= 2.4.0 =
* Allow to use environment variables for LDAP-URI configuration
= 2.3.0 =
* Allow to not overwrite existing WordPress-Users with LDAP-Users as that can be a security issue.
= 2.1.0 =
* Add search-base for groups. This might come in handy for multisite-instances
= 2.0.0 =
* This new release adds Multi-Site support. It will no longer be possible to use this plugin just in one subsite of a multisite installation!
* Adds a warning screen to the config-section when no LDAPextension could be found
* Fixes an issue with the max-length of the username
= 1.5.1 =
* Fixes an issue with escaped backslashes and quotes
= 1.5.0 =
* Allows parts of the LDAP-URI to be URLEncoded
* Drops support for PHP 5.4
= 1.4.20 =
* Allows multiple LDAP-servers to be queried (given that they use the same attributes)
* Fixes issue with URL-Encoded informations (see https://github.com/heiglandreas/authLdap/issues/108)
= 1.4.19 =
* Adds support for TLS
= 1.4.14 =
* Update to showing password-fields check (thanks to @chaplina)
= 1.4.13 =
* Removed generation of default email-address (thanks to @henryk)
* Fixes password-hashing when caching passwords (thanks to @litinoveweedle)
* Removes the possibility to reset a password for LDAP-based users (thanks to @chaplina)
* Removes the password-change-Email from 4.3 on (thanks to @litinoveweedle)
* Fixes double authentication-attempt (that resulted in failed authentication) (thanks to @litinoveweedle)
= 1.4.10 =
* Cleanup by removing deprecated code
* Fixes issues with undefined variables
* Enables internal option-versioning
* Setting users nickname initially to the realname instead of the uid
* Fixes display of password-change possibility in users profile-page
= 1.4.9 =
* Fixed an issue with changing display name on every login
* Use proper way of looking up user-roles in setups w/o DB-prefix
= 1.4.8 =
* Updated version string
= 1.4.7 =
* Use default user to retrieve group menberships and not logging in user.
* return the UID from the LDAP instead of the value given by the user
* remove unnecessary checkbox
* Adds a testsuite
* Fixes PSR2 violations
[…]
= 1.2.1 =
* Fixed an issue with group-ids
* Moved the code to GitHub (https://github.com/heiglandreas/authLdap)
= 1.1.0 =
* Changed the login-process. Now users that are not allowed to login due to
missing group-memberships are not created within your blog as was the standard
until Version 1.0.3 - Thanks to alex@tayts.com
* Changed the default mail-address that is created when no mail-address can be
retrieved from the LDAP from me@example.com to $username@example.com so that
a new user can be created even though the mail address already exists in your
blog - Also thanks to alex@tayts.com
* Added support for WordPress-Table-prefixes as the capabilities of a user
are interlany stored in a field that is named "$tablePrefix_capabilities" -
again thanks to alex@tayts.com and also to sim0n of silicium.mine.nu

View File

@ -1,6 +0,0 @@
Contact: mailto://andreas@heigl.net
Contact: https://github.com/heiglandreas/authLdap/security/advisories/new
Expires: 2026-09-07T10:00:00.000Z
Encryption: https://andreas.heigl.org/publickey/
Encryption: https://heigl.org/.well-known/openpgpkey/hu/sfqdema7hgdj146cwzo4rxgsoujxis31
Preferred-Languages: en,de

View File

@ -1,24 +0,0 @@
<?php
/**
* Copyright Andrea Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Exception;
use Exception;
class Error extends Exception
{
public function __construct($message, $line = null)
{
parent::__construct($message);
if ($line) {
$this -> line = $line;
}
}
}

View File

@ -1,74 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Exception;
use RuntimeException;
use function sprintf;
class InvalidLdapUri extends RuntimeException
{
public static function cannotparse(string $ldapUri): self
{
return new self(sprintf(
'%1$s seems not to be a valid URI',
$ldapUri
));
}
public static function wrongSchema(string $uri): self
{
return new self(sprintf(
'%1$s does not start with a valid schema',
$uri
));
}
public static function noSchema(string $uri): self
{
return new self(sprintf(
'%1$s does not provide a schema',
$uri
));
}
public static function noEnvironmentVariableSet(string $uri): self
{
return new self(sprintf(
'The environment variable %1$s does not provide a URI',
$uri
));
}
public static function noServerProvided(string $uri): self
{
return new self(sprintf(
'The LDAP-URI %1$s does not provide a server',
$uri
));
}
public static function noSearchBaseProvided(string $uri): self
{
return new self(sprintf(
'The LDAP-URI %1$s does not provide a search-base',
$uri
));
}
public static function invalidSearchBaseProvided(string $uri): self
{
return new self(sprintf(
'The LDAP-URI %1$s does not provide a valid search-base',
$uri
));
}
}

View File

@ -1,23 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Exception;
use RuntimeException;
class MissingValidLdapConnection extends Error
{
public static function get(): self
{
return new self(sprintf(
'No valid LDAP connection available'
));
}
}

View File

@ -1,24 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Exception;
use RuntimeException;
class SearchUnsuccessfull extends RuntimeException
{
public static function fromSearchFilter(string $filter): self
{
return new self(sprintf(
'Search for %1$s was not successfull',
$filter
));
}
}

View File

@ -1,24 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licensed under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Exception;
use RuntimeException;
class UnknownOption extends RuntimeException
{
public static function withKey(string $key): self
{
return new self(sprintf(
'An option "%1$s" is not known',
$key
));
}
}

View File

@ -1,93 +0,0 @@
<?php
/**
* Copyright (c) Andreas Heigl<andreas@heigl.org>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author Andreas Heigl<andreas@heigl.org>
* @copyright Andreas Heigl
* @license http://www.opensource.org/licenses/mit-license.php MIT-License
* @since 07.07.2016
* @link http://github.com/heiglandreas/authLDAP
*/
namespace Org_Heigl\AuthLdap;
use Exception;
use Org_Heigl\AuthLdap\Exception\Error;
use Org_Heigl\AuthLdap\Exception\SearchUnsuccessfull;
use Org_Heigl\AuthLdap\Manager\Ldap;
class LdapList
{
/**
* @var Ldap[]
*/
protected $items = [];
public function addLdap(Ldap $ldap)
{
$this->items[] = $ldap;
}
public function authenticate($username, $password, $filter = '(uid=%s)')
{
/** @var Ldap $item */
foreach ($this->items as $key => $item) {
if (! $item->authenticate($username, $password, $filter)) {
unset($this->items[$key]);
continue;
}
return true;
}
return false;
}
public function bind()
{
$allFailed = true;
foreach ($this->items as $key => $item) {
try {
$item->bind();
} catch (\Exception $e) {
unset($this->items[$key]);
continue;
}
$allFailed = false;
}
if ($allFailed) {
throw new Error('No bind successfull');
}
return true;
}
public function search($filter, $attributes = array('uid'), $base = '')
{
foreach ($this->items as $item) {
try {
$result = $item->search($filter, $attributes, $base);
return $result;
} catch (Exception $e) {
}
}
throw SearchUnsuccessfull::fromSearchFilter($filter);
}
}

View File

@ -1,179 +0,0 @@
<?php
/**
* Copyright (c) Andreas Heigl<andreas@heigl.org>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author Andreas Heigl<andreas@heigl.org>
* @copyright Andreas Heigl
* @license http://www.opensource.org/licenses/mit-license.php MIT-License
* @since 19.07.2020
* @link http://github.com/heiglandreas/authLDAP
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap;
use Org_Heigl\AuthLdap\Exception\InvalidLdapUri;
use function array_map;
use function error_get_last;
use function getenv;
use function is_array;
use function is_string;
use function parse_url;
use function preg_replace_callback;
use function rawurlencode;
use function strlen;
use function strpos;
use function substr;
use function trim;
use function urldecode;
final class LdapUri
{
private $server;
private $scheme;
private $port = 389;
private string $baseDn;
private $username = '';
private $password = '';
private function __construct(string $uri)
{
if (!preg_match('/^(ldap|ldaps|env)/', $uri)) {
throw InvalidLdapUri::wrongSchema($uri);
}
if (strpos($uri, 'env:') === 0) {
$newUri = getenv(substr($uri, 4));
if (false === $newUri) {
throw InvalidLdapUri::noEnvironmentVariableSet($uri);
}
$uri = (string) $newUri;
}
$uri = $this->injectEnvironmentVariables($uri);
$array = parse_url($uri);
if (!is_array($array)) {
throw InvalidLdapUri::cannotparse($uri);
}
$url = array_map(static function ($item) {
if (is_int($item)) {
return $item;
}
return urldecode($item);
}, $array);
if (!isset($url['scheme'])) {
throw InvalidLdapUri::noSchema($uri);
}
if (0 !== strpos($url['scheme'], 'ldap')) {
throw InvalidLdapUri::wrongSchema($uri);
}
if (!isset($url['host'])) {
throw InvalidLdapUri::noServerProvided($uri);
}
if (!isset($url['path'])) {
throw InvalidLdapUri::noSearchBaseProvided($uri);
}
if (1 === strlen($url['path'])) {
throw InvalidLdapUri::invalidSearchBaseProvided($uri);
}
$this->server = $url['host'];
$this->scheme = $url['scheme'];
$this->baseDn = substr($url['path'], 1);
if (isset($url['user'])) {
$this->username = $url['user'];
}
if ('' === trim($this->username)) {
$this->username = 'anonymous';
}
if (isset($url['pass'])) {
$this->password = $url['pass'];
}
if ($this->scheme === 'ldaps' && $this->port === 389) {
$this->port = 636;
}
// When someone sets the port in the URL we overwrite whatever is set.
// We have to assume they know what they are doing!
if (isset($url['port'])) {
$this->port = $url['port'];
}
}
public static function fromString(string $uri): LdapUri
{
return new LdapUri($uri);
}
private function injectEnvironmentVariables(string $base): string
{
return preg_replace_callback('/%env:([^%]+)%/', static function (array $matches) {
return rawurlencode(getenv($matches[1]));
}, $base);
}
public function toString(): string
{
return $this->scheme . '://' . $this->server . ':' . $this->port;
}
public function __toString()
{
return $this->toString();
}
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): string
{
return $this->password;
}
public function getBaseDn(): string
{
return $this->baseDn;
}
public function isAnonymous(): bool
{
if ($this->password === '') {
return true;
}
if ($this->username === 'anonymous') {
return true;
}
return false;
}
}

View File

@ -1,164 +0,0 @@
<?php
/**
* $Id: ldap.php 381646 2011-05-06 09:37:31Z heiglandreas $
*
* authLdap - Authenticate Wordpress against an LDAP-Backend.
* Copyright (c) 2008 Andreas Heigl<andreas@heigl.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* This file handles the basic LDAP-Tasks
*
* @author Andreas Heigl<andreas@heigl.org>
* @package authLdap
* @category authLdap
* @since 2008
*/
namespace Org_Heigl\AuthLdap\Manager;
use Org_Heigl\AuthLdap\Exception\Error;
use Org_Heigl\AuthLdap\Exception\MissingValidLdapConnection;
use Org_Heigl\AuthLdap\LdapUri;
use Org_Heigl\AuthLdap\Wrapper\LdapFactory;
use Org_Heigl\AuthLdap\Wrapper\LdapInterface;
class Ldap
{
/**
* This property contains the connection handle to the ldap-server
*
* @var LdapInterface|null
*/
private ?LdapInterface $connection;
private LdapUri $uri;
private LdapFactory $factory;
private $starttls;
public function __construct(LdapFactory $factory, LdapUri $uri, $starttls = false)
{
$this->starttls = $starttls;
$this->uri = $uri;
$this->factory = $factory;
$this->connection = null;
}
/**
* Connect to the given LDAP-Server
*/
public function connect(): self
{
$this->disconnect();
$this->connection = $this->factory->createFromLdapUri($this->uri->toString());
$this->connection->setOption(LDAP_OPT_PROTOCOL_VERSION, 3);
$this->connection->setOption(LDAP_OPT_REFERRALS, 0);
//if configured try to upgrade encryption to tls for ldap connections
if ($this->starttls) {
$this->connection->startTls();
}
return $this;
}
/**
* Disconnect from a resource if one is available
*/
public function disconnect(): self
{
if (null !== $this->connection) {
$this->connection->unbind();
}
$this->connection = null;
return $this;
}
/**
* Bind to an LDAP-Server with the given credentials
*
* @throws Error
*/
public function bind(): self
{
if (!$this->connection) {
$this->connect();
}
if (null === $this->connection) {
throw MissingValidLdapConnection::get();
}
if ($this->uri->isAnonymous()) {
$bind = $this->connection->bind();
} else {
$bind = $this->connection->bind($this->uri->getUsername(), $this->uri->getPassword());
}
if (!$bind) {
throw new Error('bind was not successfull: ' . $this->connection->error());
}
return $this;
}
/**
* This method does the actual ldap-serch.
*
* This is using the filter <var>$filter</var> for retrieving the attributes
* <var>$attributes</var>
*
* @return array<string|int, mixed>
* @throws Error
*/
public function search(string $filter, array $attributes = ['uid'], ?string $base = ''): array
{
if (null === $this->connection) {
throw new Error('No resource handle available');
}
if (!$base) {
$base = $this->uri->getBaseDn();
}
$result = $this->connection->search($base, $filter, $attributes);
if ($result === false) {
throw new Error('no result found');
}
$info = $this->connection->getEntries($result);
if ($info === false) {
throw new Error('invalid results found');
}
return $info;
}
/**
* This method authenticates the user <var>$username</var> using the
* password <var>$password</var>
*
* @param string $filter OPTIONAL This parameter defines the Filter to be used
* when searchin for the username. This MUST contain the string '%s' which
* will be replaced by the vaue given in <var>$username</var>
* @throws Error
*/
public function authenticate(string $username, string $password, string $filter = '(uid=%s)'): bool
{
$this->connect();
$this->bind();
$res = $this->search(sprintf($filter, $this->factory->escape($username, '', LDAP_ESCAPE_FILTER)));
if ($res ['count'] !== 1) {
return false;
}
$dn = $res[0]['dn'];
return $username && $password && $this->connection->bind($dn, $password);
}
}

View File

@ -1,27 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licensed under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap;
use function json_decode;
class OptionFactory
{
public function fromJson(string $json): Options
{
$option = new Options();
$content = json_decode($json, true);
foreach ($content as $key => $value) {
$option->set($key, $value);
}
return $option;
}
}

View File

@ -1,91 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licensed under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap;
use Org_Heigl\AuthLdap\Exception\UnknownOption;
use function array_key_exists;
class Options
{
public const ENABLED = 'Enabled';
public const CACHE_PW = 'CachePW';
public const URI = 'URI';
public const URI_SEPARATOR = 'URISeparator';
public const FILTER = 'Filter';
public const NAME_ATTR = 'NameAttr';
public const SEC_NAME = 'SecName';
public const UID_ATTR = 'UidAttr';
public const MAIL_ATTR = 'MailAttr';
public const WEB_ATTR = 'WebAttr';
public const GROUPS = 'Groups';
public const DEBUG = 'Debug';
public const GROUP_ATTR = 'GroupAttr';
public const GROUP_FILTER = 'GroupFilter';
public const DEFAULT_ROLE = 'DefaultRole';
public const GROUP_ENABLE = 'GroupEnable';
public const GROUP_OVER_USER = 'GroupOverUser';
public const VERSION = 'Version';
public const DO_NOT_OVERWRITE_NON_LDAP_USERS = 'DoNotOverwriteNonLdapUsers';
private array $settings = [
'Enabled' => false,
'CachePW' => false,
'URI' => '',
'URISeparator' => ' ',
'Filter' => '', // '(uid=%s)'
'NameAttr' => '', // 'name'
'SecName' => '',
'UidAttr' => '', // 'uid'
'MailAttr' => '', // 'mail'
'WebAttr' => '',
'Groups' => [],
'Debug' => false,
'GroupAttr' => '', // 'gidNumber'
'GroupFilter' => '', // '(&(objectClass=posixGroup)(memberUid=%s))'
'DefaultRole' => '',
'GroupEnable' => true,
'GroupOverUser' => true,
'Version' => 1,
'DoNotOverwriteNonLdapUsers' => false,
];
public function get(string $key)
{
if (! array_key_exists($key, $this->settings)) {
throw UnknownOption::withKey($key);
}
return $this->settings[$key];
}
public function has(string $key): bool
{
return array_key_exists($key, $this->settings);
}
/**
* @param mixed $value
*/
public function set(string $key, $value): void
{
if (! array_key_exists($key, $this->settings)) {
throw UnknownOption::withKey($key);
}
$this->settings[$key] = $value;
}
public function toArray(): array
{
return $this->settings;
}
}

View File

@ -1,54 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap;
use WP_User;
use function array_search;
use function in_array;
use function var_dump;
class UserRoleHandler
{
/**
* @param WP_User $user
* @param string[] $roles
* @return void
*/
public function addRolesToUser(WP_User $user, $roles): void
{
if ($roles === []) {
return;
}
if ($user->roles == $roles) {
return;
}
// Remove unused roles from existing.
foreach ($user->roles as $role) {
if (!in_array($role, $roles)) {
// Remove unused roles.
$user->remove_role($role);
continue;
}
// Remove the existing role from roles.
if (($key = array_search($role, $roles)) !== false) {
unset($roles[$key]);
}
}
// Add new ones if not already assigned.
foreach ($roles as $role) {
$user->add_role($role);
}
}
}

View File

@ -1,93 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Wrapper;
use function ldap_bind;
use function ldap_connect;
use function ldap_error;
use function ldap_escape;
use function ldap_get_entries;
use function ldap_set_option;
use function ldap_start_tls;
use function ldap_unbind;
use function var_dump;
final class Ldap implements LdapInterface
{
private $connection;
public function __construct(string $ldapUri)
{
$this->connection = ldap_connect($ldapUri);
}
public function bind($dn = null, $password = null)
{
if (null === $dn && null === $password) {
return ldap_bind($this->connection);
}
return ldap_bind($this->connection, $dn, $password);
}
public function unbind()
{
return ldap_unbind($this->connection);
}
public function setOption($option, $value)
{
return ldap_set_option($this->connection, $option, $value);
}
public function startTls()
{
return ldap_start_tls($this->connection);
}
public function error()
{
return ldap_error($this->connection);
}
public function errno()
{
return ldap_errno($this->connection);
}
public function search(
$base,
$filter,
array $attributes = [],
$attributes_only = 0,
$sizelimit = -1,
$timelimit = -1
) {
return ldap_search(
$this->connection,
$base,
$filter,
$attributes,
$attributes_only,
$sizelimit,
$timelimit
);
}
public function getEntries($search_result)
{
return ldap_get_entries($this->connection, $search_result);
}
public static function escape(string $value, string $ignore = '', int $flags = 0): string
{
return ldap_escape($value, $ignore, $flags);
}
}

View File

@ -1,24 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Wrapper;
class LdapFactory
{
public function createFromLdapUri(string $ldapUri): LdapInterface
{
return new Ldap($ldapUri);
}
public function escape($value, $ignore = '', $flags = 0): string
{
return Ldap::escape($value, $ignore, $flags);
}
}

View File

@ -1,39 +0,0 @@
<?php
/**
* Copyright Andreas Heigl <andreas@heigl.org>
*
* Licenses under the MIT-license. For details see the included file LICENSE.md
*/
declare(strict_types=1);
namespace Org_Heigl\AuthLdap\Wrapper;
interface LdapInterface
{
public function bind($dn = null, $password = null);
public function unbind();
public function setOption($option, $value);
public function startTls();
public function error();
public function errno();
public function search(
$base,
$filter,
array $attributes = [],
$attributes_only = 0,
$sizelimit = -1,
$timelimit = -1
);
public function getEntries($search_result);
public static function escape(string $value, string $ignore = '', int $flags = 0): string;
}

View File

@ -1,455 +0,0 @@
<?php
/**
* Copyright (c)2014-2014 heiglandreas
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIBILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @category
* @author Andreas Heigl<andreas@heigl.org>
* @copyright ©2014-2014 Andreas Heigl
* @license http://www.opesource.org/licenses/mit-license.php MIT-License
* @version 0.0
* @since 19.12.14
* @link https://github.com/heiglandreas/authLdap
*/
?><div class="wrap">
<?php if (! extension_loaded('ldap')) : ?>
<div class="error"><strong>Caveat:</strong> The LDAP-extension is not loaded!
Without that extension it is not possible to query an LDAP-Server! Please have a look
at <a href="http://php.net/manual/install.php">the PHP-Installation page</a>
</div>
<?php endif ?>
<h2>AuthLDAP Options</h2>
<form class="authldap-options" method="post" id="authLDAP_options" action="<?php echo $action;?>">
<input name="authLdapNonce" type="hidden" value="<?php echo wp_create_nonce('authLdapNonce'); ?>" />
<h3 class="title">General Usage of authLDAP</h3>
<fieldset class="options">
<table class="form-table">
<tr>
<th>
<label for="authLDAPAuth">Enable Authentication via LDAP?</label>
</th>
<td>
<input type="checkbox" name="authLDAPAuth" id="authLDAPAuth" value="1"<?php echo $tChecked; ?>/>
</td>
</tr>
<tr>
<th>
<label for="authLDAPDebug">Debug AuthLDAP?</label>
</th>
<td>
<input type="checkbox" name="authLDAPDebug" id="authLDAPDebug" value="1"<?php echo $tDebugChecked; ?>/>
</td>
</tr>
<tr>
<th>
<label for="authLDAPDoNotOverwriteNonLdapUsers">Do not authenticate existing WordPress-Users</label>
</th>
<td>
<input type="checkbox" name="authLDAPDoNotOverwriteNonLdapUsers" id="authLDAPDoNotOverwriteNonLdapUsers" value="1"<?php echo $tDoNotOverwriteNonLdapUsers; ?>/>
<p class="description">
Shall we prohibit authenticating already in WordPress created users using LDAP? If you enable this, LDAP-Users with the same user-ID
as existing WordPress-Users can no longer take over the WordPress-Users account. This also means that LDAP-Users with the same User-ID as existing
WordPress-Users will <strong>not</strong> be able to authenticate anymore! Accounts that have been taken over already will not be affected by this setting.
</p>
<p class="description">This should only be checked if you know what you are doing!</p>
</td>
</tr>
<tr>
<th>
<label for="authLDAPCachePW">Save entered passwords in the wordpress user table?</label>
</th>
<td>
<input type="checkbox" name="authLDAPCachePW" id="authLDAPCachePW" value="1"<?php echo $tPWChecked; ?>/>
</td>
</tr>
<tr>
<th>
<label for="authLDAPGroupEnable">Map LDAP Groups to wordpress Roles?</label>
</th>
<td>
<input type="checkbox" name="authLDAPGroupEnable" id="authLDAPGroupEnable" value="1"<?php echo $tGroupChecked; ?>/>
<p class="description">
Search LDAP for user's groups and map to Wordpress Roles.
</p>
</td>
</tr>
</table>
</fieldset>
<h3 class="title">General Server Settings</h3>
<fieldset class="options">
<table class="form-table">
<tr>
<th>
<label for="authLDAPURI">LDAP URI</label>
</th>
<td>
<input type="text" name="authLDAPURI" id="authLDAPURI" placeholder="LDAP-URI"
class="regular-text" value="<?php echo esc_attr($authLDAPURI); ?>"/>
<p class="description">
The <abbr title="Uniform Ressource Identifier">URI</abbr>
for connecting to the LDAP-Server. This usualy takes the form
<var>&lt;scheme&gt;://&lt;user&gt;:&lt;password&gt;@&lt;server&gt;/&lt;path&gt;</var>
according to RFC 1738.</p>
<p class="description">
In this case it schould be something like
<var>ldap://uid=adminuser,dc=example,c=com:secret@ldap.example.com/dc=basePath,dc=example,c=com</var>.
</p>
<p class="description">
If your LDAP accepts anonymous login, you can ommit the user and
password-Part of the URI
</p>
<p class="description">
You can use the pseudo-schema <em>env</em> to provide your LDAP-URI from an environment-variable. So if you have your
LDAP-URI in a variable called <code>LDAP_URI</code> you can enter <code>env:LDAP_URI</code> in this field and at runtime the
appropriate value will be taken from the Environment-variable <code>LDAP_URI</code>. If the varialbe is not set, then the value will be empty.
</p>
<p class="description">
You can also provide different parts of the LDP-URI from environment variables by providing
<code>%env:[VARIABLENAME]%</code> within your LDAP-URI. So if you want to provide the
password from an Environment-variable <code>LDAP_PASSWORD</code> your LDAP-URI looks like
<code>ldap://uid=adminuser,dc=example,c=com:%env:LDAP_PASSWORD%@ldap.example.com/dc=basePath,dc=example,c=com</code>
</p>
<p class="description">
<strong>Caveat!</strong><br/>
If you are using Environment-variables for parts of the LDAP-URL then those <strong>must not</strong> be URL-Encoded!<br/>
Otherwise the different parts <strong>must</strong> be URL-Encoded!
</p>
</td>
</tr>
<tr>
<th>
<label for="authLDAPURISeparator">LDAP URI-Separator</label>
</th>
<td>
<input type="text" name="authLDAPURISeparator" id="authLDAPURISeparator" placeholder="LDAP-URI Separator"
class="regular-text" value="<?php echo esc_attr($authLDAPURISeparator); ?>"/>
<p class="description">
A separator that separates multiple LDAP-URIs from one another.
You can use that feature to try to authenticate against multiple LDAP-Servers
as long as they all have the same attribute-settings. The first LDAP-Server the user can
authenticate against will be used to handle the user.
</td>
</tr>
<tr>
<th>
<label for="authLDAPStartTLS" class="description">StartTLS</label>
</th>
<td>
<input type="checkbox" name="authLDAPStartTLS" id="authLDAPStartTLS" value="1"<?php echo esc_attr($tStartTLSChecked); ?>/>
<p class="description">
Use StartTLS for encryption of ldap connections. This setting is not to be used in combination with ldaps connections (ldap:// only).
</p>
</td>
<tr>
<th scope="row">
<label for="authLDAPFilter" class="description">Filter</label>
</th>
<td>
<input type="text" name="authLDAPFilter" id="authLDAPFilter" placeholder="(uid=%s)"
class="regular-text" value="<?php echo esc_attr($authLDAPFilter); ?>"/>
<p class="description">
Please provide a valid filter that can be used for querying the
<abbr title="Lightweight Directory Access Protocol">LDAP</abbr>
for the correct user. For more information on this
feature have a look at <a href="http://andreas.heigl.org/cat/dev/wp/authldap">http://andreas.heigl.org/cat/dev/wp/authldap</a>
</p>
<p class="description">
This field <strong>should</strong> include the string <code>%s</code>
that will be replaced with the username provided during log-in
</p>
<p class="description">
If you leave this field empty it defaults to <strong>(uid=%s)</strong>
</p>
</td>
</tr>
</table>
</fieldset>
<h3 class="title">Settings for creating new Users</h3>
<fieldset class="options">
<table class="form-table">
<tr>
<th scope="row">
<label for="authLDAPUseUserAccount">User-Read</label>
</th>
<td>
<input type="checkbox" name="authLDAPUseUserAccount" id="authLDAPUseUserAccount" value="1"<?php echo esc_attr($tUserRead); ?>/><br />
<p class="description">
If checked the plugin will use the user's account to query their own information. If not it will use the admin account.
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPNameAttr">Name-Attribute</label>
</th>
<td>
<input type="text" name="authLDAPNameAttr" id="authLDAPNameAttr" placeholder="name"
class="regular-text" value="<?php echo esc_attr($authLDAPNameAttr); ?>"/><br />
<p class="description">
Which Attribute from the LDAP contains the Full or the First name
of the user trying to log in.
</p>
<p class="description">
This defaults to <strong>name</strong>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPSecName">Second Name Attribute</label>
</th>
<td>
<input type="text" name="authLDAPSecName" id="authLDAPSecName" placeholder=""
class="regular-text" value="<?php echo esc_attr($authLDAPSecName); ?>" />
<p class="description">
If the above Name-Attribute only contains the First Name of the
user you can here specify an Attribute that contains the second name.
</p>
<p class="description">
This field is empty by default
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPUidAttr">User-ID Attribute</label>
</th>
<td>
<input type="text" name="authLDAPUidAttr" id="authLDAPUidAttr" placeholder="uid"
class="regular-text" value="<?php echo esc_attr($authLDAPUidAttr); ?>" />
<p class="description">
Please give the Attribute, that is used to identify the user. This
should be the same as you used in the above <em>Filter</em>-Option
</p>
<p class="description">
This field defaults to <strong>uid</strong>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPMailAttr">Mail Attribute</label>
</th>
<td>
<input type="text" name="authLDAPMailAttr" id="authLDAPMailAttr" placeholder="mail"
class="regular-text" value="<?php echo esc_attr($authLDAPMailAttr); ?>" />
<p class="description">
Which Attribute holds the eMail-Address of the user?
</p>
<p class="description">
If more than one eMail-Address are stored in the LDAP, only the first given is used
</p>
<p class="description">
This field defaults to <strong>mail</strong>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPWebAttr">Web-Attribute</label>
</th>
<td>
<input type="text" name="authLDAPWebAttr" id="authLDAPWebAttr" placeholder=""
class="regular-text" value="<?php echo esc_attr($authLDAPWebAttr); ?>" />
<p class="description">
If your users have a personal page (URI) stored in the LDAP, it can
be provided here.
</p>
<p class="description">
This field is empty by default
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPDefaultRole">Default Role</label>
</th>
<td>
<select name="authLDAPDefaultRole" id="authLDAPDefaultRole">
<option value="" <?php echo ( $authLDAPDefaultRole == '' ? 'selected="selected"' : '' ); ?>>
None (deny access)
</option>
<?php foreach ($roles->get_names() as $group => $vals) : ?>
<option value="<?php echo $group; ?>" <?php echo ( $authLDAPDefaultRole == $group ? 'selected="selected"' : '' ); ?>>
<?php echo esc_attr($vals); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description">
Here you can select the default role for users.
If you enable LDAP Groups below, they will take precedence over the Default Role.
</p>
<p class="description">
Existing users will retain their roles unless overriden by LDAP Groups below.
</p>
</td>
</tr>
</table>
</fieldset>
<div id="authldaprolemapping">
<h3 class="title">Groups for Roles</h3>
<fieldset class="options">
<table class="form-table">
<tr>
<th>
<label for="authLDAPGroupOverUser">LDAP Groups override role of existing users?</label>
</th>
<td>
<input type="checkbox" name="authLDAPGroupOverUser" id="authLDAPGroupOverUser" value="1"<?php echo esc_attr($tGroupOverUserChecked); ?>/>
<p class="description">
If role determined by LDAP Group differs from existing Wordpress User's role, use LDAP Group.
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPGroupBase">Group-Base</label>
</th>
<td>
<input type="text" name="authLDAPGroupBase" id="authLDAPGroupBase" placeholder=""
class="regular-text" value="<?php echo esc_attr($authLDAPGroupBase); ?>" />
<p class="description">
This is the base dn to lookup groups.
</p>
<p class="description">
If empty the base dn of the LDAP URI will be used
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPGroupAttr">Group-Attribute</label>
</th>
<td>
<input type="text" name="authLDAPGroupAttr" id="authLDAPGroupAttr" placeholder="gidNumber"
class="regular-text" value="<?php echo esc_attr($authLDAPGroupAttr); ?>" />
<p class="description">
This is the attribute that defines the Group-ID that can be matched
against the Groups defined further down
</p>
<p class="description">
This field defaults to <strong>gidNumber</strong>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPGroupSeparator">Group-Separator</label>
</th>
<td>
<input type="text" name="authLDAPGroupSeparator" id="authLDAPGroupSeparator" placeholder=","
class="regular-text" value="<?php echo esc_attr($authLDAPGroupSeparator); ?>" />
<p class="description">
This attribute defines the separator used for the Group-IDs listed in the
Groups defined further down. This is useful if the value of Group-Attribute
listed above can contain a comma (for example, when using the memberof attribute)
</p>
<p class="description">
This field defaults to <strong>, (comma)</strong>
</p>
</td>
</tr>
<tr>
<th scope="row">
<label for="authLDAPGroupFilter">Group-Filter</label>
</th>
<td>
<input type="text" name="authLDAPGroupFilter" id="authLDAPGroupFilter"
placeholder="(&amp;(objectClass=posixGroup)(memberUid=%s))"
class="regular-text" value="<?php echo esc_attr($authLDAPGroupFilter); ?>" />
<p class="description">
Here you can add the filter for selecting groups for ther
currentlly logged in user
</p>
<p class="description">
The Filter should contain the string <code>%s</code> which will be replaced by
the login-name of the currently logged in user
</p>
<p class="description">
Alternatively the string <code>%dn%</code> will be replaced by the
DN of the currently logged in user. This can be helpfull if
group-memberships are defined with DNs rather than UIDs
</p>
<p class="description">This field defaults to
<strong>(&amp;(objectClass=posixGroup)(memberUid=%s))</strong>
</p>
</td>
</tr>
</table>
</fieldset>
<h3 class="title">Role - group mapping</h3>
<fieldset class="options">
<p class="description">You can set multiple values per role by separating them with a coma</p>
<p class="description">The values are empty by default</p>
<table class="form-table">
<thead>
<th scope="row">Assign this WordPress-Role</th>
<th style="width:auto;">to members of this/these LDAP-Groups</th>
</thead>
<tbody>
<?php
foreach ($roles->get_names() as $group => $vals) :
$aGroup=$authLDAPGroups[$group]; ?>
<tr>
<th scope="row" style="width:auto; min-width: 200px;">
<label for="authLDAPGroups[<?php echo $group; ?>]">
<?php echo esc_attr($vals); ?>
</label>
</th>
<td>
<textarea name="authLDAPGroups[<?php echo $group; ?>]" id="authLDAPGroups[<?php echo $group; ?>]" cols=60 rows=5><?php
echo esc_textarea($aGroup);
?></textarea>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</fieldset>
</div>
<fieldset class="buttons">
<p class="submit">
<input type="submit" name="ldapOptionsSave" class="button button-primary" value="Save Changes" />
</p>
</fieldset>
</form>
</div>
<script type="text/javascript">
elem = document.getElementById('authLDAPGroupEnable');
if(! elem.checked) {
document.getElementById('authldaprolemapping').setAttribute('style', 'display:none;');
}
elem.addEventListener('change', function(e){
if(! e.target.checked) {
document.getElementById('authldaprolemapping').setAttribute('style', 'display:none;');
} else {
document.getElementById('authldaprolemapping').removeAttribute('style');
}
});
</script>

View File

@ -1 +0,0 @@
<?php

View File

@ -0,0 +1,78 @@
<?php
/**
* Cloudron SSO
*
* This plugin provides default settings for Cloudron SSO
*
* @package Cloudron SSO
* @category General
* @author Cloudron
* @copyright 2024 Cloudron
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
* @link https://git.cloudron.io/cloudron/wordpress-managed-app
*
* @wordpress-plugin
* Plugin Name: Cloudron SSO
* Plugin URI: https://git.cloudron.io/cloudron/wordpress-managed-app
* Description: Cloudron SSO Settings
* Version: 1.0.0
* Requires at least: 5.0
* Requires PHP: 7.4
* Author: Cloudron
* Author URI: http://cloudron.io
* Text Domain: cloudron-sso
* Domain Path: /languages
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Requires Plugins: openid-connect-generic
*/
/**
* openid-connect-generic plugin tweaks
*/
/**
* Change login button text
*/
add_filter('openid-connect-generic-login-button-text', function( $text ) {
$text = __('Login with Cloudron');
return $text;
});
/**
* Set a default role on OIDC user creating
*/
add_filter('openid-connect-generic-alter-user-data', function( $user_data, $user_claim ) {
// Don't register any user with their real email address. Create a fake internal address.
if ( empty( $user_data['role'] ) ) {
$settings = get_option( 'openid_connect_generic_settings', [] );
$user_data['role'] = isset( $settings['default_role'] ) && ! empty( $settings['default_role'] ) ? $settings['default_role'] : 'editor';
}
return $user_data;
}, 10, 2);
/**
* Add default role select box on the settings page of openid-connect-generic plugin
*/
add_filter('openid-connect-generic-settings-fields', function( $fields ) {
$editable_roles = wp_roles()->roles;
$roles = [];
foreach ($editable_roles as $role => $details) {
$roles[ esc_attr( $role ) ] = translate_user_role( $details['name'] );
}
// A select field for default user role
$fields['default_role'] = array(
'title' => __( 'Default user role', 'cloudron-sso' ),
'description' => __( 'A role set to OIDC users.', 'cloudron-sso' ),
'type' => 'select',
'options' => $roles,
'section' => 'user_settings',
);
return $fields;
});

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -eu
# Activate the plugin.
cd "/app"
echo "Activating plugin..."
if ! wp plugin is-active daggerhart-openid-connect-generic 2>/dev/null; then
wp plugin activate daggerhart-openid-connect-generic --quiet
fi
echo "Done!"

View File

@ -0,0 +1,57 @@
// For format details, https://containers.dev/implementors/json_reference/.
{
"name": "WordPress Development Environment",
"dockerComposeFile": "../docker-compose.yml",
"service": "app",
"mounts": ["source=dind-var-lib-docker,target=/var/lib/docker,type=volume"],
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"customizations": {
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["ms-azuretools.vscode-docker"]
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"./local-features/welcome-message": "latest"
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [8080, 8081, 8026, 3306],
// Maps a port number, "host:port" value, range, or regular expression to a set of default options. See port attributes for available options
"portsAttributes": {
"8080": {
"label": "WordPress Development/Testing Site"
},
"8081": {
"label": "phpMyAdmin"
},
"8026": {
"label": "MailHog"
},
"3306": {
"label": "MariaDB"
}
},
// Use `onCreateCommand` to run commands as part of the container creation.
//"onCreateCommand": "chmod +x .devcontainer/install.sh && .devcontainer/install.sh",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "chmod +x .devcontainer/setup.sh && .devcontainer/setup.sh",
// Use 'postStartCommand' to run commands after the container has started.
"postStartCommand": "chmod +x .devcontainer/activate.sh && .devcontainer/activate.sh",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "wp_php",
// A set of name-value pairs that sets or overrides environment variables for the devcontainer.json supporting service / tool (or sub-processes like terminals) but not the container as a whole.
"remoteEnv": { "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" }
}

View File

@ -0,0 +1,8 @@
{
"id": "welcome-message",
"name": "Install the First Start Welcome Message",
"install": {
"app": "",
"file": "install.sh"
}
}

View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -eux
export DEBIAN_FRONTEND=noninteractive
# Copy the welcome message
if [ ! -f /usr/local/etc/vscode-dev-containers/first-run-notice.txt ]; then
echo "Installing First Run Notice..."
echo -e "👋 Welcome to \"OpenID Connect for WP Development\" in Dev Containers!\n\n🛠 Your environment is fully setup with all the required software.\n\n🚀 To get started, wait for the \"postCreateCommand\" to finish setting things up, then open the portforwarded URL and append '/wp/wp-admin'. Login to the WordPress Dashboard using \`admin/password\` for the credentials.\n" | sudo tee /usr/local/etc/vscode-dev-containers/first-run-notice.txt
fi
echo "Done!"

View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -eu
# true is shell command and always return 0
# false always return 1
if [ -z "${CODESPACES}" ] ; then
SITE_HOST="http://localhost:8080"
else
SITE_HOST="https://${CODESPACE_NAME}-8080.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
fi
PLUGIN_DIR=/workspaces/openid-connect-generic
# Attempt to make ipv4 traffic have a higher priority than ipv6.
sudo sh -c "echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf"
# Install Composer dependencies.
cd "${PLUGIN_DIR}"
echo "Installing Composer dependencies..."
COMPOSER_NO_INTERACTION=1 COMPOSER_ALLOW_XDEBUG=0 COMPOSER_MEMORY_LIMIT=-1 composer install --no-progress --quiet
# Install NPM dependencies.
cd "${PLUGIN_DIR}"
if [ ! -d "node_modules" ]; then
echo "Installing NPM dependencies..."
npm ci
fi
# Setup the WordPress environment.
cd "/app"
if ! wp core is-installed 2>/dev/null; then
echo "Setting up WordPress at $SITE_HOST"
wp core install --url="$SITE_HOST" --title="OpenID Connect Development" --admin_user="admin" --admin_email="admin@example.com" --admin_password="password" --skip-email --quiet
fi
echo "Done!"

View File

@ -0,0 +1,17 @@
# List the start up tasks. Learn more https://www.gitpod.io/docs/config-start-tasks/
tasks:
- name: WordPress Development Environment
init: npm run setup # runs during prebuild
command: |
npm start -- --update
npm stop
npm start -- --update
# List the ports to expose. Learn more https://www.gitpod.io/docs/config-ports/
ports:
- port: 8888
onOpen: notify
visibility: public
- port: 8889
onOpen: notify
visibility: public

View File

@ -0,0 +1,16 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"label": "npm: build",
"detail": "npm run grunt build"
}
]
}

View File

@ -0,0 +1,203 @@
# OpenId Connect Generic Changelog
**3.10.0**
- Chore: @timnolte - Dependency updates.
- Fix: @drzraf - Prevents running the auth url filter twice.
- Fix: @timnolte - Updates the log cleanup handling to properly retain the configured number of log entries.
- Fix: @timnolte - Updates the log display output to reflect the log retention policy.
- Chore: @timnolte - Adds Unit Testing & New Local Development Environment.
- Feature: @timnolte - Updates logging to allow for tracking processing time.
- Feature: @menno-ll - Adds a remember me feature via a new filter.
- Improvement: @menno-ll - Updates WP Cookie Expiration to Same as Session Length.
**3.9.1**
- Improvement: @timnolte - Refactors Composer setup and GitHub Actions.
- Improvement: @timnolte - Bumps WordPress tested version compatibility.
**3.9.0**
- Feature: @matchaxnb - Added support for additional configuration constants.
- Feature: @schanzen - Added support for agregated claims.
- Fix: @rkcreation - Fixed access token not updating user metadata after login.
- Fix: @danc1248 - Fixed user creation issue on Multisite Networks.
- Feature: @RobjS - Added plugin singleton to support for more developer customization.
- Feature: @jkouris - Added action hook to allow custom handling of session expiration.
- Fix: @tommcc - Fixed admin CSS loading only on the plugin settings screen.
- Feature: @rkcreation - Added method to refresh the user claim.
- Feature: @Glowsome - Added acr_values support & verification checks that it when defined in options is honored.
- Fix: @timnolte - Fixed regression which caused improper fallback on missing claims.
- Fix: @slykar - Fixed missing query string handling in redirect URL.
- Fix: @timnolte - Fixed issue with some user linking and user creation handling.
- Improvement: @timnolte - Fixed plugin settings typos and screen formatting.
- Security: @timnolte - Updated build tooling security vulnerabilities.
- Improvement: @timnolte - Changed build tooling scripts.
**3.8.5**
- Fix: @timnolte - Fixed missing URL request validation before use & ensure proper current page URL is setup for Redirect Back.
- Fix: @timnolte - Fixed Redirect URL Logic to Handle Sub-directory Installs.
- Fix: @timnolte - Fixed issue with redirecting user back when the openid_connect_generic_auth_url shortcode is used.
**3.8.4**
- Fix: @timnolte - Fixed invalid State object access for redirection handling.
- Improvement: @timnolte - Fixed local wp-env Docker development environment.
- Improvement: @timnolte - Fixed Composer scripts for linting and static analysis.
**3.8.3**
- Fix: @timnolte - Fixed problems with proper redirect handling.
- Improvement: @timnolte - Changes redirect handling to use State instead of cookies.
- Improvement: @timnolte - Refactored additional code to meet coding standards.
**3.8.2**
- Fix: @timnolte - Fixed reported XSS vulnerability on WordPress login screen.
**3.8.1**
- Fix: @timnolte - Prevent SSO redirect on password protected posts.
- Fix: @timnolte - CI/CD build issues.
- Fix: @timnolte - Invalid redirect handling on logout for Auto Login setting.
**3.8.0**
- Feature: @timnolte - Ability to use 6 new constants for setting client configuration instead of storing in the DB.
- Improvement: @timnolte - NPM version requirements for development.
- Improvement: @timnolte - Travis CI build fixes.
- Improvement: @timnolte - GrumPHP configuration updates for code contributions.
- Improvement: @timnolte - Refactored to meet WordPress coding standards.
- Improvement: @timnolte - Refactored to provide localization.
- Improvement: @timnolte - Refactored to provide a Docker-based local development environment.
**3.7.1**
- Fix: Release Version Number.
**3.7.0**
- Feature: @timnolte - Ability to enable/disable token refresh. Useful for IDPs that don't support token refresh.
- Feature: @timnolte - Support custom redirect URL(`redirect_to`) with the authentication URL & login button shortcodes.
- Supports additional attribute overrides including login `button_text`, `endpoint_login`, `scope`, `redirect_uri`.
**3.6.0**
- Improvement: @RobjS - Improved error messages during login state failure.
- Improvement: @RobjS - New developer filter for login form button URL.
- Fix: @cs1m0n - Only increment username during new user creation if the "Link existing user" setting is enabled.
- Fix: @xRy-42 - Allow periods and spaces in usernames to match what WordPress core allows.
- Feature: @benochen - New setting named "Create user if does not exist" determines whether new users are created during login attempts.
- Improvement: @flat235 - Username transliteration and normalization.
**3.5.1**
- Fix: @daggerhart - New approach to state management using transients.
**3.5.0**
- Readme fix: @thijskh - Fix syntax error in example openid-connect-generic-login-button-text
- Feature: @slavicd - Allow override of the plugin by posting credentials to wp-login.php
- Feature: @gassan - New action on use login
- Fix: @daggerhart - Avoid double question marks in auth url query string
- Fix: @drzraf - wp-cli bootstrap must not inhibit custom rewrite rules
- Syntax change: @mullikine - Change PHP keywords to comply with PSR2
**3.4.1**
- Minor documentation update and additional error checking.
**3.4.0**
- Feature: @drzraf - New filter hook: ability to filter claim and derived user data before user creation.
- Feature: @anttileppa - State time limit can now be changed on the settings page.
- Fix: @drzraf - Fix PHP notice when using traditional login, $token_response may be empty.
- Fix: @drzraf - Fixed a notice when cookie does not contain expected redirect_url
**3.3.1**
- Prefixing classes for more efficient autoloading.
- Avoid altering global wp_remote_post() parameters.
- Minor metadata updates for wp.org
**3.3.0**
- Fix: @pjeby - Handle multiple user sessions better by using the `WP_Session_Tokens` object. Predecessor to fixes for multiple other issues: #49, #50, #51
**3.2.1**
- Bug fix: @svenvanhal - Exit after issuing redirect. Fixes #46
**3.2.0**
- Feature: @robbiepaul - trigger core action `wp_login` when user is logged in through this plugin
- Feature: @moriyoshi - Determine the WP_User display name with replacement tokens on the settings page. Tokens can be any property of the user_claim.
- Feature: New setting to set redirect URL when session expires.
- Feature: @robbiepaul - New filter for modifying authentication URL
- Fix: @cedrox - Adding id_token_hint to logout URL according to spec
- Bug fix: Provide port to the request header when requesting the user_claim
**3.1.0**
- Feature: @rwasef1830 - Refresh tokens
- Feature: @rwasef1830 - Integrated logout support with end_session endpoint
- Feature: May use an alternate redirect_uri that doesn't rely on admin-ajax
- Feature: @ahatherly - Support for IDP behind reverse proxy
- Bug fix: @robertstaddon - case insensitive check for Bearer token
- Bug fix: @rwasef1830 - "redirect to origin when auto-sso" cookie issue
- Bug fix: @rwasef1830 - PHP Warnings headers already sent due to attempts to redirect and set cookies during login form message
- Bug fix: @rwasef1830 - expire session when access_token expires if no refresh token found
- UX fix: @rwasef1830 - Show login button on error redirect when using auto-sso
**3.0.8**
- Feature: @wgengarelly - Added `openid-connect-generic-update-user-using-current-claim` action hook allowing other plugins/themes
to take action using the fresh claims received when an existing user logs in.
**3.0.7**
- Bug fix: @wgengarelly - When requesting userinfo, send the access token using the Authorization header field as recommended in
section 5.3.1 of the specs.
**3.0.6**
- Bug fix: @robertstaddon - If "Link Existing Users" is enabled, allow users who login with OpenID Connect to also log in with WordPress credentials
**3.0.5**
- Feature: @robertstaddon - Added `[openid_connect_generic_login_button]` shortcode to allow the login button to be placed anywhere
- Feature: @robertstaddon - Added setting to "Redirect Back to Origin Page" after a successful login instead of redirecting to the home page.
**3.0.4**
- Feature: @robertstaddon - Added setting to allow linking existing WordPress user accounts with newly-authenticated OpenID Connect login
**3.0.3**
- Using WordPresss's is_ssl() for setcookie()'s "secure" parameter
- Bug fix: Incrementing username in case of collision.
- Bug fix: Wrong error sent when missing token body
**3.0.2**
- Added http_request_timeout setting
**3.0.1**
- Finalizing 3.0.x api
**3.0**
- Complete rewrite to separate concerns
- Changed settings keys for clarity (requires updating settings if upgrading from another version)
- Error logging
**2.1**
- Working my way closer to spec. Possible breaking change. Now checking for preferred_username as priority.
- New username determination to avoid collisions
**2.0**
Complete rewrite

View File

@ -0,0 +1,383 @@
# OpenID Connect Generic Client
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
A simple client that provides SSO or opt-in authentication against a generic OAuth2 Server implementation.
## Description
This plugin allows to authenticate users against OpenID Connect OAuth2 API with Authorization Code Flow.
Once installed, it can be configured to automatically authenticate users (SSO), or provide a "Login with OpenID Connect"
button on the login form. After consent has been obtained, an existing user is automatically logged into WordPress, while
new users are created in WordPress database.
Much of the documentation can be found on the Settings > OpenID Connect Generic dashboard page.
## Table of Contents
- [Installation](#installation)
- [Composer](#composer)
- [Frequently Asked Questions](#frequently-asked-questions)
- [What is the client's Redirect URI?](#what-is-the-clients-redirect-uri)
- [Can I change the client's Redirect URI?](#can-i-change-the-clients-redirect-uri)
- [Configuration Environment Variables/Constants](#configuration-environment-variables-constants)
- [Hooks](#hooks)
- [Filters](#filters)
- [openid-connect-generic-alter-request](#openid-connect-generic-alter-request)
- [openid-connect-generic-login-button-text](#openid-connect-generic-login-button-text)
- [openid-connect-generic-auth-url](#openid-connect-generic-auth-url)
- [openid-connect-generic-user-login-test](#openid-connect-generic-user-login-test)
- [openid-connect-generic-user-creation-test](#openid-connect-generic-user-creation-test)
- <del>[openid-connect-generic-alter-user-claim](#openid-connect-generic-alter-user-claim)</del>
- [openid-connect-generic-alter-user-data](#openid-connect-generic-alter-user-data)
- [openid-connect-generic-settings-fields](#openid-connect-generic-settings-fields)
- [Actions](#actions)
- [openid-connect-generic-user-create](#openid-connect-generic-user-create)
- [openid-connect-generic-user-update](#openid-connect-generic-user-update)
- [openid-connect-generic-update-user-using-current-claim](#openid-connect-generic-update-user-using-current-claim)
- [openid-connect-generic-redirect-user-back](#openid-connect-generic-redirect-user-back)
## Installation
1. Upload to the `/wp-content/plugins/` directory
1. Activate the plugin
1. Visit Settings > OpenID Connect and configure to meet your needs
### Composer
[OpenID Connect Generic on packagist](https://packagist.org/packages/daggerhart/openid-connect-generic)
Installation:
`composer require daggerhart/openid-connect-generic`
## Frequently Asked Questions
### What is the client's Redirect URI?
Most OAuth2 servers should require a whitelist of redirect URIs for security purposes. The Redirect URI provided
by this client is like so: `https://example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize`
Replace `example.com` with your domain name and path to WordPress.
### Can I change the client's Redirect URI?
Some OAuth2 servers do not allow for a client redirect URI to contain a query string. The default URI provided by
this module leverages WordPress's `admin-ajax.php` endpoint as an easy way to provide a route that does not include
HTML, but this will naturally involve a query string. Fortunately, this plugin provides a setting that will make use of
an alternate redirect URI that does not include a query string.
On the settings page for this plugin (Dashboard > Settings > OpenID Connect Generic) there is a checkbox for
**Alternate Redirect URI**. When checked, the plugin will use the Redirect URI
`https://example.com/openid-connect-authorize`.
## Configuration Environment Variables/Constants
- Client ID: `OIDC_CLIENT_ID`
- Client Secret Key: `OIDC_CLIENT_SECRET`
- Login Endpoint URL: `OIDC_ENDPOINT_LOGIN_URL`
- Userinfo Endpoint URL: `OIDC_ENDPOINT_USERINFO_URL`
- Token Validation Endpoint URL: `OIDC_ENDPOINT_TOKEN_URL`
- End Session Endpoint URL: `OIDC_ENDPOINT_LOGOUT_URL`
- OpenID scope: `OIDC_CLIENT_SCOPE` (space separated)
- OpenID login type: `OIDC_LOGIN_TYPE` ('button' or 'auto')
- Enforce privacy: `OIDC_ENFORCE_PRIVACY` (boolean)
- Create user if they do not exist: `OIDC_CREATE_IF_DOES_NOT_EXIST` (boolean)
- Link existing user: `OIDC_LINK_EXISTING_USERS` (boolean)
- Redirect user back to origin page: `OIDC_REDIRECT_USER_BACK` (boolean)
- Redirect on logout: `OIDC_REDIRECT_ON_LOGOUT` (boolean)
## Hooks
This plugin provides a number of hooks to allow for a significant amount of customization of the plugin operations from
elsewhere in the WordPress system.
### Filters
Filters are WordPress hooks that are used to modify data. The first argument in a filter hook is always expected to be
returned at the end of the hook.
WordPress filters API - [`add_filter()`](https://developer.wordpress.org/reference/functions/add_filter/) and
[`apply_filters()`](https://developer.wordpress.org/reference/functions/apply_filters/).
Most often you'll only need to use `add_filter()` to hook into this plugin's code.
#### `openid-connect-generic-alter-request`
Hooks directly into client before requests are sent to the OpenID Server.
Provides 2 arguments: the request array being sent to the server, and the operation currently being executed by this
plugin.
Possible operations:
- get-authentication-token
- refresh-token
- get-userinfo
```
add_filter('openid-connect-generic-alter-request', function( $request, $operation ) {
if ( $operation == 'get-authentication-token' ) {
$request['some_key'] = 'modified value';
}
return $request;
}, 10, 2);
```
#### `openid-connect-generic-login-button-text`
Modify the login button text. Default value is `__( 'Login with OpenID Connect' )`.
Provides 1 argument: the current login button text.
```
add_filter('openid-connect-generic-login-button-text', function( $text ) {
$text = __('Login to my super cool IDP server');
return $text;
});
```
#### `openid-connect-generic-auth-url`
Modify the authentication URL before presented to the user. This is the URL that will send the user to the IDP server
for login.
Provides 1 argument: the plugin generated URL.
```
add_filter('openid-connect-generic-auth-url', function( $url ) {
// Add some custom data to the url.
$url.= '&my_custom_data=123abc';
return $url;
});
```
#### `openid-connect-generic-user-login-test`
Determine whether or not the user should be logged into WordPress.
Provides 2 arguments: the boolean result of the test (default `TRUE`), and the `$user_claim` array from the server.
```
add_filter('openid-connect-generic-user-login-test', function( $result, $user_claim ) {
// Don't let Terry login.
if ( $user_claim['email'] == 'terry@example.com' ) {
$result = FALSE;
}
return $result;
}, 10, 2);
```
#### `openid-connect-generic-user-creation-test`
Determine whether or not the user should be created. This filter is called when a new user is trying to login and they
do not currently exist within WordPress.
Provides 2 arguments: the boolean result of the test (default `TRUE`), and the `$user_claim` array from the server.
```
add_filter('', function( $result, $user_claim ) {
// Don't let anyone from example.com create an account.
$email_array = explode( '@', $user_claim['email'] );
if ( $email_array[1] == 'example.com' ) {
$result = FALSE;
}
return $result;
}, 10, 2)
```
#### <del>`openid-connect-generic-alter-user-claim`</del>
Modify the `$user_claim` before the plugin builds the `$user_data` array for new user created.
**Deprecated** - This filter is not very useful due to some changes that were added later. Recommend not using this
filter, and using the `openid-connect-generic-alter-user-data` filter instead. Practically, you can only change the
user's `first_name` and `last_name` values with this filter, but you could easily do that in
`openid-connect-generic-alter-user-data` as well.
Provides 1 argument: the `$user_claim` from the server.
```
// Not a great example because the hook isn't very useful.
add_filter('openid-connect-generic-alter-user-claim', function( $user_claim ) {
// Use the beginning of the user's email address as the user's first name.
if ( empty( $user_claim['given_name'] ) ) {
$email_array = explode( '@', $user_claim['email'] );
$user_claim['given_name'] = $email_array[0];
}
return $user_claim;
});
```
#### `openid-connect-generic-alter-user-data`
Modify a new user's data immediately before the user is created.
Provides 2 arguments: the `$user_data` array that will be sent to `wp_insert_user()`, and the `$user_claim` from the
server.
```
add_filter('openid-connect-generic-alter-user-data', function( $user_data, $user_claim ) {
// Don't register any user with their real email address. Create a fake internal address.
if ( !empty( $user_data['user_email'] ) ) {
$email_array = explode( '@', $user_data['user_email'] );
$email_array[1] = 'my-fake-domain.co';
$user_data['user_email'] = implode( '@', $email_array );
}
return $user_data;
}, 10, 2);
```
#### `openid-connect-generic-settings-fields`
For extending the plugin with a new setting field (found on Dashboard > Settings > OpenID Connect Generic) that the site
administrator can modify. Also useful to alter the existing settings fields.
See `/includes/openid-connect-generic-settings-page.php` for how fields are constructed.
New settings fields will be automatically saved into the wp_option for this plugin's settings, and will be available in
the `\OpenID_Connect_Generic_Option_Settings` object this plugin uses.
**Note:** It can be difficult to get a copy of the settings from within other hooks. The easiest way to make use of
settings in your custom hooks is to call
`$settings = get_option('openid_connect_generic_settings', array());`.
Provides 1 argument: the existing fields array.
```
add_filter('openid-connect-generic-settings-fields', function( $fields ) {
// Modify an existing field's title.
$fields['endpoint_userinfo']['title'] = __('User information endpoint url');
// Add a new field that is a simple checkbox.
$fields['block_terry'] = array(
'title' => __('Block Terry'),
'description' => __('Prevent Terry from logging in'),
'type' => 'checkbox',
'section' => 'authorization_settings',
);
// A select field that provides options.
$fields['deal_with_terry'] = array(
'title' => __('Manage Terry'),
'description' => __('How to deal with Terry when he tries to log in.'),
'type' => 'select',
'options' => array(
'allow' => __('Allow login'),
'block' => __('Block'),
'redirect' => __('Redirect'),
),
'section' => 'authorization_settings',
);
return $fields;
});
```
"Sections" are where your setting appears on the admin settings page. Keys for settings sections:
- client_settings
- user_settings
- authorization_settings
- log_settings
Field types:
- text
- checkbox
- select (requires an array of "options")
### Actions
WordPress actions are generic events that other plugins can react to.
Actions API: [`add_action`](https://developer.wordpress.org/reference/functions/add_action/) and [`do_actions`](https://developer.wordpress.org/reference/functions/do_action/)
You'll probably only ever want to use `add_action` when hooking into this plugin.
#### `openid-connect-generic-user-create`
React to a new user being created by this plugin.
Provides 2 arguments: the `\WP_User` object that was created, and the `$user_claim` from the IDP server.
```
add_action('openid-connect-generic-user-create', function( $user, $user_claim ) {
// Send the user an email when their account is first created.
wp_mail(
$user->user_email,
__('Welcome to my web zone'),
"Hi {$user->first_name},\n\nYour account has been created at my cool website.\n\n Enjoy!"
);
}, 10, 2);
```
#### `openid-connect-generic-user-update`
React to the user being updated after login. This is the event that happens when a user logins and they already exist as
a user in WordPress, as opposed to a new WordPress user being created.
Provides 1 argument: the user's WordPress user ID.
```
add_action('openid-connect-generic-user-update', function( $uid ) {
// Keep track of the number of times the user has logged into the site.
$login_count = get_user_meta( $uid, 'my-user-login-count', TRUE);
$login_count += 1;
add_user_meta( $uid, 'my-user-login-count', $login_count, TRUE);
});
```
#### `openid-connect-generic-update-user-using-current-claim`
React to an existing user logging in (after authentication and authorization).
Provides 2 arguments: the `WP_User` object, and the `$user_claim` provided by the IDP server.
```
add_action('openid-connect-generic-update-user-using-current-claim', function( $user, $user_claim) {
// Based on some data in the user_claim, modify the user.
if ( !empty( $user_claim['wp_user_role'] ) ) {
if ( $user_claim['wp_user_role'] == 'should-be-editor' ) {
$user->set_role( 'editor' );
}
}
}, 10, 2);
```
#### `openid-connect-generic-redirect-user-back`
React to a user being redirected after a successful login. This hook is the last hook that will fire when a user logs
in. It will only fire if the plugin setting "Redirect Back to Origin Page" is enabled at Dashboard > Settings >
OpenID Connect Generic. It will fire for both new and existing users.
Provides 2 arguments: the url where the user will be redirected, and the `WP_User` object.
```
add_action('openid-connect-generic-redirect-user-back', function( $redirect_url, $user ) {
// Take over the redirection complete. Send users somewhere special based on their capabilities.
if ( $user->has_cap( 'edit_users' ) ) {
wp_redirect( admin_url( 'users.php' ) );
exit();
}
}, 10, 2);
```
### User Meta Data
This plugin stores meta data about the user for both practical and debugging purposes.
* `openid-connect-generic-subject-identity` - The identity of the user provided by the IDP server.
* `openid-connect-generic-last-id-token-claim` - The user's most recent `id_token` claim, decoded and stored as an array.
* `openid-connect-generic-last-user-claim` - The user's most recent `user_claim`, stored as an array.
* `openid-connect-generic-last-token-response` - The user's most recent `token_response`, stored as an array.

View File

@ -0,0 +1,125 @@
# OpenID Connect Generic Client #
**Contributors:** [daggerhart](https://profiles.wordpress.org/daggerhart/), [tnolte](https://profiles.wordpress.org/tnolte/)
**Donate link:** http://www.daggerhart.com/
**Tags:** security, login, oauth2, openidconnect, apps, authentication, autologin, sso
**Requires at least:** 5.0
**Tested up to:** 6.4.3
**Stable tag:** 3.10.0
**Requires PHP:** 7.4
**License:** GPLv2 or later
**License URI:** http://www.gnu.org/licenses/gpl-2.0.html
A simple client that provides SSO or opt-in authentication against a generic OAuth2 Server implementation.
## Description ##
This plugin allows to authenticate users against OpenID Connect OAuth2 API with Authorization Code Flow.
Once installed, it can be configured to automatically authenticate users (SSO), or provide a "Login with OpenID Connect"
button on the login form. After consent has been obtained, an existing user is automatically logged into WordPress, while
new users are created in WordPress database.
Much of the documentation can be found on the Settings > OpenID Connect Generic dashboard page.
Please submit issues to the Github repo: https://github.com/daggerhart/openid-connect-generic
## Installation ##
1. Upload to the `/wp-content/plugins/` directory
1. Activate the plugin
1. Visit Settings > OpenID Connect and configure to meet your needs
## Frequently Asked Questions ##
### What is the client's Redirect URI? ###
Most OAuth2 servers will require whitelisting a set of redirect URIs for security purposes. The Redirect URI provided
by this client is like so: https://example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize
Replace `example.com` with your domain name and path to WordPress.
### Can I change the client's Redirect URI? ###
Some OAuth2 servers do not allow for a client redirect URI to contain a query string. The default URI provided by
this module leverages WordPress's `admin-ajax.php` endpoint as an easy way to provide a route that does not include
HTML, but this will naturally involve a query string. Fortunately, this plugin provides a setting that will make use of
an alternate redirect URI that does not include a query string.
On the settings page for this plugin (Dashboard > Settings > OpenID Connect Generic) there is a checkbox for
**Alternate Redirect URI**. When checked, the plugin will use the Redirect URI
`https://example.com/openid-connect-authorize`.
## Changelog ##
### 3.10.0 ###
* Chore: @timnolte - Dependency updates.
* Fix: @drzraf - Prevents running the auth url filter twice.
* Fix: @timnolte - Updates the log cleanup handling to properly retain the configured number of log entries.
* Fix: @timnolte - Updates the log display output to reflect the log retention policy.
* Chore: @timnolte - Adds Unit Testing & New Local Development Environment.
* Feature: @timnolte - Updates logging to allow for tracking processing time.
* Feature: @menno-ll - Adds a remember me feature via a new filter.
* Improvement: @menno-ll - Updates WP Cookie Expiration to Same as Session Length.
### 3.9.1 ###
* Improvement: @timnolte - Refactors Composer setup and GitHub Actions.
* Improvement: @timnolte - Bumps WordPress tested version compatibility.
### 3.9.0 ###
* Feature: @matchaxnb - Added support for additional configuration constants.
* Feature: @schanzen - Added support for agregated claims.
* Fix: @rkcreation - Fixed access token not updating user metadata after login.
* Fix: @danc1248 - Fixed user creation issue on Multisite Networks.
* Feature: @RobjS - Added plugin singleton to support for more developer customization.
* Feature: @jkouris - Added action hook to allow custom handling of session expiration.
* Fix: @tommcc - Fixed admin CSS loading only on the plugin settings screen.
* Feature: @rkcreation - Added method to refresh the user claim.
* Feature: @Glowsome - Added acr_values support & verification checks that it when defined in options is honored.
* Fix: @timnolte - Fixed regression which caused improper fallback on missing claims.
* Fix: @slykar - Fixed missing query string handling in redirect URL.
* Fix: @timnolte - Fixed issue with some user linking and user creation handling.
* Improvement: @timnolte - Fixed plugin settings typos and screen formatting.
* Security: @timnolte - Updated build tooling security vulnerabilities.
* Improvement: @timnolte - Changed build tooling scripts.
### 3.8.5 ###
* Fix: @timnolte - Fixed missing URL request validation before use & ensure proper current page URL is setup for Redirect Back.
* Fix: @timnolte - Fixed Redirect URL Logic to Handle Sub-directory Installs.
* Fix: @timnolte - Fixed issue with redirecting user back when the openid_connect_generic_auth_url shortcode is used.
### 3.8.4 ###
* Fix: @timnolte - Fixed invalid State object access for redirection handling.
* Improvement: @timnolte - Fixed local wp-env Docker development environment.
* Improvement: @timnolte - Fixed Composer scripts for linting and static analysis.
### 3.8.3 ###
* Fix: @timnolte - Fixed problems with proper redirect handling.
* Improvement: @timnolte - Changes redirect handling to use State instead of cookies.
* Improvement: @timnolte - Refactored additional code to meet coding standards.
### 3.8.2 ###
* Fix: @timnolte - Fixed reported XSS vulnerability on WordPress login screen.
### 3.8.1 ###
* Fix: @timnolte - Prevent SSO redirect on password protected posts.
* Fix: @timnolte - CI/CD build issues.
* Fix: @timnolte - Invalid redirect handling on logout for Auto Login setting.
### 3.8.0 ###
* Feature: @timnolte - Ability to use 6 new constants for setting client configuration instead of storing in the DB.
* Improvement: @timnolte - Plugin development & contribution updates.
* Improvement: @timnolte - Refactored to meet WordPress coding standards.
* Improvement: @timnolte - Refactored to provide localization.
--------
[See the previous changelogs here](https://github.com/oidc-wp/openid-connect-generic/blob/main/CHANGELOG.md#changelog)

View File

@ -0,0 +1,17 @@
# Security Policy
## Supported Versions
We follow the [WordPress Core style of versioning](https://make.wordpress.org/core/handbook/about/release-cycle/version-numbering/) rather than traditional [SemVer](https://semver.org/). This means that a move from version 3.9 to 4.0 is no different from a move from version 3.8 to 3.9. When a **PATCH** version is released it represents a bug fix, or non-code, only change.
The latest version released is the only version that will receive security updates, generally as a **PATCH** release unless a security issue requires a functionality change in which requires a minor/major version bump.
## Reporting a Vulnerability
For security reasons, the following are acceptable options for reporting all security issues.
1. Via Keybase secure message to [timnolte](https://keybase.io/timnolte/chat) or [daggerhart](https://keybase.io/daggerhart/chat).
2. Send a DM via the [WordPress Slack](https://make.wordpress.org/chat/) to `tnolte`.
3. Via a private [security advisory](https://github.com/oidc-wp/openid-connect-generic/security/advisories) notice.
Please disclose responsibly and not via public GitHub Issues (which allows for exploiting issues in the wild before the patch is released).

View File

@ -0,0 +1,10 @@
coverage:
status:
project:
default:
target: auto
threshold: 0.5%
patch: off
comment:
require_changes: true

View File

@ -0,0 +1,32 @@
#logger-table .col-data {
width: 85%
}
#logger-table .col-data pre {
margin: 0;
white-space: pre; /* CSS 2.0 */
white-space: pre-wrap; /* CSS 2.1 */
white-space: pre-line; /* CSS 3.0 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
white-space: -moz-pre-wrap; /* Mozilla */
white-space: -hp-pre-wrap; /* HP Printers */
word-wrap: break-word; /* IE 5+ */
}
#logger-table .col-details {
width: 200px;
}
#logger-table .col-details div {
padding: 4px 0;
border-bottom: 1px solid #bbb;
}
#logger-table .col-details div:last-child {
border-bottom: none;
}
#logger-table .col-details label {
font-weight: bold;
}

View File

@ -0,0 +1,95 @@
# This is the Compose file for command-line services.
# Anything that doesn't need to be run as part of the main `docker-compose up'
# command should reside in here and be invoked by a helper script.
version: "3.7"
services:
app:
image: ghcr.io/ndigitals/wp-dev-container:php-8.0-node-16
restart: always
depends_on:
- db
- phpmyadmin
- web
- mailhog
working_dir: /workspaces/openid-connect-generic
environment: &env
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_TEST_DB_NAME: wordpress_test
CODESPACES: "${CODESPACES}"
CODESPACE_NAME: "${CODESPACE_NAME}"
GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN: "${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
volumes:
- .:/workspaces/openid-connect-generic:cached
- ./tools/local-env:/app:cached
- ./tools/php/php-cli.ini:/usr/local/etc/php/php-cli.ini:ro,cached
- .:/app/wp-content/plugins/daggerhart-openid-connect-generic:ro,cached
- ~/.composer:/root/.composer:cached
- ~/.npm:/root/.npm:cached
networks:
- oidcwp-net
web:
image: httpd
restart: unless-stopped
depends_on:
- db
ports:
- 8080:80
environment:
<<: *env
volumes:
- ./tools/local-env:/app:cached
- .:/app/wp-content/plugins/daggerhart-openid-connect-generic:ro,cached
- ./tools/apache/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro,cached
networks:
- oidcwp-net
db:
image: mariadb
restart: unless-stopped
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
volumes:
- db:/var/lib/mysql
- ./tests/db-wordpress_test.sql:/docker-entrypoint-initdb.d/db-wordpress_test.sql
networks:
- oidcwp-net
phpmyadmin:
image: phpmyadmin
restart: unless-stopped
depends_on:
- db
ports:
- 8081:8081
environment:
PMA_HOST: db
APACHE_PORT: 8081
networks:
- oidcwp-net
## SMTP Server + Web Interface for viewing and testing emails during development.
mailhog:
image: mailhog/mailhog
restart: unless-stopped
ports:
- 1025:1025 # smtp server
- 8026:8025 # web ui
networks:
- oidcwp-net
volumes:
db:
networks:
oidcwp-net:

View File

@ -0,0 +1,30 @@
<?php
/**
* Global OIDCG functions.
*
* @package OpenID_Connect_Generic
* @author Jonathan Daggerhart <jonathan@daggerhart.com>
* @copyright 2015-2020 daggerhart
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
*/
/**
* Return a single use authentication URL.
*
* @return string
*/
function oidcg_get_authentication_url() {
return \OpenID_Connect_Generic::instance()->client_wrapper->get_authentication_url();
}
/**
* Refresh a user claim and update the user metadata.
*
* @param WP_User $user The user object.
* @param array $token_response The token response.
*
* @return WP_Error|array
*/
function oidcg_refresh_user_claim( $user, $token_response ) {
return \OpenID_Connect_Generic::instance()->client_wrapper->refresh_user_claim( $user, $token_response );
}

View File

@ -0,0 +1,568 @@
<?php
/**
* Plugin OIDC/oAuth client class.
*
* @package OpenID_Connect_Generic
* @category Authentication
* @author Jonathan Daggerhart <jonathan@daggerhart.com>
* @copyright 2015-2020 daggerhart
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
*/
/**
* OpenID_Connect_Generic_Client class.
*
* Plugin OIDC/oAuth client class.
*
* @package OpenID_Connect_Generic
* @category Authentication
*/
class OpenID_Connect_Generic_Client {
/**
* The OIDC/oAuth client ID.
*
* @see OpenID_Connect_Generic_Option_Settings::client_id
*
* @var string
*/
private $client_id;
/**
* The OIDC/oAuth client secret.
*
* @see OpenID_Connect_Generic_Option_Settings::client_secret
*
* @var string
*/
private $client_secret;
/**
* The OIDC/oAuth scopes.
*
* @see OpenID_Connect_Generic_Option_Settings::scope
*
* @var string
*/
private $scope;
/**
* The OIDC/oAuth authorization endpoint URL.
*
* @see OpenID_Connect_Generic_Option_Settings::endpoint_login
*
* @var string
*/
private $endpoint_login;
/**
* The OIDC/oAuth User Information endpoint URL.
*
* @see OpenID_Connect_Generic_Option_Settings::endpoint_userinfo
*
* @var string
*/
private $endpoint_userinfo;
/**
* The OIDC/oAuth token validation endpoint URL.
*
* @see OpenID_Connect_Generic_Option_Settings::endpoint_token
*
* @var string
*/
private $endpoint_token;
/**
* The login flow "ajax" endpoint URI.
*
* @see OpenID_Connect_Generic_Option_Settings::redirect_uri
*
* @var string
*/
private $redirect_uri;
/**
* The specifically requested authentication contract at the IDP
*
* @see OpenID_Connect_Generic_Option_Settings::acr_values
*
* @var string
*/
private $acr_values;
/**
* The state time limit. States are only valid for 3 minutes.
*
* @see OpenID_Connect_Generic_Option_Settings::state_time_limit
*
* @var int
*/
private $state_time_limit = 180;
/**
* The logger object instance.
*
* @var OpenID_Connect_Generic_Option_Logger
*/
private $logger;
/**
* Client constructor.
*
* @param string $client_id @see OpenID_Connect_Generic_Option_Settings::client_id for description.
* @param string $client_secret @see OpenID_Connect_Generic_Option_Settings::client_secret for description.
* @param string $scope @see OpenID_Connect_Generic_Option_Settings::scope for description.
* @param string $endpoint_login @see OpenID_Connect_Generic_Option_Settings::endpoint_login for description.
* @param string $endpoint_userinfo @see OpenID_Connect_Generic_Option_Settings::endpoint_userinfo for description.
* @param string $endpoint_token @see OpenID_Connect_Generic_Option_Settings::endpoint_token for description.
* @param string $redirect_uri @see OpenID_Connect_Generic_Option_Settings::redirect_uri for description.
* @param string $acr_values @see OpenID_Connect_Generic_Option_Settings::acr_values for description.
* @param int $state_time_limit @see OpenID_Connect_Generic_Option_Settings::state_time_limit for description.
* @param OpenID_Connect_Generic_Option_Logger $logger The plugin logging object instance.
*/
public function __construct( $client_id, $client_secret, $scope, $endpoint_login, $endpoint_userinfo, $endpoint_token, $redirect_uri, $acr_values, $state_time_limit, $logger ) {
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->scope = $scope;
$this->endpoint_login = $endpoint_login;
$this->endpoint_userinfo = $endpoint_userinfo;
$this->endpoint_token = $endpoint_token;
$this->redirect_uri = $redirect_uri;
$this->acr_values = $acr_values;
$this->state_time_limit = $state_time_limit;
$this->logger = $logger;
}
/**
* Provides the configured Redirect URI supplied to the IDP.
*
* @return string
*/
public function get_redirect_uri() {
return $this->redirect_uri;
}
/**
* Provide the configured IDP endpoint login URL.
*
* @return string
*/
public function get_endpoint_login_url() {
return $this->endpoint_login;
}
/**
* Validate the request for login authentication
*
* @param array<string> $request The authentication request results.
*
* @return array<string>|WP_Error
*/
public function validate_authentication_request( $request ) {
// Look for an existing error of some kind.
if ( isset( $request['error'] ) ) {
return new WP_Error( 'unknown-error', 'An unknown error occurred.', $request );
}
// Make sure we have a legitimate authentication code and valid state.
if ( ! isset( $request['code'] ) ) {
return new WP_Error( 'no-code', 'No authentication code present in the request.', $request );
}
// Check the client request state.
if ( ! isset( $request['state'] ) ) {
do_action( 'openid-connect-generic-no-state-provided' );
return new WP_Error( 'missing-state', __( 'Missing state.', 'daggerhart-openid-connect-generic' ), $request );
}
if ( ! $this->check_state( $request['state'] ) ) {
return new WP_Error( 'invalid-state', __( 'Invalid state.', 'daggerhart-openid-connect-generic' ), $request );
}
return $request;
}
/**
* Get the authorization code from the request
*
* @param array<string>|WP_Error $request The authentication request results.
*
* @return string|WP_Error
*/
public function get_authentication_code( $request ) {
if ( ! isset( $request['code'] ) ) {
return new WP_Error( 'missing-authentication-code', __( 'Missing authentication code.', 'daggerhart-openid-connect-generic' ), $request );
}
return $request['code'];
}
/**
* Using the authorization_code, request an authentication token from the IDP.
*
* @param string|WP_Error $code The authorization code.
*
* @return array<mixed>|WP_Error
*/
public function request_authentication_token( $code ) {
// Add Host header - required for when the openid-connect endpoint is behind a reverse-proxy.
$parsed_url = parse_url( $this->endpoint_token );
$host = $parsed_url['host'];
$request = array(
'body' => array(
'code' => $code,
'client_id' => $this->client_id,
'client_secret' => $this->client_secret,
'redirect_uri' => $this->redirect_uri,
'grant_type' => 'authorization_code',
'scope' => $this->scope,
),
'headers' => array( 'Host' => $host ),
);
if ( ! empty( $this->acr_values ) ) {
$request['body'] += array( 'acr_values' => $this->acr_values );
}
// Allow modifications to the request.
$request = apply_filters( 'openid-connect-generic-alter-request', $request, 'get-authentication-token' );
// Call the server and ask for a token.
$start_time = microtime( true );
$response = wp_remote_post( $this->endpoint_token, $request );
$end_time = microtime( true );
$this->logger->log( $this->endpoint_token, 'request_authentication_token', $end_time - $start_time );
if ( is_wp_error( $response ) ) {
$response->add( 'request_authentication_token', __( 'Request for authentication token failed.', 'daggerhart-openid-connect-generic' ) );
}
return $response;
}
/**
* Using the refresh token, request new tokens from the idp
*
* @param string $refresh_token The refresh token previously obtained from token response.
*
* @return array|WP_Error
*/
public function request_new_tokens( $refresh_token ) {
$request = array(
'body' => array(
'refresh_token' => $refresh_token,
'client_id' => $this->client_id,
'client_secret' => $this->client_secret,
'grant_type' => 'refresh_token',
),
);
// Allow modifications to the request.
$request = apply_filters( 'openid-connect-generic-alter-request', $request, 'refresh-token' );
// Call the server and ask for new tokens.
$start_time = microtime( true );
$response = wp_remote_post( $this->endpoint_token, $request );
$end_time = microtime( true );
$this->logger->log( $this->endpoint_token, 'request_new_tokens', $end_time - $start_time );
if ( is_wp_error( $response ) ) {
$response->add( 'refresh_token', __( 'Refresh token failed.', 'daggerhart-openid-connect-generic' ) );
}
return $response;
}
/**
* Extract and decode the token body of a token response
*
* @param array<mixed>|WP_Error $token_result The token response.
*
* @return array<mixed>|WP_Error|null
*/
public function get_token_response( $token_result ) {
if ( ! isset( $token_result['body'] ) ) {
return new WP_Error( 'missing-token-body', __( 'Missing token body.', 'daggerhart-openid-connect-generic' ), $token_result );
}
// Extract the token response from token.
$token_response = json_decode( $token_result['body'], true );
// Check that the token response body was able to be parsed.
if ( is_null( $token_response ) ) {
return new WP_Error( 'invalid-token', __( 'Invalid token.', 'daggerhart-openid-connect-generic' ), $token_result );
}
if ( isset( $token_response['error'] ) ) {
$error = $token_response['error'];
$error_description = $error;
if ( isset( $token_response['error_description'] ) ) {
$error_description = $token_response['error_description'];
}
return new WP_Error( $error, $error_description, $token_result );
}
return $token_response;
}
/**
* Exchange an access_token for a user_claim from the userinfo endpoint
*
* @param string $access_token The access token supplied from authentication user claim.
*
* @return array|WP_Error
*/
public function request_userinfo( $access_token ) {
// Allow modifications to the request.
$request = apply_filters( 'openid-connect-generic-alter-request', array(), 'get-userinfo' );
/*
* Section 5.3.1 of the spec recommends sending the access token using the authorization header
* a filter may or may not have already added headers - make sure they exist then add the token.
*/
if ( ! array_key_exists( 'headers', $request ) || ! is_array( $request['headers'] ) ) {
$request['headers'] = array();
}
$request['headers']['Authorization'] = 'Bearer ' . $access_token;
// Add Host header - required for when the openid-connect endpoint is behind a reverse-proxy.
$parsed_url = parse_url( $this->endpoint_userinfo );
$host = $parsed_url['host'];
if ( ! empty( $parsed_url['port'] ) ) {
$host .= ":{$parsed_url['port']}";
}
$request['headers']['Host'] = $host;
// Attempt the request including the access token in the query string for backwards compatibility.
$start_time = microtime( true );
$response = wp_remote_post( $this->endpoint_userinfo, $request );
$end_time = microtime( true );
$this->logger->log( $this->endpoint_userinfo, 'request_userinfo', $end_time - $start_time );
if ( is_wp_error( $response ) ) {
$response->add( 'request_userinfo', __( 'Request for userinfo failed.', 'daggerhart-openid-connect-generic' ) );
}
return $response;
}
/**
* Generate a new state, save it as a transient, and return the state hash.
*
* @param string $redirect_to The redirect URL to be used after IDP authentication.
*
* @return string
*/
public function new_state( $redirect_to ) {
// New state w/ timestamp.
$state = md5( mt_rand() . microtime( true ) );
$state_value = array(
$state => array(
'redirect_to' => $redirect_to,
),
);
set_transient( 'openid-connect-generic-state--' . $state, $state_value, $this->state_time_limit );
return $state;
}
/**
* Check the existence of a given state transient.
*
* @param string $state The state hash to validate.
*
* @return bool
*/
public function check_state( $state ) {
$state_found = true;
if ( ! get_option( '_transient_openid-connect-generic-state--' . $state ) ) {
do_action( 'openid-connect-generic-state-not-found', $state );
$state_found = false;
}
$valid = get_transient( 'openid-connect-generic-state--' . $state );
if ( ! $valid && $state_found ) {
do_action( 'openid-connect-generic-state-expired', $state );
}
return boolval( $valid );
}
/**
* Get the authorization state from the request
*
* @param array<string>|WP_Error $request The authentication request results.
*
* @return string|WP_Error
*/
public function get_authentication_state( $request ) {
if ( ! isset( $request['state'] ) ) {
return new WP_Error( 'missing-authentication-state', __( 'Missing authentication state.', 'daggerhart-openid-connect-generic' ), $request );
}
return $request['state'];
}
/**
* Ensure that the token meets basic requirements.
*
* @param array $token_response The token response.
*
* @return bool|WP_Error
*/
public function validate_token_response( $token_response ) {
/*
* Ensure 2 specific items exist with the token response in order
* to proceed with confidence: id_token and token_type == 'Bearer'
*/
if ( ! isset( $token_response['id_token'] ) ||
! isset( $token_response['token_type'] ) || strcasecmp( $token_response['token_type'], 'Bearer' )
) {
return new WP_Error( 'invalid-token-response', 'Invalid token response', $token_response );
}
return true;
}
/**
* Extract the id_token_claim from the token_response.
*
* @param array $token_response The token response.
*
* @return array|WP_Error
*/
public function get_id_token_claim( $token_response ) {
// Validate there is an id_token.
if ( ! isset( $token_response['id_token'] ) ) {
return new WP_Error( 'no-identity-token', __( 'No identity token.', 'daggerhart-openid-connect-generic' ), $token_response );
}
// Break apart the id_token in the response for decoding.
$tmp = explode( '.', $token_response['id_token'] );
if ( ! isset( $tmp[1] ) ) {
return new WP_Error( 'missing-identity-token', __( 'Missing identity token.', 'daggerhart-openid-connect-generic' ), $token_response );
}
// Extract the id_token's claims from the token.
$id_token_claim = json_decode(
base64_decode(
str_replace( // Because token is encoded in base64 URL (and not just base64).
array( '-', '_' ),
array( '+', '/' ),
$tmp[1]
)
),
true
);
return $id_token_claim;
}
/**
* Ensure the id_token_claim contains the required values.
*
* @param array $id_token_claim The ID token claim.
*
* @return bool|WP_Error
*/
public function validate_id_token_claim( $id_token_claim ) {
if ( ! is_array( $id_token_claim ) ) {
return new WP_Error( 'bad-id-token-claim', __( 'Bad ID token claim.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
// Validate the identification data and it's value.
if ( ! isset( $id_token_claim['sub'] ) || empty( $id_token_claim['sub'] ) ) {
return new WP_Error( 'no-subject-identity', __( 'No subject identity.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
// Validate acr values when the option is set in the configuration.
if ( ! empty( $this->acr_values ) && isset( $id_token_claim['acr'] ) ) {
if ( $this->acr_values != $id_token_claim['acr'] ) {
return new WP_Error( 'no-match-acr', __( 'No matching acr values.', 'daggerhart-openid-connect-generic' ), $id_token_claim );
}
}
return true;
}
/**
* Attempt to exchange the access_token for a user_claim.
*
* @param array $token_response The token response.
*
* @return array|WP_Error|null
*/
public function get_user_claim( $token_response ) {
// Send a userinfo request to get user claim.
$user_claim_result = $this->request_userinfo( $token_response['access_token'] );
// Make sure we didn't get an error, and that the response body exists.
if ( is_wp_error( $user_claim_result ) || ! isset( $user_claim_result['body'] ) ) {
return new WP_Error( 'bad-claim', __( 'Bad user claim.', 'daggerhart-openid-connect-generic' ), $user_claim_result );
}
$user_claim = json_decode( $user_claim_result['body'], true );
return $user_claim;
}
/**
* Make sure the user_claim has all required values, and that the subject
* identity matches of the id_token matches that of the user_claim.
*
* @param array $user_claim The authenticated user claim.
* @param array $id_token_claim The ID token claim.
*
* @return bool|WP_Error
*/
public function validate_user_claim( $user_claim, $id_token_claim ) {
// Validate the user claim.
if ( ! is_array( $user_claim ) ) {
return new WP_Error( 'invalid-user-claim', __( 'Invalid user claim.', 'daggerhart-openid-connect-generic' ), $user_claim );
}
// Allow for errors from the IDP.
if ( isset( $user_claim['error'] ) ) {
$message = __( 'Error from the IDP.', 'daggerhart-openid-connect-generic' );
if ( ! empty( $user_claim['error_description'] ) ) {
$message = $user_claim['error_description'];
}
return new WP_Error( 'invalid-user-claim-' . $user_claim['error'], $message, $user_claim );
}
// Make sure the id_token sub equals the user_claim sub, according to spec.
if ( $id_token_claim['sub'] !== $user_claim['sub'] ) {
return new WP_Error( 'incorrect-user-claim', __( 'Incorrect user claim.', 'daggerhart-openid-connect-generic' ), func_get_args() );
}
// Allow for other plugins to alter the login success.
$login_user = apply_filters( 'openid-connect-generic-user-login-test', true, $user_claim );
if ( ! $login_user ) {
return new WP_Error( 'unauthorized', __( 'Unauthorized access.', 'daggerhart-openid-connect-generic' ), $login_user );
}
return true;
}
/**
* Retrieve the subject identity from the id_token.
*
* @param array $id_token_claim The ID token claim.
*
* @return mixed
*/
public function get_subject_identity( $id_token_claim ) {
return $id_token_claim['sub'];
}
}

View File

@ -0,0 +1,178 @@
<?php
/**
* Login form and login button handling class.
*
* @package OpenID_Connect_Generic
* @category Login
* @author Jonathan Daggerhart <jonathan@daggerhart.com>
* @copyright 2015-2020 daggerhart
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
*/
/**
* OpenID_Connect_Generic_Login_Form class.
*
* Login form and login button handling.
*
* @package OpenID_Connect_Generic
* @category Login
*/
class OpenID_Connect_Generic_Login_Form {
/**
* Plugin settings object.
*
* @var OpenID_Connect_Generic_Option_Settings
*/
private $settings;
/**
* Plugin client wrapper instance.
*
* @var OpenID_Connect_Generic_Client_Wrapper
*/
private $client_wrapper;
/**
* The class constructor.
*
* @param OpenID_Connect_Generic_Option_Settings $settings A plugin settings object instance.
* @param OpenID_Connect_Generic_Client_Wrapper $client_wrapper A plugin client wrapper object instance.
*/
public function __construct( $settings, $client_wrapper ) {
$this->settings = $settings;
$this->client_wrapper = $client_wrapper;
}
/**
* Create an instance of the OpenID_Connect_Generic_Login_Form class.
*
* @param OpenID_Connect_Generic_Option_Settings $settings A plugin settings object instance.
* @param OpenID_Connect_Generic_Client_Wrapper $client_wrapper A plugin client wrapper object instance.
*
* @return void
*/
public static function register( $settings, $client_wrapper ) {
$login_form = new self( $settings, $client_wrapper );
// Alter the login form as dictated by settings.
add_filter( 'login_message', array( $login_form, 'handle_login_page' ), 99 );
// Add a shortcode for the login button.
add_shortcode( 'openid_connect_generic_login_button', array( $login_form, 'make_login_button' ) );
$login_form->handle_redirect_login_type_auto();
}
/**
* Auto Login redirect.
*
* @return void
*/
public function handle_redirect_login_type_auto() {
if ( 'wp-login.php' == $GLOBALS['pagenow']
&& ( 'auto' == $this->settings->login_type || ! empty( $_GET['force_redirect'] ) )
// Don't send users to the IDP on logout or post password protected authentication.
&& ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], array( 'logout', 'postpass' ) ) )
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- WP Login Form doesn't have a nonce.
&& ! isset( $_POST['wp-submit'] ) ) {
if ( ! isset( $_GET['login-error'] ) ) {
wp_redirect( $this->client_wrapper->get_authentication_url() );
exit;
} else {
add_action( 'login_footer', array( $this, 'remove_login_form' ), 99 );
}
}
}
/**
* Implements filter login_message.
*
* @param string $message The text message to display on the login page.
*
* @return string
*/
public function handle_login_page( $message ) {
if ( isset( $_GET['login-error'] ) ) {
$error_message = ! empty( $_GET['message'] ) ? sanitize_text_field( wp_unslash( $_GET['message'] ) ) : 'Unknown error.';
$message .= $this->make_error_output( sanitize_text_field( wp_unslash( $_GET['login-error'] ) ), $error_message );
}
// Login button is appended to existing messages in case of error.
$message .= $this->make_login_button();
return $message;
}
/**
* Display an error message to the user.
*
* @param string $error_code The error code.
* @param string $error_message The error message test.
*
* @return string
*/
public function make_error_output( $error_code, $error_message ) {
ob_start();
?>
<div id="login_error"><?php // translators: %1$s is the error code from the IDP. ?>
<strong><?php printf( esc_html__( 'ERROR (%1$s)', 'daggerhart-openid-connect-generic' ), esc_html( $error_code ) ); ?>: </strong>
<?php print esc_html( $error_message ); ?>
</div>
<?php
return wp_kses_post( ob_get_clean() );
}
/**
* Create a login button (link).
*
* @param array $atts Array of optional attributes to override login buton
* functionality when used by shortcode.
*
* @return string
*/
public function make_login_button( $atts = array() ) {
$atts = shortcode_atts(
array(
'button_text' => __( 'Login with OpenID Connect', 'daggerhart-openid-connect-generic' ),
),
$atts,
'openid_connect_generic_login_button'
);
$text = apply_filters( 'openid-connect-generic-login-button-text', $atts['button_text'] );
$text = esc_html( $text );
$href = $this->client_wrapper->get_authentication_url( $atts );
$href = esc_url_raw( $href );
$login_button = <<<HTML
<div class="openid-connect-login-button" style="margin: 1em 0; text-align: center;">
<a class="button button-large" href="{$href}">{$text}</a>
</div>
HTML;
return $login_button;
}
/**
* Removes the login form from the HTML DOM
*
* @return void
*/
public function remove_login_form() {
?>
<script type="text/javascript">
(function() {
var loginForm = document.getElementById("user_login").form;
var parent = loginForm.parentNode;
parent.removeChild(loginForm);
})();
</script>
<?php
}
}

View File

@ -0,0 +1,266 @@
<?php
/**
* Plugin logging class.
*
* @package OpenID_Connect_Generic
* @category Logging
* @author Jonathan Daggerhart <jonathan@daggerhart.com>
* @copyright 2015-2023 daggerhart
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
*/
/**
* OpenID_Connect_Generic_Option_Logger class.
*
* Simple class for logging messages to the options table.
*
* @package OpenID_Connect_Generic
* @category Logging
*/
class OpenID_Connect_Generic_Option_Logger {
/**
* Thw WordPress option name/key.
*
* @var string
*/
const OPTION_NAME = 'openid-connect-generic-logs';
/**
* The default message type.
*
* @var string
*/
private $default_message_type = 'none';
/**
* The number of items to keep in the log.
*
* @var int
*/
private $log_limit = 1000;
/**
* Whether or not logging is enabled.
*
* @var bool
*/
private $logging_enabled = true;
/**
* Internal cache of logs.
*
* @var array
*/
private $logs;
/**
* Setup the logger according to the needs of the instance.
*
* @param string|null $default_message_type The log message type.
* @param bool|TRUE|null $logging_enabled Whether logging is enabled.
* @param int|null $log_limit The log entry limit.
*/
public function __construct( $default_message_type = null, $logging_enabled = null, $log_limit = null ) {
if ( ! is_null( $default_message_type ) ) {
$this->default_message_type = $default_message_type;
}
if ( ! is_null( $logging_enabled ) ) {
$this->logging_enabled = boolval( $logging_enabled );
}
if ( ! is_null( $log_limit ) ) {
$this->log_limit = intval( $log_limit );
}
}
/**
* Save an array of data to the logs.
*
* @param string|array<string, string>|WP_Error $data The log message data.
* @param string|null $type The log message type.
* @param float|null $processing_time Optional event processing time.
* @param int|null $time The log message timestamp (default: time()).
* @param int|null $user_ID The current WordPress user ID (default: get_current_user_id()).
* @param string|null $request_uri The related HTTP request URI (default: $_SERVER['REQUEST_URI']|'Unknown').
*
* @return bool
*/
public function log( $data, $type = null, $processing_time = null, $time = null, $user_ID = null, $request_uri = null ) {
if ( boolval( $this->logging_enabled ) ) {
$logs = $this->get_logs();
$logs[] = $this->make_message( $data, $type, $processing_time, $time, $user_ID, $request_uri );
$logs = $this->upkeep_logs( $logs );
return $this->save_logs( $logs );
}
return false;
}
/**
* Retrieve all log messages.
*
* @return array
*/
public function get_logs() {
if ( empty( $this->logs ) ) {
$this->logs = get_option( self::OPTION_NAME, array() );
}
// Call the upkeep_logs function to give the appearance that logs have been reduced to the $this->log_limit.
// The logs are actually limited during a logging action but the logger isn't available during a simple settings update.
return $this->upkeep_logs( $this->logs );
}
/**
* Get the name of the option where this log is stored.
*
* @return string
*/
public function get_option_name() {
return self::OPTION_NAME;
}
/**
* Create a message array containing the data and other information.
*
* @param string|array<string, string>|WP_Error $data The log message data.
* @param string|null $type The log message type.
* @param float|null $processing_time Optional event processing time.
* @param int|null $time The log message timestamp (default: time()).
* @param int|null $user_ID The current WordPress user ID (default: get_current_user_id()).
* @param string|null $request_uri The related HTTP request URI (default: $_SERVER['REQUEST_URI']|'Unknown').
*
* @return array
*/
private function make_message( $data, $type, $processing_time, $time, $user_ID, $request_uri ) {
// Determine the type of message.
if ( empty( $type ) ) {
$type = $this->default_message_type;
if ( is_array( $data ) && isset( $data['type'] ) ) {
$type = $data['type'];
unset( $data['type'] );
}
if ( is_wp_error( $data ) ) {
$type = $data->get_error_code();
$data = $data->get_error_message( $type );
}
}
if ( empty( $request_uri ) ) {
$request_uri = ( ! empty( $_SERVER['REQUEST_URI'] ) ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : 'Unknown';
$request_uri = preg_replace( '/code=([^&]+)/i', 'code=', $request_uri );
}
// Construct the message.
$message = array(
'type' => $type,
'time' => ! empty( $time ) ? $time : time(),
'user_ID' => ! is_null( $user_ID ) ? $user_ID : get_current_user_id(),
'uri' => $request_uri,
'data' => $data,
'processing_time' => $processing_time,
);
return $message;
}
/**
* Keep the log count under the limit.
*
* @param array $logs The plugin logs.
*
* @return array
*/
private function upkeep_logs( $logs ) {
$items_to_remove = count( $logs ) - $this->log_limit;
if ( $items_to_remove > 0 ) {
// Only keep the last $log_limit messages from the end.
$logs = array_slice( $logs, $items_to_remove );
}
return $logs;
}
/**
* Save the log messages.
*
* @param array $logs The array of log messages.
*
* @return bool
*/
private function save_logs( $logs ) {
// Save the logs.
$this->logs = $logs;
return update_option( self::OPTION_NAME, $logs, false );
}
/**
* Clear all log messages.
*
* @return void
*/
public function clear_logs() {
$this->save_logs( array() );
}
/**
* Get a simple html table of all the logs.
*
* @param array $logs The array of log messages.
*
* @return string
*/
public function get_logs_table( $logs = array() ) {
if ( empty( $logs ) ) {
$logs = $this->get_logs();
}
$logs = array_reverse( $logs );
ini_set( 'xdebug.var_display_max_depth', '-1' );
ob_start();
?>
<table id="logger-table" class="wp-list-table widefat fixed striped posts">
<thead>
<th class="col-details"><?php esc_html_e( 'Details', 'daggerhart-openid-connect-generic' ); ?></th>
<th class="col-data"><?php esc_html_e( 'Data', 'daggerhart-openid-connect-generic' ); ?></th>
</thead>
<tbody>
<?php foreach ( $logs as $log ) { ?>
<tr>
<td class="col-details">
<div>
<label><?php esc_html_e( 'Date', 'daggerhart-openid-connect-generic' ); ?></label>
<?php print esc_html( ! empty( $log['time'] ) ? gmdate( 'Y-m-d H:i:s', $log['time'] ) : '' ); ?>
</div>
<div>
<label><?php esc_html_e( 'Type', 'daggerhart-openid-connect-generic' ); ?></label>
<?php print esc_html( ! empty( $log['type'] ) ? $log['type'] : '' ); ?>
</div>
<div>
<label><?php esc_html_e( 'User', 'daggerhart-openid-connect-generic' ); ?>: </label>
<?php print esc_html( ( get_userdata( $log['user_ID'] ) ) ? get_userdata( $log['user_ID'] )->user_login : '0' ); ?>
</div>
<div>
<label><?php esc_html_e( 'URI ', 'daggerhart-openid-connect-generic' ); ?>: </label>
<?php print esc_url( ! empty( $log['uri'] ) ? $log['uri'] : '' ); ?>
</div>
<div>
<label><?php esc_html_e( 'Response&nbsp;Time&nbsp;(sec)', 'daggerhart-openid-connect-generic' ); ?></label>
<?php print esc_html( ! empty( $log['response_time'] ) ? $log['response_time'] : '' ); ?>
</div>
</td>
<td class="col-data"><pre><?php var_dump( ! empty( $log['data'] ) ? $log['data'] : '' ); ?></pre></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php
$output = ob_get_clean();
return $output;
}
}

View File

@ -0,0 +1,213 @@
<?php
/**
* WordPress options handling class.
*
* @package OpenID_Connect_Generic
* @category Settings
* @author Jonathan Daggerhart <jonathan@daggerhart.com>
* @copyright 2015-2023 daggerhart
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
*/
/**
* OpenId_Connect_Generic_Option_Settings class.
*
* WordPress options handling.
*
* @package OpenID_Connect_Generic
* @category Settings
*
* Legacy Settings:
*
* @property string $ep_login The login endpoint.
* @property string $ep_token The token endpoint.
* @property string $ep_userinfo The userinfo endpoint.
*
* OAuth Client Settings:
*
* @property string $login_type How the client (login form) should provide login options.
* @property string $client_id The ID the client will be recognized as when connecting the to Identity provider server.
* @property string $client_secret The secret key the IDP server expects from the client.
* @property string $scope The list of scopes this client should access.
* @property string $endpoint_login The IDP authorization endpoint URL.
* @property string $endpoint_userinfo The IDP User information endpoint URL.
* @property string $endpoint_token The IDP token validation endpoint URL.
* @property string $endpoint_end_session The IDP logout endpoint URL.
* @property string $acr_values The Authentication contract as defined on the IDP.
*
* Non-standard Settings:
*
* @property bool $no_sslverify The flag to enable/disable SSL verification during authorization.
* @property int $http_request_timeout The timeout for requests made to the IDP. Default value is 5.
* @property string $identity_key The key in the user claim array to find the user's identification data.
* @property string $nickname_key The key in the user claim array to find the user's nickname.
* @property string $email_format The key(s) in the user claim array to formulate the user's email address.
* @property string $displayname_format The key(s) in the user claim array to formulate the user's display name.
* @property bool $identify_with_username The flag which indicates how the user's identity will be determined.
* @property int $state_time_limit The valid time limit of the state, in seconds. Defaults to 180 seconds.
*
* Plugin Settings:
*
* @property bool $enforce_privacy The flag to indicates whether a user us required to be authenticated to access the site.
* @property bool $alternate_redirect_uri The flag to indicate whether to use the alternative redirect URI.
* @property bool $token_refresh_enable The flag whether to support refresh tokens by IDPs.
* @property bool $link_existing_users The flag to indicate whether to link to existing WordPress-only accounts or greturn an error.
* @property bool $create_if_does_not_exist The flag to indicate whether to create new users or not.
* @property bool $redirect_user_back The flag to indicate whether to redirect the user back to the page on which they started.
* @property bool $redirect_on_logout The flag to indicate whether to redirect to the login screen on session expiration.
* @property bool $enable_logging The flag to enable/disable logging.
* @property int $log_limit The maximum number of log entries to keep.
*/
class OpenID_Connect_Generic_Option_Settings {
/**
* WordPress option name/key.
*
* @var string
*/
const OPTION_NAME = 'openid_connect_generic_settings';
/**
* Stored option values array.
*
* @var array<mixed>
*/
private $values;
/**
* Default plugin settings values.
*
* @var array<mixed>
*/
private $default_settings;
/**
* List of settings that can be defined by environment variables.
*
* @var array<string,string>
*/
private $environment_settings = array(
'client_id' => 'OIDC_CLIENT_ID',
'client_secret' => 'OIDC_CLIENT_SECRET',
'endpoint_end_session' => 'OIDC_ENDPOINT_LOGOUT_URL',
'endpoint_login' => 'OIDC_ENDPOINT_LOGIN_URL',
'endpoint_token' => 'OIDC_ENDPOINT_TOKEN_URL',
'endpoint_userinfo' => 'OIDC_ENDPOINT_USERINFO_URL',
'login_type' => 'OIDC_LOGIN_TYPE',
'scope' => 'OIDC_CLIENT_SCOPE',
'create_if_does_not_exist' => 'OIDC_CREATE_IF_DOES_NOT_EXIST',
'enforce_privacy' => 'OIDC_ENFORCE_PRIVACY',
'link_existing_users' => 'OIDC_LINK_EXISTING_USERS',
'redirect_on_logout' => 'OIDC_REDIRECT_ON_LOGOUT',
'redirect_user_back' => 'OIDC_REDIRECT_USER_BACK',
'acr_values' => 'OIDC_ACR_VALUES',
'enable_logging' => 'OIDC_ENABLE_LOGGING',
'log_limit' => 'OIDC_LOG_LIMIT',
);
/**
* The class constructor.
*
* @param array<mixed> $default_settings The default plugin settings values.
* @param bool $granular_defaults The granular defaults.
*/
public function __construct( $default_settings = array(), $granular_defaults = true ) {
$this->default_settings = $default_settings;
$this->values = array();
$this->values = (array) get_option( self::OPTION_NAME, $this->default_settings );
// For each defined environment variable/constant be sure the settings key is set.
foreach ( $this->environment_settings as $key => $constant ) {
if ( defined( $constant ) ) {
$this->__set( $key, constant( $constant ) );
}
}
if ( $granular_defaults ) {
$this->values = array_replace_recursive( $this->default_settings, $this->values );
}
}
/**
* Magic getter for settings.
*
* @param string $key The array key/option name.
*
* @return mixed
*/
public function __get( $key ) {
if ( isset( $this->values[ $key ] ) ) {
return $this->values[ $key ];
}
}
/**
* Magic setter for settings.
*
* @param string $key The array key/option name.
* @param mixed $value The option value.
*
* @return void
*/
public function __set( $key, $value ) {
$this->values[ $key ] = $value;
}
/**
* Magic method to check is an attribute isset.
*
* @param string $key The array key/option name.
*
* @return bool
*/
public function __isset( $key ) {
return isset( $this->values[ $key ] );
}
/**
* Magic method to clear an attribute.
*
* @param string $key The array key/option name.
*
* @return void
*/
public function __unset( $key ) {
unset( $this->values[ $key ] );
}
/**
* Get the plugin settings array.
*
* @return array
*/
public function get_values() {
return $this->values;
}
/**
* Get the plugin WordPress options name.
*
* @return string
*/
public function get_option_name() {
return self::OPTION_NAME;
}
/**
* Save the plugin options to the WordPress options table.
*
* @return void
*/
public function save() {
// For each defined environment variable/constant be sure it isn't saved to the database.
foreach ( $this->environment_settings as $key => $constant ) {
if ( defined( $constant ) ) {
$this->__unset( $key );
}
}
update_option( self::OPTION_NAME, $this->values );
}
}

View File

@ -0,0 +1,603 @@
<?php
/**
* Plugin Admin settings page class.
*
* @package OpenID_Connect_Generic
* @category Settings
* @author Jonathan Daggerhart <jonathan@daggerhart.com>
* @copyright 2015-2023 daggerhart
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
*/
/**
* OpenID_Connect_Generic_Settings_Page class.
*
* Admin settings page.
*
* @package OpenID_Connect_Generic
* @category Settings
*/
class OpenID_Connect_Generic_Settings_Page {
/**
* Local copy of the settings provided by the base plugin.
*
* @var OpenID_Connect_Generic_Option_Settings
*/
private $settings;
/**
* Instance of the plugin logger.
*
* @var OpenID_Connect_Generic_Option_Logger
*/
private $logger;
/**
* The controlled list of settings & associated defined during
* construction for i18n reasons.
*
* @var array
*/
private $settings_fields = array();
/**
* Options page slug.
*
* @var string
*/
private $options_page_name = 'openid-connect-generic-settings';
/**
* Options page settings group name.
*
* @var string
*/
private $settings_field_group;
/**
* Settings page class constructor.
*
* @param OpenID_Connect_Generic_Option_Settings $settings The plugin settings object.
* @param OpenID_Connect_Generic_Option_Logger $logger The plugin logging class object.
*/
public function __construct( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) {
$this->settings = $settings;
$this->logger = $logger;
$this->settings_field_group = $this->settings->get_option_name() . '-group';
$fields = $this->get_settings_fields();
// Some simple pre-processing.
foreach ( $fields as $key => &$field ) {
$field['key'] = $key;
$field['name'] = $this->settings->get_option_name() . '[' . $key . ']';
}
// Allow alterations of the fields.
$this->settings_fields = $fields;
}
/**
* Hook the settings page into WordPress.
*
* @param OpenID_Connect_Generic_Option_Settings $settings A plugin settings object instance.
* @param OpenID_Connect_Generic_Option_Logger $logger A plugin logger object instance.
*
* @return void
*/
public static function register( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) {
$settings_page = new self( $settings, $logger );
// Add our options page the the admin menu.
add_action( 'admin_menu', array( $settings_page, 'admin_menu' ) );
// Register our settings.
add_action( 'admin_init', array( $settings_page, 'admin_init' ) );
}
/**
* Implements hook admin_menu to add our options/settings page to the
* dashboard menu.
*
* @return void
*/
public function admin_menu() {
add_options_page(
__( 'OpenID Connect - Generic Client', 'daggerhart-openid-connect-generic' ),
__( 'OpenID Connect Client', 'daggerhart-openid-connect-generic' ),
'manage_options',
$this->options_page_name,
array( $this, 'settings_page' )
);
}
/**
* Implements hook admin_init to register our settings.
*
* @return void
*/
public function admin_init() {
register_setting(
$this->settings_field_group,
$this->settings->get_option_name(),
array(
$this,
'sanitize_settings',
)
);
add_settings_section(
'client_settings',
__( 'Client Settings', 'daggerhart-openid-connect-generic' ),
array( $this, 'client_settings_description' ),
$this->options_page_name
);
add_settings_section(
'user_settings',
__( 'WordPress User Settings', 'daggerhart-openid-connect-generic' ),
array( $this, 'user_settings_description' ),
$this->options_page_name
);
add_settings_section(
'authorization_settings',
__( 'Authorization Settings', 'daggerhart-openid-connect-generic' ),
array( $this, 'authorization_settings_description' ),
$this->options_page_name
);
add_settings_section(
'log_settings',
__( 'Log Settings', 'daggerhart-openid-connect-generic' ),
array( $this, 'log_settings_description' ),
$this->options_page_name
);
// Preprocess fields and add them to the page.
foreach ( $this->settings_fields as $key => $field ) {
// Make sure each key exists in the settings array.
if ( ! isset( $this->settings->{ $key } ) ) {
$this->settings->{ $key } = null;
}
// Determine appropriate output callback.
switch ( $field['type'] ) {
case 'checkbox':
$callback = 'do_checkbox';
break;
case 'select':
$callback = 'do_select';
break;
case 'text':
default:
$callback = 'do_text_field';
break;
}
// Add the field.
add_settings_field(
$key,
$field['title'],
array( $this, $callback ),
$this->options_page_name,
$field['section'],
$field
);
}
}
/**
* Get the plugin settings fields definition.
*
* @return array
*/
private function get_settings_fields() {
/**
* Simple settings fields have:
*
* - title
* - description
* - type ( checkbox | text | select )
* - section - settings/option page section ( client_settings | authorization_settings )
* - example (optional example will appear beneath description and be wrapped in <code>)
*/
$fields = array(
'login_type' => array(
'title' => __( 'Login Type', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Select how the client (login form) should provide login options.', 'daggerhart-openid-connect-generic' ),
'type' => 'select',
'options' => array(
'button' => __( 'OpenID Connect button on login form', 'daggerhart-openid-connect-generic' ),
'auto' => __( 'Auto Login - SSO', 'daggerhart-openid-connect-generic' ),
),
'disabled' => defined( 'OIDC_LOGIN_TYPE' ),
'section' => 'client_settings',
),
'client_id' => array(
'title' => __( 'Client ID', 'daggerhart-openid-connect-generic' ),
'description' => __( 'The ID this client will be recognized as when connecting the to Identity provider server.', 'daggerhart-openid-connect-generic' ),
'example' => 'my-wordpress-client-id',
'type' => 'text',
'disabled' => defined( 'OIDC_CLIENT_ID' ),
'section' => 'client_settings',
),
'client_secret' => array(
'title' => __( 'Client Secret Key', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Arbitrary secret key the server expects from this client. Can be anything, but should be very unique.', 'daggerhart-openid-connect-generic' ),
'type' => 'text',
'disabled' => defined( 'OIDC_CLIENT_SECRET' ),
'section' => 'client_settings',
),
'scope' => array(
'title' => __( 'OpenID Scope', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Space separated list of scopes this client should access.', 'daggerhart-openid-connect-generic' ),
'example' => 'email profile openid offline_access',
'type' => 'text',
'disabled' => defined( 'OIDC_CLIENT_SCOPE' ),
'section' => 'client_settings',
),
'endpoint_login' => array(
'title' => __( 'Login Endpoint URL', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Identify provider authorization endpoint.', 'daggerhart-openid-connect-generic' ),
'example' => 'https://example.com/oauth2/authorize',
'type' => 'text',
'disabled' => defined( 'OIDC_ENDPOINT_LOGIN_URL' ),
'section' => 'client_settings',
),
'endpoint_userinfo' => array(
'title' => __( 'Userinfo Endpoint URL', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Identify provider User information endpoint.', 'daggerhart-openid-connect-generic' ),
'example' => 'https://example.com/oauth2/UserInfo',
'type' => 'text',
'disabled' => defined( 'OIDC_ENDPOINT_USERINFO_URL' ),
'section' => 'client_settings',
),
'endpoint_token' => array(
'title' => __( 'Token Validation Endpoint URL', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Identify provider token endpoint.', 'daggerhart-openid-connect-generic' ),
'example' => 'https://example.com/oauth2/token',
'type' => 'text',
'disabled' => defined( 'OIDC_ENDPOINT_TOKEN_URL' ),
'section' => 'client_settings',
),
'endpoint_end_session' => array(
'title' => __( 'End Session Endpoint URL', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Identify provider logout endpoint.', 'daggerhart-openid-connect-generic' ),
'example' => 'https://example.com/oauth2/logout',
'type' => 'text',
'disabled' => defined( 'OIDC_ENDPOINT_LOGOUT_URL' ),
'section' => 'client_settings',
),
'acr_values' => array(
'title' => __( 'ACR values', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Use a specific defined authentication contract from the IDP - optional.', 'daggerhart-openid-connect-generic' ),
'type' => 'text',
'disabled' => defined( 'OIDC_ACR_VALUES' ),
'section' => 'client_settings',
),
'identity_key' => array(
'title' => __( 'Identity Key', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Where in the user claim array to find the user\'s identification data. Possible standard values: preferred_username, name, or sub. If you\'re having trouble, use "sub".', 'daggerhart-openid-connect-generic' ),
'example' => 'preferred_username',
'type' => 'text',
'section' => 'client_settings',
),
'no_sslverify' => array(
'title' => __( 'Disable SSL Verify', 'daggerhart-openid-connect-generic' ),
// translators: %1$s HTML tags for layout/styles, %2$s closing HTML tag for styles.
'description' => sprintf( __( 'Do not require SSL verification during authorization. The OAuth extension uses curl to make the request. By default CURL will generally verify the SSL certificate to see if its valid an issued by an accepted CA. This setting disabled that verification.%1$sNot recommended for production sites.%2$s', 'daggerhart-openid-connect-generic' ), '<br><strong>', '</strong>' ),
'type' => 'checkbox',
'section' => 'client_settings',
),
'http_request_timeout' => array(
'title' => __( 'HTTP Request Timeout', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Set the timeout for requests made to the IDP. Default value is 5.', 'daggerhart-openid-connect-generic' ),
'example' => 30,
'type' => 'text',
'section' => 'client_settings',
),
'enforce_privacy' => array(
'title' => __( 'Enforce Privacy', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Require users be logged in to see the site.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'disabled' => defined( 'OIDC_ENFORCE_PRIVACY' ),
'section' => 'authorization_settings',
),
'alternate_redirect_uri' => array(
'title' => __( 'Alternate Redirect URI', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Provide an alternative redirect route. Useful if your server is causing issues with the default admin-ajax method. You must flush rewrite rules after changing this setting. This can be done by saving the Permalinks settings page.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'section' => 'authorization_settings',
),
'nickname_key' => array(
'title' => __( 'Nickname Key', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Where in the user claim array to find the user\'s nickname. Possible standard values: preferred_username, name, or sub.', 'daggerhart-openid-connect-generic' ),
'example' => 'preferred_username',
'type' => 'text',
'section' => 'client_settings',
),
'email_format' => array(
'title' => __( 'Email Formatting', 'daggerhart-openid-connect-generic' ),
'description' => __( 'String from which the user\'s email address is built. Specify "{email}" as long as the user claim contains an email claim.', 'daggerhart-openid-connect-generic' ),
'example' => '{email}',
'type' => 'text',
'section' => 'client_settings',
),
'displayname_format' => array(
'title' => __( 'Display Name Formatting', 'daggerhart-openid-connect-generic' ),
'description' => __( 'String from which the user\'s display name is built.', 'daggerhart-openid-connect-generic' ),
'example' => '{given_name} {family_name}',
'type' => 'text',
'section' => 'client_settings',
),
'identify_with_username' => array(
'title' => __( 'Identify with User Name', 'daggerhart-openid-connect-generic' ),
'description' => __( 'If checked, the user\'s identity will be determined by the user name instead of the email address.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'section' => 'client_settings',
),
'state_time_limit' => array(
'title' => __( 'State time limit', 'daggerhart-openid-connect-generic' ),
'description' => __( 'State valid time in seconds. Defaults to 180', 'daggerhart-openid-connect-generic' ),
'type' => 'number',
'section' => 'client_settings',
),
'token_refresh_enable' => array(
'title' => __( 'Enable Refresh Token', 'daggerhart-openid-connect-generic' ),
'description' => __( 'If checked, support refresh tokens used to obtain access tokens from supported IDPs.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'section' => 'client_settings',
),
'link_existing_users' => array(
'title' => __( 'Link Existing Users', 'daggerhart-openid-connect-generic' ),
'description' => __( 'If a WordPress account already exists with the same identity as a newly-authenticated user over OpenID Connect, login as that user instead of generating an error.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'disabled' => defined( 'OIDC_LINK_EXISTING_USERS' ),
'section' => 'user_settings',
),
'create_if_does_not_exist' => array(
'title' => __( 'Create user if does not exist', 'daggerhart-openid-connect-generic' ),
'description' => __( 'If the user identity is not linked to an existing WordPress user, it is created. If this setting is not enabled, and if the user authenticates with an account which is not linked to an existing WordPress user, then the authentication will fail.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'disabled' => defined( 'OIDC_CREATE_IF_DOES_NOT_EXIST' ),
'section' => 'user_settings',
),
'redirect_user_back' => array(
'title' => __( 'Redirect Back to Origin Page', 'daggerhart-openid-connect-generic' ),
'description' => __( 'After a successful OpenID Connect authentication, this will redirect the user back to the page on which they clicked the OpenID Connect login button. This will cause the login process to proceed in a traditional WordPress fashion. For example, users logging in through the default wp-login.php page would end up on the WordPress Dashboard and users logging in through the WooCommerce "My Account" page would end up on their account page.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'disabled' => defined( 'OIDC_REDIRECT_USER_BACK' ),
'section' => 'user_settings',
),
'redirect_on_logout' => array(
'title' => __( 'Redirect to the login screen when session is expired', 'daggerhart-openid-connect-generic' ),
'description' => __( 'When enabled, this will automatically redirect the user back to the WordPress login page if their access token has expired.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'disabled' => defined( 'OIDC_REDIRECT_ON_LOGOUT' ),
'section' => 'user_settings',
),
'enable_logging' => array(
'title' => __( 'Enable Logging', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Very simple log messages for debugging purposes.', 'daggerhart-openid-connect-generic' ),
'type' => 'checkbox',
'disabled' => defined( 'OIDC_ENABLE_LOGGING' ),
'section' => 'log_settings',
),
'log_limit' => array(
'title' => __( 'Log Limit', 'daggerhart-openid-connect-generic' ),
'description' => __( 'Number of items to keep in the log. These logs are stored as an option in the database, so space is limited.', 'daggerhart-openid-connect-generic' ),
'type' => 'number',
'disabled' => defined( 'OIDC_LOG_LIMIT' ),
'section' => 'log_settings',
),
);
return apply_filters( 'openid-connect-generic-settings-fields', $fields );
}
/**
* Sanitization callback for settings/option page.
*
* @param array $input The submitted settings values.
*
* @return array
*/
public function sanitize_settings( $input ) {
$options = array();
// Loop through settings fields to control what we're saving.
foreach ( $this->settings_fields as $key => $field ) {
if ( isset( $input[ $key ] ) ) {
$options[ $key ] = sanitize_text_field( trim( $input[ $key ] ) );
} else {
$options[ $key ] = '';
}
}
return $options;
}
/**
* Output the options/settings page.
*
* @return void
*/
public function settings_page() {
wp_enqueue_style( 'daggerhart-openid-connect-generic-admin', plugin_dir_url( __DIR__ ) . 'css/styles-admin.css', array(), OpenID_Connect_Generic::VERSION, 'all' );
$redirect_uri = admin_url( 'admin-ajax.php?action=openid-connect-authorize' );
if ( $this->settings->alternate_redirect_uri ) {
$redirect_uri = site_url( '/openid-connect-authorize' );
}
?>
<div class="wrap">
<h2><?php print esc_html( get_admin_page_title() ); ?></h2>
<form method="post" action="options.php">
<?php
settings_fields( $this->settings_field_group );
do_settings_sections( $this->options_page_name );
submit_button();
// Simple debug to view settings array.
if ( isset( $_GET['debug'] ) ) {
var_dump( $this->settings->get_values() );
}
?>
</form>
<h4><?php esc_html_e( 'Notes', 'daggerhart-openid-connect-generic' ); ?></h4>
<p class="description">
<strong><?php esc_html_e( 'Redirect URI', 'daggerhart-openid-connect-generic' ); ?></strong>
<code><?php print esc_url( $redirect_uri ); ?></code>
</p>
<p class="description">
<strong><?php esc_html_e( 'Login Button Shortcode', 'daggerhart-openid-connect-generic' ); ?></strong>
<code>[openid_connect_generic_login_button]</code>
</p>
<p class="description">
<strong><?php esc_html_e( 'Authentication URL Shortcode', 'daggerhart-openid-connect-generic' ); ?></strong>
<code>[openid_connect_generic_auth_url]</code>
</p>
<?php if ( $this->settings->enable_logging ) { ?>
<h2><?php esc_html_e( 'Logs', 'daggerhart-openid-connect-generic' ); ?></h2>
<div id="logger-table-wrapper">
<?php print wp_kses_post( $this->logger->get_logs_table() ); ?>
</div>
<?php } ?>
</div>
<?php
}
/**
* Output a standard text field.
*
* @param array $field The settings field definition array.
*
* @return void
*/
public function do_text_field( $field ) {
?>
<input type="<?php print esc_attr( $field['type'] ); ?>"
id="<?php print esc_attr( $field['key'] ); ?>"
class="large-text<?php echo ( ! empty( $field['disabled'] ) && boolval( $field['disabled'] ) === true ) ? ' disabled' : ''; ?>"
name="<?php print esc_attr( $field['name'] ); ?>"
<?php echo ( ! empty( $field['disabled'] ) && boolval( $field['disabled'] ) === true ) ? ' disabled' : ''; ?>
value="<?php print esc_attr( $this->settings->{ $field['key'] } ); ?>">
<?php
$this->do_field_description( $field );
}
/**
* Output a checkbox for a boolean setting.
* - hidden field is default value so we don't have to check isset() on save.
*
* @param array $field The settings field definition array.
*
* @return void
*/
public function do_checkbox( $field ) {
$hidden_value = 0;
if ( ! empty( $field['disabled'] ) && boolval( $field['disabled'] ) === true ) {
$hidden_value = intval( $this->settings->{ $field['key'] } );
}
?>
<input type="hidden" name="<?php print esc_attr( $field['name'] ); ?>" value="<?php print esc_attr( strval( $hidden_value ) ); ?>">
<input type="checkbox"
id="<?php print esc_attr( $field['key'] ); ?>"
name="<?php print esc_attr( $field['name'] ); ?>"
<?php echo ( ! empty( $field['disabled'] ) && boolval( $field['disabled'] ) === true ) ? ' disabled="disabled"' : ''; ?>
value="1"
<?php checked( $this->settings->{ $field['key'] }, 1 ); ?>>
<?php
$this->do_field_description( $field );
}
/**
* Output a select control.
*
* @param array $field The settings field definition array.
*
* @return void
*/
public function do_select( $field ) {
$current_value = isset( $this->settings->{ $field['key'] } ) ? $this->settings->{ $field['key'] } : '';
?>
<select
id="<?php print esc_attr( $field['key'] ); ?>"
name="<?php print esc_attr( $field['name'] ); ?>"
<?php echo ( ! empty( $field['disabled'] ) && boolval( $field['disabled'] ) === true ) ? ' disabled' : ''; ?>
>
<?php foreach ( $field['options'] as $value => $text ) : ?>
<option value="<?php print esc_attr( $value ); ?>" <?php selected( $value, $current_value ); ?>><?php print esc_html( $text ); ?></option>
<?php endforeach; ?>
</select>
<?php
$this->do_field_description( $field );
}
/**
* Output the field description, and example if present.
*
* @param array $field The settings field definition array.
*
* @return void
*/
public function do_field_description( $field ) {
?>
<p class="description">
<?php print wp_kses_post( $field['description'] ); ?>
<?php if ( isset( $field['example'] ) ) : ?>
<br/><strong><?php esc_html_e( 'Example', 'daggerhart-openid-connect-generic' ); ?>: </strong>
<code><?php print esc_html( $field['example'] ); ?></code>
<?php endif; ?>
</p>
<?php
}
/**
* Output the 'Client Settings' plugin setting section description.
*
* @return void
*/
public function client_settings_description() {
esc_html_e( 'Enter your OpenID Connect identity provider settings.', 'daggerhart-openid-connect-generic' );
}
/**
* Output the 'WordPress User Settings' plugin setting section description.
*
* @return void
*/
public function user_settings_description() {
esc_html_e( 'Modify the interaction between OpenID Connect and WordPress users.', 'daggerhart-openid-connect-generic' );
}
/**
* Output the 'Authorization Settings' plugin setting section description.
*
* @return void
*/
public function authorization_settings_description() {
esc_html_e( 'Control the authorization mechanics of the site.', 'daggerhart-openid-connect-generic' );
}
/**
* Output the 'Log Settings' plugin setting section description.
*
* @return void
*/
public function log_settings_description() {
esc_html_e( 'Log information about login attempts through OpenID Connect Generic.', 'daggerhart-openid-connect-generic' );
}
}

View File

@ -0,0 +1,531 @@
# Copyright (C) 2024 daggerhart
# This file is distributed under the GPL-2.0+.
msgid ""
msgstr ""
"Project-Id-Version: OpenID Connect Generic 3.10.0\n"
"Report-Msgid-Bugs-To: "
"https://github.com/daggerhart/openid-connect-generic/issues\n"
"POT-Creation-Date: 2024-04-09 01:24:09+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2024-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-Country: United States\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: "
"__;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;_nx_noop:1,2,3c;esc_"
"attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;esc_html_x:1,2c;\n"
"X-Poedit-Basepath: ../\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-Bookmarks: \n"
"X-Textdomain-Support: yes\n"
"X-Generator: grunt-wp-i18n 1.0.3\n"
#: includes/openid-connect-generic-client-wrapper.php:293
msgid "Session expired. Please login again."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:540
msgid "User identity is not linked to an existing WordPress user."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:598
msgid "Invalid user."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:816
msgid "No appropriate username found."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:826
#. translators: %1$s is the santitized version of the username from the IDP.
msgid "Username %1$s could not be sanitized."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:848
#. translators: %1$s is the configured User Claim nickname key.
msgid "No nickname found in user claim using key: %1$s."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:945
msgid "User claim incomplete."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:1048
msgid "Bad user claim result."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:1114
msgid "Can not authorize."
msgstr ""
#: includes/openid-connect-generic-client-wrapper.php:1143
msgid "Failed user creation."
msgstr ""
#: includes/openid-connect-generic-client.php:176
msgid "Missing state."
msgstr ""
#: includes/openid-connect-generic-client.php:180
msgid "Invalid state."
msgstr ""
#: includes/openid-connect-generic-client.php:195
msgid "Missing authentication code."
msgstr ""
#: includes/openid-connect-generic-client.php:240
msgid "Request for authentication token failed."
msgstr ""
#: includes/openid-connect-generic-client.php:273
msgid "Refresh token failed."
msgstr ""
#: includes/openid-connect-generic-client.php:288
msgid "Missing token body."
msgstr ""
#: includes/openid-connect-generic-client.php:296
msgid "Invalid token."
msgstr ""
#: includes/openid-connect-generic-client.php:349
msgid "Request for userinfo failed."
msgstr ""
#: includes/openid-connect-generic-client.php:409
msgid "Missing authentication state."
msgstr ""
#: includes/openid-connect-generic-client.php:446
msgid "No identity token."
msgstr ""
#: includes/openid-connect-generic-client.php:453
msgid "Missing identity token."
msgstr ""
#: includes/openid-connect-generic-client.php:480
msgid "Bad ID token claim."
msgstr ""
#: includes/openid-connect-generic-client.php:485
msgid "No subject identity."
msgstr ""
#: includes/openid-connect-generic-client.php:491
msgid "No matching acr values."
msgstr ""
#: includes/openid-connect-generic-client.php:511
msgid "Bad user claim."
msgstr ""
#: includes/openid-connect-generic-client.php:531
msgid "Invalid user claim."
msgstr ""
#: includes/openid-connect-generic-client.php:536
msgid "Error from the IDP."
msgstr ""
#: includes/openid-connect-generic-client.php:545
msgid "Incorrect user claim."
msgstr ""
#: includes/openid-connect-generic-client.php:552
msgid "Unauthorized access."
msgstr ""
#: includes/openid-connect-generic-login-form.php:122
#. translators: %1$s is the error code from the IDP.
msgid "ERROR (%1$s)"
msgstr ""
#: includes/openid-connect-generic-login-form.php:141
msgid "Login with OpenID Connect"
msgstr ""
#: includes/openid-connect-generic-option-logger.php:228
msgid "Details"
msgstr ""
#: includes/openid-connect-generic-option-logger.php:229
msgid "Data"
msgstr ""
#: includes/openid-connect-generic-option-logger.php:236
msgid "Date"
msgstr ""
#: includes/openid-connect-generic-option-logger.php:240
msgid "Type"
msgstr ""
#: includes/openid-connect-generic-option-logger.php:244
msgid "User"
msgstr ""
#: includes/openid-connect-generic-option-logger.php:248
msgid "URI "
msgstr ""
#: includes/openid-connect-generic-option-logger.php:252
msgid "Response&nbsp;Time&nbsp;(sec)"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:108
msgid "OpenID Connect - Generic Client"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:109
msgid "OpenID Connect Client"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:133
msgid "Client Settings"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:140
msgid "WordPress User Settings"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:147
msgid "Authorization Settings"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:154
msgid "Log Settings"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:212
msgid "Login Type"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:213
msgid "Select how the client (login form) should provide login options."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:216
msgid "OpenID Connect button on login form"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:217
msgid "Auto Login - SSO"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:223
msgid "Client ID"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:224
msgid ""
"The ID this client will be recognized as when connecting the to Identity "
"provider server."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:231
msgid "Client Secret Key"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:232
msgid ""
"Arbitrary secret key the server expects from this client. Can be anything, "
"but should be very unique."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:238
msgid "OpenID Scope"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:239
msgid "Space separated list of scopes this client should access."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:246
msgid "Login Endpoint URL"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:247
msgid "Identify provider authorization endpoint."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:254
msgid "Userinfo Endpoint URL"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:255
msgid "Identify provider User information endpoint."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:262
msgid "Token Validation Endpoint URL"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:263
msgid "Identify provider token endpoint."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:270
msgid "End Session Endpoint URL"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:271
msgid "Identify provider logout endpoint."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:278
msgid "ACR values"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:279
msgid "Use a specific defined authentication contract from the IDP - optional."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:285
msgid "Identity Key"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:286
msgid ""
"Where in the user claim array to find the user's identification data. "
"Possible standard values: preferred_username, name, or sub. If you're "
"having trouble, use \"sub\"."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:292
msgid "Disable SSL Verify"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:294
#. translators: %1$s HTML tags for layout/styles, %2$s closing HTML tag for
#. styles.
msgid ""
"Do not require SSL verification during authorization. The OAuth extension "
"uses curl to make the request. By default CURL will generally verify the "
"SSL certificate to see if its valid an issued by an accepted CA. This "
"setting disabled that verification.%1$sNot recommended for production "
"sites.%2$s"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:299
msgid "HTTP Request Timeout"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:300
msgid "Set the timeout for requests made to the IDP. Default value is 5."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:306
msgid "Enforce Privacy"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:307
msgid "Require users be logged in to see the site."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:313
msgid "Alternate Redirect URI"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:314
msgid ""
"Provide an alternative redirect route. Useful if your server is causing "
"issues with the default admin-ajax method. You must flush rewrite rules "
"after changing this setting. This can be done by saving the Permalinks "
"settings page."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:319
msgid "Nickname Key"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:320
msgid ""
"Where in the user claim array to find the user's nickname. Possible "
"standard values: preferred_username, name, or sub."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:326
msgid "Email Formatting"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:327
msgid ""
"String from which the user's email address is built. Specify \"{email}\" as "
"long as the user claim contains an email claim."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:333
msgid "Display Name Formatting"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:334
msgid "String from which the user's display name is built."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:340
msgid "Identify with User Name"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:341
msgid ""
"If checked, the user's identity will be determined by the user name instead "
"of the email address."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:346
msgid "State time limit"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:347
msgid "State valid time in seconds. Defaults to 180"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:352
msgid "Enable Refresh Token"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:353
msgid ""
"If checked, support refresh tokens used to obtain access tokens from "
"supported IDPs."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:358
msgid "Link Existing Users"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:359
msgid ""
"If a WordPress account already exists with the same identity as a "
"newly-authenticated user over OpenID Connect, login as that user instead of "
"generating an error."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:365
msgid "Create user if does not exist"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:366
msgid ""
"If the user identity is not linked to an existing WordPress user, it is "
"created. If this setting is not enabled, and if the user authenticates with "
"an account which is not linked to an existing WordPress user, then the "
"authentication will fail."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:372
msgid "Redirect Back to Origin Page"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:373
msgid ""
"After a successful OpenID Connect authentication, this will redirect the "
"user back to the page on which they clicked the OpenID Connect login "
"button. This will cause the login process to proceed in a traditional "
"WordPress fashion. For example, users logging in through the default "
"wp-login.php page would end up on the WordPress Dashboard and users logging "
"in through the WooCommerce \"My Account\" page would end up on their "
"account page."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:379
msgid "Redirect to the login screen when session is expired"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:380
msgid ""
"When enabled, this will automatically redirect the user back to the "
"WordPress login page if their access token has expired."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:386
msgid "Enable Logging"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:387
msgid "Very simple log messages for debugging purposes."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:393
msgid "Log Limit"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:394
msgid ""
"Number of items to keep in the log. These logs are stored as an option in "
"the database, so space is limited."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:456
msgid "Notes"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:459
msgid "Redirect URI"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:463
msgid "Login Button Shortcode"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:467
msgid "Authentication URL Shortcode"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:472
msgid "Logs"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:561
msgid "Example"
msgstr ""
#: includes/openid-connect-generic-settings-page.php:574
msgid "Enter your OpenID Connect identity provider settings."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:583
msgid "Modify the interaction between OpenID Connect and WordPress users."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:592
msgid "Control the authorization mechanics of the site."
msgstr ""
#: includes/openid-connect-generic-settings-page.php:601
msgid "Log information about login attempts through OpenID Connect Generic."
msgstr ""
#: openid-connect-generic.php:242
msgid "Private site"
msgstr ""
#. Plugin Name of the plugin/theme
msgid "OpenID Connect Generic"
msgstr ""
#. Plugin URI of the plugin/theme
msgid "https://github.com/daggerhart/openid-connect-generic"
msgstr ""
#. Description of the plugin/theme
msgid ""
"Connect to an OpenID Connect identity provider using Authorization Code "
"Flow."
msgstr ""
#. Author of the plugin/theme
msgid "daggerhart"
msgstr ""
#. Author URI of the plugin/theme
msgid "http://www.daggerhart.com"
msgstr ""

View File

@ -0,0 +1,434 @@
<?php
/**
* OpenID Connect Generic Client
*
* This plugin provides the ability to authenticate users with Identity
* Providers using the OpenID Connect OAuth2 API with Authorization Code Flow.
*
* @package OpenID_Connect_Generic
* @category General
* @author Jonathan Daggerhart <jonathan@daggerhart.com>
* @copyright 2015-2023 daggerhart
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
* @link https://github.com/daggerhart
*
* @wordpress-plugin
* Plugin Name: OpenID Connect Generic
* Plugin URI: https://github.com/daggerhart/openid-connect-generic
* Description: Connect to an OpenID Connect identity provider using Authorization Code Flow.
* Version: 3.10.0
* Requires at least: 5.0
* Requires PHP: 7.4
* Author: daggerhart
* Author URI: http://www.daggerhart.com
* Text Domain: daggerhart-openid-connect-generic
* Domain Path: /languages
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* GitHub Plugin URI: https://github.com/daggerhart/openid-connect-generic
*/
/*
Notes
Spec Doc - http://openid.net/specs/openid-connect-basic-1_0-32.html
Filters
- openid-connect-generic-alter-request - 3 args: request array, plugin settings, specific request op
- openid-connect-generic-settings-fields - modify the fields provided on the settings page
- openid-connect-generic-login-button-text - modify the login button text
- openid-connect-generic-cookie-redirect-url - modify the redirect url stored as a cookie
- openid-connect-generic-user-login-test - (bool) should the user be logged in based on their claim
- openid-connect-generic-user-creation-test - (bool) should the user be created based on their claim
- openid-connect-generic-auth-url - modify the authentication url
- openid-connect-generic-alter-user-claim - modify the user_claim before a new user is created
- openid-connect-generic-alter-user-data - modify user data before a new user is created
- openid-connect-modify-token-response-before-validation - modify the token response before validation
- openid-connect-modify-id-token-claim-before-validation - modify the token claim before validation
Actions
- openid-connect-generic-user-create - 2 args: fires when a new user is created by this plugin
- openid-connect-generic-user-update - 1 arg: user ID, fires when user is updated by this plugin
- openid-connect-generic-update-user-using-current-claim - 2 args: fires every time an existing user logs in and the claims are updated.
- openid-connect-generic-redirect-user-back - 2 args: $redirect_url, $user. Allows interruption of redirect during login.
- openid-connect-generic-user-logged-in - 1 arg: $user, fires when user is logged in.
- openid-connect-generic-cron-daily - daily cron action
- openid-connect-generic-state-not-found - the given state does not exist in the database, regardless of its expiration.
- openid-connect-generic-state-expired - the given state exists, but expired before this login attempt.
Callable actions
User Meta
- openid-connect-generic-subject-identity - the identity of the user provided by the idp
- openid-connect-generic-last-id-token-claim - the user's most recent id_token claim, decoded
- openid-connect-generic-last-user-claim - the user's most recent user_claim
- openid-connect-generic-last-token-response - the user's most recent token response
Options
- openid_connect_generic_settings - plugin settings
- openid-connect-generic-valid-states - locally stored generated states
*/
/**
* OpenID_Connect_Generic class.
*
* Defines plugin initialization functionality.
*
* @package OpenID_Connect_Generic
* @category General
*/
class OpenID_Connect_Generic {
/**
* Singleton instance of self
*
* @var OpenID_Connect_Generic
*/
protected static $_instance = null;
/**
* Plugin version.
*
* @var string
*/
const VERSION = '3.10.0';
/**
* Plugin settings.
*
* @var OpenID_Connect_Generic_Option_Settings
*/
private $settings;
/**
* Plugin logs.
*
* @var OpenID_Connect_Generic_Option_Logger
*/
private $logger;
/**
* Openid Connect Generic client
*
* @var OpenID_Connect_Generic_Client
*/
private $client;
/**
* Client wrapper.
*
* @var OpenID_Connect_Generic_Client_Wrapper
*/
public $client_wrapper;
/**
* Setup the plugin
*
* @param OpenID_Connect_Generic_Option_Settings $settings The settings object.
* @param OpenID_Connect_Generic_Option_Logger $logger The loggin object.
*
* @return void
*/
public function __construct( OpenID_Connect_Generic_Option_Settings $settings, OpenID_Connect_Generic_Option_Logger $logger ) {
$this->settings = $settings;
$this->logger = $logger;
self::$_instance = $this;
}
// @codeCoverageIgnoreStart
/**
* WordPress Hook 'init'.
*
* @return void
*/
public function init() {
$this->client = new OpenID_Connect_Generic_Client(
$this->settings->client_id,
$this->settings->client_secret,
$this->settings->scope,
$this->settings->endpoint_login,
$this->settings->endpoint_userinfo,
$this->settings->endpoint_token,
$this->get_redirect_uri( $this->settings ),
$this->settings->acr_values,
$this->get_state_time_limit( $this->settings ),
$this->logger
);
$this->client_wrapper = OpenID_Connect_Generic_Client_Wrapper::register( $this->client, $this->settings, $this->logger );
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return;
}
OpenID_Connect_Generic_Login_Form::register( $this->settings, $this->client_wrapper );
// Add a shortcode to get the auth URL.
add_shortcode( 'openid_connect_generic_auth_url', array( $this->client_wrapper, 'get_authentication_url' ) );
// Add actions to our scheduled cron jobs.
add_action( 'openid-connect-generic-cron-daily', array( $this, 'cron_states_garbage_collection' ) );
$this->upgrade();
if ( is_admin() ) {
OpenID_Connect_Generic_Settings_Page::register( $this->settings, $this->logger );
}
}
/**
* Get the default redirect URI.
*
* @param OpenID_Connect_Generic_Option_Settings $settings The settings object.
*
* @return string
*/
public function get_redirect_uri( OpenID_Connect_Generic_Option_Settings $settings ) {
$redirect_uri = admin_url( 'admin-ajax.php?action=openid-connect-authorize' );
if ( $settings->alternate_redirect_uri ) {
$redirect_uri = site_url( '/openid-connect-authorize' );
}
return $redirect_uri;
}
/**
* Get the default state time limit.
*
* @param OpenID_Connect_Generic_Option_Settings $settings The settings object.
*
* @return int
*/
public function get_state_time_limit( OpenID_Connect_Generic_Option_Settings $settings ) {
$state_time_limit = 180;
// State time limit cannot be zero.
if ( $settings->state_time_limit ) {
$state_time_limit = intval( $settings->state_time_limit );
}
return $state_time_limit;
}
/**
* Check if privacy enforcement is enabled, and redirect users that aren't
* logged in.
*
* @return void
*/
public function enforce_privacy_redirect() {
if ( $this->settings->enforce_privacy && ! is_user_logged_in() ) {
// The client endpoint relies on the wp-admin ajax endpoint.
if (
! defined( 'DOING_AJAX' ) ||
! boolval( constant( 'DOING_AJAX' ) ) ||
! isset( $_GET['action'] ) ||
'openid-connect-authorize' != $_GET['action'] ) {
auth_redirect();
}
}
}
/**
* Enforce privacy settings for rss feeds.
*
* @param string $content The content.
*
* @return mixed
*/
public function enforce_privacy_feeds( $content ) {
if ( $this->settings->enforce_privacy && ! is_user_logged_in() ) {
$content = __( 'Private site', 'daggerhart-openid-connect-generic' );
}
return $content;
}
/**
* Handle plugin upgrades
*
* @return void
*/
public function upgrade() {
$last_version = get_option( 'openid-connect-generic-plugin-version', 0 );
$settings = $this->settings;
if ( version_compare( self::VERSION, $last_version, '>' ) ) {
// An upgrade is required.
self::setup_cron_jobs();
// @todo move this to another file for upgrade scripts
if ( isset( $settings->ep_login ) ) {
$settings->endpoint_login = $settings->ep_login;
$settings->endpoint_token = $settings->ep_token;
$settings->endpoint_userinfo = $settings->ep_userinfo;
unset( $settings->ep_login, $settings->ep_token, $settings->ep_userinfo );
$settings->save();
}
// Update the stored version number.
update_option( 'openid-connect-generic-plugin-version', self::VERSION );
}
}
/**
* Expire state transients by attempting to access them and allowing the
* transient's own mechanisms to delete any that have expired.
*
* @return void
*/
public function cron_states_garbage_collection() {
global $wpdb;
$states = $wpdb->get_col( "SELECT `option_name` FROM {$wpdb->options} WHERE `option_name` LIKE '_transient_openid-connect-generic-state--%'" );
if ( ! empty( $states ) ) {
foreach ( $states as $state ) {
$transient = str_replace( '_transient_', '', $state );
get_transient( $transient );
}
}
}
/**
* Ensure cron jobs are added to the schedule.
*
* @return void
*/
public static function setup_cron_jobs() {
if ( ! wp_next_scheduled( 'openid-connect-generic-cron-daily' ) ) {
wp_schedule_event( time(), 'daily', 'openid-connect-generic-cron-daily' );
}
}
/**
* Activation hook.
*
* @return void
*/
public static function activation() {
self::setup_cron_jobs();
}
/**
* Deactivation hook.
*
* @return void
*/
public static function deactivation() {
wp_clear_scheduled_hook( 'openid-connect-generic-cron-daily' );
}
/**
* Simple autoloader.
*
* @param string $class The class name.
*
* @return void
*/
public static function autoload( $class ) {
$prefix = 'OpenID_Connect_Generic_';
if ( stripos( $class, $prefix ) !== 0 ) {
return;
}
$filename = $class . '.php';
// Internal files are all lowercase and use dashes in filenames.
if ( false === strpos( $filename, '\\' ) ) {
$filename = strtolower( str_replace( '_', '-', $filename ) );
} else {
$filename = str_replace( '\\', DIRECTORY_SEPARATOR, $filename );
}
$filepath = __DIR__ . '/includes/' . $filename;
if ( file_exists( $filepath ) ) {
require_once $filepath;
}
}
/**
* Instantiate the plugin and hook into WordPress.
*
* @return void
*/
public static function bootstrap() {
/**
* This is a documented valid call for spl_autoload_register.
*
* @link https://www.php.net/manual/en/function.spl-autoload-register.php#71155
*/
spl_autoload_register( array( 'OpenID_Connect_Generic', 'autoload' ) );