Skip to content

Commit

Permalink
Merge pull request #565 from apognu/dev/oidc-custom-config
Browse files Browse the repository at this point in the history
  • Loading branch information
kradalby authored May 2, 2022
2 parents ddb87af + 7cc58af commit fd452d5
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Fix send on closed channel crash in polling [#542](https://github.com/juanfont/headscale/pull/542)
- Fixed spurious calls to setLastStateChangeToNow from ephemeral nodes [#566](https://github.com/juanfont/headscale/pull/566)
- Add command for moving nodes between namespaces [#362](https://github.com/juanfont/headscale/issues/362)
- Added more configuration parameters for OpenID Connect (scopes, free-form paramters, domain and user allowlist)

## 0.15.0 (2022-03-20)

Expand Down
4 changes: 4 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ type OIDCConfig struct {
Issuer string
ClientID string
ClientSecret string
Scope []string
ExtraParams map[string]string
AllowedDomains []string
AllowedUsers []string
StripEmaildomain bool
}

Expand Down
6 changes: 6 additions & 0 deletions cmd/headscale/cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/juanfont/headscale"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -67,6 +68,7 @@ func LoadConfig(path string) error {
viper.SetDefault("cli.timeout", "5s")
viper.SetDefault("cli.insecure", false)

viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
viper.SetDefault("oidc.strip_email_domain", true)

if err := viper.ReadInConfig(); err != nil {
Expand Down Expand Up @@ -367,6 +369,10 @@ func getHeadscaleConfig() headscale.Config {
Issuer: viper.GetString("oidc.issuer"),
ClientID: viper.GetString("oidc.client_id"),
ClientSecret: viper.GetString("oidc.client_secret"),
Scope: viper.GetStringSlice("oidc.scope"),
ExtraParams: viper.GetStringMapString("oidc.extra_params"),
AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"),
AllowedUsers: viper.GetStringSlice("oidc.allowed_users"),
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
},

Expand Down
15 changes: 15 additions & 0 deletions config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ unix_socket_permission: "0770"
# client_id: "your-oidc-client-id"
# client_secret: "your-oidc-client-secret"
#
# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query
# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email".
#
# scope: ["openid", "profile", "email", "custom"]
# extra_params:
# domain_hint: example.com
#
# List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the
# authentication request will be rejected.
#
# allowed_domains:
# - example.com
# allowed_users:
# - alice@example.com
#
# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed.
# This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name`
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
Expand Down
34 changes: 32 additions & 2 deletions oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (h *Headscale) initOIDC() error {
"%s/oidc/callback",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
Scopes: h.cfg.OIDC.Scope,
}
}

Expand Down Expand Up @@ -91,7 +91,14 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
// place the machine key into the state cache, so it can be retrieved later
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)

authURL := h.oauth2Config.AuthCodeURL(stateStr)
// Add any extra parameter provided in the configuration to the Authorize Endpoint request
extras := make([]oauth2.AuthCodeOption, 0, len(h.cfg.OIDC.ExtraParams))

for k, v := range h.cfg.OIDC.ExtraParams {
extras = append(extras, oauth2.SetAuthURLParam(k, v))
}

authURL := h.oauth2Config.AuthCodeURL(stateStr, extras...)
log.Debug().Msgf("Redirecting to %s for authentication", authURL)

ctx.Redirect(http.StatusFound, authURL)
Expand Down Expand Up @@ -187,6 +194,29 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return
}

// If AllowedDomains is provided, check that the authenticated principal ends with @<alloweddomain>.
if len(h.cfg.OIDC.AllowedDomains) > 0 {
if at := strings.LastIndex(claims.Email, "@"); at < 0 ||
!IsStringInSlice(h.cfg.OIDC.AllowedDomains, claims.Email[at+1:]) {
log.Error().Msg("authenticated principal does not match any allowed domain")
ctx.String(
http.StatusBadRequest,
"unauthorized principal (domain mismatch)",
)

return
}
}

// If AllowedUsers is provided, check that the authenticated princial is part of that list.
if len(h.cfg.OIDC.AllowedUsers) > 0 &&
!IsStringInSlice(h.cfg.OIDC.AllowedUsers, claims.Email) {
log.Error().Msg("authenticated principal does not match any allowed user")
ctx.String(http.StatusBadRequest, "unauthorized principal (user mismatch)")

return
}

// retrieve machinekey from state cache
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)

Expand Down
10 changes: 10 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,13 @@ func GenerateRandomStringURLSafe(n int) (string, error) {

return base64.RawURLEncoding.EncodeToString(b), err
}

func IsStringInSlice(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}

return false
}

0 comments on commit fd452d5

Please sign in to comment.