WordPress Integration Guide

wordpress

(Adam Capriola) #1

I’ve been offering a service to do WordPress integrations, but (a) I’m often unavailable because of other priorities and (b) freelancing this stuff hasn’t been lucrative … so I figured I might as well share my bits of code. Hopefully the community here can benefit from what I’ve learned working on integrations the past year.

My belief is that there isn’t a one-size-fits-all solution for integrations, so what I’m going to do is guide you through creating a custom plugin file with different functions you can add and tweak to fulfill your needs.

##I. Create Plugin File

I stick all of my code inside my custom WordPress theme (which you can do too), but for the purposes of this guide we’ll use a plugin file to house all of our functions.

Create a file in your favorite text editor with the name discourse-wordpress-integration.php. Begin it with the following and customize the definition values:

<?php
/*
Plugin Name: Discourse WordPress Integration
Description: This plugin contains all of the functions that deal with integrating WordPress and Discourse.
Version: 1.0
Author: Your Name
Author URI: http://example.com
*/

/**
 * Discourse Definitions
 * 
 */
define( 'DISCOURSE_URL', 'http://discourse.example.com' ); // No trailing slash!
define( 'DISCOURSE_API_KEY', 'abc123' ); // Your global API key created + found here: http://discourse.example.com/admin/api
define( 'DISCOURSE_API_USERNAME', 'system' ); // Username of an administrator ("system" is a safe bet).
define( 'DISCOURSE_SSO_SECRET', 'super_duper_secret' ); // Your "sso secret" value created + found here: http://discourse.example.com/admin/site_settings/category/login

Then we will add any blocks of code below to the file as needed.

##II. SSO Functionality

###A. Include Helper Class

We’ll begin by adding @ArmedGuy’s [helper class][1]:

/**
 * Discourse SSO Class
 * 
 */
add_action( 'plugins_loaded', 'ac_discourse_sso_class_init' );

function ac_discourse_sso_class_init() {

	if ( ! class_exists( 'Discourse_SSO' ) ) {

		class Discourse_SSO {

			private $sso_secret;

			function __construct( $secret ) {
				$this->sso_secret = $secret;
			}

			public function validate( $payload, $sig ) {
				$payload = urldecode( $payload );
				if ( hash_hmac( "sha256", $payload, $this->sso_secret ) === $sig ) {
					return true;
				} else {
					return false;
				}
			}

			public function getNonce( $payload ) {
				$payload = urldecode( $payload );
				$query = array();
				parse_str( base64_decode( $payload ), $query );
				if ( isset( $query["nonce"] ) ) {
					return $query["nonce"];
				} else {
					throw new Exception( "Nonce not found in payload!" );
				}
			}

			public function buildLoginString( $params ) {
				if ( ! isset($params["external_id"] ) ) {
					throw new Exception( "Missing required parameter 'external_id'" );
				}
				if ( ! isset($params["nonce"] ) ) {
					throw new Exception( "Missing required parameter 'nonce'" );
				}
				if ( ! isset($params["email"] ) ) {
					throw new Exception( "Missing required parameter 'email'" );
				}
				$payload = base64_encode( http_build_query( $params ) );
				$sig = hash_hmac( "sha256", $payload, $this->sso_secret );

				return http_build_query( array( "sso" => $payload, "sig" => $sig ) );
			}

		}

	}

}

###B. Process SSO Requests

This function handles requests to log in (and log out) via SSO. There are two values you will need to set in Discourse:

  1. “sso url” = http://example.com/?request=sso
  2. “logout redirect” = http://example.com/?request=logout
/**
 * Process SSO Requests
 * 
 */
add_action( 'parse_request', 'ac_parse_request' );

function ac_parse_request() {

	// Check for SSO request
	if ( isset( $_GET['request'] ) && $_GET['request'] == 'sso' ) {

		// Variables
		$sso_secret = DISCOURSE_SSO_SECRET;
		$discourse_url = DISCOURSE_URL;

		//
		// Check if user is logged in to WordPress
		//

		// Not logged in to WordPress, redirect to WordPress login page with redirect back to here
		if ( ! is_user_logged_in() ) {

			// Preserve sso and sig parameters
			$redirect = add_query_arg( '', '' );

			// Change %0A to %0B so it's not stripped out in wp_sanitize_redirect or esc_url
			$redirect = str_replace( '%0A', '%0B', $redirect );

			// Build login URL
			$login = wp_login_url( esc_url_raw( $redirect ) );

			// Redirect to login
			wp_redirect( $login );
			exit;

		}

		// Logged in to WordPress, now try to log in to Discourse with WordPress user information
		else {

			// Payload and signature
			$payload = $_GET['sso'];
			$sig = $_GET['sig'];

			// Change %0B back to %0A
			$payload = urldecode( str_replace( '%0B', '%0A', urlencode( $payload ) ) );

			// Check for helper class
			if ( ! class_exists( 'Discourse_SSO' ) ) {

				// Error message
				echo( 'Helper class is not properly included.' );

				// Terminate
				exit;

			}

			// Validate signature
			$sso = new Discourse_SSO( $sso_secret );

			if ( ! ( $sso->validate( $payload, $sig ) ) ) {

				// Error message
				echo( 'Something went wrong. An administrator has been notified and will look into the issue.' );
				
				// Notify administrator
				wp_mail( get_option( 'admin_email' ), 'Invalid SSO Request', $current_user->user_login . ' ' . $current_user->user_email );

				// Terminate
				exit;

			}

			// Nonce    
			$nonce = $sso->getNonce( $payload );

			// Current user
			$current_user = wp_get_current_user();

			// Map information
			$params = array(
				'nonce' => $nonce,
				'name' => $current_user->display_name,
				'username' => $current_user->user_login,
				'email' => $current_user->user_email,
				'external_id' => $current_user->ID
			);

			// Build login string
			$q = $sso->buildLoginString( $params );

			// Remove fresh login
			delete_user_meta( $current_user->ID, 'fresh_login' );

			// Redirect back to Discourse
			wp_redirect( $discourse_url . '/session/sso_login?' . $q );
			exit;

		}

	}

	// Check for logout request
	if ( isset( $_GET['request'] ) && $_GET['request'] == 'logout' ) {
	
		wp_logout();

		wp_redirect( $discourse_url );
		exit;

	}

}

Note: In certain spots (like above) I use the wp_mail function to email myself in case of an error cropping up that I may not immediately notice otherwise. You may want to customize this reporting or remove it altogether.

Also, [see here][2] for more keys you can maps besides name, username, email, and external_id.

###C. Sync WordPress Login to Discourse

When a user logs into WordPress, this code will also log them into Discourse (without the user needing to initiate the Discourse login).

/**
 * Add user meta to signify the user has just logged in
 * 
 */
add_action( 'wp_login', 'ac_fresh_login', 10, 2 );

function ac_fresh_login( $user_login, $user ) {

	update_user_meta( $user->ID, 'fresh_login', 1 );

}

/**
 * One-time Discourse SSO iframe
 * 
 */
add_action( 'wp_footer', 'ac_discourse_sso_iframe' );
add_action( 'admin_footer', 'ac_discourse_sso_iframe' );

function ac_discourse_sso_iframe() {

	// Only show iframe on first page the user hits after logging in
	
	$fresh_login = get_user_meta( get_current_user_id(), 'fresh_login', true );

	if ( $fresh_login == 1 ) {

		echo '<iframe src="' . DISCOURSE_URL . '/session/sso" width="0" height="0" tabindex="-1" title="Discourse SSO" style="display:none" hidden>' . "\n";

	}

}

Note: @Grex315 has mentioned having better luck using an [embed instead of iframe][3]. The iframe has worked for me so I’ve kept using it.

Another note: The fresh_login key gets cleared during a successful SSO request before the redirect in the ac_parse_request function.

Also, your WordPress install may need to be located at the root http://example.com rather than http://www.example.com for this to work. I haven’t tested the www scenario. (I just have a hunch it may not work – if someone could confirm either way that would be awesome.)

###D. Sync Logouts

This logs the user out of Discourse when they log out of WordPress. The reverse scenario is covered above when setting the “logout redirect” option.

/**
 * Logout Sync
 * 
 */
add_action( 'clear_auth_cookie', 'ac_discourse_logout' );

function ac_discourse_logout() {

	// Variables
	$discourse_url = DISCOURSE_URL;
	$api_key = DISCOURSE_API_KEY;
	$api_username = DISCOURSE_API_USERNAME;

	global $current_user;

	//
	// Get Discourse ID
	//

	// URL
	$url = sprintf(
		'%s/users/%s/activity.json',
		$discourse_url,
		$current_user->user_login
	);

	// cURL
	$ch = curl_init();
	curl_setopt( $ch, CURLOPT_URL, $url );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
	$body = curl_exec( $ch );
	curl_close( $ch );

	// Interpret results
	$json = json_decode( $body, true );

	// Check for ID
	if ( ! isset( $json['user']['id'] ) ) {
		return;
	}

	//
	// Log out
	//

	// URL
	$url = sprintf(
		'%s/admin/users/%s/log_out?api_key=%s&api_username=%s',
		$discourse_url,
		$json['user']['id'],
		$api_key,
		$api_username
	);

	// Parameters
	$parameters = array(
		'username_or_email' => $current_user->user_login
	);

	// cURL
	$ch = curl_init();
	curl_setopt( $ch, CURLOPT_URL, $url );
	curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $parameters ) );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
	curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'POST' );
	$body = curl_exec( $ch );
	curl_close( $ch );

}

Note: I think there is a more direct endpoint to get a user’s Discourse ID which might have a slight performance benefit, but this works fine for me.

###E. Username Validation

WordPress doesn’t place many restrictions on usernames, but Discourse does.

/**
 * Back-end username validation
 * 
 */
add_filter( 'registration_errors', 'ac_registration_errors', 10, 3 );

function ac_registration_errors( $errors, $sanitized_user_login, $user_email ) {

	// Username must be 3-20 characters: letters, numbers, underscores only and can't begin with an underscore
	if ( ! preg_match('/^[A-Za-z0-9][A-Za-z0-9_]{2,19}$/', $sanitized_user_login ) ) {
		$errors->add( 'validation_error', '<strong>ERROR</strong>: Username is invalid.' );
	}

	return $errors;

}

This will catch any invalid usernames when you use the native WordPress registration. If you use a different method of registering users, this code may not do anything. Be sure to test your setup and verify that invalid usernames are caught.

As an aside, if you have any previously registered usernames that are invalid, they aren’t going to be able to log in to Discourse. You’ll need to change them to use legal characters and length. I use
[Username Changer][4].

Also, I check the Discourse option “sso overrides username”.

##III. Integrations

###A. Update Email

This will update the user’s email in Discourse when they update it from their WordPress profile page (http://example.com/wp-admin/profile.php).

/**
 * Listen for change in email and sync it with Discourse
 * 
 */
add_action( 'personal_options_update', 'ac_discourse_email_listener' );

function ac_discourse_email_listener( $user_id ) {

	$userdata = get_userdata( $user_id );

	if ( $userdata->user_email != $_POST['email'] ) {

		wp_schedule_single_event( time() + 15, 'ac_discourse_email_sync_event', array( $user_id ) ); // delay to ensure sync happens accurately

	}

}

add_action( 'ac_discourse_email_sync_event', 'ac_discourse_email_sync' );

function ac_discourse_email_sync( $user_id ) {

	// Define variables
	$discourse_url = DISCOURSE_URL;
	$api_key = DISCOURSE_API_KEY;
	$api_username = DISCOURSE_API_USERNAME;

	// Get userdata
	$userdata = get_userdata( $user_id );

	// URL
	$url = sprintf(
		'%s/users/%s/preferences/email?api_key=%s&api_username=%s',
		$discourse_url,
		$userdata->user_login,
		$api_key,
		$api_username
	);

	// Parameters
	$paramArray = array(
		'email' => $userdata->user_email
	);

	// cURL
	$ch = curl_init();
	curl_setopt( $ch, CURLOPT_URL, $url );
	curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $paramArray ) );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
	curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'PUT' );
	$body = curl_exec( $ch );
	curl_close( $ch );

}

Note: You can use other hooks to utilize the function depending on your setup. I personally use a Gravity Form on the front end rather than the default back end WordPress profile page for letting users update their email, so I hook the ac_discourse_email_sync function into there.

Also, you should add this to your custom CSS in Discourse:

.user-preferences .pref-email {
    display: none !important;
}

This isn’t a perfect solution, but basically we want to hide the option for a user to change their email on the Discourse side to keep it synchronized. Ideally we would let users change their email in Discourse too and sync it back over to WordPress, but I don’t know how to do that.

###B. Update Avatar

I rely on Discourse to provide the avatar for my WordPress users because WordPress isn’t packaged with an avatar uploader. The value is stored in user meta key discourse_avatar which I can then display in templates.

The way I run my site, there are only a handful of users that have their avatar displayed on the WordPress side, so I don’t have this very tightly integrated. Ideally, when a user’s avatar is updated in Discourse it would subsequently update in WordPress.

Instead, I hook this function into the SSO request, so each time the user logs in their avatar will be synced.

/**
 * Avatar Sync
 *
 */
add_action( 'ac_discourse_avatar_sync_event', 'ac_discourse_avatar_sync' );

function ac_discourse_avatar_sync( $user_id ) {

	// Define variables
	$discourse_url = DISCOURSE_URL;
	$userdata = get_userdata( $user_id );

	$url = sprintf(
		'%s/users/%s/activity.json',
		$discourse_url,
		$userdata->user_login
	);

	// cURL
	$ch = curl_init();
	curl_setopt( $ch, CURLOPT_URL, $url );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
	$body = curl_exec( $ch );
	curl_close( $ch );

	// Interpret results
	$json = json_decode( $body, true );

	// Update user meta
	if ( isset( $json['user']['avatar_template'] ) ) {

		update_user_meta( $user_id, 'discourse_avatar', $json['user']['avatar_template'] );

	}

}

In the SSO request, add this before the redirect back to Discourse:

// Sync avatars
if ( function_exists( 'ac_discourse_avatar_sync' ) ) {
	ac_discourse_avatar_sync( $current_user->ID );
}
```

###C. Group Sync

This is going to require customization based on how you manage your users in WordPress. Here is the basic code I use:

```php
/**
 * Discourse group sync
 * 
 */
add_action( 'ac_discourse_groups_sync_event', 'ac_discourse_groups_sync', 10, 2 ); // For delay requests

function ac_discourse_groups_sync( $user_id, $groups ) {

	//
	// Define variables
	//
	$discourse_url = DISCOURSE_URL;
	$api_key = DISCOURSE_API_KEY;
	$api_username = DISCOURSE_API_USERNAME;

	// Username
	$userdata = get_userdata( $user_id );
	$username = $userdata->user_login;

	// Cycle through groups array
	foreach ( $groups as $group_name ) {
		
		//
		// Get usernames of all group members
		//

		// URL
		$url = sprintf(
			'%s/groups/%s/members.json?api_key=%s&api_username=%s&limit=9999',
			$discourse_url,
			$group_name,
			$api_key,
			$api_username
		);

		// cURL
		$ch = curl_init();
		curl_setopt( $ch, CURLOPT_URL, $url );
		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
		$body = curl_exec( $ch );
		curl_close( $ch );

		// Intrepret results
		$json = json_decode( $body, true );

		// Error check for invalid group name
		if ( ! isset( $json['members'] ) ) {
			wp_mail( get_option( 'admin_email' ), 'Group Retrieval Error', $username . ' ' . $group_name );
			continue;
		}

		// Create arrays of usernames and ids
		$usernames = $ids = array();
		if ( count( $json['members'] ) > 0 ) {
			foreach ( $json['members'] as $key => $value ) {
				$usernames[] = $value['username'];
				$ids[ $value['username'] ] = $value['id'];
			}
		}

		//
		// Action determinations
		//

		switch( $group_name ) {

			// Premium
			case 'premium':
			if ( user_can( $user_id, 'premium_member' ) ) {
				$action = 'add';
			}
			else {
				$action = 'remove';
			}
			$group_number = 12; // Found by monitoring network when editing group
			break;

			// Add more cases here

		}

		//
		// Do action
		//

		// Add
		if ( $action == 'add' && ! in_array( $username, $usernames ) ) {

			// Parameters
			$parameters = array(
				'usernames' => $username
			);

			// Request
			$request = 'PUT';

		}
		
		// Remove
		elseif ( $action == 'remove' && in_array( $username, $usernames ) ) {

			// Parameters
			$parameters = array(
				'user_id' => $ids[ $username ]
			);

			// Request
			$request = 'DELETE';

		}
		
		// User is already in or not in group
		else {
			continue;
		}

		// URL
		$url = sprintf(
			'%s/admin/groups/%s/members.json?api_key=%s&api_username=%s',
			$discourse_url,
			$group_number,
			$api_key,
			$api_username
		);

		// cURL
		$ch = curl_init();
		curl_setopt( $ch, CURLOPT_URL, $url );
		curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $parameters ) );
		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
		curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $request );
		$body = curl_exec( $ch );
		curl_close( $ch );

		// Intrepret results
		$json = json_decode( $body, true );

		// Error check
		if ( ( ! isset( $json['success'] ) || $json['success'] != 'OK' ) && $membership_type != 'shared' ) {
			wp_mail( get_option( 'admin_email' ), 'Group Sync Error', $username . ' ' . $group_name );
		}

	}

}
```

You will be passing a user ID and an array of group names to the function. You'll need to do some kind of a check to see whether the user is in or not in the groups. 

This function can be hooked in with the SSO request, but the best way to keep groups synced is to make some kind of listener function that get fired every time the a user's meta updates. (I've probably lost you at this point ...)

Here's an abbreviated example:

```php
/**
 * Listen for membership_type changes
 * 
 */
add_action( 'added_user_meta', 'ac_membership_type_listener', 11, 4 );
add_action( 'updated_user_meta', 'ac_membership_type_listener', 11, 4 );

function ac_membership_type_listener( $meta_id, $user_id, $meta_key, $meta_value ) {

	if ( $meta_key == 'membership_type' ) {

		// Discourse group sync (short delay because they might be joining Premium on new user registration and not already have Discourse account)
		if ( function_exists( 'ac_discourse_groups_sync' ) ) {
			wp_schedule_single_event( time() + 15, 'ac_discourse_groups_sync_event', array( $user_id, array( 'premium' ) ) );
		}

	}

}
```

Depending on what management system you're using you might hook into actions other than `added_user_meta` and `updated_user_meta`.

---

The group management is the most confusing part to all of this. The rest isn't too bad.

Anyway, take each part that you need and add it to your plugin file, then upload the plugin file to your server and activate it. (Maybe add one piece at a time and verify it works.) Hopefully everything is somewhat clear. There is a lot that goes into a tight integration and everyone's setup is going to be slightly different.


  [1]: https://meta.discourse.org/t/single-sign-on-help-class-for-php/13104
  [2]: https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045
  [3]: https://meta.discourse.org/t/cross-origin-framing/26984/4?u=adamcapriola
  [4]: https://wordpress.org/plugins/username-changer/

Can you please help me with WP SSO? (Official SSO for Discourse plugin
Official Single-Sign-On for Discourse (sso)
WordPress SSO Page Template
Set SSO record fields via API
Need to refresh in order to get logged into discourse. Anyone else experiencing this?
Create user automatically in Discourse when they sign up in Wordpress?
Is Digital Ocean $10/month Package Good for Discourse?
Redirect back to Discourse Topic From Another Domain
WordPress SSO Page Template
Create user automatically in Discourse when they sign up in Wordpress?
SSO Client avatars
Cannot redirect to Discourse. Error: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource
Problems with WP Multisite SSO
(pjv) #2

Haven’t had a chance to dig in here yet, but THANKS @AdamCapriola for sharing this code. I’m sure it will save me countless hours as I work on integrating Discourse into a WP site I manage.

I’m intending to replace native WP comments with Discourse and I’m using the wp-discourse plugin for that. My main issue is how to deal with the very extensive (hundreds and even thousands) historical comments on some of the posts and pages in the existing blog. Right now I am thinking about creating a custom WP template that would present those old comments as a “historical archive” while simultaneously implementing the Discourse “continue the discussion in our forum” link for new stuff.

Just wondering if in your integration travels you have stumbled on any smart solutions to that issue.


(Adam Capriola) #3

I used Disqus before switching to Discourse for comments, so I keep the Disqus comment display for old articles (like you are thinking about doing.) I think there’s some value is preserving the way things looked and functioned in the past.

I haven’t tried to migrate old comments over but I’m sure it’s possible. You’d need to run a query that cycles through comments and connects to the Discourse API to publish them. Nesting would probably be lost though and I’m not sure how unregistered users would be handled. For me it wasn’t worth the hassle to mess around doing this.


(pjv) #4

Word. I originally thought I would do that too, and then I started looking through the various threaded lists of comments and commenters and imagined myself running the script over and over, tweaking it for this and that edge case for a month. Errrr… no.


(Adam Capriola) #5

Fixed a typo and applied a security fix (as per the announcement today). When processing SSO requests, change:

$login = wp_login_url( $redirect );

to

$login = wp_login_url( esc_url_raw( $redirect ) );

(pjv) #6

I have now hacked the wp-discourse plugin to allow showing existing WP comments as a “historical archive” beneath the new Discourse comments. For details, see:


(Andrew ) #7

How much of this integration is compatible with the wp-discourse plugin? Obviously there are SSO functions in both. Can these play together nicely? If not, which is the current best practice: A) disabling SSO in wp-discourse or B) skipping the SSO scripts @AdamCapriola has so awesomely provided?

I couldn’t manage to get the scripts to work with or without wp-discourse activated, and I’m trying to eliminate as many variables as possible.


(Adam Capriola) #8

You may need to change class Discourse_SSO to something more arbitrary like My_Discourse_SSO to prevent conflicts with wp-discourse and you will want to disable the SSO functionality of wp-discourse, but I run this code alongside the plugin.


(pjv) #9

I have been unable to get technique “C” (sync WP login to Discourse) working on my site.

I tried both iframe and embed and I get a 521 error from the Discourse server and a CORS error as well, even though I enabled CORS in my app.yaml and then listed both the http and https URLs of the WP site in the Discourse admin CORS origins field.

Here’s what the console looks like immediately after logging in:


(Adam Capriola) #10

The code is missing the closing </iframe>. Maybe including it will help? I haven’t encountered any CORS issues myself so I’m not sure about a workaround. I’ve never had to enable any CORS options on the sites I’ve worked on.


(Kane York) #11

521 is a CloudFlare “website is down” error, so that’s (probably) not your fault; also you should be using <embed> to avoid the second error.


(pjv) #12

Thanks @riking. Yes, I discovered that was a CloudFlare error code and that led me to discovering that the underlying problem was actually an SSL misconfiguration in my Discourse container (was my fault). I am using <embed>. Once I cleared up the SSL issue the sync login code started working.


(Adam Capriola) #13

Here’s a helpful bit of code to grab a user’s Discourse username. Ideally their WordPress and Discourse usernames will be identical, but depending on how or when your integration is implemented that may not always be the case.

/**
 * Get Discourse username
 * 
 */
function ac_get_discourse_username( $user_id ) {

	// First try to get username from meta value
	$discourse_username = get_user_meta( $user_id, 'discourse_username', true );

	if ( ! empty( $discourse_username ) ) {

		return $discourse_username;

	}

	// Then try to get username by id	
	if ( function_exists( 'ac_get_discourse_username_by_id' ) ) {
		
		$username_by_id = ac_get_discourse_username_by_id( $user_id );

	}

	if ( ! empty( $username_by_id ) ) {
		
		// Save to user meta (because the user definitely has a Discourse account)
		update_user_meta( $user_id, 'discourse_username', $username_by_id );

		return $username_by_id;

	}

	// At this point we know the user has not connected via SSO yet, so let's get their userdata to work it
	$userdata = get_userdata( $user_id );

	// We could simply return the user's WordPress username
	return $userdata->user_login;

	// Or optionally we could filter their WordPress username to match the Discourse username rules
	// https://meta.discourse.org/t/why-are-usernames-so-restrictive/1315/43?u=adamcapriola

	// For example, we could strip out the invalid characters and replace them with underscores
	return preg_replace( '/[^A-Za-z0-9_\.\-]+/', '_', $userdata->user_login );

	// Or trim the username down to at most 20 characters
	return substr( $userdata->user_login, 0, 20 );

}

/**
 * Get Discourse username by ID
 * 
 */
function ac_get_discourse_username_by_id( $user_id ) {

	// Variables
	$discourse_url = 'http://discourse.example.com';
	$api_key = 'abc123';
	$api_username = 'system';

	// URL
	$url = sprintf(
		'%s/users/by-external/%s.json?api_key=%s&api_username=%s',
		untrailingslashit( $discourse_url ),
		$user_id,
		$api_key,
		$api_username
	);

	// cURL
	$ch = curl_init();
	curl_setopt( $ch, CURLOPT_URL, $url );
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
	$body = curl_exec( $ch );
	curl_close( $ch );

	// Decode JSON
	$json = json_decode( $body, true );

	// Check for username
	if ( isset( $json['user']['username'] ) ) {
	
		return $json['user']['username'];
	
	}

	return false;

}

#14

I just wanted to say thank you very much for sharing your work, this has saved me an exorbitant amount of time and stress :smiley:

EDIT: Hmm I may have spoken too soon. I keep getting the ‘Invalid SSO Request’ emails. If I log in via Discourse after being logged in already on WP (which I thought this would eradicate), I’m directed to your built in message: Something went wrong. An administrator has been notified and will look into the issue.

I am running the WP site off MAMP locally and the Discourse site is live on a DO Droplet, could that be the issue?

EDIT 2: NVM GOT IT FIXED :smiley: