Securing Linkding with OIDC
Using PocketID for passwordless authentication for my Linkding instance
I have self-hosted various services for years. The primary reason for this is so that I have control over these and they do not change under my feet (whether in terms of feature set, pricing, hardware requirements, etc).1
This has generally included a 'read-it-later' service. I find such a service quite useful when going through my RSS feeds — my usual workflow is to scan through the headlines and abstracts quickly, and mark interesting articles to be read later via a 'read-it-later' service.
I've used many 'read-it-later' services over the years, including:
- Instapaper (stopped allowing access to archived articles and doubled its subscription price)
- Pocket (acquired by Mozilla and subsequently shut down)
- Omnivore (acquired by ElevenLabs and subsequently shut down)
- Wallabag (worked well but I found it a bit slow and I didn't quite like the user interface)
I've now settled on Linkding, which I have been happily using for over a year. It's fast, light, easy to self-host (single docker container + sqlite database) and does everything I need. There is a Firefox extension allowing you to quickly add articles when browsing the web (and a keyboard shortcut: Alt ShiftL).
The one thing it is doesn't have built-in is a mobile app. However, it has a simple API2 and the Linkding documentation includes instructions to add a custom share intent using HTTP Shortcuts on Android to share articles to your Linkding instance. There are similar instructions using iOS Shortcuts.
If I want to view the URLs I've saved on Linkding on mobile, I just visit my Linkding instance in Firefox (you can add the site to your home screen if you like).
The broader picture
My Linkding instance does not contain any particularly sensitive data, hence I have been happy to expose it to the public internet (behind a reverse proxy), with the built-in username and password authentication.
Lately however, as part of a general review and refresh of my self-hosted systems, I have been looking at harmonising authentication across all my self-hosted systems and implementing single-sign on (SSO), so I don't need separate usernames and passwords for every service. The built-in authentication methods for some of my self-hosted services are quite basic (e.g. just username and password for Linkding, no MFA/2FA), so the hope is that implementing SSO would also raise the security posture of my services (or at least the authentication aspects) of my services to a certain floor.
Rather than a single username and password to rule them all, my plan is to advance directly to passwordless authentication, using passkeys. I have a few physical FIDO tokens and I use software passkeys as well, so this should work well. The other users of my self-hosted services should also have access to software passkeys (e.g. via Google or Apple), or if need be I can hand them a physical security key. This would also conveniently avoid the security issues arising when users set weak and easily-guessable passwords, or are too lazy to set up and use MFA/2FA.
PocketID
This is all at a fairly preliminary stage. As a low-risk trial before a broader rollout, I have decided to try implementing OpenID Connect (OIDC) authentication with passkeys for Linkding.
There are many self-hostable OIDC services available, including Authelia and Keycloak. I selected PocketID because I didn't need all the bells and whistles of the more fully-featured solutions and I wanted something that would be lightweight and simple to self-host and maintain.
I set up PocketID as a docker container in my usual way with a reverse proxy and followed the helpful PocketID documentation to configure OIDC with Linkding via environment variables. It worked very well!
Migration
Ideally OIDC should be set up right at at start, i.e. when you first initialise your Linkding instance and create your user. Unfortunately, I did not have such foresight.
The difficulty I had was essentially this:
- I set up my PocketID user and my Linkding user with different email addresses
- Upon OIDC authentication, Linkding creates a new user with the authenticating email address. You can force a certain username using the
OIDC_USERNAME_CLAIMenvironment variable and setting up a custom claim on the PocketID side, but this still creates a new user. If you have an existing user with that username, the new user creation process will fail and therefore this does not fully solve the problem I had. - Even if I changed my Linkding user to use the same email address as my PocketID email address, the core issue was that Linkding would always attempt to create a new user rather than allow me to log in as an existing user.
I've been using my Linkding instance for awhile and my user has quite a lot of bookmarks and other user data that I would like to retain. Whilst I believe I could export my bookmmarks and import them into my new PocketID user, there didn't seem to be a built-in way to transfer over my API keys, which would mean I would need to reconfigure my various Linkding integrations (e.g. mobile and eilmeldung integrations). As far as possible, I wanted to login via PocketID to my existing Linkding user. Fortunately, I managed to achieve this.
My hackish solution was to login with PocketID, which created a new user, then shutdown the Linkding instance, muck around with the sqlite database (I used the very useful sqlit for this)3 by swapping the row IDs of my two users, then stand the instance back up. This worked because the foreign keys in the bookmarks and other tables are the user row IDs.
I was quite satisfied with this, so I went ahead and disabled login via username and password by setting LD_DISABLE_LOGIN_FORM = True.
- See in particular: https://en.wikipedia.org/wiki/Enshittification↩
- I have also configured Linkding support for my favourite RSS reader, eilmeldung — this will likely be the subject of a subsequent article.↩
- Linkding does expose a Django admin dashboard to admin users, but I couldn't swap the user IDs via the dashboard because it didn't actually expose the user IDs in the GUI (only the usernames).↩