Skip to content

Commit

Permalink
Redo OIDC configuration (#2020)
Browse files Browse the repository at this point in the history
expand user, add claims to user

This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.

This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.

remove usernames in magic dns, normalisation of emails

this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.

In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.

Email are no longer normalised as part of the policy processing.

untagle oidc and regcache, use typed cache

This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.

try to make reauth/register branches clearer in oidc

Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.

This commit tries to split this into what to do if the node
exists, if it needs to register etc.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
  • Loading branch information
kradalby authored Oct 2, 2024
1 parent bc9e83b commit 218138a
Show file tree
Hide file tree
Showing 32 changed files with 606 additions and 895 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@

## Next

### BREAKING

- Remove `dns.use_username_in_magic_dns` configuration option [#2020](https://github.com/juanfont/headscale/pull/2020)
- Having usernames in magic DNS is no longer possible.
- Redo OpenID Connect configuration [#2020](https://github.com/juanfont/headscale/pull/2020)
- `strip_email_domain` has been removed, domain is _always_ part of the username for OIDC.
- Users are now identified by `sub` claim in the ID token instead of username, allowing the username, name and email to be updated.
- User has been extended to store username, display name, profile picture url and email.
- These fields are forwarded to the client, and shows up nicely in the user switcher.
- These fields can be made available via the API/CLI for non-OIDC users in the future.
- Remove versions older than 1.56 [#2149](https://github.com/juanfont/headscale/pull/2149)
- Clean up old code required by old versions

### Changes

- Improved compatibilty of built-in DERP server with clients connecting over WebSocket.
- Allow nodes to use SSH agent forwarding [#2145](https://github.com/juanfont/headscale/pull/2145)


## 0.23.0 (2024-09-18)

This release was intended to be mainly a code reorganisation and refactoring, significantly improving the maintainability of the codebase. This should allow us to improve further and make it easier for the maintainers to keep on top of the project.
Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

# When updating go.mod or go.sum, a new sha will need to be calculated,
# update this if you have a mismatch after doing a change to thos files.
vendorHash = "sha256-/CPUkLLCwNKK3z3UZyF+AY0ArMnLaDmH0HV3/RYHo4c=";
vendorHash = "sha256-SDJSFji6498WI9bJLmY62VGt21TtD2GxrxRAWyYyr0c=";

subPackages = ["cmd/headscale"];

Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ require (
github.com/coder/websocket v1.8.12
github.com/coreos/go-oidc/v3 v3.11.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/deckarep/golang-set/v2 v2.6.0
github.com/glebarez/sqlite v1.11.0
github.com/go-gormigrate/gormigrate/v2 v2.1.2
github.com/gofrs/uuid/v5 v5.3.0
Expand All @@ -19,7 +18,6 @@ require (
github.com/klauspost/compress v1.17.9
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
github.com/ory/dockertest/v3 v3.11.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/philip-bui/grpc-zerolog v1.0.1
github.com/pkg/profile v1.7.0
github.com/prometheus/client_golang v1.20.2
Expand Down Expand Up @@ -49,6 +47,7 @@ require (
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7
zgo.at/zcache/v2 v2.1.0
)

require (
Expand Down
12 changes: 2 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 h1:vrC07UZcgPzu/OjWsmQKMGg3LoPSz9jh/pQXIrHjUj4=
github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
Expand Down Expand Up @@ -364,8 +362,6 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw=
Expand Down Expand Up @@ -729,11 +725,7 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.75.0-pre.0.20240925091311-031f291c98fe h1:3+E/vlEsZa2FpWBz2Ly6/L4zh4utVO8z54Ms75HitrQ=
tailscale.com v1.75.0-pre.0.20240925091311-031f291c98fe/go.mod h1:G4R9objdXe2zAcLaLkDOcHfqN9XnspBifyBHGNwTzKg=
tailscale.com v1.75.0-pre.0.20240925102642-c17c476c0d59 h1:GSuB+bmPiVfBLRqVyLOFSU+9V00lXBz9HakAewevYZA=
tailscale.com v1.75.0-pre.0.20240925102642-c17c476c0d59/go.mod h1:G4R9objdXe2zAcLaLkDOcHfqN9XnspBifyBHGNwTzKg=
tailscale.com v1.75.0-pre.0.20240926030905-c90c9938c8a2 h1:ivZ1GEXMzCNI1VRp2TjUWmLuOtno7TqW26lZf7MlF4k=
tailscale.com v1.75.0-pre.0.20240926030905-c90c9938c8a2/go.mod h1:xKxYf3B3PuezFlRaMT+VhuVu8XTFUTLy+VCzLPMJVmg=
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7 h1:nfRWV6ECxwNvvXKtbqSVstjlEi1BWktzv3FuxWpyyx0=
tailscale.com v1.75.0-pre.0.20240926101731-7d1160ddaab7/go.mod h1:xKxYf3B3PuezFlRaMT+VhuVu8XTFUTLy+VCzLPMJVmg=
zgo.at/zcache/v2 v2.1.0 h1:USo+ubK+R4vtjw4viGzTe/zjXyPw6R7SK/RL3epBBxs=
zgo.at/zcache/v2 v2.1.0/go.mod h1:gyCeoLVo01QjDZynjime8xUGHHMbsLiPyUTBpDGd4Gk=
38 changes: 26 additions & 12 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"syscall"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
Expand All @@ -33,15 +32,13 @@ import (
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/patrickmn/go-cache"
zerolog "github.com/philip-bui/grpc-zerolog"
"github.com/pkg/profile"
"github.com/prometheus/client_golang/prometheus/promhttp"
zl "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/oauth2"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
Expand All @@ -57,6 +54,7 @@ import (
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/util/dnsname"
zcache "zgo.at/zcache/v2"
)

var (
Expand Down Expand Up @@ -95,10 +93,9 @@ type Headscale struct {
mapper *mapper.Mapper
nodeNotifier *notifier.Notifier

oidcProvider *oidc.Provider
oauth2Config *oauth2.Config
registrationCache *zcache.Cache[string, types.Node]

registrationCache *cache.Cache
authProvider AuthProvider

pollNetMapStreamWG sync.WaitGroup
}
Expand All @@ -123,7 +120,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
return nil, fmt.Errorf("failed to read or create Noise protocol private key: %w", err)
}

registrationCache := cache.New(
registrationCache := zcache.New[string, types.Node](
registerCacheExpiration,
registerCacheCleanup,
)
Expand All @@ -138,7 +135,9 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {

app.db, err = db.NewHeadscaleDatabase(
cfg.Database,
cfg.BaseDomain)
cfg.BaseDomain,
registrationCache,
)
if err != nil {
return nil, err
}
Expand All @@ -154,16 +153,30 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
}
})

var authProvider AuthProvider
authProvider = NewAuthProviderWeb(cfg.ServerURL)
if cfg.OIDC.Issuer != "" {
err = app.initOIDC()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
oidcProvider, err := NewAuthProviderOIDC(
ctx,
cfg.ServerURL,
&cfg.OIDC,
app.db,
app.nodeNotifier,
app.ipAlloc,
)
if err != nil {
if cfg.OIDC.OnlyStartIfOIDCIsAvailable {
return nil, err
} else {
log.Warn().Err(err).Msg("failed to set up OIDC provider, falling back to CLI based authentication")
}
} else {
authProvider = oidcProvider
}
}
app.authProvider = authProvider

if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS
// TODO(kradalby): revisit why this takes a list.
Expand Down Expand Up @@ -429,10 +442,11 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {

router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
router.HandleFunc("/register/{mkey}", h.RegisterWebAPI).Methods(http.MethodGet)
router.HandleFunc("/register/{mkey}", h.authProvider.RegisterHandler).Methods(http.MethodGet)

router.HandleFunc("/oidc/register/{mkey}", h.RegisterOIDC).Methods(http.MethodGet)
router.HandleFunc("/oidc/callback", h.OIDCCallback).Methods(http.MethodGet)
if provider, ok := h.authProvider.(*AuthProviderOIDC); ok {
router.HandleFunc("/oidc/callback", provider.OIDCCallbackHandler).Methods(http.MethodGet)
}
router.HandleFunc("/apple", h.AppleConfigMessage).Methods(http.MethodGet)
router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig).
Methods(http.MethodGet)
Expand Down
41 changes: 10 additions & 31 deletions hscontrol/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"time"

"github.com/juanfont/headscale/hscontrol/db"
Expand All @@ -19,6 +18,11 @@ import (
"tailscale.com/types/ptr"
)

type AuthProvider interface {
RegisterHandler(http.ResponseWriter, *http.Request)
AuthURL(key.MachinePublic) string
}

func logAuthFunc(
registerRequest tailcfg.RegisterRequest,
machineKey key.MachinePublic,
Expand Down Expand Up @@ -125,7 +129,6 @@ func (h *Headscale) handleRegister(
h.registrationCache.Set(
machineKey.String(),
newNode,
registerCacheExpiration,
)

h.handleNewNode(writer, regReq, machineKey)
Expand Down Expand Up @@ -164,15 +167,15 @@ func (h *Headscale) handleRegister(
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L648
if !regReq.Expiry.IsZero() &&
regReq.Expiry.UTC().Before(now) {
h.handleNodeLogOut(writer, *node, machineKey)
h.handleNodeLogOut(writer, *node)

return
}

// If node is not expired, and it is register, we have a already accepted this node,
// let it proceed with a valid registration
if !node.IsExpired() {
h.handleNodeWithValidRegistration(writer, *node, machineKey)
h.handleNodeWithValidRegistration(writer, *node)

return
}
Expand All @@ -185,7 +188,6 @@ func (h *Headscale) handleRegister(
writer,
regReq,
*node,
machineKey,
)

return
Expand All @@ -198,7 +200,6 @@ func (h *Headscale) handleRegister(
writer,
regReq,
*node,
machineKey,
)

return
Expand Down Expand Up @@ -226,7 +227,6 @@ func (h *Headscale) handleRegister(
h.registrationCache.Set(
machineKey.String(),
*node,
registerCacheExpiration,
)

return
Expand Down Expand Up @@ -386,7 +386,7 @@ func (h *Headscale) handleAuthKey(
}
}

h.db.Write(func(tx *gorm.DB) error {
err = h.db.Write(func(tx *gorm.DB) error {
return db.UsePreAuthKey(tx, pak)
})
if err != nil {
Expand Down Expand Up @@ -447,17 +447,7 @@ func (h *Headscale) handleNewNode(
// The node registration is new, redirect the client to the registration URL
logTrace("The node seems to be new, sending auth url")

if h.oauth2Config != nil {
resp.AuthURL = fmt.Sprintf(
"%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
machineKey.String(),
)
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
machineKey.String())
}
resp.AuthURL = h.authProvider.AuthURL(machineKey)

respBody, err := json.Marshal(resp)
if err != nil {
Expand All @@ -480,7 +470,6 @@ func (h *Headscale) handleNewNode(
func (h *Headscale) handleNodeLogOut(
writer http.ResponseWriter,
node types.Node,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}

Expand Down Expand Up @@ -563,7 +552,6 @@ func (h *Headscale) handleNodeLogOut(
func (h *Headscale) handleNodeWithValidRegistration(
writer http.ResponseWriter,
node types.Node,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}

Expand Down Expand Up @@ -609,7 +597,6 @@ func (h *Headscale) handleNodeKeyRefresh(
writer http.ResponseWriter,
registerRequest tailcfg.RegisterRequest,
node types.Node,
machineKey key.MachinePublic,
) {
resp := tailcfg.RegisterResponse{}

Expand Down Expand Up @@ -685,15 +672,7 @@ func (h *Headscale) handleNodeExpiredOrLoggedOut(
Str("node_key_old", regReq.OldNodeKey.ShortString()).
Msg("Node registration has expired or logged out. Sending a auth url to register")

if h.oauth2Config != nil {
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
machineKey.String())
} else {
resp.AuthURL = fmt.Sprintf("%s/register/%s",
strings.TrimSuffix(h.cfg.ServerURL, "/"),
machineKey.String())
}
resp.AuthURL = h.authProvider.AuthURL(machineKey)

respBody, err := json.Marshal(resp)
if err != nil {
Expand Down
Loading

0 comments on commit 218138a

Please sign in to comment.