
Sign in to Plone with anything, or sign in to anything with Plone
pas.plugins.identity lets Plone sign users in with GitHub, Google, OIDC or magic links, or act as the identity provider.
The Plone Foundation wants to know its community. Who contributes, who is a member, who shows up at sprints, who answers questions on the forum. Today that knowledge is scattered across plone.org, community.plone.org, plone.org.br, GitHub and a handful of internal tools, each with its own login and its own idea of who you are.
The plan is easy to say: one identity, at id.plone.org, and every community site signs in against it.
During the Beethoven Sprint 2026, Fred van Dijk and I talked about this challenge, and about setting up a Keycloak instance for the Plone community. We both use Keycloak in our day jobs, so it seemed the natural choice. Months later, each of us had spent long sessions with our "LLM colleagues" (ChatGPT for Fred, Claude and Gemini for me), and we had separately reached the same conclusion. The customization needed to build and maintain the id.plone.org plan on Keycloak would be extensive. It made more sense to build the IdP on top of Plone, which already had every other moving piece we needed: workflows, permissions, groups and extensible user profiles.
That decision has a consequence. Plone has to do both halves of the job: accept people coming in from GitHub, Google or an email address, and be the place other sites send people to sign in. That is what pas.plugins.identity is for.
One person, many logins
Here is the problem in its smallest form. The same human has a GitHub account, a Google account and, maybe, an account on their employer's Keycloak. They use whichever is at hand. Plone should see one user.
Plone already has two good options for external authentication, and neither quite does this:
- pas.plugins.authomatic handles many providers, but it is built on authomatic, which is no longer maintained upstream.
- pas.plugins.oidc handles one OpenID Connect provider, and handles it well.
What neither does is map several external identities onto one canonical Plone user id. For a community IdP, that mapping is the whole point: "who is this person?" has to have exactly one answer.
pas.plugins.identity
So I wrote pas.plugins.identity, with @plone-collective/volto-identity for the frontend. It is built on authlib and joserfc, so the protocol work is done by libraries that do nothing else.
The design is arranged around linking:
- An identity is a (provider, subject) pair, attached to one Plone user. A user can have as many as they like. An identity already linked to someone is never silently re-attached to someone else: a collision is a hard error, not a merge.
- Providers are configured through the web. Site Setup gets an Identity providers control panel, with a form generated from each driver's own schema. Client secrets are write-only through every API surface, GenericSetup export included.
- There is an audit log. Successes and refusals, per user or site-wide, bounded and purged on write. IP address and user agent are off by default.
- There is a documented event contract. The audit log, the profile machinery and your own integrations all consume the same events. Nothing reaches into anything else.
- Profiles and groups are content, but user properties, enumeration and group membership are served from a dedicated catalog. No content object is woken up to answer "what groups is this user in?", and the test suite asserts that rather than claiming it.
Sign in to Plone with anything
Out of the box there are drivers for generic OpenID Connect, Keycloak, Google, GitHub, another Plone site and an emailed magic link.
Each driver comes with sensible defaults, and every default can be overridden per provider. Whether to trust a provider's email_verified claim, for example, is a fact about your deployment, not about the driver, so it is your call. The same goes for mapping a provider's group claim onto local groups, or letting an account link itself by email.
The magic link deserves a word, because it is the one people are most nervous about. Links are single-use signed tokens that last at most fifteen minutes. Requests are rate limited per address and per IP. The answer is the same whether or not the address belongs to an account, so the form cannot be used to find out who is registered.
If a provider you need is missing, writing a driver is a documented how-to, not an archaeology project.
Sign in to anything with Plone
Now the other half. Install the [server] extra, and the site becomes an OAuth 2.1 and OpenID Connect provider in its own right.
This is what id.plone.org runs. People sign in there once, with GitHub, Google or a link sent by email, and the IdP keeps their profile: badges from their GitHub teams and, for Foundation members, their membership. That is the "know its community" part, in one place.
Its first clients are already connected. plone.org.br signs in against it with the plone-identity driver, so that is Plone signing in to Plone. The next plone.org does the same. And because the server speaks plain OpenID Connect, the clients don't have to be Plone at all: Portainer, for the Plone cluster, and Matomo, for the sites' analytics, sign in through id.plone.org too. community.plone.org is next on the list.
If you want to see it without deploying anything, the repository has a demo stack with two Plone sites, one signing in to the other:
make demo-stack-startThe federation tutorial tells you what to click once it is up.
One thing I learned the hard way, which the docs now spell out: the issuer is a string, and it is compared as one. A relying party fetches the discovery document and checks that its issuer matches the URL byte for byte. http://id.localhost and http://id.localhost/ are different issuers. So is the same host on another port. That is why the issuer is a registry setting, and every URL in the discovery document is built from it, rather than from portal_url. Deriving it would work in development and fail behind a load balancer, which is the worst possible place to find out.
Already using authomatic or oidc?
Migrations from both have shipped. They are dry-run by default, idempotent, and report what they would do before you let them do it. Where the old plugin never recorded something the new one needs, the migration refuses rather than guesses. The migration guides spell out what each one can and cannot recover.
And to be fair to pas.plugins.oidc: if one OpenID Connect provider is all your site needs, it is the smaller and more mature dependency. There is no reason to move.
The fine print
It is 1.0.0a14. Alpha means alpha: the API can still move.
The backend needs Plone 6.2 and Python 3.12 or later; the frontend needs Volto 19. The backend works on its own. It authenticates users, links identities and serves all of it over the REST API without any frontend. The frontend needs the backend, because everything it renders comes from there.
The core installs without any extras, and that is a tested configuration, not a promise: an import-linter contract in CI asserts that core never imports the server layer. If you only want people to sign in to your site, you don't carry an authorization server around.
Try it
Backend, on Plone 6.2 and Python 3.12 or later:
uv add pas.plugins.identityOr, to make the site an identity provider as well:
uv add "pas.plugins.identity[server]"Then install pas.plugins.identity from the add-ons control panel, and configure a provider in Site Setup > Identity providers.
Frontend, on Volto 19: add it to your project's package.json, in both places:
"addons": [
"@plone-collective/volto-identity"
],
"dependencies": {
"@plone-collective/volto-identity": "*"
}The documentation covers installing, configuring a provider, registering an OAuth client, reading the audit log and writing your own driver. The code lives at collective/pas-plugins-identity.
If you try it, tell me what breaks. And tell me which driver you need next.