Added an OIDC AllowGroups option for authorization.

This commit is contained in:
Zachary Newell 2022-12-07 00:08:01 +00:00 committed by Kristoffer Dalby
parent 4453728614
commit 70f2f5d750
4 changed files with 44 additions and 0 deletions

View file

@ -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

View file

@ -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
# #

View file

@ -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
View file

@ -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(