oidc-wp / openid-connect-generic

WordPress plugin to provide an OpenID Connect Generic client
https://wordpress.org/plugins/daggerhart-openid-connect-generic/
258 stars 154 forks source link

Easier multisite config and use #414

Open fvdm opened 2 years ago

fvdm commented 2 years ago

Is your feature request related to a problem? Please describe. On a multisite installation you have to repeat the same configuration on each site, while only the domain name is changing. This is a lot of work to set up and maintain.

Describe the solution you'd like

  1. Allow overriding all settings in the wp-config.php;
  2. On logout destroy all WP sessions (at all the sites) with the same sid param.
  3. Have a front channel logout url at the main blog domain, which ends the sessions at all sites.

I love the simplicity of this plugin. Having all settings in one place would make a big difference for multisite.

Describe alternatives you've considered For now I have spend an evening to manually copy the OIDC params from the main site to the other 14 sites and setup individual OAuth apps in Azure AD for each blog, because the logout URL in AD needs to relate to the blog instead of only the main site.

Additional context

timnolte commented 1 year ago

@fvdm I'm wondering if you have tried using the configuration constants in the sunrise.php file or in the wp-config.php file? That may actually work to configure all of the sites at once.

fvdm commented 1 year ago

@timnolte Sorry I forgot to update this issue. Yes, the constants do the trick although not all settings can be forced this way.

I resolved the multisite (multi domain) single-logout by adding hooks in our theme and setting only the mainsite logout URL in Azure using a WP REST endpoint. See the code below.

Feels like it could be much easier from the plugin. Maybe someone with time can use it for a nicer integration. It was a lot of work to figure out the right order of code execution. The session management in WP is very limited. I even had to override the wp_logout() pluggable.

What it does:

In Azure app details

Store provider session details

/**
 * Add user claim on successful OpenID login
 * needs to run early to be available in the OIDC plugin
 *
 * @param   array   $session  Session token data
 * @param   string  $user_id  WP User ID
 *
 * @return  array             Updated session token data
 */

add_filter( 'attach_session_information', 'sso_update_session', 1, 2 );

function sso_update_session( $session, $user_id ) {
  // Debug friendly sessions
  $session['site_url'] = get_site_url();
  $session['site_id'] = get_current_blog_id();

  // Only continue for OIDC sessions
  $backtrace = debug_backtrace();
  $continue = false;

  foreach ( $backtrace as $caller ) {
    if (
      str_contains( $caller['file'], '/openid-connect-generic-client-wrapper.php' )
      && $caller['class'] === 'WP_Session_Tokens'
      && $caller['function'] === 'create'
    ) {
      $continue = true;
    }
  }

  if ( ! $continue ) {
    return $session;
  }

  // Process the user claim
  $claim = get_user_meta( $user_id, 'openid-connect-generic-last-id-token-claim', true );

  if ( ! is_array( $claim ) ) {
    return $session;
  }

  $session['oidc_claim'] = $claim;
  return $session;
}

Logout callback

/**
 * SSO logout URL for multisite single sign out
 */

add_action( 'rest_api_init', 'sso_logout_restapi' );

function sso_logout_restapi() {
  register_rest_route( 'mytheme/v1', '/sso-logout', array(
    'methods'             => 'GET',
    'callback'            => 'sso_logout',
    'permission_callback' => '__return_true',
  ) );
}

/**
 * Allow nonce-less iframe embed for SSO logout
 * WP sets the X-Frame-Options which blocks the Azure single-signout requests.
 *
 * Set SSO_LOGOUT_SOURCE in wp-config.php
 * Set https://domain/wp-json/mytheme/v1/sso-logout?source=XX at the SSO IdP
 */

function sso_logout( $request ) {
  // Validate request
  // sid = UUID format
  if ( $_GET['source'] !== SSO_LOGOUT_SOURCE || ! preg_match( '/^[0-f]{8}-[0-f]{4}-[0-f]{4}-[0-f]{4}-[0-f]{12}$/', $_REQUEST['sid'] ) ) {
    return array(
      'success' => false,
      'message' => 'invalid request',
    );
  }

  // Allow iframe and process sessions
  header_remove( 'X-Frame-Options' );
  wp_logout( $_REQUEST['sid'] );

  // Done
  return array(
    'success' => true,
    'message' => 'logout complete',
  );
}

Customized wp_logout()

/wp-contents/mu-plugins/0_wp_logout.php

<?php
/**
* Before logout destroy all sessions with the same SSO OpenID `sid`
* replacing `wp_logout()` to keep access to the session token.
*
* @param   string  [$sid]  SSO session id (`sid` param)
*
* @return  void
*/

if ( ! function_exists( 'wp_logout' ) ) {
  function wp_logout( $sid = null ) {
    $user_id = get_current_user_id();

    // Sanitize input (UUID)
    if ( ! is_string( $sid ) || ! preg_match( '/^[0-f]{8}-[0-f]{4}-[0-f]{4}-[0-f]{4}-[0-f]{12}$/i', $sid ) ) {
      $sid = null;
    }

    // Find the user corresponding to the `sid`
    if ( ! $user_id && $sid ) {
      global $wpdb;

      // SQL because the session tokens are not always readable
      $sql = "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key='session_tokens' AND meta_value LIKE '%s:3:\"sid\";s:36:\"{$sid}\";%' LIMIT 1";
      $user_id = (int) $wpdb->get_var( $sql );
    }

    // Find the `sid` corresponding to the user
    if ( $user_id && ! $sid ) {
      $manager = WP_Session_Tokens::get_instance( $user_id );
      $token = wp_get_session_token();
      $session = $manager->get( $token );

      if ( is_array( $session['oidc_claim'] ) && isset( $session['oidc_claim']['sid'] ) ) {
        $sid = $session['oidc_claim']['sid'];
      }
    }

    // Destroy the user sessions related to the `sid`
    if ( $user_id && $sid ) {
      $user_sessions = get_user_meta( $user_id, 'session_tokens', true );

      if ( is_array( $user_sessions ) && count( $user_sessions ) ) {
        foreach( $user_sessions as $verifier => $sess ) {
          if (
            isset( $sess['oidc_claim'] )
            && isset( $sess['oidc_claim']['sid'] )
            && $sess['oidc_claim']['sid'] === $sid
          ) {
            unset( $user_sessions[$verifier] );
          }
        }

        update_user_meta( $user_id, 'session_tokens', $user_sessions );
      }
    }

    // Default wp_logout()
    wp_destroy_current_session();
    wp_clear_auth_cookie();
    wp_set_current_user( 0 );

    /**
     * Fires after a user is logged out.
     *
     * @since 1.5.0
     * @since 5.5.0 Added the `$user_id` parameter.
     *
     * @param int $user_id ID of the user that was logged out.
     */
     do_action( 'wp_logout', $user_id );
  }
}

I feel like I missed a part but not sure. This session stuff is a world of its own, so many layers.