Improve admin privilege handling for OAuth. Update documentation.

This commit is contained in:
Christoph Haas 2025-01-18 11:55:56 +01:00
parent 6523a87dfb
commit 662e9c0549
15 changed files with 1036 additions and 191 deletions

View File

@ -133,3 +133,9 @@ build-docker:
.PHONY: helm-docs
helm-docs:
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file
#< run-mkdocs: Run a local instance of MkDocs
.PHONY: run-mkdocs
run-mkdocs:
python -m venv venv; source venv/bin/activate; pip install mike cairosvg mkdocs-material mkdocs-minify-plugin mkdocs-swagger-ui-tag
venv/bin/mkdocs serve

198
README.md
View File

@ -55,98 +55,107 @@ By default, WireGuard Portal uses a SQLite database. The database is stored in *
### Configuration Options
The following configuration options are available:
| configuration key | parent key | default_value | description |
|----------------------------------|------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
| log_json | advanced | false | Logs in JSON format. |
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
| use_ip_v6 | advanced | true | Enable IPv6 support. |
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html |
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
| host | mail | 127.0.0.1 | The mail-server address. |
| port | mail | 25 | The mail-server SMTP port. |
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
| username | mail | | The SMTP user name. |
| password | mail | | The SMTP password. |
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
| client_id | auth/oidc | | The OAuth client id. |
| client_secret | auth/oidc | | The OAuth client secret. |
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
| client_id | auth/oauth | | The OAuth client id. |
| client_secret | auth/oauth | | The OAuth client secret. |
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
| token_url | auth/oauth | | The URL for the token endpoint. |
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
| scopes | auth/oauth | | OAuth scopes. |
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
| tls_key_path | auth/ldap | | A path to the TLS key. |
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
| bind_pass | auth/ldap | | The bind password. |
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| debug | database | false | Debug database statements (log each statement). |
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
| request_logging | web | false | Log all HTTP requests. |
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
| listening_address | web | :8888 | The listening port of the web server. |
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
| session_secret | web | very_secret | The session secret for the web frontend. |
| csrf_secret | web | extremely_secret | The CSRF secret. |
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
| cert_file | web | | (Optional) Path to the TLS certificate file |
| key_file | web | | (Optional) Path to the TLS certificate key file |
| configuration key | parent key | default_value | description |
|----------------------------------|------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
| log_json | advanced | false | Logs in JSON format. |
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
| use_ip_v6 | advanced | true | Enable IPv6 support. |
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html |
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
| host | mail | 127.0.0.1 | The mail-server address. |
| port | mail | 25 | The mail-server SMTP port. |
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
| username | mail | | The SMTP user name. |
| password | mail | | The SMTP password. |
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
| client_id | auth/oidc | | The OAuth client id. |
| client_secret | auth/oidc | | The OAuth client secret. |
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department, is_admin and user_groups. |
| admin_mapping | auth/oidc | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| log_user_info | auth/oidc | | If true, the user info retrieved from the OIDC provider will be logged in trace level. |
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
| client_id | auth/oauth | | The OAuth client id. |
| client_secret | auth/oauth | | The OAuth client secret. |
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
| token_url | auth/oauth | | The URL for the token endpoint. |
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
| scopes | auth/oauth | | OAuth scopes. |
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin and user_groups. |
| admin_mapping | auth/oauth | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| log_user_info | auth/oauth | | If true, the user info retrieved from the OAuth provider will be logged in trace level. |
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
| tls_key_path | auth/ldap | | A path to the TLS key. |
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
| bind_pass | auth/ldap | | The bind password. |
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
| log_user_info | auth/ldap | | If true, the user info retrieved from the LDAP provider will be logged in trace level. |
| debug | database | false | Debug database statements (log each statement). |
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
| request_logging | web | false | Log all HTTP requests. |
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
| listening_address | web | :8888 | The listening port of the web server. |
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
| session_secret | web | very_secret | The session secret for the web frontend. |
| csrf_secret | web | extremely_secret | The CSRF secret. |
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
| cert_file | web | | (Optional) Path to the TLS certificate file |
| key_file | web | | (Optional) Path to the TLS certificate key file |
A sample config file can be found in the repository: [config.yml.sample](config.yml.sample).
More detailed information about the configuration can be found in the [documentation](https://wgportal.org/master/documentation/overview/) on [wgportal.org](https://wgportal.org/master/documentation/overview/).
## Upgrading from V1
@ -173,16 +182,13 @@ Ensure that the new database does not contain any data!
## V2 TODOs
* Public REST API
* Translations
* Documentation
* Audit UI
## Building
To build a standalone application, use the Makefile provided in the repository.
Go version 1.22 or higher has to be installed to build WireGuard Portal.
Go version 1.23 or higher has to be installed to build WireGuard Portal.
If you want to re-compile the frontend, NodeJS 18 and NPM >= 9 is required.
```shell

View File

@ -1,3 +1,5 @@
# More information about the configuration can be found in the documentation: https://wgportal.org/master/documentation/overview/
advanced:
log_level: trace
@ -22,7 +24,7 @@ auth:
base_dn: DC=YOURCOMPANY,DC=LOCAL
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
synchronize: false
sync_interval: 0 # sync disabled
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
registration_enabled: true
oidc:
@ -63,5 +65,28 @@ auth:
email: email
firstname: name
user_identifier: sub
is_admin: roles
registration_enabled: true
is_admin: this-attribute-must-be-true
registration_enabled: true
- id: google_plain_oauth_with_groups
provider_name: google4
display_name: Login with</br>Google4
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
- i-want-some-groups
field_map:
email: email
firstname: name
user_identifier: sub
user_groups: groups
admin_mapping:
admin_value_regex: ^true$
admin_group_regex: ^admin-group-name$
registration_enabled: true
log_user_info: true

View File

@ -0,0 +1,176 @@
Below are some sample YAML configurations demonstrating how to override some default values.
## Basic Configuration
```yaml
core:
admin_user: test@example.com
admin_password: password
import_existing: false
create_default_peer: true
self_provisioning_allowed: true
web:
site_title: My WireGuard Server
site_company_name: My Company
listening_address: :8080
external_url: https://my.externa-domain.com
csrf_secret: super-s3cr3t-csrf
session_secret: super-s3cr3t-session
request_logging: true
advanced:
log_level: trace
log_pretty: true
log_json: false
config_storage_path: /etc/wireguard
expiry_check_interval: 5m
database:
debug: true
type: sqlite
dsn: data/sqlite.db
```
## LDAP Authentication and Synchronization Configuration
```yaml
# ... (basic configuration)
auth:
ldap:
# a sample LDAP provider with user sync enabled
- id: ldap
provider_name: Active Directory
display_name: Login with</br>AD
url: ldap://srv-ad1.company.local:389
bind_user: ldap_wireguard@company.local
bind_pass: super-s3cr3t-ldap
base_dn: DC=COMPANY,DC=LOCAL
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
sync_interval: 15m
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
disable_missing: true
field_map:
user_identifier: sAMAccountName
email: mail
firstname: givenName
lastname: sn
phone: telephoneNumber
department: department
memberof: memberOf
admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL
registration_enabled: true
log_user_info: true
```
## OpenID Connect (OIDC) Authentication Configuration
```yaml
# ... (basic configuration)
auth:
oidc:
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
- id: oidc-with-admin-attribute
provider_name: google
display_name: Login with</br>Google
base_url: https://accounts.google.com
client_id: the-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
extra_scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
field_map:
user_identifier: sub
email: email
firstname: given_name
lastname: family_name
phone: phone_number
department: department
is_admin: wg_admin
admin_mapping:
- admin_value_regex: ^true$
registration_enabled: true
log_user_info: true
# a sample provider where users in the group `the-admin-group` are considered as admins
- id: oidc-with-admin-group
provider_name: google2
display_name: Login with</br>Google2
base_url: https://accounts.google.com
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
extra_scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
field_map:
user_identifier: sub
email: email
firstname: given_name
lastname: family_name
phone: phone_number
department: department
user_groups: groups
admin_mapping:
- admin_group_regex: ^the-admin-group$
registration_enabled: true
log_user_info: true
```
## Plain OAuth2 Authentication Configuration
```yaml
# ... (basic configuration)
auth:
oauth:
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
# are considered as admins
- id: google_plain_oauth-with-admin-attribute
provider_name: google3
display_name: Login with</br>Google3
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
field_map:
user_identifier: sub
email: email
firstname: name
is_admin: this-attribute-must-be-true
admin_mapping:
- admin_value_regex: ^(True|true)$
registration_enabled: true
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
# users in the group `admin-group-name` are considered as admins
- id: google_plain_oauth_with_groups
provider_name: google4
display_name: Login with</br>Google4
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
auth_url: https://accounts.google.com/o/oauth2/v2/auth
token_url: https://oauth2.googleapis.com/token
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- openid
- email
- profile
- i-want-some-groups
field_map:
email: email
firstname: name
user_identifier: sub
is_admin: this-attribute-must-be-true
user_groups: groups
admin_mapping:
admin_value_regex: ^true$
admin_group_regex: ^admin-group-name$
registration_enabled: true
log_user_info: true
```

View File

@ -0,0 +1,453 @@
# WireGuard Portal Configuration
This page provides an overview of **all available configuration options** for WireGuard Portal.
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
Complete configuration examples are available in the [Configuration Examples](./examples.md) page.
Below you will find sections like `core`, `advanced`, `statistics`, `mail`, `auth`, `database`, and `web`.
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
---
## Core
These are the primary configuration options that control fundamental WireGuard Portal behavior.
More advanced options are found in the subsequent `Advanced` section.
### `admin_user`
- **Default:** `admin@wgportal.local`
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
### `admin_password`
- **Default:** `wgportal`
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
### `editable_keys`
- **Default:** `true`
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
### `create_default_peer`
- **Default:** `false`
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
### `create_default_peer_on_creation`
- **Default:** `false`
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
### `re_enable_peer_after_user_enable`
- **Default:** `true`
- **Description:** Re-enable all peers that were previously disabled if the associated user is re-enabled.
### `delete_peer_after_user_deleted`
- **Default:** `false`
- **Description:** If a user is deleted, remove all linked peers. Otherwise, peers remain but are disabled.
### `self_provisioning_allowed`
- **Default:** `false`
- **Description:** Allow registered (non-admin) users to self-provision peers from their profile page.
### `import_existing`
- **Default:** `true`
- **Description:** On startup, import existing WireGuard interfaces and peers into WireGuard Portal.
### `restore_state`
- **Default:** `true`
- **Description:** Restore the WireGuard interface states (up/down) that existed before WireGuard Portal started.
---
## Advanced
Additional or more specialized configuration options for logging and interface creation details.
### `log_level`
- **Default:** `info`
- **Description:** The log level used by the application. Valid options are: `trace`, `debug`, `info`, `warn`, `error`.
### `log_pretty`
- **Default:** `false`
- **Description:** If `true`, log messages are colorized and formatted for readability (pretty-print).
### `log_json`
- **Default:** `false`
- **Description:** If `true`, log messages are structured in JSON format.
### `start_listen_port`
- **Default:** `51820`
- **Description:** The first port to use when automatically creating new WireGuard interfaces.
### `start_cidr_v4`
- **Default:** `10.11.12.0/24`
- **Description:** The initial IPv4 subnet to use when automatically creating new WireGuard interfaces.
### `start_cidr_v6`
- **Default:** `fdfd:d3ad:c0de:1234::0/64`
- **Description:** The initial IPv6 subnet to use when automatically creating new WireGuard interfaces.
### `use_ip_v6`
- **Default:** `true`
- **Description:** Enable or disable IPv6 support.
### `config_storage_path`
- **Default:** *(empty)*
- **Description:** Path to a directory where `wg-quick` style configuration files will be stored (if you need local filesystem configs).
### `expiry_check_interval`
- **Default:** `15m`
- **Description:** Interval after which existing peers are checked if they are expired. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
### `rule_prio_offset`
- **Default:** `20000`
- **Description:** Offset for IP route rule priorities when configuring routing.
### `route_table_offset`
- **Default:** `20000`
- **Description:** Offset for IP route table IDs when configuring routing.
### `api_admin_only`
- **Default:** `true`
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
---
## Database
Configuration for the underlying database used by WireGuard Portal.
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
### `debug`
- **Default:** `false`
- **Description:** If `true`, logs all database statements (verbose).
### `slow_query_threshold`
- **Default:** 0
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If empty or zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
### `type`
- **Default:** `sqlite`
- **Description:** The database type. Valid options: `sqlite`, `mssql`, `mysql`, `postgres`.
### `dsn`
- **Default:** `data/sqlite.db`
- **Description:** The Data Source Name (DSN) for connecting to the database.
For example:
```text
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
```
---
## Statistics
Controls how WireGuard Portal collects and reports usage statistics, including ping checks and Prometheus metrics.
### `use_ping_checks`
- **Default:** `true`
- **Description:** Enable periodic ping checks to verify that peers remain responsive.
### `ping_check_workers`
- **Default:** `10`
- **Description:** Number of parallel worker processes for ping checks.
### `ping_unprivileged`
- **Default:** `false`
- **Description:** If `false`, ping checks run without root privileges. This is currently considered BETA.
### `ping_check_interval`
- **Default:** `1m`
- **Description:** Interval between consecutive ping checks for all peers. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
### `data_collection_interval`
- **Default:** `1m`
- **Description:** Interval between data collection cycles (bytes sent/received, handshake times, etc.). Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
### `collect_interface_data`
- **Default:** `true`
- **Description:** If `true`, collects interface-level data (bytes in/out) for monitoring and statistics.
### `collect_peer_data`
- **Default:** `true`
- **Description:** If `true`, collects peer-level data (bytes, last handshake, endpoint, etc.).
### `collect_audit_data`
- **Default:** `true`
- **Description:** If `true`, logs certain portal events (such as user logins) to the database.
### `listening_address`
- **Default:** `:8787`
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787`).
---
## Mail
Options for configuring email notifications or sending peer configurations via email.
### `host`
- **Default:** `127.0.0.1`
- **Description:** Hostname or IP of the SMTP server.
### `port`
- **Default:** `25`
- **Description:** Port number for the SMTP server.
### `encryption`
- **Default:** `none`
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
### `cert_validation`
- **Default:** `false`
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
### `username`
- **Default:** *(empty)*
- **Description:** Optional SMTP username for authentication.
### `password`
- **Default:** *(empty)*
- **Description:** Optional SMTP password for authentication.
### `auth_type`
- **Default:** `plain`
- **Description:** SMTP authentication type. Valid values: `plain`, `login`, `crammd5`.
### `from`
- **Default:** `Wireguard Portal <noreply@wireguard.local>`
- **Description:** The default "From" address when sending emails.
### `link_only`
- **Default:** `false`
- **Description:** If `true`, emails only contain a link to WireGuard Portal, rather than attaching the full configuration.
---
## Auth
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
Each can have multiple providers configured. Below are the relevant keys.
---
### OIDC Provider Properties
The `oidc` array contains a list of OpenID Connect providers.
Below are the properties for each OIDC provider entry inside `auth.oidc`:
#### `provider_name`
- **Default:** *(empty)*
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
#### `display_name`
- **Default:** *(empty)*
- **Description:** A user-friendly name shown on the login page (e.g., "Login with Google").
#### `base_url`
- **Default:** *(empty)*
- **Description:** The OIDC providers base URL (e.g., `https://accounts.google.com`).
#### `client_id`
- **Default:** *(empty)*
- **Description:** The OAuth client ID from the OIDC provider.
#### `client_secret`
- **Default:** *(empty)*
- **Description:** The OAuth client secret from the OIDC provider.
#### `extra_scopes`
- **Default:** *(empty)*
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical OIDC Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
| `lastname` | `family_name` | The users last (family) name, typically provided by the IdP in the `family_name` claim. |
| `phone` | `phone_number` | The users phone number. This may require additional scopes/permissions from the IdP to access. |
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
#### `admin_mapping`
- **Default:** *(empty)*
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
#### `registration_enabled`
- **Default:** *(empty)*
- **Description:** If `true`, a new user will be created in WireGuard Portal if not already present.
#### `log_user_info`
- **Default:** *(empty)*
- **Description:** If `true`, OIDC user data is logged at the trace level upon login (for debugging).
---
### OAuth Provider Properties
The `oauth` array contains a list of plain OAuth2 providers.
Below are the properties for each OAuth provider entry inside `auth.oauth`:
#### `provider_name`
- **Default:** *(empty)*
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
#### `display_name`
- **Default:** *(empty)*
- **Description:** A user-friendly name shown on the login page.
#### `client_id`
- **Default:** *(empty)*
- **Description:** The OAuth client ID for the provider.
#### `client_secret`
- **Default:** *(empty)*
- **Description:** The OAuth client secret for the provider.
#### `auth_url`
- **Default:** *(empty)*
- **Description:** URL of the authentication endpoint.
#### `token_url`
- **Default:** *(empty)*
- **Description:** URL of the token endpoint.
#### `user_info_url`
- **Default:** *(empty)*
- **Description:** URL of the user information endpoint.
#### `scopes`
- **Default:** *(empty)*
- **Description:** A list of OAuth scopes.
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
| **Field** | **Typical Claim** | **Explanation** |
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because its guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if its unique. |
| `email` | `email` | The users email address as provided by the IdP. Not always verified, depending on IdP settings. |
| `firstname` | `given_name` | The users first name, typically provided by the IdP in the `given_name` claim. |
| `lastname` | `family_name` | The users last (family) name, typically provided by the IdP in the `family_name` claim. |
| `phone` | `phone_number` | The users phone number. This may require additional scopes/permissions from the IdP to access. |
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
#### `admin_mapping`
- **Default:** *(empty)*
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
#### `registration_enabled`
- **Default:** *(empty)*
- **Description:** If `true`, new users are created automatically on successful login.
#### `log_user_info`
- **Default:** *(empty)*
- **Description:** If `true`, logs user info at the trace level upon login.
---
### LDAP Provider Properties
The `ldap` array contains a list of LDAP authentication providers.
Below are the properties for each LDAP provider entry inside `auth.ldap`:
#### `url`
- **Default:** *(empty)*
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
#### `start_tls`
- **Default:** *(empty)*
- **Description:** If `true`, use STARTTLS to secure the LDAP connection.
#### `cert_validation`
- **Default:** *(empty)*
- **Description:** If `true`, validate the LDAP servers TLS certificate.
#### `tls_certificate_path`
- **Default:** *(empty)*
- **Description:** Path to a TLS certificate if needed for LDAP connections.
#### `tls_key_path`
- **Default:** *(empty)*
- **Description:** Path to the corresponding TLS certificate key.
#### `base_dn`
- **Default:** *(empty)*
- **Description:** The base DN for user searches (e.g., `DC=COMPANY,DC=LOCAL`).
#### `bind_user`
- **Default:** *(empty)*
- **Description:** The bind user for LDAP (e.g., `company\\ldap_wireguard` or `ldap_wireguard@company.local`).
#### `bind_pass`
- **Default:** *(empty)*
- **Description:** The bind password for LDAP authentication.
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps LDAP attributes to WireGuard Portal fields.
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|----------------------------|----------------------------|--------------------------------------------------------------|
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
| email | mail / userPrincipalName | Stores the user's primary email address. |
| firstname | givenName | Contains the user's first (given) name. |
| lastname | sn | Contains the user's last (surname) name. |
| phone | telephoneNumber / mobile | Holds the user's phone or mobile number. |
| department | departmentNumber / ou | Specifies the department or organizational unit of the user. |
| memberof | memberOf | Lists the groups and roles to which the user belongs. |
#### `login_filter`
- **Default:** *(empty)*
- **Description:** An LDAP filter to restrict which users can log in. Use `{{login_identifier}}` to insert the username.
For example:
```text
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
```
#### `admin_group`
- **Default:** *(empty)*
- **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal.
For example:
```text
CN=WireGuardAdmins,OU=Some-OU,DC=YOURDOMAIN,DC=LOCAL
```
#### `sync_interval`
- **Default:** *(empty)*
- **Description:** How frequently (in duration, e.g. `30m`) to synchronize users from LDAP. Empty or `0` disables sync. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
Only users that match the `sync_filter` are synchronized, if `disable_missing` is `true`, users not found in LDAP are disabled.
#### `sync_filter`
- **Default:** *(empty)*
- **Description:** An LDAP filter to select which users get synchronized into WireGuard Portal.
For example:
```text
(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
```
#### `disable_missing`
- **Default:** *(empty)*
- **Description:** If `true`, any user **not** found in LDAP (during sync) is disabled in WireGuard Portal.
#### `registration_enabled`
- **Default:** *(empty)*
- **Description:** If `true`, new user accounts are created in WireGuard Portal upon first login.
#### `log_user_info`
- **Default:** *(empty)*
- **Description:** If `true`, logs LDAP user data at the trace level upon login.

View File

@ -1,5 +1,5 @@
To build a standalone application, use the Makefile provided in the repository.
Go version **1.22** or higher has to be installed to build WireGuard Portal.
Go version **1.23** or higher has to be installed to build WireGuard Portal.
If you want to re-compile the frontend, NodeJS **18** and NPM >= **9** is required.
```shell

View File

@ -8,7 +8,7 @@ A sample docker-compose.yml:
version: '3.6'
services:
wg-portal:
image: wgportal/wg-portal:v2
image: wgportal/wg-portal:latest
restart: unless-stopped
cap_add:
- NET_ADMIN
@ -64,18 +64,4 @@ You should mount those directories as a volume:
- /app/data
- /app/config
### Configuration Options
All available YAML configuration options are available [here](https://github.com/h44z/wg-portal#configuration).
A very basic example:
```yaml
core:
admin_user: test@wg-portal.local
admin_password: secret
web:
external_url: http://localhost:8888
request_logging: true
```
A detailed description of the configuration options can be found [here](../configuration/overview.md).

View File

@ -22,4 +22,15 @@ For example:
```
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yml** configuration file.
Ensure that the new database does not contain any data!
Ensure that the new database does not contain any data!
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
```yaml
services:
wg-portal:
image: wgportal/wg-portal:latest
# ... other settings
restart: no
command: ["-migrateFrom=/app/data/wg_portal.db"]
```

View File

@ -2,6 +2,7 @@ package auth
import (
"context"
"encoding/json"
"fmt"
"strings"
@ -9,6 +10,7 @@ import (
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
)
type LdapAuthenticator struct {
@ -78,7 +80,10 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
return nil
}
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (map[string]interface{}, error) {
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (
map[string]interface{},
error,
) {
conn, err := internal.LdapConnect(l.cfg)
if err != nil {
return nil, fmt.Errorf("failed to setup connection: %w", err)
@ -109,6 +114,11 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
users := internal.LdapConvertEntries(sr, &l.cfg.FieldMap)
if l.cfg.LogUserInfo {
contents, _ := json.Marshal(users[0])
logrus.Tracef("User info from LDAP source %s for %s: %v", l.GetName(), userId, string(contents))
}
return users[0], nil
}

View File

@ -6,12 +6,11 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
@ -21,10 +20,16 @@ type PlainOauthAuthenticator struct {
userInfoEndpoint string
client *http.Client
userInfoMapping config.OauthFields
userAdminMapping *config.OauthAdminMapping
registrationEnabled bool
userInfoLogging bool
}
func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *config.OAuthProvider) (*PlainOauthAuthenticator, error) {
func newPlainOauthAuthenticator(
_ context.Context,
callbackUrl string,
cfg *config.OAuthProvider,
) (*PlainOauthAuthenticator, error) {
var provider = &PlainOauthAuthenticator{}
provider.name = cfg.ProviderName
@ -44,7 +49,9 @@ func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *conf
}
provider.userInfoEndpoint = cfg.UserInfoURL
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
provider.userAdminMapping = &cfg.AdminMapping
provider.registrationEnabled = cfg.RegistrationEnabled
provider.userInfoLogging = cfg.LogUserInfo
return provider, nil
}
@ -65,11 +72,19 @@ func (p PlainOauthAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCo
return p.cfg.AuthCodeURL(state, opts...)
}
func (p PlainOauthAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
func (p PlainOauthAuthenticator) Exchange(
ctx context.Context,
code string,
opts ...oauth2.AuthCodeOption,
) (*oauth2.Token, error) {
return p.cfg.Exchange(ctx, code, opts...)
}
func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, _ string) (map[string]interface{}, error) {
func (p PlainOauthAuthenticator) GetUserInfo(
ctx context.Context,
token *oauth2.Token,
_ string,
) (map[string]interface{}, error) {
req, err := http.NewRequest("GET", p.userInfoEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("failed to create user info get request: %w", err)
@ -93,57 +108,13 @@ func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.
return nil, fmt.Errorf("failed to parse user info: %w", err)
}
if p.userInfoLogging {
logrus.Tracef("User info from OAuth source %s: %v", p.name, string(contents))
}
return userFields, nil
}
func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, p.userInfoMapping.IsAdmin, ""))
userInfo := &domain.AuthenticatorUserInfo{
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, p.userInfoMapping.UserIdentifier, "")),
Email: internal.MapDefaultString(raw, p.userInfoMapping.Email, ""),
Firstname: internal.MapDefaultString(raw, p.userInfoMapping.Firstname, ""),
Lastname: internal.MapDefaultString(raw, p.userInfoMapping.Lastname, ""),
Phone: internal.MapDefaultString(raw, p.userInfoMapping.Phone, ""),
Department: internal.MapDefaultString(raw, p.userInfoMapping.Department, ""),
IsAdmin: isAdmin,
}
return userInfo, nil
}
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
defaultMap := config.OauthFields{
BaseFields: config.BaseFields{
UserIdentifier: "sub",
Email: "email",
Firstname: "given_name",
Lastname: "family_name",
Phone: "phone",
Department: "department",
},
IsAdmin: "admin_flag",
}
if f.UserIdentifier != "" {
defaultMap.UserIdentifier = f.UserIdentifier
}
if f.Email != "" {
defaultMap.Email = f.Email
}
if f.Firstname != "" {
defaultMap.Firstname = f.Firstname
}
if f.Lastname != "" {
defaultMap.Lastname = f.Lastname
}
if f.Phone != "" {
defaultMap.Phone = f.Phone
}
if f.Department != "" {
defaultMap.Department = f.Department
}
if f.IsAdmin != "" {
defaultMap.IsAdmin = f.IsAdmin
}
return defaultMap
return parseOauthUserInfo(p.userInfoMapping, p.userAdminMapping, raw)
}

View File

@ -2,14 +2,14 @@ package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
@ -19,15 +19,22 @@ type OidcAuthenticator struct {
verifier *oidc.IDTokenVerifier
cfg *oauth2.Config
userInfoMapping config.OauthFields
userAdminMapping *config.OauthAdminMapping
registrationEnabled bool
userInfoLogging bool
}
func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.OpenIDConnectProvider) (*OidcAuthenticator, error) {
func newOidcAuthenticator(
_ context.Context,
callbackUrl string,
cfg *config.OpenIDConnectProvider,
) (*OidcAuthenticator, error) {
var err error
var provider = &OidcAuthenticator{}
provider.name = cfg.ProviderName
provider.provider, err = oidc.NewProvider(context.Background(), cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
provider.provider, err = oidc.NewProvider(context.Background(),
cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
if err != nil {
return nil, fmt.Errorf("failed to create new oidc provider: %w", err)
}
@ -45,7 +52,9 @@ func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.O
Scopes: scopes,
}
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
provider.userAdminMapping = &cfg.AdminMapping
provider.registrationEnabled = cfg.RegistrationEnabled
provider.userInfoLogging = cfg.LogUserInfo
return provider, nil
}
@ -66,11 +75,17 @@ func (o OidcAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOpti
return o.cfg.AuthCodeURL(state, opts...)
}
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (
*oauth2.Token,
error,
) {
return o.cfg.Exchange(ctx, code, opts...)
}
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error) {
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
map[string]interface{},
error,
) {
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.New("token does not contain id_token")
@ -88,20 +103,14 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
}
if o.userInfoLogging {
contents, _ := json.Marshal(tokenFields)
logrus.Tracef("User info from OIDC source %s: %v", o.name, string(contents))
}
return tokenFields, nil
}
func (o OidcAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, o.userInfoMapping.IsAdmin, ""))
userInfo := &domain.AuthenticatorUserInfo{
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, o.userInfoMapping.UserIdentifier, "")),
Email: internal.MapDefaultString(raw, o.userInfoMapping.Email, ""),
Firstname: internal.MapDefaultString(raw, o.userInfoMapping.Firstname, ""),
Lastname: internal.MapDefaultString(raw, o.userInfoMapping.Lastname, ""),
Phone: internal.MapDefaultString(raw, o.userInfoMapping.Phone, ""),
Department: internal.MapDefaultString(raw, o.userInfoMapping.Department, ""),
IsAdmin: isAdmin,
}
return userInfo, nil
return parseOauthUserInfo(o.userInfoMapping, o.userAdminMapping, raw)
}

View File

@ -0,0 +1,88 @@
package auth
import (
"strings"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
// parseOauthUserInfo parses the raw user info from the oauth provider and maps it to the internal user info struct
func parseOauthUserInfo(
mapping config.OauthFields,
adminMapping *config.OauthAdminMapping,
raw map[string]interface{},
) (*domain.AuthenticatorUserInfo, error) {
var isAdmin bool
// first try to match the is_admin field against the given regex
if mapping.IsAdmin != "" {
re := adminMapping.GetAdminValueRegex()
if re.MatchString(strings.TrimSpace(internal.MapDefaultString(raw, mapping.IsAdmin, ""))) {
isAdmin = true
}
}
// next try to parse the user's groups
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
re := adminMapping.GetAdminGroupRegex()
for _, group := range userGroups {
if re.MatchString(strings.TrimSpace(group)) {
isAdmin = true
break
}
}
}
userInfo := &domain.AuthenticatorUserInfo{
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
Email: internal.MapDefaultString(raw, mapping.Email, ""),
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
Department: internal.MapDefaultString(raw, mapping.Department, ""),
IsAdmin: isAdmin,
}
return userInfo, nil
}
// getOauthFieldMapping returns the default field mapping for the oauth provider
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
defaultMap := config.OauthFields{
BaseFields: config.BaseFields{
UserIdentifier: "sub",
Email: "email",
Firstname: "given_name",
Lastname: "family_name",
Phone: "phone",
Department: "department",
},
IsAdmin: "admin_flag",
}
if f.UserIdentifier != "" {
defaultMap.UserIdentifier = f.UserIdentifier
}
if f.Email != "" {
defaultMap.Email = f.Email
}
if f.Firstname != "" {
defaultMap.Firstname = f.Firstname
}
if f.Lastname != "" {
defaultMap.Lastname = f.Lastname
}
if f.Phone != "" {
defaultMap.Phone = f.Phone
}
if f.Department != "" {
defaultMap.Department = f.Department
}
if f.IsAdmin != "" {
defaultMap.IsAdmin = f.IsAdmin
}
return defaultMap
}

View File

@ -1,9 +1,11 @@
package config
import (
"regexp"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/sirupsen/logrus"
)
type Auth struct {
@ -23,7 +25,67 @@ type BaseFields struct {
type OauthFields struct {
BaseFields `yaml:",inline"`
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin.
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin.
UserGroups string `yaml:"user_groups"` // This value specifies the claim name that contains the users groups.
}
// OauthAdminMapping contains all necessary information to extract information about administrative privileges
// from the user info fields.
//
// WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression.
// Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the
// `user_group` claim.
// If one of the cases evaluates to true, the user is granted admin rights.
type OauthAdminMapping struct {
// If the regex specified in that field matches the contents of the is_admin field, the user is an admin.
AdminValueRegex string `yaml:"admin_value_regex"`
// If any of the groups listed in the groups field matches the group specified in the admin_group_regex field, ]
// the user is an admin.
AdminGroupRegex string `yaml:"admin_group_regex"`
// internal cache fields
adminValueRegex *regexp.Regexp
adminGroupRegex *regexp.Regexp
}
func (o *OauthAdminMapping) GetAdminValueRegex() *regexp.Regexp {
if o.adminValueRegex != nil {
return o.adminValueRegex // return cached value
}
if o.AdminValueRegex == "" {
o.adminValueRegex = regexp.MustCompile("^true$") // default value is "true"
return o.adminValueRegex
}
adminRegex, err := regexp.Compile(o.AdminValueRegex)
if err != nil {
logrus.Fatalf("failed to compile admin_value_regex: %v", err)
}
o.adminValueRegex = adminRegex
return o.adminValueRegex
}
func (o *OauthAdminMapping) GetAdminGroupRegex() *regexp.Regexp {
if o.adminGroupRegex != nil {
return o.adminGroupRegex // return cached value
}
if o.AdminGroupRegex == "" {
o.adminGroupRegex = regexp.MustCompile("^wg_portal_default_admin_group$") // default value is "wg_portal_default_admin_group"
return o.adminGroupRegex
}
groupRegex, err := regexp.Compile(o.AdminGroupRegex)
if err != nil {
logrus.Fatalf("failed to compile admin_group_regex: %v", err)
}
o.adminGroupRegex = groupRegex
return o.adminGroupRegex
}
type LdapFields struct {
@ -58,6 +120,9 @@ type LdapProvider struct {
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
RegistrationEnabled bool `yaml:"registration_enabled"`
// If LogUserInfo is set to true, the user info retrieved from the LDAP provider will be logged in trace level.
LogUserInfo bool `yaml:"log_user_info"`
}
type OpenIDConnectProvider struct {
@ -81,8 +146,15 @@ type OpenIDConnectProvider struct {
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"`
// AdminMapping contains all necessary information to extract information about administrative privileges
// from the user info fields.
AdminMapping OauthAdminMapping `yaml:"admin_mapping"`
// If RegistrationEnabled is set to true, missing users will be created in the database
RegistrationEnabled bool `yaml:"registration_enabled"`
// If LogUserInfo is set to true, the user info retrieved from the OIDC provider will be logged in trace level.
LogUserInfo bool `yaml:"log_user_info"`
}
type OAuthProvider struct {
@ -108,6 +180,13 @@ type OAuthProvider struct {
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"`
// AdminMapping contains all necessary information to extract information about administrative privileges
// from the user info fields.
AdminMapping OauthAdminMapping `yaml:"admin_mapping"`
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
RegistrationEnabled bool `yaml:"registration_enabled"`
// If LogUserInfo is set to true, the user info retrieved from the OAuth provider will be logged in trace level.
LogUserInfo bool `yaml:"log_user_info"`
}

View File

@ -79,6 +79,27 @@ func MapDefaultString(m map[string]interface{}, key string, dflt string) string
}
}
// MapDefaultStringSlice returns the string slice value for the given key or a default value
func MapDefaultStringSlice(m map[string]interface{}, key string, dflt []string) []string {
if m == nil {
return dflt
}
if tmp, ok := m[key]; !ok {
return dflt
} else {
switch v := tmp.(type) {
case []string:
return v
case string:
return []string{v}
case nil:
return dflt
default:
return []string{fmt.Sprintf("%v", v)}
}
}
}
// UniqueStringSlice removes duplicates in the given string slice
func UniqueStringSlice(slice []string) []string {
keys := make(map[string]struct{})

View File

@ -21,6 +21,7 @@ theme:
features:
- navigation.instant
- navigation.tabs
- navigation.expand
plugins:
- search
@ -61,4 +62,7 @@ nav:
- Building: documentation/getting-started/building.md
- Docker Container: documentation/getting-started/docker.md
- Upgrade from V1: documentation/getting-started/upgrade.md
- Configuration:
- Overview: documentation/configuration/overview.md
- Examples: documentation/configuration/examples.md
- REST API: documentation/rest-api/api-doc.md