Migration from LDAP to OIDC: Nextcloud

This article is part of a series:
  1. Forgejo
  2. Nextcloud
  3. Matrix

In my hackerspace we operate various services for our members. Up until this month, most of these services used to do user authentication against a LDAP server. For a multitude of reasons, we replaced the LDAP server with an OpenID Connect SSO using a Keycloak server as the OIDC Identity Provider.

In this series of articles I summarize the efforts required to migrate each of the services from LDAP to OIDC authentication. This article covers the setup and migration in Nextcloud.

OIDC Setup in Nextcloud

While LDAP integration is part of the Nextcloud core, OIDC integration must be installed as an additional app, user_oidc. Once this app is installed and enabled (occ app:enable user_oidc), you'll find a new tab in Nextcloud's admin settings called "OpenID Connect", where you can add your Identity Provider.

When adding a new IdP, you need to pay attention to choose an user ID claim that matches the user IDs from LDAP: If you used the uid or sAMAccountName attribute in the LDAP backend, chances are you can simply use the preferred_username claim in the OIDC backend and call it a day. (Please test whether these indeed match for all users!) However, if you used e.g. the objectGUID LDAP attribute, you need to make sure that the same attribute is available as an OIDC token claim.

Finally, remove the Use unique user id check mark, otherwise the OIDC backend generates its own user IDs anyway (by using a hash over the OIDC username and the name of the IdP).

Migration of Users From LDAP to OIDC

Users in Nextcloud are not managed in one central database table. Instead, each user backend maintains its own set of users and mapping to Nextcloud user IDs, usually each using its own database table, such as:

  • oc_users: Users created directly in Nextcloud.
  • oc_ldap_user_mapping: Users maintained by the user_ldap backend and the mapping from LDAP DNs to Nextcloud usernames.
    • Groups and group memberships synced from LDAP are stored in oc_ldap_group_mapping and oc_ldap_group_members.
  • oc_user_oidc: Users maintained by the user_oidc backend.

This is why, as soon as the user_ldap app is disabled, all users and groups that were synced from LDAP disappear immediately. Don't worry - the users aren't gone. If you re-enable the user_ldap app, they automagically reappear. But the LDAP user mappings need to be migrated to the oc_user_oidc table so that the users are still around when the LDAP backend is disabled. For this we first need to understand the table schemas:

MariaDB [nextcloud]> select * from oc_ldap_user_mapping;
| ldap_dn                              | owncloud_name | directory_uuid | ldap_dn_hash                                                     |
| uid=s3lph,cn=users,dc=example,dc=org | s3lph         | s3lph          | faea045b8b1be34c7e02fa0771f3a2d6b7cbad4297a68c3c3fc1c39ff3cd2988 |
| uid=s4lph,cn=users,dc=example,dc=org | s4lph         | s4lph          | 14df2aecf103280c8fd693b77959f7fdab4c530fce9ebc35575d9b763fd1c5e6 |
| uid=s5lph,cn=users,dc=example,dc=org | s5lph         | s5lph          | a5938499eb735d4cdb2aebe641a155aa930bb0962003e595ce960eb2a956d10d |

MariaDB [nextcloud]> select * from oc_user_oidc;
| id | user_id   | display_name |
|  2 | s3lph     | s3lph        |

In the oc_ldap_user_mapping table, we have a mapping of DNs (ldap_dn) to usernames in Nextcloud (owncloud_name). The key component here is the owncloud_name column. This is the Nextcloud internal name, and also the name that must be provided by the OIDC backend (i.e. it must be present as a claim in the id_token, and that claim must be chosen as the "User ID mapping" attribute in the OIDC provider settings in Nextcloud. The oc_user_oidc table, on the other hand, does not store any mappings between OIDC and Nextcloud IDs. Instead it only contains the user ID and display name as provided by the OIDC provider.

The user migration from LDAP to OIDC can then be performed with a single SQL statement:

INSERT IGNORE INTO oc_user_oidc (user_id, display_name) SELECT owncloud_name, owncloud_name FROM oc_ldap_user_mapping;

Though you may want to keep the display name in mind. With our Nextcloud instance, we used the username as display name as well, so we simply used the same value for both columns. I don't think the LDAP display names are stored in the database, so if you have different display names, this simple insert statement may not work as well for you.


Synchronization of user groups from your OIDC IdP to Nextcloud is not possible yet, but there's an open pull request that implements this feature. This means that until this is merged and released, group memberships that have previously been synced from LDAP won't be updated automatically anymore. Furthermore, if/when you disable the user_ldap app, any group that was synced from LDAP will disappear, and has to be recreated.

Another limitation is that OIDC users are only created in Nextcloud once they sign in for the first time. This means that you e.g. can't share files with users that have never signed in before. If your IdP has an automatable impersonation feature, you could work around this issue with a workflow such as:

  1. Make sure the option "Check Bearer token on API and WebDav requests" is enabled in Nextcloud's OIDC settings.
  2. Use the IdP's impersonation API to obtain an id_token for the new user.
  3. Use this id_token to perform a simple request to Nextcloud, e.g. a WebDAV PROPFIND.
  4. Nextcloud automatically creates the new user.

Tips and Tricks

Nextcloud does not automatically redirect to the IdP for login - users always have to click the "Login with SSO" button. If you want your users to be redirected to the SSO automatically when they try to log in, you have to configure this in your web server. For example, in Apache 2:

RewriteEngine on
RewriteCond %{QUERY_STRING} !local=1
RewriteRule ^/login$ /apps/user_oidc/login/1 [L,R,QSD]

Replace the number at the end of the URL (/1) with the ID of the OIDC provider in Nextcloud. You can obtain this ID with occ user_oidc:provider. The RewriteConds in this config allow you to append ?local=1 to disable the redirect, e.g. if you need to sign in as a local admin user.