H-ID is an OpenID Connect identity provider and forward authentication gateway.
This is a hobby project. There are ABSOLUTELY NO security guarantees!
- Sign up (with optional invite-only mode)
- Admin accounts
- user management (invite/view/delete registered users)
- Sign in
- Email/password authentication
- 2FA with TOTP and recovery codes
- Passkeys
- Forgot password
- Account settings
- Set/update profile picture
- Change name/email
- OAuth2 client management
- every user can register/manage their own clients
- OAuth2/OpenID Connect
- Authorization Code Flow
- Available scopes:
openid,profile,email - Consent dialog (remembered per user-client combination)
- Auth gateway
- Assign groups to users
- Allow access per (sub)domain for users/groups
- Interface
- Mobile & desktop layout
- Automatic light/dark mode
Create a h-id directory and execute these commands in it:
mkdir ./data
mkdir ./profile-pictures
touch docker-compose.ymlPaste the following content into docker-compose.yml and change the environment variables to your desired values:
services:
h-id:
image: ghcr.io/juho05/h-id
restart: unless-stopped
user: 1000:1000 # change to your uid:gid combination
environment:
- BASE_URL=https://id.example.com # the URL where you plan to host H-ID
- TRUSTED_PROXY_COUNT=1 # the number of TRUSTED proxies between this app and the open internet
- INVITE_ONLY=false # set to true if you want to disable user sign-up without invitation
- EMAIL_HOST=<smtp-host> # SMTP host, e.g. smtp.gmail.com:587
- EMAIL_USERNAME=<smtp-username> # SMTP username, e.g. example@gmail.com
- EMAIL_PASSWORD=<password> # SMTP password (use an app password if 2FA is activated on your email account)
- HCAPTCHA_SITE_KEY=<site-key> # leave blank to disable CAPTCHAs
- HCAPTCHA_SECRET=<secret> # leave blank to disable CAPTCHAs
- SESSION_LIFETIME=24h # lifetime of user sessions, e.g. 8h15m,96h,15m,86400s (I recommend short durations when H-ID is not used as an auth gateway)
- SESSION_IDLE_TIMEOUT=4h # time after which the user is signed out without activity, e.g. 8h15m,96h,15m,86400s (I recommend short durations when H-ID is not used as an auth gateway)
volumes:
- ./data:/data
- ./profile-pictures:/profile_pictures
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/data:/data
- ./caddy/config:/configTo configure the reverse proxy caddy create a new directory caddy in h-id and create a file named Caddyfile in it with the following content (change id.example.com to your domain):
id.example.com {
reverse_proxy h-id:8080
}You should now be able to start H-ID by executing the following command in the h-id directory:
docker compose up -dH-ID will be accessible (if you can access port 443 on your server and your domain is properly linked) at https://<your-domain>.
You can promote a user to be an administrator with:
docker compose exec h-id /h-id-cli set-admin user@example.com true # you can also use the user ID instead of the email addressThis will make the /admin endpoint accessible to that user.
To invite a user from the CLI (useful if INVITE_ONLY is set before a user was created) execute:
docker compose exec h-id /h-id-cli invite user@example.comTo use H-ID as an auth gateway in front of another service make these changes to docker-compose.yml:
services:
# ...
h-id:
# ...
environment:
# ...
- AUTH_GATEWAY_CONFIG=/gateway.json
- AUTH_GATEWAY_DOMAIN=example.com # all of your services (including H-ID) must be hosted under this domain/a subdomain
volumes:
# ...
- ./gateway.json:/gateway.json:roAdditionally, it's recommended to use Postgres as the database backend to prevent locking errors.
Access rules can be configured in ./gateway.json, e.g.:
{
"users": {
"user1": {
"id": "<user_id>",
"groups": ["group1"]
},
"user2": {
"id": "<user_id>",
"groups": ["group1"]
}
},
"domains": {
"test.example.com": {
"groups": ["group1"]
},
"*.example.com": {
"users": ["user1"]
}
}
}Group/user names can be freely chosen. User IDs must match the ID of the user in H-ID.
Domain names can either be fully qualified or use a * as a wildcard to match all subdomains (and sub-subdomains):
example.com # valid
test.example.com # valid
*.example.com # valid (matches foo.example.com, bar.example.com, foo.bar.example.com, …)
*.*.example.com # invalidExample configuration to protect a service hosted with Caddy with H-ID:
my-service.example.com {
forward_auth h-id:8080 {
uri /gateway/verify
copy_headers Remote-User
}
reverse_proxy my-service:80
}Make sure that caddy, my-service and h-id are all on the same Docker network.
Bold: required
| Name | Values | Default | Description |
|---|---|---|---|
| BASE_URL | e.g. https://example.com, https://foo.example.com |
empty | The base URL where H-ID is hosted |
| EMAIL_HOST | e.g. smpt.google.com:587 |
empty | The SMTP host to use for sending emails |
| EMAIL_USERNAME | string | empty | The username of the email account to use for sending emails |
| EMAIL_PASSWORD | string | empty | The password (or app password if 2FA is activated on the email account) of the email account to use for sending emails |
| AUTO_MIGRATE | true/false |
true (Docker), false (otherwise) |
Applies database migrations on start |
| Local | true/false |
false |
Hosts H-ID on localhost instead of 0.0.0.0 |
| INVITE_ONLY | true/false |
false |
Requires an invitation to register a new user. Invitations can be sent by an admin at /admin/user/invite |
| TRUSTED_PROXY_COUNT | integer | 0 |
The number of TRUSTED proxies that append the X-Forwarded-For header between this H-ID and the open internet. Used for client-IP detection when rate-limiting. This value MUST exactly match the real number of proxies that append X-Forwarded-For, and H-ID MUST only be reachable through them. Setting it too high lets a client forge extra X-Forwarded-For entries to control the detected IP and bypass rate limiting. Setting it too low keys the limiter on an internal proxy IP so unrelated clients share one bucket. Leave at 0 when H-ID is exposed directly. NOTE: it might be necessary to configure the trusted proxies setting for your proxies too for correct results. |
| PORT | 1-65535 | 8080 |
The port H-ID listens on |
| LOG_LEVEL | 0-5 | 4 |
The log level of H-IDs logger. Min: 0 (no logs), max: 5 (trace) |
| LOG_FILE | filepath, e.g. ./h-id.log |
STDERR | Where to write log messages |
| LOG_APPEND | true/false |
false |
Whether to append logs to an existing file or replace the file on start |
| BCRYPT_COST | >0 | 12 |
The bcrypt cost to use for password hashing (larger values are more secure but lead to longer login times) |
| DB_FILE | filepath, e.g. ./h-id.db |
/database.sqlite (Docker), database.sqlite (otherwise) |
Where the database file is located. The database is created if it does not already exist. |
| POSTGRES_HOST | e.g. localhost, 127.0.0.1 |
empty | The host where the Postgres database is located. Enables Postgres database backend |
| POSTGRES_PORT | 1-65535 | 5432 | The port of the Postgres database |
| POSTGRES_DB | string | hid |
The name of the Postgres database |
| POSTGRES_USER | string | hid |
The user to use when connecting to Postgres |
| POSTGRES_PASSWORD | string | empty | (required when POSTGRES_HOST is set) The password of $POSTGRES_USER |
| SESSION_LIFETIME | 24h,60m,3h5m3s |
72h |
The lifetime of user sessions. I recommend short values when H-ID is not used as an auth gateway. |
| SESSION_IDLE_TIMEOUT | 24h,64m,3h5m3s |
24h |
The time after which users without activity are signed out. I recommend short values when H-ID is not used as an auth gateway. |
| AUTH_GATEWAY_CONFIG | filepath, e.g. ./gateway.json |
empty | The location of the auth gateway config file. Empty file -> access always denied |
| AUTH_GATEWAY_DOMAIN | domain, e.g. example.com, foo.example.com |
domain of H-ID | The parent domain of H-ID and all services protected by H-ID |
| AUTH_GATEWAY_LIFETIME | 24h,64m,3h5m3s |
720h |
The lifetime of the forward-auth (h-id_gateway) cookie used by the auth gateway. |
| TLS_CERT | filepath, e.g. ./cert.pem |
empty | Path to a TLS certificate. Empty -> no HTTPS (usually not necessary because TLS is handled by a reverse proxy like Caddy) |
| TLS_KEY | filepath, e.g. ./key.pem |
empty | Path to a TLS key. Empty -> no HTTPS (usually not necessary because TLS is handled by a reverse proxy like Caddy) |
| PROFILE_PICTURE_DIR | dirpath, e.g. ./profile-pictures |
./profile_pictures |
Directory where profile pictures are stored |
| PROFILE_PICTURE_SIZE | width/height in pixels, e.g. 512, 1024, 128, 2048, … |
1024 |
The size in pixel at which profile pictures are stored on disk |
| HCAPTCHA_SITE_KEY | string | empty | hCaptcha site key. Empty -> CAPTCHAs disabled |
| HCAPTCHA_SECRET | string | empty | hCaptcha secret. Empty -> CAPTCHAs disabled |
Execute the following commands in the h-id directory:
docker compose pull h-id caddy
docker compose up -d --force-recreateSimply stop the service with docker compose down, back up the h-id directory with your favorite backup tool and start H-ID again with docker compose up -d.
make migrate-upmake runmake watchmakedocker buildx build --platform linux/arm64,linux/amd64 --tag example.com/username/repo:latest --push .make cleanCopyright (c) 2023-2026 Julian Hofmann
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.