mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-29 18:33:05 +00:00
Merge pull request #565 from apognu/dev/oidc-custom-config
This commit is contained in:
commit
fd452d52ca
6 changed files with 68 additions and 2 deletions
|
@ -9,6 +9,7 @@
|
||||||
- Fix send on closed channel crash in polling [#542](https://github.com/juanfont/headscale/pull/542)
|
- 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)
|
- 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)
|
- 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)
|
## 0.15.0 (2022-03-20)
|
||||||
|
|
||||||
|
|
4
app.go
4
app.go
|
@ -119,6 +119,10 @@ type OIDCConfig struct {
|
||||||
Issuer string
|
Issuer string
|
||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
|
Scope []string
|
||||||
|
ExtraParams map[string]string
|
||||||
|
AllowedDomains []string
|
||||||
|
AllowedUsers []string
|
||||||
StripEmaildomain bool
|
StripEmaildomain bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/juanfont/headscale"
|
"github.com/juanfont/headscale"
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -67,6 +68,7 @@ func LoadConfig(path string) error {
|
||||||
viper.SetDefault("cli.timeout", "5s")
|
viper.SetDefault("cli.timeout", "5s")
|
||||||
viper.SetDefault("cli.insecure", false)
|
viper.SetDefault("cli.insecure", false)
|
||||||
|
|
||||||
|
viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
|
||||||
viper.SetDefault("oidc.strip_email_domain", true)
|
viper.SetDefault("oidc.strip_email_domain", true)
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
@ -367,6 +369,10 @@ func getHeadscaleConfig() headscale.Config {
|
||||||
Issuer: viper.GetString("oidc.issuer"),
|
Issuer: viper.GetString("oidc.issuer"),
|
||||||
ClientID: viper.GetString("oidc.client_id"),
|
ClientID: viper.GetString("oidc.client_id"),
|
||||||
ClientSecret: viper.GetString("oidc.client_secret"),
|
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"),
|
StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,21 @@ unix_socket_permission: "0770"
|
||||||
# client_id: "your-oidc-client-id"
|
# client_id: "your-oidc-client-id"
|
||||||
# client_secret: "your-oidc-client-secret"
|
# 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.
|
# 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`
|
# 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
|
# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following
|
||||||
|
|
34
oidc.go
34
oidc.go
|
@ -53,7 +53,7 @@ func (h *Headscale) initOIDC() error {
|
||||||
"%s/oidc/callback",
|
"%s/oidc/callback",
|
||||||
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
strings.TrimSuffix(h.cfg.ServerURL, "/"),
|
||||||
),
|
),
|
||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
Scopes: h.cfg.OIDC.Scope,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
// place the machine key into the state cache, so it can be retrieved later
|
||||||
h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
|
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)
|
log.Debug().Msgf("Redirecting to %s for authentication", authURL)
|
||||||
|
|
||||||
ctx.Redirect(http.StatusFound, authURL)
|
ctx.Redirect(http.StatusFound, authURL)
|
||||||
|
@ -187,6 +194,29 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
|
||||||
return
|
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
|
// retrieve machinekey from state cache
|
||||||
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
|
machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
|
||||||
|
|
||||||
|
|
10
utils.go
10
utils.go
|
@ -317,3 +317,13 @@ func GenerateRandomStringURLSafe(n int) (string, error) {
|
||||||
|
|
||||||
return base64.RawURLEncoding.EncodeToString(b), err
|
return base64.RawURLEncoding.EncodeToString(b), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsStringInSlice(slice []string, str string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue