Skip to content

Manage IdP bindings

An IdP binding tells a Domain which upstream OIDC provider to drive for sign-in. plexctl domain-idp wraps /v1/admin/idp.

Prerequisites

  • An authenticated session — see Log in with plexctl.
  • The Domain UUID and the OIDC client credentials from your provider.
  • The client secret saved to a file (inline literals are rejected).

Steps

Create a binding

The client secret is passed by reference (@<path>), so stage it in a private scratch file first. Set a tight umask so the file is created 0600, and use mktemp rather than a predictable /tmp/idp-secret path:

shell
umask 077
secret_file="$(mktemp)"
printf '%s' "$OIDC_CLIENT_SECRET" > "$secret_file"

plexctl domain-idp create \
  --domain-id <domain-uuid> \
  --client-id plexsphere \
  --client-secret "@${secret_file}" \
  --discovery-url https://idp.example.com/.well-known/openid-configuration \
  --issuer https://idp.example.com \
  --jit-policy allow \
  --claim-mapping email=preferred_email
# ID  DOMAIN  CLIENT_ID   ISSUER                    JIT_POLICY  STATUS  CLIENT_SECRET_REF
# …   …       plexsphere  https://idp.example.com   allow       active  ***

--client-secret must be @<path>; the file is read once and trimmed. Secret-shaped response fields are masked unless you pass --reveal-secrets (audit-logged).

Remove the secret file. The scratch file holds the plaintext client secret. Even at 0600 it is readable by your own other processes and survives until you delete it — it does not vanish when the shell exits. Remove it as soon as the binding is created:

shell
rm -f "$secret_file"

List bindings

shell
plexctl domain-idp list --domain-id <domain-uuid>

Get or update a binding

shell
plexctl domain-idp get    --id <binding-uuid>
plexctl domain-idp update --id <binding-uuid> --jit-policy deny

update is a partial PATCH: only the flags you set (--jit-policy, --discovery-url, --required-acr, --required-amr, --claim-mapping, --alias, --primary) are forwarded. Status is changed through enable/disable, not update.

Name a binding and pick a primary

Give a binding a human-friendly alias so it can be addressed by name, and mark one binding per Domain as the primary so a Domain-only login resolves to it even when the Domain has several bindings:

shell
# At create time.
plexctl domain-idp create \
  --domain-id <domain-uuid> \
  --client-id plexsphere \
  --client-secret "@${secret_file}" \
  --discovery-url https://idp.example.com/.well-known/openid-configuration \
  --issuer https://idp.example.com \
  --jit-policy allow \
  --alias github \
  --primary

# Or promote an existing binding later.
plexctl domain-idp update --id <binding-uuid> --primary

The alias is normalised to lowercase kebab-case, so --alias GitHub stores github. It is unique per Domain among active bindings; a collision returns 409 alias-conflict. Only one active binding per Domain may be primary — promoting a second while one is already primary returns 409 primary-conflict, so demote the current primary first with --primary=false. Clear an alias with --alias "".

With an alias and a primary set, you can log in by Domain alone or pin the binding by name:

shell
plexctl login --domain-id <domain-uuid>                   # resolves the primary
plexctl login --domain-id <domain-uuid> --idp-binding github  # resolves by alias

Share one IdP across Domains

When the same upstream IdP serves many tenants, register it once as a platform-scoped (shared) binding instead of duplicating a binding per Domain. A platform binding is owned by no Domain and is usable by any Domain; the just-in-time provisioned user joins the Domain selected at login, not the binding's.

shell
# Register the shared binding once (platform-scoped: no --domain-id, no --primary).
plexctl platform-idp create \
  --client-id plexsphere-shared \
  --client-secret "@${secret_file}" \
  --discovery-url https://github-idp.example.com/.well-known/openid-configuration \
  --issuer https://github-idp.example.com \
  --jit-policy allow \
  --alias github

# List the platform bindings (domain_id renders as `platform`).
plexctl platform-idp list

# Each Domain that owns no binding resolves the shared one on a
# Domain-only login; the user lands in the login Domain.
plexctl login --domain-id <domain-a-uuid>
plexctl login --domain-id <domain-b-uuid>

Platform create and list gate on the platform manage / read permissions, distinct from the per-Domain domain#manage / domain#read gates. A Domain's own primary or sole binding still wins over a shared one — the shared binding fills in only when the Domain owns nothing applicable. The per-Domain plexctl domain-idp list shows a Domain's effective set (its own bindings plus the platform ones).

A shared binding is administered through the same by-id commands a Domain binding is (domain-idp enable / disable / delete and the by-id read), and those follow the binding's own scope: acting on a shared binding needs the platform permission, not one on the Domain you happened to list it from. So a Domain admin who sees a shared binding in domain-idp list gets 403 when addressing it by id — that binding is the platform's to manage. Use plexctl platform-idp with a platform grant instead.

Enable, disable, or delete

shell
plexctl domain-idp enable  --id <binding-uuid>
plexctl domain-idp disable --id <binding-uuid>
plexctl domain-idp delete  --id <binding-uuid>

Verification

shell
plexctl domain-idp list --domain-id <domain-uuid> --output json \
  | jq '.[] | {id, issuer, status}'
# {
#   "id": "…",
#   "issuer": "https://idp.example.com",
#   "status": "active"
# }

The new binding appears with status: "active".

See also