|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Plugin Name: WP SAML Auth for UW |
| 4 | + * Version: 0.0.0 |
| 5 | + * |
| 6 | + */ |
| 7 | + |
| 8 | +define('WP_SAML_AUTH_UW_GROUP_STEM', 'uw_asa_it_web'); |
| 9 | + |
| 10 | +function wpsax_filter_option( $value, $option_name ) { |
| 11 | + $defaults = array( |
| 12 | + /** |
| 13 | + * Type of SAML connection bridge to use. |
| 14 | + * |
| 15 | + * 'internal' uses OneLogin bundled library; 'simplesamlphp' uses SimpleSAMLphp. |
| 16 | + * |
| 17 | + * Defaults to SimpleSAMLphp for backwards compatibility. |
| 18 | + * |
| 19 | + * @param string |
| 20 | + */ |
| 21 | + 'connection_type' => 'internal', |
| 22 | + /** |
| 23 | + * Configuration options for OneLogin library use. |
| 24 | + * |
| 25 | + * See comments with "Required:" for values you absolutely need to configure. |
| 26 | + * |
| 27 | + * @param array |
| 28 | + */ |
| 29 | + 'internal_config' => array( |
| 30 | + // Validation of SAML responses is required. |
| 31 | + 'strict' => true, |
| 32 | + 'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, |
| 33 | + 'baseurl' => home_url(), |
| 34 | + 'sp' => array( |
| 35 | + 'entityId' => network_site_url('sp'), |
| 36 | + 'assertionConsumerService' => array( |
| 37 | + 'url' => site_url('wp-login.php'), |
| 38 | + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', |
| 39 | + ), |
| 40 | + // 'x509cert' => file_get_contents(ABSPATH . '/private/sp.crt'), |
| 41 | + // 'privateKey' => file_get_contents(ABSPATH . '/private/sp.key'), |
| 42 | + // 'x509certNew' => file_get_contents(ABSPATH . '/private/sp-new.crt'), |
| 43 | + ), |
| 44 | + 'idp' => array( |
| 45 | + // Required: Set based on provider's supplied value. |
| 46 | + 'entityId' => 'urn:mace:incommon:washington.edu', |
| 47 | + 'singleSignOnService' => array( |
| 48 | + // Required: Set based on provider's supplied value. |
| 49 | + 'url' => 'https://idp.u.washington.edu/idp/profile/SAML2/Redirect/SSO', |
| 50 | + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', |
| 51 | + ), |
| 52 | + 'singleLogoutService' => array( |
| 53 | + // Required: Set based on provider's supplied value. |
| 54 | + 'url' => 'https://idp.u.washington.edu/idp/logout', |
| 55 | + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', |
| 56 | + ), |
| 57 | + // Required: Contents of the IDP's public x509 certificate. |
| 58 | + // Use file_get_contents() to load certificate contents into scope. |
| 59 | + 'x509cert' => file_get_contents(ABSPATH . '/private/uw-idp-md-cert.pem'), |
| 60 | + // Optional: Instead of using the x509 cert, you can specify the fingerprint and algorithm. |
| 61 | + 'certFingerprint' => '', |
| 62 | + 'certFingerprintAlgorithm' => '', |
| 63 | + ), |
| 64 | + ), |
| 65 | + /** |
| 66 | + * Path to SimpleSAMLphp autoloader. |
| 67 | + * |
| 68 | + * Follow the standard implementation by installing SimpleSAMLphp |
| 69 | + * alongside the plugin, and provide the path to its autoloader. |
| 70 | + * Alternatively, this plugin will work if it can find the |
| 71 | + * `SimpleSAML_Auth_Simple` class. |
| 72 | + * |
| 73 | + * @param string |
| 74 | + */ |
| 75 | + 'simplesamlphp_autoload' => dirname( __FILE__ ) . '/simplesamlphp/lib/_autoload.php', |
| 76 | + /** |
| 77 | + * Authentication source to pass to SimpleSAMLphp |
| 78 | + * |
| 79 | + * This must be one of your configured identity providers in |
| 80 | + * SimpleSAMLphp. If the identity provider isn't configured |
| 81 | + * properly, the plugin will not work properly. |
| 82 | + * |
| 83 | + * @param string |
| 84 | + */ |
| 85 | + 'auth_source' => 'default-sp', |
| 86 | + /** |
| 87 | + * Whether or not to automatically provision new WordPress users. |
| 88 | + * |
| 89 | + * When WordPress is presented with a SAML user without a |
| 90 | + * corresponding WordPress account, it can either create a new user |
| 91 | + * or display an error that the user needs to contact the site |
| 92 | + * administrator. |
| 93 | + * |
| 94 | + * @param bool |
| 95 | + */ |
| 96 | + 'auto_provision' => true, |
| 97 | + /** |
| 98 | + * Whether or not to permit logging in with username and password. |
| 99 | + * |
| 100 | + * If this feature is disabled, all authentication requests will be |
| 101 | + * channeled through SimpleSAMLphp. |
| 102 | + * |
| 103 | + * @param bool |
| 104 | + */ |
| 105 | + 'permit_wp_login' => ($_GET['saml_sso'] === 'false' ? true : false), |
| 106 | + /** |
| 107 | + * Attribute by which to get a WordPress user for a SAML user. |
| 108 | + * |
| 109 | + * @param string Supported options are 'email' and 'login'. |
| 110 | + */ |
| 111 | + 'get_user_by' => 'login', |
| 112 | + /** |
| 113 | + * SAML attribute which includes the user_login value for a user. |
| 114 | + * |
| 115 | + * @param string |
| 116 | + */ |
| 117 | + 'user_login_attribute' => 'urn:oid:0.9.2342.19200300.100.1.1', // 'uid', 'uwNetID' |
| 118 | + /** |
| 119 | + * SAML attribute which includes the user_email value for a user. |
| 120 | + * |
| 121 | + * @param string |
| 122 | + */ |
| 123 | + 'user_email_attribute' => 'urn:oid:0.9.2342.19200300.100.1.3', // 'mail', 'email' |
| 124 | + /** |
| 125 | + * SAML attribute which includes the display_name value for a user. |
| 126 | + * |
| 127 | + * @param string |
| 128 | + */ |
| 129 | + 'display_name_attribute' => 'urn:oid:2.5.4.3', // 'cn' |
| 130 | + /** |
| 131 | + * SAML attribute which includes the first_name value for a user. |
| 132 | + * |
| 133 | + * @param string |
| 134 | + */ |
| 135 | + 'first_name_attribute' => 'urn:oid:2.5.4.42', // 'givenName' |
| 136 | + /** |
| 137 | + * SAML attribute which includes the last_name value for a user. |
| 138 | + * |
| 139 | + * @param string |
| 140 | + */ |
| 141 | + 'last_name_attribute' => 'urn:oid:2.5.4.4', // 'surname' |
| 142 | + /** |
| 143 | + * Default WordPress role to grant when provisioning new users. |
| 144 | + * |
| 145 | + * @param string |
| 146 | + */ |
| 147 | + 'default_role' => get_option( 'default_role' ), |
| 148 | + ); |
| 149 | + $value = isset( $defaults[ $option_name ] ) ? $defaults[ $option_name ] : $value; |
| 150 | + return $value; |
| 151 | +} |
| 152 | +add_filter( 'wp_saml_auth_option', 'wpsax_filter_option', 10, 2 ); |
| 153 | + |
| 154 | +function custom_query_vars_filter($vars) { |
| 155 | + $vars[] .= 'saml_sso'; |
| 156 | + return $vars; |
| 157 | +} |
| 158 | +add_filter( 'query_vars', 'custom_query_vars_filter' ); |
| 159 | + |
| 160 | +add_action( 'login_form', function() { |
| 161 | + if (get_query_var('saml_sso')) { |
| 162 | + ?><input type="text" name="saml_sso" value="false" /><?php |
| 163 | + } |
| 164 | +}); |
| 165 | + |
| 166 | + |
| 167 | +/* |
| 168 | + * Return an associative array of groups, indexed by role |
| 169 | + */ |
| 170 | +function site_role_groups() { |
| 171 | + $domain = parse_url(site_url(), PHP_URL_HOST); |
| 172 | + |
| 173 | + /** |
| 174 | + * reduce the hostname down to just the first section, after removing prefixes like "dev." or "test." |
| 175 | + */ |
| 176 | + $site = preg_replace('/^((dev|test)\.)/', '', $domain); |
| 177 | + $site = preg_replace('/\..*/', '', $site); |
| 178 | + |
| 179 | + $site_stem = WP_SAML_AUTH_UW_GROUP_STEM.'_'.$site; |
| 180 | + |
| 181 | + $role_map = array(); |
| 182 | + foreach (wp_roles()->role_names as $role => $name) { |
| 183 | + $role_map[$role] = $site_stem.'_'.$role; |
| 184 | + } |
| 185 | + return $role_map; |
| 186 | +} |
| 187 | + |
| 188 | +/* |
| 189 | + * Return the super admin group |
| 190 | + */ |
| 191 | +function super_admin_group() { |
| 192 | + return WP_SAML_AUTH_UW_GROUP_STEM.'_admin'; |
| 193 | +} |
| 194 | + |
| 195 | +/* |
| 196 | + * Add user to roles according to the groups given in attributes |
| 197 | + */ |
| 198 | +function add_user_roles( $user, $attributes ) { |
| 199 | + $groups_attribute = 'urn:oid:1.3.6.1.4.1.5923.1.5.1.1'; |
| 200 | + if (!($user_groups = $attributes[$groups_attribute])) { |
| 201 | + return; |
| 202 | + } |
| 203 | + |
| 204 | + if (in_array('urn:mace:washington.edu:groups:' . super_admin_group(), $user_groups)) { |
| 205 | + grant_super_admin($user->ID); |
| 206 | + } else { |
| 207 | + revoke_super_admin($user->ID); |
| 208 | + } |
| 209 | + |
| 210 | + foreach (site_role_groups() as $role => $uw_group) { |
| 211 | + if (in_array('urn:mace:washington.edu:groups:'.$uw_group, $user_groups)) { |
| 212 | + $user->add_role($role); |
| 213 | + } else { |
| 214 | + $user->remove_role($role); |
| 215 | + } |
| 216 | + } |
| 217 | +} |
| 218 | +add_action( 'wp_saml_auth_existing_user_authenticated', 'add_user_roles', 10, 2); |
| 219 | +add_action( 'wp_saml_auth_new_user_authenticated', 'add_user_roles', 10, 2); |
| 220 | + |
| 221 | +add_action( 'admin_menu', function() { |
| 222 | + add_options_page( |
| 223 | + __( 'WP SAML Auth UW Settings', 'wp-saml-auth-uw' ), |
| 224 | + __( 'WP SAML Auth UW', 'wp-saml-auth-uw' ), |
| 225 | + 'manage_options', |
| 226 | + 'wp-saml-auth-uw-settings', |
| 227 | + function() { |
| 228 | + $config = apply_filters( 'wp_saml_auth_option', null, 'internal_config' ); |
| 229 | + $groups = site_role_groups(); |
| 230 | + ?> |
| 231 | + <div class="wrap"> |
| 232 | + <h2><?php esc_html_e( 'WP SAML Auth UW Settings', 'wp-saml-auth-uw' ); ?></h2> |
| 233 | + <h2>Service Provider Settings</h2> |
| 234 | + <p>Ensure this metadata is present in the UW Service Provider Registry:</p> |
| 235 | + <table class="form-table" role="presentation"> |
| 236 | + <tr><th scope="row">Entity Id</th> |
| 237 | + <td><input readonly="readonly" type="text" class="regular-text" value="<?= $config['sp']['entityId'] ?>" /></td></tr> |
| 238 | + <tr><th scope="row">Assertion Consumer Service URL</th> |
| 239 | + <td><input readonly="readonly" type="text" class="regular-text" value="<?= $config['sp']['assertionConsumerService']['url'] ?>" /></td></tr> |
| 240 | + </table> |
| 241 | + <h2>Role Mapping</h2> |
| 242 | + <p>Roles will be granted based on membership in these UW Groups:</p> |
| 243 | + <table class="form-table" role="presentation"> |
| 244 | + <tr><th scope="row">Super Admin</th> |
| 245 | + <td><input readonly="readonly" type="text" class="regular-text" value="<?= super_admin_group() ?>" /></td></tr> |
| 246 | +<?php foreach (wp_roles()->get_names() as $role => $name): ?> |
| 247 | + <tr><th scope="row"><?= $name ?></th> |
| 248 | + <td><input readonly="readonly" type="text" class="regular-text" value="<?= $groups[$role] ?>" /></td></tr> |
| 249 | +<?php endforeach; ?> |
| 250 | + </table> |
| 251 | + </div> |
| 252 | + <?php |
| 253 | + } |
| 254 | + ); |
| 255 | +}); |
0 commit comments