Skip to content

Commit

Permalink
Added an OIDC AllowGroups option for authorization.
Browse files Browse the repository at this point in the history
  • Loading branch information
newellz2 authored and kradalby committed Dec 7, 2022
1 parent 4453728 commit 70f2f5d
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

### Changes
Expand Down
3 changes: 3 additions & 0 deletions config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ unix_socket_permission: "0770"
#
# allowed_domains:
# - example.com
# Groups from keycloak have a leading '/'
# allowed_groups:
# - /headscale
# allowed_users:
# - [email protected]
#
Expand Down
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type OIDCConfig struct {
ExtraParams map[string]string
AllowedDomains []string
AllowedUsers []string
AllowedGroups []string
StripEmaildomain bool
}

Expand Down Expand Up @@ -568,6 +569,7 @@ func GetHeadscaleConfig() (*Config, error) {
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"),
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
},

Expand Down
38 changes: 38 additions & 0 deletions oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
errEmptyOIDCCallbackParams = Error("empty OIDC callback params")
errNoOIDCIDToken = Error("could not extract ID Token for OIDC callback")
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")
errOIDCInvalidMachineState = Error("requested machine state key expired before authorisation completed")
errOIDCNodeKeyMissing = Error("could not get node key from cache")
Expand Down Expand Up @@ -209,6 +210,10 @@ func (h *Headscale) OIDCCallback(
return
}

if err := validateOIDCAllowedGroups(writer, h.cfg.OIDC.AllowedGroups, claims); err != nil {
return
}

if err := validateOIDCAllowedUsers(writer, h.cfg.OIDC.AllowedUsers, claims); err != nil {
return
}
Expand Down Expand Up @@ -404,6 +409,39 @@ func validateOIDCAllowedDomains(
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,
// that the authenticated principal is part of that list.
func validateOIDCAllowedUsers(
Expand Down

0 comments on commit 70f2f5d

Please sign in to comment.