idp
smol/idp is a full-blown authentication stack that offers an OpenID
Connect API. It includes all the functionality required to operate a
(relatively basic) identity provider.
Feature highlights:
- Extensive OpenID Connect support (by way of
zitadel/oidc)
- Modern login UI for web protocols:
- 2FA support
- Account recovery (one-time tokens, email validation)
- Ability to enforce post-login workflows (password resets,
notifications, etc)
- Audit logs and new device detection
- User management UI for self-service credentials management
- Application-specific passwords for non-interactive protocols
- Minimal storage requirements
- Customizable SQL user backend
Why?
The authentication space already provides a number of good, featureful
solutions. So if you're looking for something of a professional
quality you should definitely look into one of:
This project targets a minimalistic resource (and operational)
environment, so it makes different compromises than most other
solutions out there:
-
Minimal storage requirements: instead of requiring a separate SQL
server for operation, we wanted something capable of working with an
embedded SQLite backend (where fault tolerance can be provided by
something like Litestream).
-
Simpler data dependencies: authentication systems ultimately require
integration with larger-scoped user management systems and existing
user databases. As a scalable solution, we provide a fully
customizable SQL user backend, along with a default, ready-to-use
SQLite embedded implementation. The user database can even be scaled
down to "static JSON file" for even simpler deployments.
-
Modularity: to simplify deployments, while allowing the design of
fault tolerant architectures, we've split the design into components
that can be run separately. This allows a separation of stateful vs.
stateless components, as well as short-term and long-term data
storages, all of which imply potentially different fault tolerance
strategies.
Architecture
The stack consists of a few independent layers that talk to each other
via GRPC. Some components require their own, independently managed
storage. You should run a single instance of these, if you're using
the default SQLite database backend!

User database
Most components need access to the user database. There are two
possibilities to consider:
-
There is no extant, external user database, your service doesn't
require one, perhaps you are only setting up authentication for a
small group of admins and don't require any "business logic" on top
of that;
-
your organization already has a database of user accounts, possibly
with additional information related to other services or business
requirements.
Stand-alone (static)
The user database can be just a static JSON file, with a list of users
and their associated credentials (including two-factor
authentication).
This is a good fit for a small group of operators, where
authentication controls access to administrative resources, and you
are running a service with no "users", or where SSO integration is not
necessary. Then, all changes to authentication credentials have to go
through source control, which comes with its own additional audit
mechanisms.
Stand-alone (dynamic)
The account server can manage its own database, usually running it as
an embedded SQLite database.
This option currently requires you to use the account server
component (see below).
Integration with an existing database
The user database layer can be configured to talk to an external
Postgres SQL server. Individual queries can be configured to adapt to
whatever schema the existing database might have.
Integration via RPC
For even more customization, it is possible to have idp talk to your
user database over GRPC. You can then implement the Account service
(cf. account/proto/account.proto)
yourself.
An example of such an implementation is the Account server (see
below), a built-in idp component that exposes a stand-alone user
database over GRPC.
Mapping the user database model to Oauth2
The smol/idp user database model includes three main identifiers for
users: an opaque, unique ID, a username, and an email address. The
software however makes no assumptions on their semantics, allowing
support for a wide range of use cases.
The only requirement is that the primary unique user identifier does
not change. The software uses the user ID as the identity principal
in the generated OAuth tokens, while the other identity attributes can
be retrieved via profile or openid claims.
The simplest user databases might have all three identifiers set to
the same value (for the standard scenario of "email as username"), or
perhaps just the first two (when no email integration is required),
while more sophisticated solutions will use a database-level unique
identifier for the ID, allowing username / email changes while
maintaining the same identity.
Authentication server
The authentication server API is documented in
authn/proto/authn.proto and offers an
interface for low-level user authentication, supporting multiple
services (in the style of PAM) with potentially different backends
and parameters.
It requires access to a short-term storage for ratelimits, anti-replay
protection and WebAuthN sessions. This can either be Memcache
(multiple servers supported), or just an in-memory cache.
Account server
While normally every service manages its own connection to the user
database, this is not possible if you want to use a SQLite database
and want to run components as separate processes. In this
scenario, it is possible to run the account server, which owns the
SQLite database and provides a GRPC API that other services can
connect to.
Audit server
The audit server stores login and account change information
long-term, in order to provide an audit trail for logins, and to
provide analytical insights on the suspiciousness of user logins (for
instance, to notify users of logins from new / unknown devices).
The server uses its own SQLite database, which is periodically
compacted (removing old / unnecessary entries) to avoid unrestricted
growth over time.
Login UI
The login application lets users perform the authentication workflow.
The actual authentication is delegated to the authentication server.
The OIDC machinery requires its own storage for registered tokens and
other data (currently not split between short-term-only and long-term,
though this is planned).
Management UI
This component can be thought of as a standalone application, tightly
integrated with the login workflow: it lets users manage their
authentication credentials.
Note that it is not a full user management solution, it is focused
exclusively on self-service credentials management. It offers no way
of, for instance, creating new users or managing users in bulk, as
normally these operations require additional business logic.
Running
The idp service is shipped as a single binary (smol-idp), which
supports a number of commands to select the service component to run:
auth
- run the Authentication server
audit
- run the Audit server
account
- run the Account server
idp
- run the HTTP service (OIDC and management UI)
bundle
- run the auth, audit and idp services together in a
single stand-alone process
All commands support an --addr command-line option to specify the
listening address, and a --metrics-addr option for a separate HTTP
listener to export Prometheus metrics.
Server commands take a configuration file, in YAML format.
Configuration can be fairly complex, so each command provides a
--help-config option that will provide further, detailed
documentation on the configuration structure and semantics.
Furthermore, most configuration variables (with the exception of
list-like containers) can be set via environment variables, also
documented in the --help-config output.
Example bundle configuration
A simple example of a bundle.yml configuration file for the
all-in-one service setup:
userdb:
type: static
params:
users:
- name: admin
password: "$argon2id$..."
email: admin@example.com
webauthn_registrations:
- public_key: ...
key_handle: ...
comment: "my little hardware token"
authn:
services:
login:
interactive: true
webauthn:
display_name: Example
rpid: https://auth.example.com
origin: auth.example.com
confirm:
disable_audit: true
oidc:
db_path: "/data/oidc.db"
issuer: "https://auth.example.com"
encryption_key: "<random 32-byte string>"
clients:
myapp:
name: "My App"
type: web
secret: "..."
redirect_uris:
- "https://my-app.example.com/oauth2/callback"
login:
auth_service: login
mgmt:
confirm_auth_service: confirm
webauthn:
display_name: Example
rpid: https://auth.example.com
origin: auth.example.com
This defines a static user database with a single "admin" user, and
sets up an OpenID Connect issuer at https://auth.example.com, with a
"myapp" static client. The login service is configured for WebAuthN
support.
mTLS
The services support mutual TLS authentication and authorization, see
MTLS.md for further details.
Hashed passwords should be in a format that the underlying password
library can understand. Argon2 algorithms are currently supported. It
is possible to generate such a hash with the pwhash command:
smol-idp pwhash $PASSWORD
Generation of static WebAuthN registrations isn't currently supported
by this tool, but you can use
webauthn-cred for
that:
go install git.autistici.org/ai3/tools/webauthn-cred@latest
webauthn-cred --rpid auth.example.com