Added an OIDC AllowGroups option for authorization.
This commit is contained in:
parent
4453728614
commit
70f2f5d750
4 changed files with 44 additions and 0 deletions
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## 0.18.x (2022-xx-xx)
|
## 0.18.x (2022-xx-xx)
|
||||||
|
|
||||||
|
- Added an OIDC AllowGroups Configuration options and authorization check [#1041](https://github.com/juanfont/headscale/pull/1041)
|
||||||
- Reworked routing and added support for subnet router failover [#1024](https://github.com/juanfont/headscale/pull/1024)
|
- Reworked routing and added support for subnet router failover [#1024](https://github.com/juanfont/headscale/pull/1024)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
|
@ -273,6 +273,9 @@ unix_socket_permission: "0770"
|
||||||
#
|
#
|
||||||
# allowed_domains:
|
# allowed_domains:
|
||||||
# - example.com
|
# - example.com
|
||||||
|
# Groups from keycloak have a leading '/'
|
||||||
|
# allowed_groups:
|
||||||
|
# - /headscale
|
||||||
# allowed_users:
|
# allowed_users:
|
||||||
# - alice@example.com
|
# - alice@example.com
|
||||||
#
|
#
|
||||||
|
|
|
@ -96,6 +96,7 @@ type OIDCConfig struct {
|
||||||
ExtraParams map[string]string
|
ExtraParams map[string]string
|
||||||
AllowedDomains []string
|
AllowedDomains []string
|
||||||
AllowedUsers []string
|
AllowedUsers []string
|
||||||
|
AllowedGroups []string
|
||||||
StripEmaildomain bool
|
StripEmaildomain bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -568,6 +569,7 @@ func GetHeadscaleConfig() (*Config, error) {
|
||||||
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
|
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
|
||||||
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
|
||||||
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
|
||||||
|
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
|
||||||
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
38
oidc.go
38
oidc.go
|
@ -25,6 +25,7 @@ const (
|
||||||
errEmptyOIDCCallbackParams = Error("empty OIDC callback params")
|
errEmptyOIDCCallbackParams = Error("empty OIDC callback params")
|
||||||
errNoOIDCIDToken = Error("could not extract ID Token for OIDC callback")
|
errNoOIDCIDToken = Error("could not extract ID Token for OIDC callback")
|
||||||
errOIDCAllowedDomains = Error("authenticated principal does not match any allowed domain")
|
errOIDCAllowedDomains = Error("authenticated principal does not match any allowed domain")
|
||||||
|
errOIDCAllowedGroups = Error("authenticated principal is not in any allowed group")
|
||||||
errOIDCAllowedUsers = Error("authenticated principal does not match any allowed user")
|
errOIDCAllowedUsers = Error("authenticated principal does not match any allowed user")
|
||||||
errOIDCInvalidMachineState = Error("requested machine state key expired before authorisation completed")
|
errOIDCInvalidMachineState = Error("requested machine state key expired before authorisation completed")
|
||||||
errOIDCNodeKeyMissing = Error("could not get node key from cache")
|
errOIDCNodeKeyMissing = Error("could not get node key from cache")
|
||||||
|
@ -209,6 +210,10 @@ func (h *Headscale) OIDCCallback(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateOIDCAllowedGroups(writer, h.cfg.OIDC.AllowedGroups, claims); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := validateOIDCAllowedUsers(writer, h.cfg.OIDC.AllowedUsers, claims); err != nil {
|
if err := validateOIDCAllowedUsers(writer, h.cfg.OIDC.AllowedUsers, claims); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -404,6 +409,39 @@ func validateOIDCAllowedDomains(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateOIDCAllowedGroups checks if AllowedGroups is provided,
|
||||||
|
// and that the user has one group in the list.
|
||||||
|
// claims.Groups can be populated by adding a client scope named
|
||||||
|
// 'groups' that contains group membership.
|
||||||
|
func validateOIDCAllowedGroups(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
allowedGroups []string,
|
||||||
|
claims *IDTokenClaims,
|
||||||
|
) error {
|
||||||
|
if len(allowedGroups) > 0 {
|
||||||
|
for _, group := range allowedGroups {
|
||||||
|
if IsStringInSlice(claims.Groups, group) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().Msg("authenticated principal not in any allowed groups")
|
||||||
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, err := writer.Write([]byte("unauthorized principal (allowed groups)"))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errOIDCAllowedGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateOIDCAllowedUsers checks that if AllowedUsers is provided,
|
// validateOIDCAllowedUsers checks that if AllowedUsers is provided,
|
||||||
// that the authenticated principal is part of that list.
|
// that the authenticated principal is part of that list.
|
||||||
func validateOIDCAllowedUsers(
|
func validateOIDCAllowedUsers(
|
||||||
|
|
Loading…
Reference in a new issue