WP Manifestindependent plugin directory
manifest / users / community-login-idp

Community Login IdP

by George Stephanis · github.com/georgestephanis/community-login-idp · website

★ 0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/georgestephanis/community-login-idp/archive/refs/heads/trunk.zip

A WordPress plugin that lets people register and log in with Slack or Discord as the identity provider. Useful when your site sits alongside a community chat and you want membership there to be the source of truth for accounts here.

Both providers use the same OAuth 2.0 authorization-code flow, so the plugin is one generic flow plus a small table of endpoints.

Requirements

  • WordPress 6.0+
  • PHP 7.4+

Install

Download the zip from the latest release and install it through Plugins → Add New → Upload Plugin. Release zips have the stylesheet already compiled.

To install from a clone instead, build the assets first — build/ is not committed, and without it the sign-in buttons render unstyled:

composer install   # dev tooling only (PHPCS/WPCS/PHPUnit) — not needed at runtime
npm install
npm run build      # compiles src/ → build/, required for the button styles

Then activate the plugin and go to Settings → Community Login.

Try it without installing anything

The badge at the top boots a throwaway WordPress in your browser, installs this plugin straight from trunk, and drops you on the settings screen. It runs blueprint.json — edit that file and Playground picks up the change on the next boot.

Two things to know: the sandbox is gone when you close the tab, and the sign-in buttons render unstyled there because build/ is not committed. It is enough to see the settings screen and the login form, not to complete a real OAuth round trip — the redirect URL changes on every boot, so you would be re-registering the provider app each time.

Setting up the provider apps

Each provider needs its own app so you have a client ID and secret. The settings screen prints the exact Redirect URL to paste into each app — it looks like:

https://example.com/wp-login.php?action=community-login-idp&provider=slack

Slack

  1. Create an app at https://api.slack.com/apps.
  2. Under OAuth & Permissions, add the redirect URL, and add openid, email and profile as User Token Scopes. (Slack no longer shows a separate OpenID Connect screen on new apps; those three user scopes are what makes it an OIDC app. If the scope picker will not offer them, paste them into Settings → App Manifest instead.)
  3. No bot scopes and no bot token are needed.
  4. Copy the Client ID and Client Secret into the settings screen.
  5. Optionally set the Workspace ID (T…) to restrict sign-in to one workspace. It both pre-selects the workspace and is verified server-side after login.

Finding your Workspace ID. Open Slack in a browser and read it out of the URL — app.slack.com/client/T0123456789/…. The T… part is the workspace ID. The same value appears in the address bar under Settings & administration → Workspace settings.

Discord

  1. Create an application at https://discord.com/developers/applications.
  2. Under OAuth2, add the redirect URL.
  3. Scopes are requested by the plugin (identify email, plus guilds when a Server ID is set).
  4. Copy the Client ID and Client Secret into the settings screen.
  5. Set the Server ID to restrict sign-in to members of one Discord server. Turn on Developer Mode (User Settings → Advanced), then right-click the server and choose Copy Server ID. Leave it empty and any Discord account in the world can register.

App tokens

Both providers have an optional App token field. Nothing needs it to sign in — signing in uses the client ID and secret and the user's own token, and that token is discarded as soon as the profile is read.

It exists for lookups the sign-in flow cannot make: things that need permissions the person signing in does not have, or that have to happen when they are not there. Whether a Slack account has been deactivated, or what a Discord role ID is actually called, for example.

  • Slack — a bot token (xoxb-…) from the app's OAuth & Permissions screen.
  • Discord — a bot token from the app's Bot screen.

The token is checked against the provider when you save, and the result is shown under the field. A wrong or expired token should be a red line on a settings screen, not a login that mysteriously fails three weeks later.

How accounts are matched

On each sign-in, in order:

  1. Existing link — a WordPress user with community_login_idp_<provider>_id user meta matching the remote account ID. Logs them in.
  2. Linking — if the flow was started by someone already logged in (the Link account button on their profile), the remote account is attached to that user.
  3. Verified email match — only if Automatically link to an existing account with the same verified email address is enabled. Off by default: turning it on means anyone who controls that email address at the provider can sign in as the matching WordPress user, including an administrator. Enable it only if you trust the provider's email verification.
  4. Registration — if registration is enabled in the plugin settings, a new user is created with the site's default role, or with a role from the Role mapping field below. A verified email address is required.

Otherwise the attempt is rejected with a message on the login form.

Role mapping

Each provider has an optional Role mapping field: one rule per line, written as remote role = WordPress role. Blank lines and lines starting with # are ignored. The first line that matches wins, so put the most privileged rule at the top; anyone matching nothing gets the site's default role.

owner = editor
admin = editor
member = contributor

Slack has no roles, so the left-hand side is one of owner, admin, guest, single_channel_guest, bot or member. These overlap — an owner is also an admin, and everyone is a member — which is what makes the ordering useful. Reading them needs the App token; without one nobody matches and everyone gets the default role.

Discord uses role IDs on the left. Turn on Developer Mode, then right-click a role in Server Settings → Roles and choose Copy Role ID. This needs the Server ID to be set, and adds the guilds.members.read permission to the consent screen people see.

What this deliberately does not do

The mapping applies only when an account is first created. Roles are never touched on later sign-ins. That means:

  • Someone who loses a role in Slack or Discord keeps the WordPress role they were given. Change it in WordPress.
  • A role you assign by hand in WordPress is never overwritten by the provider.
  • The provider cannot demote anyone, so a compromised Slack or Discord account cannot take away access here.

It also means the mapping is one-way and drifts over time, which is the trade. Syncing on every login would keep the two in step, but it would also hand your workspace's administrators the ability to change WordPress roles on demand — see #11.

Mapping anything to Administrator gives whoever administers your workspace or server full control of this site. There is nothing stopping you, but consider mapping to Editor and promoting by hand.

Linking accounts from the profile screen

Users → Profile has a Linked Accounts section. Linking there runs the same OAuth flow, but because the person is already logged in it attaches the provider account to their account and skips every matching rule above.

That means the email addresses do not have to match. If your Slack email differs from your WordPress email, link from the profile screen once and every later sign-in goes through the stored account ID, not the email.

To swap to a different remote account: unlink, then link again and authorize as the other account.

Two guards apply:

  • An account already linked to a different WordPress user cannot be linked twice.
  • If password sign-in is disabled, the last remaining link cannot be removed — that would lock the account out with no way back in. Link another provider first.

Administrators see the same section on other users' profiles, but unlink only. Linking authenticates whoever is clicking, so it is necessarily self-service.

Making the provider the only way in

Enable Disable username and password sign-in in the settings to deprecate local passwords. When it is on:

  • Interactive sign-in must go through a configured provider. The password form and the register / lost-password links are hidden.
  • Application passwords keep working for the REST API and XML-RPC, so scripts and integrations are unaffected. The plugin uses core's own application_password_is_api_request definition to decide what counts as an API request.
  • Password resets are disabled, since the resulting password could not be used to log in.

Two safety catches stop this from locking you out:

  1. It has no effect unless at least one provider is enabled and has credentials.

  2. Adding this to wp-config.php restores password sign-in unconditionally:

    define( 'COMMUNITY_LOGIN_IDP_ALLOW_PASSWORDS', true );

Link your own account to a provider before turning this on.

Note that this controls authentication, not access. It does not make the site private on its own — see Requiring login to view the site below for that.

Requiring login to view the site

Off by default. Settings → Community Login → Site visibility → Require people to be logged in to view the site sends logged-out visitors to the login form.

This is the access half of the pair; disabling password sign-in above is the authentication half. A private community site usually wants both.

The redirect hangs off template_redirect, which is the right hook mostly for what it does not run for — so these need no exemption and cannot be broken by one:

  • wp-login.php, including the provider callback URL. The way in stays reachable.
  • wp-admin, admin-ajax.php, wp-cron.php and XML-RPC.

Two things do run through that hook and are exempted explicitly: robots.txt and the favicon. Redirecting those breaks crawlers and browsers and reveals nothing.

Feeds and the REST API are separate checkboxes, both off. Closing them breaks things quietly — feeds stop updating in readers, and a closed REST API stops oEmbed previews of your posts on other sites along with any theme or plugin that reads the API without a session. Application passwords keep working either way, so authenticated integrations are unaffected.

The path allowlist takes one path per line, matched as a prefix, so /shop covers everything under it. Blank lines and lines starting with # are ignored, so you can annotate it.

If it collides with something, exempt that request rather than switching the feature off:

add_filter( 'community_login_idp_require_login', function ( $require ) {
    return is_page( 'press-kit' ) ? false : $require;
} );

Session length

Settings → Community Login → Session length controls how long someone stays signed in after using a provider button. It defaults to 48 hours, the same as a WordPress login without Remember Me.

This matters more than it looks. Workspace and server membership is only checked during sign-in, so the session length is also how long someone keeps access to the site after leaving your community. A fourteen-day session means a fortnight of access for someone you removed on day one. Shorten it if that matters; lengthen it if you would rather not make people sign in often.

Blocking someone

When an account from the provider has to go, Users → hover a row → Block Slack sign-in (or Discord). That remote account can no longer authenticate, whatever else would otherwise let it in — it is checked before account lookup, linking or creation, so a blocked account cannot register a fresh WordPress user either.

Blocking does not touch the WordPress user. The account stays, its content stays, and existing sessions are not terminated — delete or edit the user separately if that is what you want. The reasoning is that "stop them getting back in" and "erase them" are different decisions and should not be one button.

Blocked accounts are listed at the bottom of Settings → Community Login, with an Unblock button. The list caches the display name and email as they were at the time of blocking, so it reads as people rather than as a wall of U… strings.

The login form tells a blocked person only that the account cannot be used to sign in. That is deliberate: telling someone exactly why they are blocked tells them what to work around.

Avatars

Off by default. Turn on Use the profile picture from the provider and avatars come from Slack's or Discord's CDN instead of Gravatar. The URL is refreshed on every sign-in, so changing your picture in the chat changes it here.

Anyone without a linked provider, or without a picture set there, keeps the site's normal avatar — this only ever adds a source, it never removes one.

It is off by default because hotlinking the provider's CDN tells Slack or Discord the IP address of every visitor who loads a page with an avatar on it, including visitors who have nothing to do with your community. That is the same objection people raise about Gravatar, so it is not a new category of problem, but it should be your decision rather than a default. Sideloading the images into the media library would avoid it at the cost of storage, staleness, and cleanup on uninstall; not worth it yet.

Troubleshooting

Failures come back to the login form with a message. Turn on WP_DEBUG and the provider's raw error body is written to the PHP error log prefixed [community-login-idp] — usually the fastest route to the real cause, since the messages below are deliberately non-specific to the person seeing them.

Message What it means
We could not complete the sign-in with that service. The token exchange failed. Almost always a wrong Client ID or Client Secret, or a Redirect URL that does not match the one registered with the provider character for character — including http vs https and any www.
That service did not tell us who you are. The token worked, the profile request did not return a usable profile. Check the app's scopes: Slack needs openid email profile, Discord identify email.
That account is not a member of this community. The account is not in the configured Slack workspace or Discord server. Check the Workspace ID / Server ID, and that they really are a member.
We could not check your membership of this community. Discord only. The membership lookup itself failed — a network problem or a Discord outage, not a rejection. Retrying usually works.
That login attempt expired. More than ten minutes between starting and finishing, or the state transient was dropped. If it happens constantly, suspect an object cache dropping transients or a page cache serving the login page.
An account already exists with that email address. A WordPress user already has that email and automatic email linking is off. Log in with the password and link from the profile screen — see How accounts are matched.
That account is already linked to a different user. One remote account maps to one WordPress user. Unlink it from the other user first.
We could not get a verified email address from that account. Registration needs one. Slack only returns an email for confirmed workspace members; Discord only when the address is verified.
New registrations are closed. Create a new WordPress account… is off in the settings, and this person has no existing account to match.
That account cannot be used to sign in to this site. The remote account is on the blocklist. See Settings → Community Login.
That sign-in method is not available. The provider is disabled, or missing a client ID or secret.

The buttons render unstyled if build/ is missing — install from a release zip, or run npm install && npm run build.

Security notes

  • CSRF is handled by the OAuth state parameter, stored in a 10-minute transient and verified before any response data is read. The callback is an external redirect, so a WordPress nonce is not possible there.
  • Client secrets are stored in the community_login_idp option in plaintext, like every other WordPress OAuth plugin. If that is not acceptable, filter pre_option_community_login_idp to inject values from environment variables instead.
  • Disabling password sign-in (above) covers interactive logins only. Application passwords are intentionally left working; revoke those per-user under Users → Profile if you need to cut off API access too.

Releasing

Tag and push:

git tag v0.2.0 && git push --tags

.github/workflows/release.yml builds the assets, assembles a zip of everything in .distignore's complement, and attaches it to a generated GitHub release. That zip is also what WordPress.org would want.

Development

npm run start       # watch build
npm run lint:js
npm run lint:css
npm run format      # prettier via wp-scripts

composer run lint   # phpcs (WPCS + PHPCompatibility)
composer run format # phpcbf

What is deliberately left out

  • Display-name and email syncing on every login. Profile fields are set once at registration and then left alone. Avatars are the exception — see below.
  • Requiring login to view the site. Authentication only; see the note above.
  • SAML, generic OIDC, or any other provider. Adding one is a new entry in providers() plus a branch in normalize_identity().

License

GPL-2.0-or-later