Setting up Keycloak for SSO

I saw https://www.datamate.org/installation-keycloak-sso-ubuntu-18-04/ which gave me some insights as to where to start with getting Seafile set up with Keycloak, but it seems things have changed enough that it’s no longer working quite the same way. So, after figuring out what else I needed, I figured I’d write something up in case somebody else wants to do this.

This is using Keycloak 26.0.0 and Seafile CE server 11.0.12. It also assumes you have a working Keycloak installation already.

To start with, you’ll need to set up an OpenID client in Keycloak. You can use settings like these for it (though adding additional randomness to the client ID can increase the effective complexity of the secret).

That should give you a client that looks something like this:

You should also have a credentials tab (which is made available by turning on client authentication):

The client secret will be used in the Seahub configuration, so you’ll want to click the copy button to get a copy of it and paste it somewhere temporarily.

From there, you just need to note the realm you use and your Keycloak server’s hostname, and you can start making changes on the Seafile side. Those changes are made in seahub_settings.py. Specifically, they’re:

ENABLE_OAUTH = True
OAUTH_CREATE_UNKNOWN_USER = True
OAUTH_ACTIVATE_USER_AFTER_CREATION = True
OAUTH_CLIENT_ID = "client_id"
OAUTH_CLIENT_SECRET = "client_secret"
OAUTH_REDIRECT_URL = "https://seafile.example.com/oauth/callback/"

OAUTH_PROVIDER_DOMAIN = 'seafile.example.com'
OAUTH_AUTHORIZATION_URL = 'https://keycloakinstance.com/realms/KeycloakRealm/protocol/openid-connect/auth'
OAUTH_TOKEN_URL = 'https://keycloakinstance.com/realms/KeycloakRealm/protocol/openid-connect/token'
OAUTH_USER_INFO_URL = 'https://keycloakinstance.com/realms/KeycloakRealm/protocol/openid-connect/userinfo'
OAUTH_SCOPE = ["openid", "profile", "email"]
OAUTH_ATTRIBUTE_MAP = {
    "sub": (True, "uid"),
    "email": (False, "contact_email"),
    "name": (False, "name")
}

The real magic there is OAUTH_SCOPE and OAUTH_ATTRIBUTE_MAP. The scope requests an OpenID token with profile and email data (as defined in the OpenID specification), and the attribute map maps values returned by Keycloak to specific key fields within Seafile. In particular, it maps the sub field, which is a unique user identifier on the Keycloak side, to the uid field in Seafile, maps the email field in Keycloak to contact_email in Seafile (since the user’s e-mail address will be generated as an internal one in Seafile; having a contact e-mail address set allows the user to have something actually usable), and name in Keycloak to name in Seafile. It also declares that the uid field in the token is mandatory (the True part), and the other fields are optional (the False parts).

Finally, it allows users to be created and activated if they successfully authenticate against the Keycloak instance.

2 Likes

Is there a way to link SSO user with existing user?

I’ve followed this tutorial to a tee, yet still receive the following:

image

But don’t see any logs

Installed via docker

Now receiving this in DEBUG and logs. (found them)

Environment:


Request Method: GET
Request URL: http://seafile.wapnitsky.com/oauth/callback/?state=2c0vR93ZpUV51PEOrROFolpHlqIS19&session_state=78fc2412-b6c3-4eb2-b651-05169bafef8a&iss=https%3A%2F%2Fauth.wapnet.wapnitsky.com%2Frealms%2Fwapnet&code=68cf819d-41b0-4e93-805c-d9dd47bbb050.78fc2412-b6c3-4eb2-b651-05169bafef8a.2a280015-dafe-45b6-bab8-457616b1c99d

Django Version: 4.2.23
Python Version: 3.12.3
Installed Applications:
['django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'seahub.base',
 'django.contrib.auth',
 'registration',
 'captcha',
 'statici18n',
 'constance',
 'constance.backends.database',
 'termsandconditions',
 'webpack_loader',
 'djangosaml2',
 'seahub.api2',
 'seahub.avatar',
 'seahub.contacts',
 'seahub.institutions',
 'seahub.invitations',
 'seahub.wiki',
 'seahub.wiki2',
 'seahub.group',
 'seahub.notifications',
 'seahub.options',
 'seahub.onlyoffice',
 'seahub.profile',
 'seahub.share',
 'seahub.help',
 'seahub.ai',
 'seahub.thumbnail',
 'seahub.password_session',
 'seahub.admin_log',
 'seahub.wopi',
 'seahub.tags',
 'seahub.revision_tag',
 'seahub.two_factor',
 'seahub.role_permissions',
 'seahub.trusted_ip',
 'seahub.repo_tags',
 'seahub.file_tags',
 'seahub.related_files',
 'seahub.work_weixin',
 'seahub.weixin',
 'seahub.dingtalk',
 'seahub.file_participants',
 'seahub.repo_api_tokens',
 'seahub.repo_metadata',
 'seahub.abuse_reports',
 'seahub.repo_auto_delete',
 'seahub.ocm',
 'seahub.ocm_via_webdav',
 'seahub.search',
 'seahub.sysadmin_extra',
 'seahub.organizations',
 'seahub.krb5_auth',
 'seahub.django_cas_ng',
 'seahub.seadoc',
 'seahub.subscription',
 'seahub.billing',
 'seahub.exdraw',
 'gunicorn']
Installed Middleware:
['django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.locale.LocaleMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'seahub.auth.middleware.AuthenticationMiddleware',
 'seahub.base.middleware.BaseMiddleware',
 'seahub.base.middleware.InfobarMiddleware',
 'seahub.password_session.middleware.CheckPasswordHash',
 'seahub.base.middleware.ForcePasswdChangeMiddleware',
 'termsandconditions.middleware.TermsAndConditionsRedirectMiddleware',
 'seahub.two_factor.middleware.OTPMiddleware',
 'seahub.two_factor.middleware.ForceTwoFactorAuthMiddleware',
 'seahub.trusted_ip.middleware.LimitIpMiddleware',
 'seahub.organizations.middleware.RedirectMiddleware',
 'seahub.base.middleware.UserAgentMiddleWare']



Traceback (most recent call last):
  File "/opt/seafile/seafile-server-13.0.8/seahub/thirdpart/requests/models.py", line 976, in json
    return complexjson.loads(self.text, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

During handling of the above exception (Expecting value: line 1 column 1 (char 0)), another exception occurred:
  File "/opt/seafile/seafile-server-13.0.8/seahub/thirdpart/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/seafile/seafile-server-13.0.8/seahub/thirdpart/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/seafile/seafile-server-13.0.8/seahub/seahub/oauth/views.py", line 101, in _decorated
    return func(request)
           ^^^^^^^^^^^^^
  File "/opt/seafile/seafile-server-13.0.8/seahub/seahub/oauth/views.py", line 171, in oauth_callback
    user_info_json = user_info_resp.json()
                     ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/seafile/seafile-server-13.0.8/seahub/thirdpart/requests/models.py", line 980, in json
    raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Exception Type: JSONDecodeError at /oauth/callback/
Exception Value: Expecting value: line 1 column 1 (char 0)

Everything works up until the callback