minioidc is a lightweight OpenID Connect (OIDC) / OAuth 2.0 authorization server designed to provide Single Sign-On (SSO) functionality for small or home networks. It is not a complete OIDC implementation, but it covers the most common grant types and endpoints.
Supported grant types:
| Grant type | RFC | Availability |
|---|---|---|
Authorization Code + PKCE (S256) |
RFC 7636 | Default. PKCE is mandatory for public clients |
| Refresh Token | RFC 6749 §6 | Default. Rotated on every use unless reuse_refresh_tokens is set |
| Client Credentials | RFC 6749 §4.4 | Confidential clients with client_credentials in grant_types |
| Device Code | RFC 8628 | Clients with the device code grant type |
| JWT Bearer | RFC 7523 §2.1 | Confidential clients with the jwt-bearer grant type and keys |
| Implicit and Hybrid | OIDC Core §3.2, §3.3 | Discouraged (RFC 9700): grants.implicit and implicit in the client grant_types |
| Resource Owner Password | RFC 6749 §4.3 | Discouraged (RFC 9700): grants.password and password in the client grant_types |
Each client can only use the grant types of its grant_types (by default authorization_code and refresh_token). The discouraged flows are also disabled in the whole server unless they are enabled in the grants section.
The supported scopes are openid, profile (preferred_username, name, given_name, family_name), email (email, email_verified), address (address), phone (phone_number), groups (groups) and offline_access (refresh tokens).
The authorization endpoint (GET and POST) supports the OIDC parameters:
prompt:none(returnslogin_requiredorinteraction_requiredinstead of showing a page),loginandselect_account(the user logs in again) andconsent(the consent is implicit: the clients are registered by the administrator).max_age: the user logs in again if the last login is older.id_token_hint: the user logs in again if the hinted user is not the logged in user.login_hint: prefills the username in the login page.nonceandstate(optional, butnonceis required when an ID token is returned by the authorization endpoint),display,ui_locales,claims_localesandacr_values(accepted and ignored).response_mode:query(code flow) andfragment(required by the implicit and hybrid flows).requestandrequest_uriare not supported (request_not_supported/request_uri_not_supported).
The responses include the iss parameter (RFC 9207). The ID token includes auth_time, amr (pwd, plus otp and mfa after the MFA verification), azp, at_hash, c_hash (hybrid flow), sid and nonce.
If a code or a rotated refresh token is used twice, all the refresh tokens issued from it are revoked (one of the requests could be from an attacker).
The token, userinfo, revocation, introspection, device authorization, discovery and JWKS endpoints allow CORS requests (without cookies), so they can be used from SPAs.
It also implements:
- Client authentication:
client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt(RFC 7523) andnone(public clients). - Token introspection (RFC 7662).
- Token revocation (RFC 7009) of refresh tokens. The access tokens are short lived JWTs: revoking them answers
unsupported_token_type, and they stop working when their session is closed. - RP-Initiated Logout, with
id_token_hint,post_logout_redirect_uriandstate. Withoutid_token_hint, the user confirms the logout. - Key rotation: previous signing keys keep being published in the JWKS.
- Simple and Lightweight: minioidc is designed to be a straightforward solution for enabling SSO in small or home networks.
- Onboarding: Start it without a configuration file and create the administrator from the browser.
- OAuth 2.0 grant types: Authorization Code with PKCE, Refresh Token, Client Credentials, Device Code (RFC 8628) and JWT Bearer (RFC 7523), plus the discouraged Implicit, Hybrid and Password grants (disabled by default).
- Client and User Configuration: Configure clients and users in the YAML configuration file, or manage them from the admin web.
- Flexible Data Storage: Store the data in memory or in a SQLite v3 database: sessions, tokens, MFA, users and clients.
- LDAP Users Integration: Use the users of an LDAP server to log in.
- LDAP server: Serve the users as a read only LDAP directory, for the applications that only support LDAP.
- MFA: Require MFA for all users using TOTP App (e.g. Google Authenticator).
- User profile: The users change their password and manage their authenticator apps.
- Responsive pages: The login, MFA, device and profile pages work on phones and computers, with light and dark modes. The templates can be customized with the components of the design system.
- Admin web: Manage users, clients, sessions and tokens from a web served by minioidc, for the members of an admin group.
- Brute force protection: Logins, MFA codes, LDAP binds, password grants and device user codes are blocked for 15 minutes after 5 failed attempts.
- Secure by Default: Enable HTTP Strict Transport Security (HSTS), Content Security Policy (CSP), secure cookies, and more with a single configuration option.
The endpoints are published under the path of the issuer: if the issuer is https://example.com/auth, the discovery document is at https://example.com/auth/.well-known/openid-configuration.
| Endpoint | Path |
|---|---|
| Discovery | /.well-known/openid-configuration |
| JWKS | /.well-known/jwks.json |
| Authorization | /connect/authorize |
| Token | /connect/token |
| Userinfo | /connect/userinfo |
| Introspection (RFC 7662) | /connect/introspect |
| Revocation (RFC 7009) | /connect/revoke |
| End session (RP-Initiated Logout) | /connect/endsession |
| Device authorization (RFC 8628) | /connect/deviceauthorization |
| Device activation page | /connect/device |
| Login page | /login |
| MFA pages | /mfa/create, /mfa/verify |
| User profile | /profile, /profile/password, /profile/mfa |
| Admin web (optional) | /admin/ |
| Health check | /healthz |
- OIDC Discovery
- Authorization Code with PKCE
- Refresh Token
- CSRF
- Yaml config file
- ENV secrets inside yaml config file
- Sqlite database for grants, sessions, MFA, users and clients
- MFA with TOTP App (e.g. Google Authenticator)
- MFA with email?
- Ldap Users integration
- RP-Initiated Logout
- Token revocation and introspection
- Client credentials, device code and JWT bearer grants
- Implicit, hybrid and password grants (disabled by default)
- User profile (password and MFA)
-
client_secret_jwtandprivate_key_jwtclient authentication - Signing key rotation
- Admin web
- LDAP server
- Go 1.26 or later (to run from source or to use it as a library)
- Node.js 24 or later, only to build the admin web and the stylesheet of the pages (
make build-admin) - Docker (optional, for the containerised setup)
git clone https://github.com/fernandoescolar/minioidc.git
cd minioidc
make build-all # builds the admin web and ./bin/minioidcWithout Node.js, make build builds the server: everything works except the admin web, that shows how to build it.
make build-docker # builds the image (with the admin web)
make run-docker # starts the container on :8000 with example2_config.ymlWithout a configuration file, the data is kept in the /data volume (see Onboarding):
docker run -p 8000:8000 -v minioidc-data:/data minioidc:latestgo get github.com/fernandoescolar/minioidcRun the server from the root of the repository (the paths of the templates and static files are relative to the working directory):
make build-all && ./bin/minioidcThe log shows a link like http://localhost:8000/setup?code=ABCD-EFGH-IJKL-MNOP: open it, create the administrator and follow the next steps (register your first application in the admin web, add an authenticator app…). See Onboarding.
- Copy the example configuration and edit the issuer, the clients and the users (see Configuration):
cp example2_config.yml config.yml- Run the server from the root of the repository (the paths of the templates and static files are relative to the working directory):
MINIOIDC_CONFIG=config.yml make run- Visit http://localhost:8000. If you enable the LDAP server, publish its port too in Docker (e.g.
-p 3389:3389).
There are several commands available in the Makefile:
make build- Build the binarymake build-admin- Build the admin web (requires Node.js). It is embedded in the binary, so build it beforemake buildmake build-all- Build the admin web and the binarymake build-css- Build the stylesheet of the pages (static/styles.css) after changing the templatesmake run- Run the binarymake build-docker- Build the docker image (it builds the admin web too)make run-docker- Run the docker imagemake test- Run the testsmake clean- Clean the binarymake lint- Run the lintermake hash text=plain_text_to_hash- Generate a hash from a giventext
The server reads these environment variables (and the .env file, if it exists):
MINIOIDC_ADDR- The address to listen on (default::8000)MINIOIDC_CONFIG- The path to the yaml configuration file. Without it, the server starts with the default configurationMINIOIDC_DATA- WithoutMINIOIDC_CONFIG, the directory of the data (default:data,/datain Docker)MINIOIDC_ISSUER- WithoutMINIOIDC_CONFIG, the issuer (default:http://localhost:<port>/)MINIOIDC_SETUP_CODE- The code to create the first administrator (at least 12 characters). By default it is random and written in the log
The configuration file is a yaml file with the following structure:
name: My MiniOIDC
masterkey: ${MINIOIDC_MASTERKEY} # at least 32 characters
issuer: https://sso.example.com
audience: https://sso.example.com
require_mfa: true
reuse_refresh_tokens: false
grants:
implicit: false # the implicit and hybrid flows (discouraged)
password: false # the resource owner password grant (discouraged)
private_rsa_key_path: private_key.pem
previous_rsa_key_paths: []
middlewares:
hsts: true
csp: true
secure_cookies: true
forward_headers: true
log_requests: true
ttl:
access: 20 # minutes
refresh: 129600 # 90 days
session: 129600 # 90 days
code: 5 # minutes
csrf: 5 # minutes
sqlite:
filepath: db.sqlite3
use_in_grants: true
use_in_sessions: true
use_in_mfa: true
use_in_client_assertions: true
use_in_users: true
use_in_clients: true
admin:
enabled: true
group: admin
ldap_server:
enabled: true
address: :3389
base_dn: dc=example,dc=com
bind_dn: cn=service,dc=example,dc=com
bind_password_hash: $2a$12$...
allow_anonymous: false
users_can_search: false
tls:
mode: starttls
cert_path: /certificates/ldap.crt
key_path: /certificates/ldap.key
templates:
base: templates/base.html
login: templates/login.html
mfa_create: templates/mfa_create.html
mfa_verify: templates/mfa_verify.html
logout: templates/logout.html
device: templates/device.html
error: templates/error.html
profile: templates/profile.html
profile_password: templates/profile_password.html
profile_mfa: templates/profile_mfa.html
setup: templates/setup.html
clients:
- id: myclient
secret_hash: $2a$06$L6/zALdtbkYajjHTZUW29ePBEb/hwhgjhXC4YpHANavvKDJl69ctK # secret
redirect_uris:
- https://myapp.example.com/callback
post_logout_redirect_uris:
- https://myapp.example.com/
- id: myspa
token_endpoint_auth_method: none # public client: PKCE is required
redirect_uris:
- https://spa.example.com/callback
- id: mytv
token_endpoint_auth_method: none
grant_types:
- urn:ietf:params:oauth:grant-type:device_code
- refresh_token
users:
- subject: 1
email: user@example.com
email_verified: true
preferred_username: user
name: John Doe
given_name: John
family_name: Doe
password_hash: $2a$06$03dduqc0lMbsb5go/l6RI.cRb03Hos9CMpgm5/yYuRsSQPHtrFwSq # password
phone: +1234567890
address: 1 Main St. City, State 12345
groups:
- adminThe yaml configuration file can also reference environment variables:
value1: ${ENV_VAR}
value2: $OTHER_ENV_VARname- The display name of the server (used in the pages and as TOTP issuer)masterkey- The key used to encrypt cookies, CSRF tokens and theclient_secret_jwtsecrets. It must have at least 32 characters. If it is not set, a random one is generated, and the sessions do not survive a restartissuer- The OIDC issuer: anhttp(s)URL without query or fragment. The endpoints are published under its pathaudience- The audience of the access tokens (ID tokens use the client ID as audience)require_mfa- Whether to require MFA for all users (default:false)reuse_refresh_tokens- Whether to allow re-use refresh tokens (default:false)grants.implicit- Whether the clients with theimplicitgrant type can use the implicit and hybrid flows (default:false). They are discouraged by RFC 9700: the tokens are sent through the browsergrants.password- Whether the clients with thepasswordgrant type can use the resource owner password grant (default:false). It is discouraged by RFC 9700: the client sees the password of the user. The users with MFA write their TOTP code at the end of the password, and the failed attempts count as failed loginsprivate_rsa_key_path- The path to the private RSA key, PKCS#1 or PKCS#8 PEM (if not set, a new random key will be generated)previous_rsa_key_paths- The paths to the previous signing keys (public or private PEM). To rotate the signing key, set the new key inprivate_rsa_key_pathand move the old one here: tokens signed with it are still valid and it keeps being published in the JWKS. Remove it once its tokens have expired.
hsts- Whether to enable HTTP Strict Transport Security (HSTS). It also redirects the HTTP requests to HTTPS (default:false)csp- Whether to enable Content Security Policy (CSP) and other security headers (default:false)secure_cookies- Whether to enable secure cookies (default:false)forward_headers- Whether to use theX-Forwarded-*andForwardedheaders of a reverse proxy (default:false)log_requests- Whether to log requests (default:false)
access- The access and ID tokens TTL in minutes (default:20)refresh- The refresh token TTL in minutes (default:129600, 90 days)session- The session TTL in minutes (default:129600, 90 days)code- The authorization code TTL in minutes (default:5)csrf- The CSRF token TTL in minutes (default:5)
filepath- The path to the SQLite database file. It is mandatory if anyuse_in_*option is set totrue. The database uses WAL mode, so the-waland-shmfiles are created next to it. The tables are created and migrated on start upuse_in_grants- Whether to store the codes, refresh tokens and device codes in the database (default:false)use_in_sessions- Whether to store the sessions in the database (default:false)use_in_mfa- Whether to store the MFA secrets in the database (default:false)use_in_users- Whether to store the users in the database, so they can be managed from the admin web (default:false). It cannot be used with LDAP usersuse_in_clients- Whether to store the clients in the database, so they can be managed from the admin web (default:false)use_in_client_assertions- Whether to store the usedjtiof theclient_secret_jwtandprivate_key_jwtassertions in the database (default:false). Enable it when several instances share the database, so an assertion used in one instance cannot be replayed in another one
With
use_in_usersanduse_in_clients, the users and clients of the configuration file are only added to the database when they do not exist: after that, the database is the source of truth, so the changes made in the admin web are kept after a restart.
Several instances can share the SQLite file only in a local file system (not a network file system like NFS or SMB, where the SQLite locks are not reliable).
Use the users of an LDAP server to log in:
ldap:
server: localhost:389
bind: uid=admin,cn=users,dc=example,dc=com
password: ${LDAP_PASSWORD}
base_dn: dc=example,dc=com
filter_dn: (&(uid={username})(objectClass=person))
email_verified: false
attributes:
subject: uidNumber
name: uid
display_name: cn
given_name: givenName
family_name: sn
email: mail
phone: telephoneNumber
address: postalAddress
groups: memberOfserver- The LDAP server addressbind- The LDAP bind DNpassword- The LDAP bind passwordbase_dn- The LDAP base DNfilter_dn- The LDAP filter to find a user.{username}is replaced with the escaped usernameemail_verified- Whether the emails of the directory are verified (default:false)attributes.subject- The LDAP attribute to use for the user subjectattributes.name- The LDAP attribute to use for the username (preferred_usernameclaim)attributes.display_name- Optional. The LDAP attribute to use for thenameclaimattributes.given_name- Optional. The LDAP attribute to use for thegiven_nameclaimattributes.family_name- Optional. The LDAP attribute to use for thefamily_nameclaimattributes.email- The LDAP attribute to use for the user emailattributes.phone- The LDAP attribute to use for the user phoneattributes.address- The LDAP attribute to use for the user addressattributes.groups- Optional. The LDAP attribute with the groups of the user (e.g.memberOf)
If you use LDAP users, you can not configure the
userssection,sqlite.use_in_usersor theldap_serversection.
base- The base templatelogin- The login page templatemfa_create- The MFA create page templatemfa_verify- The MFA verify page templatelogout- The logout page templatedevice- The device activation page templateerror- The error pages template (e.g. not found)profile,profile_password,profile_mfa- The user profile page templatessetup- The onboarding page template: the form of the first administrator and the next steps
The paths are relative to the working directory: if a template does not exist, minioidc does not start.
In your templates, use
{{ basePath }}to build the local URLs (e.g.{{ basePath }}/static/styles.css), so they work when the issuer has a path.
The pages and the admin web share a design system built with Tailwind CSS (web/theme/components.css), with light and dark modes that follow the system. static/styles.css has all its components, so your templates can use them without compiling the stylesheet:
| Classes | Use |
|---|---|
page, page-brand, page-card, page-title, page-subtitle, page-footer |
The layout of base.html: a centered card |
stack, menu, menu-item |
Vertical spacing and lists of options |
btn-primary, btn-secondary, btn-danger, btn-ghost, btn-block |
Buttons |
label, input, code-input, hint |
Forms (code-input is the 6-digit code input) |
alert-error, alert-success, alert-warning, alert-info |
Messages |
card, badge, link, brand-mark, qr, secret, step, step-done |
Other components |
If you use other Tailwind classes in your templates, compile the stylesheet with make build-css.
You can add your static files (like css, js, images, ...) in the
staticfolder and use them in your templates (e.g.<link rel="stylesheet" href="{{ basePath }}/static/css/style.css">).
The OIDC clients. Each client has the following properties:
id- The client IDsecret_hash- The client secret hash (use themake hashcommand to generate a hash). Leave it empty for public clients (SPAs, mobile apps): they must use PKCEredirect_uris- The list of allowed redirect URIs. Leave it empty for resource servers that only introspect tokenspost_logout_redirect_uris- The list of allowed redirect URIs after logouttoken_endpoint_auth_method- The only authentication method the client can use:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwtornone. If it is not set, the client can useclient_secret_basicandclient_secret_postwhen it hassecret_hash, ornoneotherwisesecret- The plain secret, only forclient_secret_jwt(at least 32 characters). The assertions are signed with HMAC, so the server needs the secret itself: use an environment variable (e.g.secret: ${MYCLIENT_SECRET})jwks_uri- Forprivate_key_jwt: the URL of the client JWKS. It is cached for 10 minutes and downloaded again when an assertion uses an unknown key (at most once per minute)public_key_paths- Forprivate_key_jwt: PEM files with the public keys (RSA, EC or Ed25519) or certificates of the clientgrant_types- The grant types the client can use (default:authorization_codeandrefresh_token):authorization_code,refresh_token,client_credentials(confidential clients),urn:ietf:params:oauth:grant-type:device_code,urn:ietf:params:oauth:grant-type:jwt-bearer(with keys),implicitandpassword(these two also need thegrantssection)
The JWT bearer grant (RFC 7523 section 2.1) lets a client get tokens of any user with an assertion signed with its own keys (jwks_uri, public_key_paths or the client_secret_jwt secret): iss is the client ID, sub the subject of the user, aud the issuer or the token endpoint URL, and it needs a unique jti and an exp. It issues no ID token (the user has not authenticated). Enable it only for trusted clients.
The device flow (RFC 8628) shows the user code as XXXX-XXXX (the users can type it in lowercase and without the dash). The users confirm the client and the permissions in /connect/device before authorizing it. The device codes are grants: they are stored with the codes and refresh tokens (in SQLite with sqlite.use_in_grants, so they are kept after a restart and shared by the instances that use the same database). They expire in 10 minutes.
clients:
- id: backend
token_endpoint_auth_method: private_key_jwt
jwks_uri: https://backend.example.com/.well-known/jwks.json
redirect_uris:
- https://backend.example.com/callback
- id: worker
token_endpoint_auth_method: client_secret_jwt
secret: ${WORKER_SECRET}
redirect_uris:
- https://worker.example.com/callback
- id: my-api # a resource server that only introspects tokens
secret_hash: $2a$12$...The JWT assertions (client_assertion) must have iss and sub equal to the client ID, aud equal to the issuer, the token endpoint URL or the URL of the endpoint where they are used, a unique jti and an exp at most one hour ahead. Each jti can only be used once: the used ones are kept in memory, or in SQLite with use_in_client_assertions.
The introspection endpoint can be used by any confidential client to introspect access tokens (e.g. resource servers registered as clients without redirect URIs). Refresh tokens can only be introspected by the client they were issued to. Public clients cannot use it.
The users to log in. Each user has the following properties:
subject- The user subject (subclaim)preferred_username- The username used to log inpassword_hash- The password hash (use themake hashcommand to generate a hash)email- The user emailemail_verified- Whether the user email is verified (default:false)name,given_name,family_name- Optional. The user namesphone- The user phone numberaddress- The user addressgroups- The list of user groups (groupsclaim). The members of the admin group can use the admin web
enabled- Whether to serve the admin web at<issuer>/admin/(default:false). It requiressqlite.use_in_clientsgroup- The group of the administrators (default:admin)
See Admin web.
enabled- Whether to serve the users as an LDAP directory (default:false)address- The address to listen on (default::3389)base_dn- The base DN of the directory (e.g.dc=example,dc=com)bind_dnandbind_password_hash- Optional. The service account the applications use to search the directory (use themake hashcommand to generate the hash)allow_anonymous- Whether anonymous clients can search the directory (default:false)users_can_search- Whether the users bound with their own credentials can search all the directory (default:false: they can only read their own entry)tls.mode-""(plain LDAP),ldapsorstarttlstls.cert_pathandtls.key_path- The PEM certificate and private key forldapsandstarttls
See LDAP server.
When the clients (and optionally the users) are stored in SQLite, minioidc can serve an admin web at <issuer>/admin/:
admin:
enabled: true
group: admin # default: admin
sqlite:
filepath: minioidc.sqlite3
use_in_users: true # manage the users (not available with LDAP)
use_in_clients: true # required by the admin web
use_in_grants: true
use_in_sessions: true
use_in_mfa: true
use_in_client_assertions: trueOnly the members of the admin group can use it (with the MFA verified, if it is required). With LDAP users, map the groups with ldap.attributes.groups (e.g. memberOf): the admin group can be the group DN or its name (e.g. admin matches cn=admin,ou=groups,dc=example,dc=com).
From the admin web you can:
- Users: create, edit and delete users, change their passwords and groups, see and close their sessions, reset their MFA and unblock them. The users of LDAP or of the configuration file (without
use_in_users) are read only. - Clients: create, edit and delete clients, rotate their secrets (the secrets are generated by the server and shown only once) and revoke their tokens.
- Maintenance: purge the expired data and log out all the users.
A client of the configuration file deleted in the admin web is added again in the next start: remove it from the configuration file too.
The admin web is in web/admin (TypeScript, React, Vite and Tailwind CSS) and it is embedded in the binary: run make build-admin before make build (the docker image builds it). Without it, /admin/ shows how to build it. To develop it, run minioidc at http://localhost:8000, log in as an administrator and run npm run dev in web/admin: the API requests are proxied to minioidc.
When the users are stored in SQLite (sqlite.use_in_users) and there are none, minioidc starts the onboarding:
- It writes a link with a setup code in the log:
There are no users yet: create the administrator at http://localhost:8000/setup?code=ABCD-EFGH-IJKL-MNOP. Only who can read the log (who runs the server) can use it. - Until the administrator exists, the login page sends to
<issuer>/setup, where the setup code (if you do not open the link), the username, the name, the email and the password are asked. - The administrator is created in the admin group (
admin.group) and signed in (withrequire_mfa, the authenticator app is added first). The welcome page shows the next steps: register the first application in the admin web, add an authenticator app, and the issuer and discovery URLs for the applications.
After that, /setup is closed. Set MINIOIDC_SETUP_CODE to choose the code (e.g. in automated deployments); otherwise, a new random one is generated in every start while there are no users. The users of the configuration file are added before, so there is no onboarding when there are users in users.
Without MINIOIDC_CONFIG, minioidc starts with this configuration, so you only need the binary (or the docker image) to try it:
- All the data in SQLite (
<MINIOIDC_DATA>/minioidc.sqlite3): users, clients, sessions, grants, MFA and client assertions. - The admin web enabled, for the
admingroup. - The master key (
masterkey) and the signing key (private.pem) generated in the first start and kept inMINIOIDC_DATA(only readable by the owner): back them up with the database. - The issuer and the audience from
MINIOIDC_ISSUER. - The Content Security Policy enabled, and with an
httpsissuer, the secure cookies and HSTS.
When you need more options (LDAP, the LDAP server, MFA required, TTLs…), write a configuration file with the same sqlite.filepath, masterkey and private_rsa_key_path: the users and clients created in the admin web are kept.
minioidc can serve its users (from SQLite or the configuration file, not from LDAP) as a read only LDAP directory, so the applications that only support LDAP (NAS, media servers, git servers…) can use the same users:
ldap_server:
enabled: true
address: :3389 # default
base_dn: dc=example,dc=com
bind_dn: cn=service,dc=example,dc=com # service account for the applications (optional)
bind_password_hash: $2a$12$... # make hash text=...
allow_anonymous: false # anonymous searches (default: false)
users_can_search: false # the users can search all the directory (default: only their own entry)
tls:
mode: starttls # "" (plain LDAP), ldaps or starttls
cert_path: /certificates/ldap.crt
key_path: /certificates/ldap.keyThe directory has this structure:
dc=example,dc=com
├── ou=users uid=<username> (inetOrgPerson: uid, cn, sn, givenName, displayName, mail,
│ telephoneNumber, postalAddress, memberOf, employeeNumber = OIDC subject)
└── ou=groups cn=<group> (groupOfNames: cn, member)
- Bind: the service account, the users (
uid=<username>,ou=users,<base dn>or just the username) and anonymous binds. The passwords are never published. - MFA: an LDAP bind cannot ask for a second factor, so the users with MFA (or all the users with
require_mfa) must write their TOTP code at the end of the password:mypassword123456. Withrequire_mfa, the users that have not enrolled an authenticator app yet cannot bind. - Brute force protection: the failed binds count as failed logins: the user is blocked for 15 minutes after 5 failures, both in LDAP and in the login page.
- Search: base, one level and subtree searches with any filter, attribute selection and size limit. The changes are visible at once (e.g. a user created in the admin web).
- Read only: add, modify and delete answer
unwillingToPerform.
Without TLS, the passwords are sent in clear text: use
ldapsorstarttlsunless the network is trusted. Withstarttls, the clients must start TLS before sending credentials.
The databases are migrated on start up, including the SQLite databases of all the previous versions: make a backup first. Read the changes of your version:
- The users sign in again: the cookies, codes, refresh tokens and CSRF tokens of previous versions are no longer valid (they are encrypted with AES-GCM and the tokens are stored hashed).
issuerandaudienceare required, and themasterkeymust have at least 32 characters.- The clients can only use the grant types of their
grant_types(default:authorization_codeandrefresh_token): addclient_credentials, the device code or the JWT bearer grant types to the clients that use them. - The Implicit, Hybrid and Password grants are disabled by default: enable them in the
grantssection and in thegrant_typesof the clients that need them. - PKCE
plainis no longer supported (onlyS256), and it is required for the public clients. email_verifiedisfalseunless it is set in the users (or inldap.email_verified).phone_numberandaddressare returned with thephoneandaddressscopes (not withprofile), andaddressis a JSON object ({"formatted": "..."}).- The endpoints are published under the path of the issuer. The OAuth endpoints are under
/connect/(/connect/authorize,/connect/token…, see Endpoints): the clients that use the discovery document are not affected. - The MFA enrollments with the SHA256 TOTP of previous versions keep working. The new ones use SHA1, supported by every authenticator app.
- Templates: the pages have a new design and there is a new
errortemplate. The Content Security Policy does not allow inline scripts any more: if you have custom templates, move their scripts to files instatic/and use{{ basePath }}in their URLs.
These versions had security issues, fixed now. The behaviour of some endpoints is now the standard one, so check your clients:
- The token endpoint authenticates the clients in all the grants, and the codes, refresh tokens and device codes can only be used by the client that received them, and only once.
expires_inis in seconds (it was in nanoseconds), and the ID tokens live as long as the access tokens (they lived as long as the refresh tokens).- The errors follow RFC 6749:
invalid_grantis a 400 (it was a 401), an unknown grant type isunsupported_grant_type, and the errors of the authorization endpoint are redirected to the client once the client and theredirect_uriare valid. - The introspection returns
"active": falseas a boolean (it was a string), does not accept ID tokens and needs a confidential client. - The revocation answers 200 (it was 204) and revoking an access token answers
unsupported_token_type: they are short lived JWTs that stop working when their session is closed. - The JWT bearer grant only accepts assertions signed by the client (RFC 7523), not the tokens of minioidc, and it issues no ID token.
- The Client Credentials tokens have no user scopes and no refresh token.
- The logout validates
post_logout_redirect_uriwith the registeredpost_logout_redirect_uris, and withoutid_token_hintthe user confirms it. - Removing an authenticator app in the profile requires a code of a current one.
You can use the minioidc package in your projects to add OIDC authentication to your web applications:
package main
import (
"log"
"net/http"
"github.com/fernandoescolar/minioidc/pkg/api"
)
func main() {
builder := &api.Builder{
Audience: "https://api.example.com",
Issuer: "https://minioidc.example.com",
MasterKey: "a master key of at least 32 characters",
Clients: []api.Client{
{
ID: "myclient",
SecretHash: "$2a$06$L6/zALdtbkYajjHTZUW29ePBEb/hwhgjhXC4YpHANavvKDJl69ctK", // secret
RedirectURIs: []string{"https://api.example.com/callback"},
},
},
Users: []api.User{
{
Subject: "0000001",
PreferredUsername: "user",
PasswordHash: "$2a$06$03dduqc0lMbsb5go/l6RI.cRb03Hos9CMpgm5/yYuRsSQPHtrFwSq", // password
},
},
}
minioidc, err := builder.Build()
if err != nil {
log.Fatal(err)
}
defer minioidc.Close()
// optional: only if builder.LDAPServer is set
if err := minioidc.StartLDAPServer(); err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
handler := minioidc.Wrap(mux)
log.Printf("Listening http://localhost:8000")
err = http.ListenAndServe(":8000", handler)
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}The builder has the following fields:
Name string- The display name (default:minioidc)MasterKey string- The key to encrypt the internal data, at least 32 characters (if not set, a new random key will be generated)Issuer string- The OIDC issuer (MANDATORY)Audience string- The audience of the access tokens (MANDATORY)RequireMFA bool- Whether to require MFA for all users (default:false)ReuseRefreshTokens bool- Whether to allow re-use refresh tokens (default:false)AllowImplicitFlow,AllowPasswordGrant bool- Enable the discouraged flows (default:false)PrivateRSAKey *rsa.PrivateKeyorPrivateRSAKeyFilepath string- The signing key (if not set, a new random key will be generated)PreviousRSAPublicKeys []*rsa.PublicKeyandPreviousRSAKeyFilepaths []string- The previous signing keys (key rotation)UseHSTS,UseCSP,UseSecureCookie,UseForwardedHeaders,LogRequests bool- The middlewares (default:false)AccessTTL,RefreshTTL,SessionTTL,CodeTTL,CSRFTTL time.Duration- The TTLsBaseTemplateFilepath,LoginTemplateFilepath,MFACreateTemplateFilepath,MFAVerifyTemplateFilepath,LogoutTemplateFilepath,DeviceTemplateFilepath,ProfileTemplateFilepath,ProfilePasswordTemplateFilepath,ProfileMFATemplateFilepath string- The templatesClientStore,UserStore,GrantStore,SessionStore,MFACodeStore,UserBlockedStore,ClientAssertionStore,DeviceCodeStore- Custom stores (default: in memory, or SQLite withUseSQLite)Clients []Client- The clients:ID,SecretHash,Secret,RedirectURIs,PostLogoutRedirectURIs,TokenEndpointAuthMethod,JWKSURI,PublicKeyPaths,PublicKeysandGrantTypesUsers []User- The users:Subject,PreferredUsername,PasswordHash,Email,EmailVerified,Name,GivenName,FamilyName,Phone,AddressandGroupsAdminEnabled boolandAdminGroup string- The admin webLDAPServer *domain.LDAPServerConfig- The LDAP server (nil to disable it)
And the builder has the following methods:
UseSQLite(string, SqliteDatabases)- Set the SQLite database file path and the data stored in it (flags:NoSqliteDatabases,Grants,Sessions,MFA,ClientAssertions,Users,ClientsorAll, e.g.api.Grants | api.Sessions)UseLDAP(string, LDAPConfig)- Set the LDAP server address and LDAP config to use
The LDAPConfig struct has the following fields:
Bind string- The LDAP bind DNPassword string- The LDAP bind passwordBaseDN string- The LDAP base DNFilterDN string- The LDAP filter to find a user ({username}is replaced with the escaped username)SubjectAttribute string- The LDAP attribute to use for the user subjectNameAttribute string- The LDAP attribute to use for the username (preferred_username)DisplayNameAttribute,GivenNameAttribute,FamilyNameAttribute string- Optional. The attributes of the user namesEmailAttribute string- The LDAP attribute to use for the user emailPhoneAttribute string- The LDAP attribute to use for the user phoneAddressAttribute string- The LDAP attribute to use for the user addressGroupsAttribute string- Optional. The LDAP attribute with the groups of the userEmailVerified bool- Whether the emails of the directory are verified
The Minioidc returned by Build() has the following methods:
Handler() http.Handler- A handler with all the endpointsWrap(*http.ServeMux) http.Handler- Adds the endpoints to your muxStartLDAPServer() error- Starts the LDAP server, if it is configuredClose()- Stops the background jobs and the LDAP serverCleanExpired()- Removes the expired data (it also runs every hour)
make test # all the tests
go test ./tests/integration/... # the end to end tests of the flows
go test -race ./... # with the race detectorThe integration tests (tests/integration) run a server with all the grant types enabled and test the flows as a client. pkg/api has the end to end tests of the configuration features (SQLite, admin web, LDAP server, issuer path…).
minioidc is intended for small / home networks and has not undergone a formal security audit. For production use, please review the following:
- Run it behind a TLS-terminating reverse proxy (nginx, Caddy, Traefik) and enable
secure_cookies,hstsandcsp(andforward_headers, so it knows the original scheme). - Use a strong, random
masterkey(32+ characters) and keep it secret: it encrypts the cookies and theclient_secret_jwtsecrets. - Keep the RSA private key outside the repository and rotate it with
previous_rsa_key_paths. - Enable MFA (
require_mfa) and keep the discouraged grants (grants) disabled unless a client really needs them. - Enable the JWT bearer grant only for trusted clients: they can get tokens of any user.
- Use
ldapsorstarttlsin the LDAP server unless the network is trusted.
The server protects against brute force attacks (5 failed attempts block a user for 15 minutes), CSRF, open redirects, token replays (a reused code or refresh token revokes its family) and clickjacking.
To report a vulnerability, please open a GitHub issue marked [SECURITY] or contact the maintainer directly.
Contributions are welcome. Please:
- Fork the repository and create a feature branch.
- Add tests for any new functionality (
tests/integrationfor the flows,pkg/apifor the configuration features). - Run
make lintandmake testand ensure both pass. If you change the templates or the admin web, runmake build-admin. - Open a pull request with a clear description of the change.
This project is licensed under the MIT License - see the LICENSE file for details