-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial work on OIDC (SSO) integration #126
Merged
Merged
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
e7a2501
initial work on OIDC (SSO) integration
b22a978
fix linter errors, error out if jwt does not contain a key id
8248b71
Merge branch 'main' into main
kradalby cc054d7
Merge branch 'main' into main
kradalby 0393ab5
Merge branch 'main' into main
kradalby c487591
use go-oidc instead of verifying and extracting tokens ourselves, ren…
35795c7
Handle trailing slash on uris
e407d42
updates from code review
2997f4d
Merge branch 'main' into main
kradalby 74e6c14
updates from code review
8843188
add notes to README.md about OIDC
0603e29
add login details to RegisterResponse so GUI clients show login displ…
afbfc1d
Merge branch 'main' into main
unreality d0cd5af
fix incorrect merge
710616f
Merge branch 'main' into main
kradalby a347d27
Fix broken machine test
kradalby 677bd9b
Implement namespace matching
kradalby 8fe72dc
Merge pull request #1 from kradalby/namespace-mappings
unreality da14750
Merge branch 'main' into main
kradalby e742422
Merge branch 'main' into main
kradalby dbe193a
Fix up leftovers from kradalby PR
kradalby 2d252da
suggested documentation and comments
cbf3f5d
Resolve merge conflict
kradalby cd2914d
Make note about oidc being experimental
kradalby bac8117
Remove lint from generated testcode
kradalby 73d22cd
Merge pull request #2 from kradalby/oidc-1
unreality File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import ( | |
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/rs/zerolog/log" | ||
|
@@ -64,7 +65,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { | |
Str("handler", "Registration"). | ||
Err(err). | ||
Msg("Cannot parse machine key") | ||
machineRegistrations.WithLabelValues("unkown", "web", "error", "unknown").Inc() | ||
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() | ||
c.String(http.StatusInternalServerError, "Sad!") | ||
return | ||
} | ||
|
@@ -75,42 +76,69 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { | |
Str("handler", "Registration"). | ||
Err(err). | ||
Msg("Cannot decode message") | ||
machineRegistrations.WithLabelValues("unkown", "web", "error", "unknown").Inc() | ||
machineRegistrations.WithLabelValues("unknown", "web", "error", "unknown").Inc() | ||
c.String(http.StatusInternalServerError, "Very sad!") | ||
return | ||
} | ||
|
||
now := time.Now().UTC() | ||
var m Machine | ||
if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||
m, err := h.GetMachineByMachineKey(mKey.HexString()) | ||
if errors.Is(err, gorm.ErrRecordNotFound) { | ||
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine") | ||
m = Machine{ | ||
Expiry: &req.Expiry, | ||
MachineKey: mKey.HexString(), | ||
Name: req.Hostinfo.Hostname, | ||
NodeKey: wgkey.Key(req.NodeKey).HexString(), | ||
LastSuccessfulUpdate: &now, | ||
newMachine := Machine{ | ||
Expiry: &time.Time{}, | ||
MachineKey: mKey.HexString(), | ||
Name: req.Hostinfo.Hostname, | ||
} | ||
if err := h.db.Create(&m).Error; err != nil { | ||
if err := h.db.Create(&newMachine).Error; err != nil { | ||
log.Error(). | ||
Str("handler", "Registration"). | ||
Err(err). | ||
Msg("Could not create row") | ||
machineRegistrations.WithLabelValues("unkown", "web", "error", m.Namespace.Name).Inc() | ||
machineRegistrations.WithLabelValues("unknown", "web", "error", m.Namespace.Name).Inc() | ||
return | ||
} | ||
m = &newMachine | ||
} | ||
|
||
if !m.Registered && req.Auth.AuthKey != "" { | ||
h.handleAuthKey(c, h.db, mKey, req, m) | ||
h.handleAuthKey(c, h.db, mKey, req, *m) | ||
return | ||
} | ||
|
||
resp := tailcfg.RegisterResponse{} | ||
|
||
// We have the updated key! | ||
if m.NodeKey == wgkey.Key(req.NodeKey).HexString() { | ||
if m.Registered { | ||
|
||
// The client sends an Expiry in the past if the client is requesting a logout | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a reference for this? would be good to have in the comment, for example if it can be found in the Tailscale source code. |
||
if !req.Expiry.IsZero() && req.Expiry.UTC().Before(now) { | ||
kradalby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
log.Info(). | ||
Str("handler", "Registration"). | ||
Str("machine", m.Name). | ||
Msg("Client requested logout") | ||
|
||
m.Expiry = &req.Expiry // save the expiry so that the machine is marked as expired | ||
h.db.Save(&m) | ||
|
||
resp.AuthURL = "" | ||
resp.MachineAuthorized = false | ||
resp.User = *m.Namespace.toUser() | ||
respBody, err := encode(resp, &mKey, h.privateKey) | ||
if err != nil { | ||
log.Error(). | ||
Str("handler", "Registration"). | ||
Err(err). | ||
Msg("Cannot encode message") | ||
c.String(http.StatusInternalServerError, "") | ||
return | ||
} | ||
c.Data(200, "application/json; charset=utf-8", respBody) | ||
return | ||
} | ||
|
||
if m.Registered && m.Expiry.UTC().After(now) { | ||
// The machine registration is valid, respond with redirect to /map | ||
log.Debug(). | ||
Str("handler", "Registration"). | ||
Str("machine", m.Name). | ||
|
@@ -119,6 +147,8 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { | |
resp.AuthURL = "" | ||
resp.MachineAuthorized = true | ||
resp.User = *m.Namespace.toUser() | ||
resp.Login = *m.Namespace.toLogin() | ||
|
||
respBody, err := encode(resp, &mKey, h.privateKey) | ||
if err != nil { | ||
log.Error(). | ||
|
@@ -134,12 +164,23 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { | |
return | ||
} | ||
|
||
// The client has registered before, but has expired | ||
log.Debug(). | ||
Str("handler", "Registration"). | ||
Str("machine", m.Name). | ||
Msg("Not registered and not NodeKey rotation. Sending a authurl to register") | ||
resp.AuthURL = fmt.Sprintf("%s/register?key=%s", | ||
h.cfg.ServerURL, mKey.HexString()) | ||
Msg("Machine registration has expired. Sending a authurl to register") | ||
|
||
if h.cfg.OIDC.Issuer != "" { | ||
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", | ||
strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString()) | ||
} else { | ||
resp.AuthURL = fmt.Sprintf("%s/register?key=%s", | ||
strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString()) | ||
} | ||
|
||
m.RequestedExpiry = &req.Expiry // save the requested expiry time for retrieval later in the authentication flow | ||
h.db.Save(&m) | ||
|
||
respBody, err := encode(resp, &mKey, h.privateKey) | ||
if err != nil { | ||
log.Error(). | ||
|
@@ -155,8 +196,8 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { | |
return | ||
} | ||
|
||
// The NodeKey we have matches OldNodeKey, which means this is a refresh after an key expiration | ||
if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() { | ||
// The NodeKey we have matches OldNodeKey, which means this is a refresh after a key expiration | ||
if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() && m.Expiry.UTC().After(now) { | ||
log.Debug(). | ||
Str("handler", "Registration"). | ||
Str("machine", m.Name). | ||
|
@@ -179,35 +220,22 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { | |
return | ||
} | ||
|
||
// We arrive here after a client is restarted without finalizing the authentication flow or | ||
// when headscale is stopped in the middle of the auth process. | ||
if m.Registered { | ||
log.Debug(). | ||
Str("handler", "Registration"). | ||
Str("machine", m.Name). | ||
Msg("The node is sending us a new NodeKey, but machine is registered. All clear for /map") | ||
resp.AuthURL = "" | ||
resp.MachineAuthorized = true | ||
resp.User = *m.Namespace.toUser() | ||
respBody, err := encode(resp, &mKey, h.privateKey) | ||
if err != nil { | ||
log.Error(). | ||
Str("handler", "Registration"). | ||
Err(err). | ||
Msg("Cannot encode message") | ||
c.String(http.StatusInternalServerError, "") | ||
return | ||
} | ||
c.Data(200, "application/json; charset=utf-8", respBody) | ||
return | ||
} | ||
|
||
// The machine registration is new, redirect the client to the registration URL | ||
log.Debug(). | ||
Str("handler", "Registration"). | ||
Str("machine", m.Name). | ||
Msg("The node is sending us a new NodeKey, sending auth url") | ||
resp.AuthURL = fmt.Sprintf("%s/register?key=%s", | ||
h.cfg.ServerURL, mKey.HexString()) | ||
if h.cfg.OIDC.Issuer != "" { | ||
resp.AuthURL = fmt.Sprintf("%s/oidc/register/%s", strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString()) | ||
} else { | ||
resp.AuthURL = fmt.Sprintf("%s/register?key=%s", | ||
strings.TrimSuffix(h.cfg.ServerURL, "/"), mKey.HexString()) | ||
} | ||
|
||
m.RequestedExpiry = &req.Expiry // save the requested expiry time for retrieval later in the authentication flow | ||
m.NodeKey = wgkey.Key(req.NodeKey).HexString() // save the NodeKey | ||
h.db.Save(&m) | ||
|
||
respBody, err := encode(resp, &mKey, h.privateKey) | ||
if err != nil { | ||
log.Error(). | ||
|
@@ -390,6 +418,8 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, | |
m.RegisterMethod = "authKey" | ||
db.Save(&m) | ||
|
||
h.updateMachineExpiry(&m) // TODO: do we want to do different expiry times for AuthKeys? | ||
|
||
pak.Used = true | ||
db.Save(&pak) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, github suggestions does not handle this well, there is a "end of codeblock" missing