Driver contract#

What a driver must declare and satisfy.

To write one, follow Write a driver. This page is the checklist.

The interface#

Attributes#

Attribute

Purpose

driver_id

Unique id, such as github. Matches the utility name.

title

Shown in the control panel.

settings_schema

The schema the configuration form is generated from.

default_scope

Tuple of scopes requested when the provider names none.

subject_keys

Claim keys tried, in order, to find the subject.

static_metadata

The endpoint set, for a provider that publishes fixed URLs. Empty means discovery.

issuer

The issuer to discover from, when the driver fixes it. Empty means the operator types it.

default_propertymap

Claim path → Profile field, seeded into a new provider.

default_group_claim

The claim groups arrive in, or empty for a provider with none.

default_groupmap

Provider group → local group. Empty for every shipped driver.

default_trust_email_verification

Whether this provider's verification counts by default.

supports_manual_link

Whether an identity may be linked from the account page.

Base class defaults (core/drivers/base.py): settings_schema is IOAuth2Settings, default_scope is (), subject_keys is ('sub',), static_metadata is {}, issuer is '', default_group_claim is '', default_trust_email_verification is False.

Methods#

Method

Contract

normalize_claims(payload) -> Claims

Turn the provider's payload into this package's claim names.

subject(payload) -> str

The stable identifier. Raises when no key in subject_keys is present.

BaseDriver also provides enrichment_endpoint, merge_enrichment, reported_addresses and _email_verified for drivers that need a second call to the provider.

The rules#

Each is a rule a driver must satisfy, and what catches a violation.

#

Rule

Enforced by

1

A field marked secret stays secret: masked on the way out of every API surface, omitted from GenericSetup export. Do not invent your own credential storage.

the serializers and the export tests

2

Every field has an order, and no two share one.

the driver contract test—a tie fails it rather than falling back to the alphabet

3

default_scope is a tuple, not a space-delimited string.

the contract test

4

default_propertymap names only fields a login writes on the shipped user type, written against normalized claim names.

the contract test, against the same fields the login filters on—and the registry, which refuses any other target

5

default_group_claim is set to the claim groups arrive in, or left empty when the provider has none.

the contract test

6

default_groupmap is empty unless the driver genuinely knows the far end's groups.

convention; every shipped driver is empty

7

email_verified is normalized to a boolean, and only True counts.

the contract test and _email_verified

8

default_trust_email_verification stays False unless the provider refuses to call an address verified until the account has answered mail at it.

review; only google and github set it

9

A driver names one metadata source, not both: endpoints it publishes, or an issuer to discover from.

the contract test

10

Every URL a driver declares—each static_metadata endpoint, and issuer—is absolute and https.

the contract test

Why order is a number#

A configuration schema travels as a JSON object, and plone.restapi serializes those with sorted keys, so the order fields are declared in is gone by the time the control panel builds a form. The number is what survives.

The inherited fields are spaced by ten, so a new field can be slotted between two of them.

Why only True counts#

Several providers send the string "true", and several send 1. Everything that reads the flag refuses anything that is not literally True, because a forged unverified address that reads as truthy is an account takeover.

A provider that really does send a string is handled by the per-provider accept_string_booleans switch, not by a driver being lenient.

See About email verification.

Why the endpoints live on the driver#

A driver is everything this package knows about a kind of provider, and a provider's endpoints are the most basic thing there is to know. They were a dict keyed by driver id in core/flows/metadata.py until 1.0.0a5, which had two costs: GitHubDriver.enrichment_endpoint read a key another module defined, and a driver shipped by anybody else could not add a row to it.

metadata.py keeps every resolving concern—the discovery fetch, the per-issuer cache, its TTL, forget(). Which of the two sources applies is the driver's answer.

An empty static_metadata means discovery, which is the answer for every OpenID Connect provider: the endpoints come from the issuer's document and change when the provider says so. github fills it in because GitHub is plain OAuth2 and publishes no discovery document at all.

An empty issuer means the operator supplies one, and that is decided by the issuer field in settings_schema rather than by a list of driver ids—so a subclass of GenericOIDCDriver this package has never heard of is discovered exactly as its parent is. A driver declaring neither is refused rather than sent to discovery with an empty URL: email, whose magic link never leaves the site, is that case.

Why an empty group claim is not neutral#

Leaving default_group_claim empty switches the feature off for that driver: no group_claim field appears in the configuration form, and nobody is asked to map the groups of a provider that has none.

A map stored against such a provider grants nothing rather than guessing at a claim name.

Operators can override the claim with a dotted path, so a provider nesting groups under realm_access.roles needs no driver of its own.

Registration#

A driver is a named utility providing IDriver, registered under its driver_id. The control panel lists whatever is registered, and the form comes from settings_schema, so a third-party driver needs no frontend change.