LinOTP: a self-hosted two factor authentication server you can inspect
LinOTP - the open source solution for two factor authentication
At a glance
- What is it?
- LinOTP is a Python two factor authentication server that runs as a container next to PostgreSQL and keeps token logic, policies and user resolution on infrastructure you control. It is a good fit for organisations that already run their own identity stack, and a poor fit for anyone who wants a hosted login service in an afternoon.
- Who is it for?
- Adopt LinOTP when your organisation already operates its own directory, database and TLS termination and wants a multi factor server it can read and modify, since the AGPL keeps the source open and the environment variables keep configuration in plain sight. Do not adopt it if you need a hosted identity provider with a managed uptime commitment, or if you cannot carry the cost of pinning dependencies yourself.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 10 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What problem LinOTP actually solves
LinOTP sits between an application and its users and demands a second factor once a password check has passed. The project describes itself as a server component for multi-factor authentication, and the repository layout matches that claim: the top level holds a Python package named `linotp`, a `linotpapp.py` entry point, a Flask-based server and an `api-doc/` directory for the HTTP interface.
The interesting part is where the second factor comes from. Instead of tying you to one hardware vendor, LinOTP uses a modular approach in which each token type is a self-contained module. The documentation names OATH standards such as HMAC under RFC 4226 and time-based HMAC, and it also describes custom token families built on challenge-response, QR codes and push-type messages. The dependency list backs that up: `pyotp`-style behaviour is joined by `fido2`, `pyrad`, `qrcode[png]`, `pysodium`, `pycryptodomex` and `bcrypt`, which is a set of libraries that only makes sense if the server is expected to carry many different second factor mechanisms at once.
The target reader is an administrator or platform team that already runs LDAP, a database and a reverse proxy, and wants the second factor to be its own service rather than a checkbox inside someone else's identity product.
The compose stack: one Python container, one PostgreSQL container
The repository ships a `compose.yaml` that defines exactly two services. The `linotp` service builds from the repository context and mounts two named volumes, `linotp_data` on `/data` and `linotp_cache` on `/cache`. The second service, `linotp-db`, is a `postgres:latest` image with three environment variables and a `db_data` volume on the standard PostgreSQL data path. The application service declares `depends_on` the database and publishes port 5000 on the host.
That gives a clean data flow. The container command is `--with-bootstrap`, which is what makes the first start create an administrator account rather than leaving the instance unusable. Everything the server needs to survive a restart, including encryption keys and configuration, lives in the `linotp_data` volume, so the container itself stays disposable.
One detail is worth flagging for production planning: the database image is pinned to `postgres:latest`, a moving tag. Combined with a `build:` context rather than a pinned image reference, the compose file is a development convenience. An operator who cares about reproducible rebuilds will want to pin both, and the documentation points at PostgreSQL, MySQL or MariaDB as the supported production options rather than endorsing one.
Every configuration key has a LINOTP_ environment variable twin
Configuration in LinOTP is delivered through the process environment, and the mapping is mechanical rather than curated. If a setting inside LinOTP is called `XYZ`, then a variable named `LINOTP_XYZ` in the environment sets `XYZ`. The compose file shows the pattern in practice with `LINOTP_ADMIN_PASSWORD`, `LINOTP_DATABASE_URI`, `LINOTP_LOG_LEVEL` and `LINOTP_SESSION_COOKIE_SECURE`.
This is a genuinely nice property for container deployments because configuration and secrets can live in the same place as the secret manager of your choice, and there is no separate file format to learn. The cost is that the configuration surface is only as well documented as the environment variable convention itself. The project documentation hands off to a separate detailed documentation set for the full list of options, and the in-repository files show only the handful of variables that the quick start needs.
The security-relevant settings are also the ones with the sharpest edges. `LINOTP_SESSION_COOKIE_SECURE` ships as `false` in the compose file, which is correct for a local trial over plain HTTP and wrong for anything reachable from a network. The same applies to the default administrator password, which is literally the string `admin` and has to be replaced before the instance leaves the machine.
First run: bring up the stack, then log in and enrol
The quick start is a single command from the repository root. It builds the image, starts PostgreSQL, bootstraps the admin user and publishes the server on port 5000.
docker compose upAfter that, the README points at two endpoints. The management interface lives under `/manage` and the API endpoint sits at the root of the service. The same document gives the initial credentials as username `admin` and password `admin`, with the password described as configurable through `LINOTP_ADMIN_PASSWORD`.
The logical first session is therefore: sign in at the management interface, create resolvers to say where user attributes come from, group users into the partitions that policies then attach to, write policies that decide which factor applies to each group, and then enrol tokens. The mail templates in the `examples/` directory, including `mailtemplate-enroll.eml` and `mailtemplate-set-pin.eml`, suggest that the enrolment flow sends the user a message rather than handing over a secret by hand.
For a production deployment the documentation asks for a strong admin password, secure cookies, SSL or TLS termination through a reverse proxy such as nginx, and a production database. It also asks for the `/data` and `/cache` mounts and a persistent database volume, which the compose file already provides.
AGPL-3.0 and what it means for a network service
LinOTP ships under the AGPL v3. The `pyproject.toml` declares the licence as AGPL v3, the classifiers carry the GNU Affero General Public License v3 identifier, and the `Makefile` header carries the standard notice. The README frames this as one of two senses of openness, the other being the modular architecture.
The practical consequence for an engineer is narrower than a scare quote would suggest but still real. Running a modified LinOTP as a service that users reach over a network brings the source-offer obligation of the Affero licence into play, so a fork that changes authentication logic is not a private matter. Unmodified deployment with configuration through environment variables does not create that problem, because no covered code is being conveyed to users in a modified form.
This is a licensing observation rather than legal advice, and the sensible next step is to read the licence text in the repository before distributing any internal or customer-facing modification. The copyright line is also worth noticing: the README attributes work from 2010 to 2019 to KeyIdentity GmbH and from 2019 onward to netgo software GmbH, which matches the maintainer entry in `pyproject.toml`. The project is maintained by a company that sells support around it.
Pinned dependencies tell you where the friction will be
The dependency list in `pyproject.toml` reads like a maintenance diary. `bcrypt` is held below 5.0 because of a change in how long passwords are handled, `click` is pinned because 8.2 is not compatible with Flask 2.1.x, `SQLAlchemy` is held below 2.1 because 2.1 changes the default PostgreSQL driver to psycopg 3, and `urllib3` carries a lower bound of 2.5.0 for CVE reasons. Two comments in that list explain the pins directly, which tells you the rest are non-obvious.
The runtime stack is Flask with SQLAlchemy, served by gunicorn in the production container, with `flask-jwt-extended` for tokens and `Flask-Babel` for translations. Integration points are broad: `python-ldap` for directory lookups, `smpplib` for SMS, `mysqlclient` and `psycopg2-binary` for databases, `httplib2` and `requests` for outbound calls, and `qrcode[png]` for the enrolment codes.
Python 3.11 or newer is required, and the Docker base stage starts from `python:3.11-slim-bookworm`. A team that wants to run this on a newer interpreter will need to confirm each pinned dependency still resolves, and that is exactly the upgrade cost the project pushes onto its operators.
Where LinOTP is the wrong tool
The honest limits are visible in the repository rather than hidden. First, there are no GitHub releases. The repository has no tagged release artifacts, so version discovery means reading the changelog file in the tree and tracking the `master` branch, which is a poor fit for change-averse environments that pin to a released version.
Second, the quick start is deliberately unsafe. Default password `admin`, secure cookies off, a moving `postgres:latest` tag and a development-grade `build:` context. There is a difference between a project that gives you a safe default and one that assumes you will read the production checklist, and LinOTP belongs to the second group.
Third, the breadth of the design is also its cost. A modular server that supports HMAC, time-based HMAC, FIDO2, RADIUS, LDAP resolution, mail and SMS enrolment has many more moving parts than a single-purpose authenticator app, and the repository carries a separate test environment image, an end-to-end image and a Makefile with unit, functional and integration test targets. That is a well-instrumented project, but it is a project with a nontrivial surface.
Finally, if your goal is simply to add a rotating code to a handful of internal tools, a hosted service or a smaller self-hosted authenticator will cost far less in operations than running a Python server with PostgreSQL, LDAP and a reverse proxy in front of it.
Editorial conclusion
Adopt LinOTP when your organisation already operates its own directory, database and TLS termination and wants a multi factor server it can read and modify, since the AGPL keeps the source open and the environment variables keep configuration in plain sight. Do not adopt it if you need a hosted identity provider with a managed uptime commitment, or if you cannot carry the cost of pinning dependencies yourself. Before committing, verify the token types you actually need against the module list, confirm that the last push on 2026-10-13 is followed by the release notes you depend on, and check that your chosen database matches the URI you pass to LINOTP_DATABASE_URI.
Frequently asked questions
What is LinOTP and what is it used for?
LinOTP is a self-hosted server component for multi-factor authentication, written in Python and licensed under AGPL v3. It verifies second factors such as HMAC and time-based HMAC tokens and exposes both a management interface and an HTTP API for enrolment and policy work.
How do I install and run LinOTP?
The project is designed to run containerised, and the documented quick start is `docker compose up` from the repository root. That builds the LinOTP image, starts a PostgreSQL container, bootstraps an admin user and publishes the service on port 5000, with the management interface at the `/manage` path.
What database and configuration does LinOTP need in production?
The documentation lists PostgreSQL, MySQL or MariaDB as production databases, and settings are passed through environment variables. For example `LINOTP_ADMIN_PASSWORD` sets the admin password, `LINOTP_DATABASE_URI` supplies the connection string, and `LINOTP_SESSION_COOKIE_SECURE` controls secure cookies, which should be set to true when the service is reachable over the network.
Does LinOTP support FIDO2 and push style tokens?
Yes. The project documentation describes a modular token system covering OATH standards such as HMAC and time-based HMAC, and it says that custom tokens with different algorithms are easy to create, including challenge-response tokens, tokens based on QR codes and tokens based on push-type messages. The dependency list includes `fido2`.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/linotp-linotp)Was this useful?