README.md 6.2 KB

uidc, a lightweight OpenID Connect server

uidc is a lightweight OpenID Connect (OIDC) server, implementing the OpenID Connect Core specification. It's designed as a replacement for heavyweight systems like Keycloak when you want to have SSO without a dedicated machine running it, for example on personal or small-scale infrastructure. Since it supports OIDC, it also functions perfectly well as an OAuth2 provider.

uidc is designed in an opinionated fashion, eschewing general flexibility for simplicity, which means it is most definitely not a one-size-fits-all solution. If you're planning on dealing with hundreds of thousands of users or complicated authentication flows, look elsewhere. But, if you want:

  • lightweight (runtime memory usage is less than 20MB, even under moderate load, and CPU usage is minimal)
  • simple (single statically-linked executable and minimal supporting files)
  • ready to use out of the box (as long as your authentication needs fall into the first 95% of use cases)
  • easily configured (configuration is done via a single TOML file and a CLI with tab completion)
  • flexible, within limits (implements generic role-based access control)

... then uidc might be what you're looking for.

Getting started

(The following will assume that $UIDC points to the uidc executable.)

The general format of a uidc invocation runs something like the following:

./path/to/uidc --config-path $PATH_TO_CONFIG <noun> <verb> <options>

If a config file is not explicitly passed via --config-path, it will default to using a file called uidc.toml in the current directory; you can also set the environment variable UIDC_CONFIG if that's more convenient. The config path is elided from the following examples for brevity.

Initial setup

Start with a very simple configuration file, something like the following:

db_path = "uidc.db"
base_url = "https://externally-visible-url"

Then initialize the database and create a signing key:

$UIDC init
$UIDC key generate rsa2048

Create yourself a user and add a password (and 2FA if you want, by passing a -pt instead of -p):

$UIDC user create <username>
$UIDC user update-auth -p <username>

Finally, create an OIDC client:

$UIDC client create example-client

And then run the server! By default, it listens on port 2114, but that can be changed with --port.

$UIDC serve

Realms

uidc implements 'realms', or independent authentication domains. By default, everything is in the primary realm, but you can switch to another realm with -r. For example:

$UIDC realm create secondary
$UIDC -r secondary user create someuser

Each realm has its own set of users, groups, roles, and clients.

RBAC

uidc implements a simple role-based authentication schema. it works as follows:

  • users are part of groups
  • groups and OIDC clients have roles (permissions) assigned to them
  • roles are further grouped into scopes

When authentication tokens (or refresh tokens!) are created, a list of scopes is requested. The resulting set of roles in the authentication token is the following:

attached_roles(user_groups) ∩ attached_roles(given_scopes)

That is, a role is only included in the authentication token if it is both a) requested during the token grant, and b) available to the user through a group they are a member of.

For illustration, let's create three roles, put them into two groups, and assign each group to a single user. We'll also put all the roles into a scope to make all this available to clients.

# users
$UIDC user create user1
$UIDC user create user2

# roles
$UIDC role create roleA
$UIDC role create roleB
$UIDC role create roleC

# groups
$UIDC group create group1
$UIDC group create group2

$UIDC group attach-user group1 user1
$UIDC group attach-role group1 roleA
$UIDC group attach-role group1 roleB

$UIDC group attach-user group2 user2
$UIDC group attach-role group2 roleB
$UIDC group attach-role group2 roleC

# scope
$UIDC scope create example-scope
$UIDC scope attach-role example-scope roleA
$UIDC scope attach-role example-scope roleB
$UIDC scope attach-role example-scope roleC

Now, if we generate some authentication tokens for user1 and user2 and peek at the JWT claims, we'll see the roles split between the two users:

$UIDC token generate-auth --client example-client --username user1 --scopes example-scope \
    | awk -F. '{print $2}' | base64 -d | jq
{
  "sub": "user1",
  "iss": "https://externally-visible-url/primary",
  "aud": "example-client",
  "iat": 1699926032,
  "exp": 1699926632,
  "roles": [
    "roleA",
    "roleB"
  ]
}

and

$UIDC token generate-auth --client example-client --username user2 --scopes example-scope \
    | awk -F. '{print $2}' | base64 -d | jq
{
  "sub": "user2",
  "iss": "https://externally-visible-url/primary",
  "aud": "example-client",
  "iat": 1699926035,
  "exp": 1699926635,
  "roles": [
    "roleB",
    "roleC"
  ]
}

Provider configuration

Each realm gets its own set of token endpoints, available at https://uidc-base-url/<realm>/.well-known/openid-configuration. An example configuration might look something like the following:

{
  "authorization_endpoint": "https://base-url/primary/oidc/authorize",
  "id_token_signing_alg_values_supported": [
    "EdDSA"
  ],
  "issuer": "https://base-url/primary",
  "jwks_uri": "https://base-url/primary/oidc/jwks",
  "response_types_supported": [
    "code",
    "id_token",
    "token id_token"
  ],
  "subject_types_supported": [
    "public"
  ],
  "token_endpoint": "https://base-url/primary/oidc/token",
  "token_endpoint_auth_signing_alg_values_supported": [
    "EdDSA"
  ]
}

Implementation and licensing details

Core components of uidc:

  • schema: database schema types for type-safe use of SQLite via microrm
  • server: server runtime
  • *_management: implementations of manipulation helpers for CLI and/or REST API interfaces

uidc is licensed under a 4-clause BSD license.