openid-connect-generic
openid-connect-generic copied to clipboard
Easier multisite config and use
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
- Allow overriding all settings in the wp-config.php;
- On logout destroy all WP sessions (at all the sites) with the same
sidparam. - 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
- All sites in our network have their own domainname, so it is not possible to share cookies.
- The idP is Azure AD (MS 365)
- All the sites can share the same
Client ID,Client Secret, OAuth urls and stuff. Only theRedirect URIis different.
@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.
@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:
- Save the info from the provider in the session. Especially the OpenID
sidtoken, which is provided in the sign out requests. - Add a REST API endpoint for the logout, because Azure allows only one logout URL.
- Remove the
X-Frame-Optionsheader for that specific URL to allow iframes. TheSSO_LOGOUT_SOURCEconstant should be a secret key that is only shared with the provider, because of the reduced iframe security. - On logout find all the related WP sessions with the same
sidtoken across all domain names and destroy them.
In Azure app details
- Set the logout URL to
https://mainsite.tld/wp-json/mytheme/v1/sso-logout?source=SECRET - Set the login redirect URL for each multisite domain.
- Add the
sidclaim to the ID token.
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.