The SSO app (nodejs/models/user_ldap.js addPosixAccount) tags every new user
with objectClasses [inetOrgPerson, sudoRole, ldapPublicKey, posixAccount, top,
theta42Person] and writes sudoHost/sudoCommand/sudoUser + sshPublicKey. The
all-in-one image's slapd.conf only included core/cosine/inetorgperson/nis +
theta42, so creating a user failed: sudoRole and ldapPublicKey were unknown
objectClasses (LDAP objectClassViolation 65) and sudoHost/sudoCommand/sudoUser
and sshPublicKey were unknown attributes.
Ship the two missing schemas and include them in slapd.conf:
- ops/schema/sudo.schema (sudoRole + sudo* attributes)
- ops/schema/openssh-lpk.schema (sshPublicKey + ldapPublicKey)
sudoRole is AUXILIARY here, not STRUCTURAL as in upstream sudo. The app
attaches sudoRole directly onto the user entry, which is already inetOrgPerson
(STRUCTURAL); two unrelated structural classes violate RFC 4512 and OpenLDAP
rejects with 65. AUXILIARY lets it coexist with inetOrgPerson — the app's
per-user-sudoers model. sudo's LDAP backend still finds entries via
(objectClass=sudoRole) regardless. ldapPublicKey is AUXILIARY as in upstream
openssh-lpk.
Also fix the build error from the previous theta42 schema PR: .dockerignore
excluded all of ops/, so 'COPY ops/schema/theta42.schema' failed at build
time ('not found' — file is git-tracked but stripped from the context). Re-
include ops/schema/*.schema with !exceptions, matching the existing
README.md/tos.md pattern.
Co-Authored-By: Claude <noreply@anthropic.com>
SSO manager
API docs
Server set up
The server requires:
- NodeJS 13.x
- LDAP server
OpenLDAP configuration
Password hashing (required)
Passwords are stored using {SSHA512} (salted SHA-512). The pw-sha2 module must be loaded in slapd before creating or resetting any passwords.
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: pw-sha2
EOF
Verify the module is working:
slappasswd -h {SSHA512} -s testpassword
Existing {MD5} password hashes continue to work after the module is loaded — users are migrated to SSHA512 the next time they change their password.
Account locking (required for active/inactive toggle)
User activation and deactivation uses the OpenLDAP ppolicy overlay. When a user is marked inactive, pwdAccountLockedTime is set on their entry, which causes all LDAP binds to fail — including logins to Emby, Gitea, and any other LDAP-backed service.
The easy way: run
ops/ldap-setup.shon the LDAP server. It is idempotent, auto-detects the correct user database, applies everything below (pw-sha2, ppolicy module/overlay/schema, custom schema, policy entry, SSO groups) and verifies ppolicy is active at the end:sudo ./ops/ldap-setup.sh -p <admin-password>If the app returns
503 OpenLDAP ppolicy overlay is not configuredonPUT /api/user/<uid>/active, run this script — it means the overlay is not attached to the database holding your users. The manual steps below are equivalent and kept for reference.
1. Load the ppolicy module:
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy
EOF
2. Add the overlay to your user database:
⚠️ The database index below (
{1}mdb) is not the same on every install. Confirm yours first — the overlay must go on the database whoseolcSuffixis your base DN, or account locking silently won't apply to your users:ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b cn=config \ '(&(objectClass=olcDatabaseConfig)(olcSuffix=dc=theta42,dc=com))' dn
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=ppolicy,ou=policies,dc=theta42,dc=com
olcPPolicyUseLockout: TRUE
olcPPolicyHashCleartext: FALSE
EOF
3. Create the policies container and default policy:
ldapadd -x -D "cn=admin,dc=theta42,dc=com" -W << 'EOF'
dn: ou=policies,dc=theta42,dc=com
objectClass: organizationalUnit
ou: policies
dn: cn=ppolicy,ou=policies,dc=theta42,dc=com
objectClass: top
objectClass: organizationalRole
objectClass: pwdPolicy
cn: ppolicy
pwdAttribute: 2.5.4.35
pwdLockout: FALSE
pwdMustChange: FALSE
pwdAllowUserChange: TRUE
EOF
Verify by locking a test account and confirming bind fails:
ldapmodify -x -D "cn=admin,dc=theta42,dc=com" -W << 'EOF'
dn: cn=testuser,ou=people,dc=theta42,dc=com
changetype: modify
replace: pwdAccountLockedTime
pwdAccountLockedTime: 000001010000Z
EOF
Custom schema (required for date of birth)
User accounts store a dateOfBirth field (ISO 8601 YYYY-MM-DD) for age verification. This requires a custom attribute type and auxiliary objectClass to be loaded into the OpenLDAP schema before any accounts are created.
Load the schema:
ldapadd -Y EXTERNAL -H ldapi:/// << 'EOF'
dn: cn=theta42,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: theta42
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1
NAME 'dateOfBirth'
DESC 'Date of birth in ISO 8601 format YYYY-MM-DD'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.99999.2.1
NAME 'theta42Person'
DESC 'Theta42 SSO extended person attributes'
AUXILIARY
MAY ( dateOfBirth ) )
EOF
Verify the schema loaded:
ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=theta42,cn=schema,cn=config" olcAttributeTypes olcObjectClasses
Note: The OID prefix
1.3.6.1.4.1.99999is used for internal/private schemas. If this deployment is ever connected to a federated directory, register a proper PEN at https://www.iana.org/assignments/enterprise-numbers and update the OIDs.
Required LDAP groups
| Group | Purpose |
|---|---|
app_sso_admin |
Full admin access: manage users, groups, OAuth clients |
app_sso_oauth_admin |
Manage OAuth clients only |
Logs (Docker)
The all-in-one image runs the Node app and slapd (OpenLDAP) in one container,
both writing to the container's stdout/stderr, so docker compose logs is the
primary view (slapd runs with -d 0, so LDAP output is there too).
docker compose logs -f sso-manager # app + slapd (stdout/stderr)
# or, by container name:
docker logs -f sso-manager
docker compose logs --tail=200 --since=10m sso-manager # recent context
# Query the directory directly to confirm LDAP is healthy
docker compose exec sso-manager ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,$LDAP_BASE_DN" -W -b "$LDAP_BASE_DN"
See DEPLOYMENT.md → Troubleshooting for LDAP-specific errors.