Skip to content

Azure AD Role Mapping

Tim Nolte edited this page Oct 10, 2020 · 1 revision

The Microsoft Azure AD implementation of OpenID Connect doesn't provide a way to include the roles claim in the Access Token it's found in the ID Token. Currently the plugin doesn't provide an authentication hook prior to user login that can perform a role mapping with the ID Token. This current limitation is being evaluated to determine a way to update the plugin to so that the ID Token can be leveraged during authentication prior to login.

Given the nature of the current hooks available the only way to accomplish user role mapping with Azure AD is to use the openid-connect-generic-user-logged-in hook. The problem with this hook is that it runs after the user is already logged in to WordPress so the user will need to be updated, logged out, then logged back in.

The example below can be included in a Must User plugin to accomplish user role mapping. This code provides additional plugin settings to manage the role mapping from the WordPress Dashboard. A version of this code has been tested and used in a production Azure AD environment.

/**
 * Adds new settings that allows mapping IDP roles to WordPress roles.
 *
 * @link https://github.com/daggerhart/openid-connect-generic#openid-connect-generic-settings-fields
 *
 * @param array<mixed> $fields The array of settings fields.
 *
 * @return array<mixed>
 */
function azuread_oidc_role_mapping_setting( $fields ) {

	// @var WP_Roles $wp_roles_obj
	$wp_roles_obj = wp_roles();
	// @var array<string> $roles
	$roles = $wp_roles_obj->get_names();

	foreach ( $roles as $role ) {
		$fields[ 'oidc_idp_' . str_replace( ' ', '_', strtolower( $role ) ) . '_roles' ] = array(

			/*
			 * Translators: The replaced text comes from the WordPress roles so that
			 * is the text that should have a translation.
			 */
			'title'       => sprintf( __( 'IDP Role for WordPress %ss', 'forum-one-mu-plugins' ), $role ),

			/*
			 * Translators: The replaced text comes from the WordPress roles so that
			 * is the text that should have a translation.
			 */
			'description' => sprintf( __( 'Semi-colon(;) separated list of IDP roles to map to the %s WordPress role', 'forum-one-mu-plugins' ), $role ),
			'type'        => 'text',
			'section'     => 'user_settings',
		);
	}

	return $fields;

}
add_filter( 'openid-connect-generic-settings-fields', 'azuread_oidc_role_mapping_setting', 10, 1 );

/**
 * Clear and assign WordPress roles based on IDP roles claim.
 *
 * @param WP_User      $user  The authenticated user's WP_User object.
 * @param array<mixed> $token The Identity Token.
 *
 * @return WP_User
 */
function azuread_set_user_roles( $user, $token ) {

	// @var array<mixed> $settings
	$settings = get_option( 'openid_connect_generic_settings', array() );
	// @var string $default_role
	$default_role = get_option( 'default_role', '' );

	// Check the token for the `roles` key to lookup the WordPress role for mapping.
	if ( ! empty( $settings ) && ! empty( $token['roles'] ) ) {
		// @var WP_Roles $wp_roles_obj
		$wp_roles_obj = wp_roles();
		// @var array<string> $roles
		$roles = $wp_roles_obj->get_names();

		// Clear all roles.
		foreach ( $roles as $role_id => $role_name ) {
			$user->remove_role( $role_id );
		}
		// @var int $role_count
		$role_count = 0;

		foreach ( $token['roles'] as $idp_role ) {
			foreach ( $roles as $role_id => $role_name ) {
				$role_key = 'oidc_idp_' . str_replace( ' ', '_', strtolower( $role_name ) ) . '_roles';
				if ( ! empty( $settings[ $role_key ] ) ) {
					if ( in_array( $idp_role, explode( ';', $settings[ $role_key ] ) ) ) {
						$user->add_role( $role_id );
						$role_count++;
					}
				}
			}
		}

		if ( intval( $role_count ) == 0 && ! empty( $default_role ) ) {
			if ( boolval( $default_role ) ) {
				$user->set_role( $default_role );
			}
		}
	}

	return $user;
}

/**
 * Set user WordPress roles on based on IDP roles after authentication.
 *
 * @param WP_User $user The authenticated user's WP_User object.
 *
 * @return void
 */
function azuread_oidc_map_user_role_token_claim( $user ) {

	// @var array<mixed> $settings
	$settings = get_option( 'openid_connect_generic_settings', array() );

	// @var array<mixed> $token The IDP provided Identity Token claims array.
	$token = get_user_meta( $user->ID, 'openid-connect-generic-last-id-token-claim', true );

	$user = azuread_set_user_roles( $user, $token );

	/**
	 * Login the user again after updating their roles.
	 */
	wp_cache_delete( $user->ID, 'users' );
	wp_cache_delete( $user->user_login, 'userlogins' );
	wp_set_current_user( $user->ID, $user->user_login );
	wp_set_auth_cookie( $user->ID );
	do_action( 'wp_login', $user->user_login );

}
add_action( 'openid-connect-generic-user-logged-in', 'azuread_oidc_map_user_role_token_claim', 10, 1 );