mirror of
https://github.com/h44z/wg-portal.git
synced 2026-04-11 18:06:21 +00:00
Signed-off-by: Michael Tupitsyn <michael.tupitsyn@gmail.com>
This commit is contained in:
@@ -144,6 +144,9 @@ auth:
|
|||||||
extra_scopes:
|
extra_scopes:
|
||||||
- https://www.googleapis.com/auth/userinfo.email
|
- https://www.googleapis.com/auth/userinfo.email
|
||||||
- https://www.googleapis.com/auth/userinfo.profile
|
- https://www.googleapis.com/auth/userinfo.profile
|
||||||
|
allowed_user_groups:
|
||||||
|
- the-admin-group
|
||||||
|
- vpn-users
|
||||||
field_map:
|
field_map:
|
||||||
user_identifier: sub
|
user_identifier: sub
|
||||||
email: email
|
email: email
|
||||||
@@ -201,6 +204,9 @@ auth:
|
|||||||
- email
|
- email
|
||||||
- profile
|
- profile
|
||||||
- i-want-some-groups
|
- i-want-some-groups
|
||||||
|
allowed_user_groups:
|
||||||
|
- admin-group-name
|
||||||
|
- vpn-users
|
||||||
field_map:
|
field_map:
|
||||||
email: email
|
email: email
|
||||||
firstname: name
|
firstname: name
|
||||||
|
|||||||
@@ -561,6 +561,10 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
|||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||||
|
|
||||||
|
#### `allowed_user_groups`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values.
|
||||||
|
|
||||||
#### `field_map`
|
#### `field_map`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||||
@@ -639,6 +643,10 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
|||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||||
|
|
||||||
|
#### `allowed_user_groups`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values.
|
||||||
|
|
||||||
#### `field_map`
|
#### `field_map`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||||
|
|||||||
@@ -66,6 +66,40 @@ auth:
|
|||||||
- "outlook.com"
|
- "outlook.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Limiting Login to Specific User Groups
|
||||||
|
|
||||||
|
You can limit the login to specific user groups by setting the `allowed_user_groups` property for OAuth2 or OIDC providers.
|
||||||
|
If this property is not empty, the user's `user_groups` claim must contain at least one matching group.
|
||||||
|
|
||||||
|
To use this feature, ensure your group claim is mapped via `field_map.user_groups`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
- provider_name: "oidc1"
|
||||||
|
# ... other settings
|
||||||
|
allowed_user_groups:
|
||||||
|
- "wg-users"
|
||||||
|
- "wg-admins"
|
||||||
|
field_map:
|
||||||
|
user_groups: "groups"
|
||||||
|
```
|
||||||
|
|
||||||
|
If `allowed_user_groups` is configured and the authenticated user has no matching group in `user_groups`, login is denied.
|
||||||
|
|
||||||
|
Minimal deny-by-group example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oauth:
|
||||||
|
- provider_name: "oauth1"
|
||||||
|
# ... other settings
|
||||||
|
allowed_user_groups:
|
||||||
|
- "vpn-users"
|
||||||
|
field_map:
|
||||||
|
user_groups: "groups"
|
||||||
|
```
|
||||||
|
|
||||||
#### Limit Login to Existing Users
|
#### Limit Login to Existing Users
|
||||||
|
|
||||||
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth2 or OIDC providers.
|
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth2 or OIDC providers.
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ type AuthenticatorOauth interface {
|
|||||||
RegistrationEnabled() bool
|
RegistrationEnabled() bool
|
||||||
// GetAllowedDomains returns the list of whitelisted domains
|
// GetAllowedDomains returns the list of whitelisted domains
|
||||||
GetAllowedDomains() []string
|
GetAllowedDomains() []string
|
||||||
|
// GetAllowedUserGroups returns the list of whitelisted user groups.
|
||||||
|
// If non-empty, at least one user group must match.
|
||||||
|
GetAllowedUserGroups() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticatorLdap is the interface for all LDAP authenticators.
|
// AuthenticatorLdap is the interface for all LDAP authenticators.
|
||||||
@@ -497,6 +500,33 @@ func isDomainAllowed(email string, allowedDomains []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAnyAllowedUserGroup(userGroups, allowedUserGroups []string) bool {
|
||||||
|
if len(allowedUserGroups) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed := make(map[string]struct{}, len(allowedUserGroups))
|
||||||
|
for _, group := range allowedUserGroups {
|
||||||
|
trimmed := strings.TrimSpace(group)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allowed[trimmed] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allowed) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range userGroups {
|
||||||
|
if _, ok := allowed[strings.TrimSpace(group)]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
|
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
|
||||||
// fetching the user information.
|
// fetching the user information.
|
||||||
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
|
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
|
||||||
@@ -524,6 +554,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
|
|||||||
return nil, fmt.Errorf("user %s is not in allowed domains", userInfo.Email)
|
return nil, fmt.Errorf("user %s is not in allowed domains", userInfo.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isAnyAllowedUserGroup(userInfo.UserGroups, oauthProvider.GetAllowedUserGroups()) {
|
||||||
|
return nil, fmt.Errorf("user %s is not in allowed user groups", userInfo.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
ctx = domain.SetUserInfo(ctx,
|
ctx = domain.SetUserInfo(ctx,
|
||||||
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
|
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
|
||||||
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(),
|
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type PlainOauthAuthenticator struct {
|
|||||||
userInfoLogging bool
|
userInfoLogging bool
|
||||||
sensitiveInfoLogging bool
|
sensitiveInfoLogging bool
|
||||||
allowedDomains []string
|
allowedDomains []string
|
||||||
|
allowedUserGroups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlainOauthAuthenticator(
|
func newPlainOauthAuthenticator(
|
||||||
@@ -60,6 +61,7 @@ func newPlainOauthAuthenticator(
|
|||||||
provider.userInfoLogging = cfg.LogUserInfo
|
provider.userInfoLogging = cfg.LogUserInfo
|
||||||
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
||||||
provider.allowedDomains = cfg.AllowedDomains
|
provider.allowedDomains = cfg.AllowedDomains
|
||||||
|
provider.allowedUserGroups = cfg.AllowedUserGroups
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,10 @@ func (p PlainOauthAuthenticator) GetAllowedDomains() []string {
|
|||||||
return p.allowedDomains
|
return p.allowedDomains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p PlainOauthAuthenticator) GetAllowedUserGroups() []string {
|
||||||
|
return p.allowedUserGroups
|
||||||
|
}
|
||||||
|
|
||||||
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
||||||
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
|
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
|
||||||
return p.registrationEnabled
|
return p.registrationEnabled
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type OidcAuthenticator struct {
|
|||||||
userInfoLogging bool
|
userInfoLogging bool
|
||||||
sensitiveInfoLogging bool
|
sensitiveInfoLogging bool
|
||||||
allowedDomains []string
|
allowedDomains []string
|
||||||
|
allowedUserGroups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOidcAuthenticator(
|
func newOidcAuthenticator(
|
||||||
@@ -61,6 +62,7 @@ func newOidcAuthenticator(
|
|||||||
provider.userInfoLogging = cfg.LogUserInfo
|
provider.userInfoLogging = cfg.LogUserInfo
|
||||||
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
||||||
provider.allowedDomains = cfg.AllowedDomains
|
provider.allowedDomains = cfg.AllowedDomains
|
||||||
|
provider.allowedUserGroups = cfg.AllowedUserGroups
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
@@ -74,6 +76,10 @@ func (o OidcAuthenticator) GetAllowedDomains() []string {
|
|||||||
return o.allowedDomains
|
return o.allowedDomains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o OidcAuthenticator) GetAllowedUserGroups() []string {
|
||||||
|
return o.allowedUserGroups
|
||||||
|
}
|
||||||
|
|
||||||
// RegistrationEnabled returns whether registration is enabled for this authenticator.
|
// RegistrationEnabled returns whether registration is enabled for this authenticator.
|
||||||
func (o OidcAuthenticator) RegistrationEnabled() bool {
|
func (o OidcAuthenticator) RegistrationEnabled() bool {
|
||||||
return o.registrationEnabled
|
return o.registrationEnabled
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ func parseOauthUserInfo(
|
|||||||
) (*domain.AuthenticatorUserInfo, error) {
|
) (*domain.AuthenticatorUserInfo, error) {
|
||||||
var isAdmin bool
|
var isAdmin bool
|
||||||
var adminInfoAvailable bool
|
var adminInfoAvailable bool
|
||||||
|
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
||||||
|
|
||||||
// first try to match the is_admin field against the given regex
|
// first try to match the is_admin field against the given regex
|
||||||
if mapping.IsAdmin != "" {
|
if mapping.IsAdmin != "" {
|
||||||
@@ -29,7 +30,6 @@ func parseOauthUserInfo(
|
|||||||
// next try to parse the user's groups
|
// next try to parse the user's groups
|
||||||
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
||||||
adminInfoAvailable = true
|
adminInfoAvailable = true
|
||||||
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
|
||||||
re := adminMapping.GetAdminGroupRegex()
|
re := adminMapping.GetAdminGroupRegex()
|
||||||
for _, group := range userGroups {
|
for _, group := range userGroups {
|
||||||
if re.MatchString(strings.TrimSpace(group)) {
|
if re.MatchString(strings.TrimSpace(group)) {
|
||||||
@@ -42,6 +42,7 @@ func parseOauthUserInfo(
|
|||||||
userInfo := &domain.AuthenticatorUserInfo{
|
userInfo := &domain.AuthenticatorUserInfo{
|
||||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||||
|
UserGroups: userGroups,
|
||||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ func Test_parseOauthUserInfo_admin_group(t *testing.T) {
|
|||||||
assert.Equal(t, info.Firstname, "Test User")
|
assert.Equal(t, info.Firstname, "Test User")
|
||||||
assert.Equal(t, info.Lastname, "")
|
assert.Equal(t, info.Lastname, "")
|
||||||
assert.Equal(t, info.Email, "test@mydomain.net")
|
assert.Equal(t, info.Email, "test@mydomain.net")
|
||||||
|
assert.Equal(t, info.UserGroups, []string{"abuse@mydomain.net", "postmaster@mydomain.net", "wgportal-admins@mydomain.net"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_parseOauthUserInfo_admin_value(t *testing.T) {
|
func Test_parseOauthUserInfo_admin_value(t *testing.T) {
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ type OpenIDConnectProvider struct {
|
|||||||
// AllowedDomains defines the list of allowed domains
|
// AllowedDomains defines the list of allowed domains
|
||||||
AllowedDomains []string `yaml:"allowed_domains"`
|
AllowedDomains []string `yaml:"allowed_domains"`
|
||||||
|
|
||||||
|
// AllowedUserGroups defines the list of allowed user groups.
|
||||||
|
// If not empty, at least one group from the user's group claim must match.
|
||||||
|
AllowedUserGroups []string `yaml:"allowed_user_groups"`
|
||||||
|
|
||||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||||
FieldMap OauthFields `yaml:"field_map"`
|
FieldMap OauthFields `yaml:"field_map"`
|
||||||
|
|
||||||
@@ -303,6 +307,10 @@ type OAuthProvider struct {
|
|||||||
// AllowedDomains defines the list of allowed domains
|
// AllowedDomains defines the list of allowed domains
|
||||||
AllowedDomains []string `yaml:"allowed_domains"`
|
AllowedDomains []string `yaml:"allowed_domains"`
|
||||||
|
|
||||||
|
// AllowedUserGroups defines the list of allowed user groups.
|
||||||
|
// If not empty, at least one group from the user's group claim must match.
|
||||||
|
AllowedUserGroups []string `yaml:"allowed_user_groups"`
|
||||||
|
|
||||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||||
FieldMap OauthFields `yaml:"field_map"`
|
FieldMap OauthFields `yaml:"field_map"`
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type LoginProviderInfo struct {
|
|||||||
type AuthenticatorUserInfo struct {
|
type AuthenticatorUserInfo struct {
|
||||||
Identifier UserIdentifier
|
Identifier UserIdentifier
|
||||||
Email string
|
Email string
|
||||||
|
UserGroups []string
|
||||||
Firstname string
|
Firstname string
|
||||||
Lastname string
|
Lastname string
|
||||||
Phone string
|
Phone string
|
||||||
|
|||||||
Reference in New Issue
Block a user