### uidc, a lightweight OpenID Connect server ### uidc is a lightweight OpenID Connect (OIDC) server, implementing the [OpenID Connect Core][openid-spec] specification. It's designed as a replacement for heavyweight systems like [Keycloak][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. [openid-spec]: https://openid.net/specs/openid-connect-core-1_0.html [keycloak]: https://keycloak.org/ 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: ```shell ./path/to/uidc --config-path $PATH_TO_CONFIG ``` 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: ```toml db_path = "uidc.db" base_url = "https://externally-visible-url" ``` Then initialize the database and create a signing key: ```shell $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`): ```shell $UIDC user create $UIDC user update-auth -p ``` Finally, create an OIDC client: ```shell $UIDC client create example-client ``` And then run the server! By default, it listens on port 2114, but that can be changed with `--port`. ```shell $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: ```shell $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. ```shell # 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: ```shell $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 ```shell $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//.well-known/openid-configuration`. An example configuration might look something like the following: ```json { "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](https://crates.io/crates/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.