2023-05-10 09:24:05 +02:00
|
|
|
package hscontrol
|
2020-06-21 12:32:08 +02:00
|
|
|
|
|
|
|
|
import (
|
2025-11-11 22:46:57 -05:00
|
|
|
"bytes"
|
2020-06-21 12:32:08 +02:00
|
|
|
"encoding/json"
|
2023-05-11 09:09:18 +02:00
|
|
|
"errors"
|
2023-09-28 12:33:53 -07:00
|
|
|
"fmt"
|
2024-11-22 20:23:05 +08:00
|
|
|
"io"
|
2020-06-21 12:32:08 +02:00
|
|
|
"net/http"
|
2023-06-06 17:14:56 +02:00
|
|
|
"strconv"
|
Redo OIDC configuration (#2020)
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>
2024-10-02 14:50:17 +02:00
|
|
|
"strings"
|
2025-11-11 22:46:57 -05:00
|
|
|
"time"
|
2020-06-21 12:32:08 +02:00
|
|
|
|
2025-11-12 14:23:15 +01:00
|
|
|
"github.com/juanfont/headscale/hscontrol/assets"
|
2024-10-04 04:39:24 -07:00
|
|
|
"github.com/juanfont/headscale/hscontrol/templates"
|
2025-01-26 22:20:11 +01:00
|
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
2021-11-13 08:39:04 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2023-06-06 17:14:56 +02:00
|
|
|
"tailscale.com/tailcfg"
|
2020-06-21 12:32:08 +02:00
|
|
|
)
|
|
|
|
|
|
2021-11-18 08:49:55 +00:00
|
|
|
const (
|
2026-02-06 21:45:32 +01:00
|
|
|
// NoiseCapabilityVersion is used by Tailscale clients to indicate
|
2023-06-06 17:14:56 +02:00
|
|
|
// their codebase version. Tailscale clients can communicate over TS2021
|
|
|
|
|
// from CapabilityVersion 28, but we only have good support for it
|
|
|
|
|
// since https://github.com/tailscale/tailscale/pull/4323 (Noise in any HTTPS port).
|
|
|
|
|
//
|
|
|
|
|
// Related to this change, there is https://github.com/tailscale/tailscale/pull/5379,
|
|
|
|
|
// where CapabilityVersion 39 is introduced to indicate #4323 was merged.
|
|
|
|
|
//
|
|
|
|
|
// See also https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go
|
|
|
|
|
NoiseCapabilityVersion = 39
|
|
|
|
|
|
2023-05-11 09:09:18 +02:00
|
|
|
reservedResponseHeaderSize = 4
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-10 23:38:55 +02:00
|
|
|
// httpError logs an error and sends an HTTP error response with the given.
|
2025-02-01 15:25:18 +01:00
|
|
|
func httpError(w http.ResponseWriter, err error) {
|
2026-02-06 21:39:35 +00:00
|
|
|
if herr, ok := errors.AsType[HTTPError](err); ok {
|
2025-02-01 15:25:18 +01:00
|
|
|
http.Error(w, herr.Msg, herr.Code)
|
|
|
|
|
log.Error().Err(herr.Err).Int("code", herr.Code).Msgf("user msg: %s", herr.Msg)
|
|
|
|
|
} else {
|
|
|
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
|
|
|
log.Error().Err(err).Int("code", http.StatusInternalServerError).Msg("http internal server error")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 08:46:00 +00:00
|
|
|
// httpUserError logs an error and sends a styled HTML error page.
|
|
|
|
|
// Use this for browser-facing error paths (OIDC, registration confirm)
|
|
|
|
|
// where the user should see a branded page instead of plain text.
|
|
|
|
|
// Technical details go to the server log; the HTML page only shows
|
|
|
|
|
// an actionable message derived from the HTTP status code.
|
|
|
|
|
func httpUserError(w http.ResponseWriter, err error) {
|
|
|
|
|
code := http.StatusInternalServerError
|
|
|
|
|
|
|
|
|
|
if herr, ok := errors.AsType[HTTPError](err); ok {
|
|
|
|
|
if herr.Code != 0 {
|
|
|
|
|
code = herr.Code
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Error().Err(herr.Err).Int("code", code).Msgf("user msg: %s", herr.Msg)
|
|
|
|
|
} else {
|
|
|
|
|
log.Error().Err(err).Int("code", code).Msg("http internal server error")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userMsg := userMessageForStatusCode(code)
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
w.WriteHeader(code)
|
|
|
|
|
|
|
|
|
|
page := templates.AuthError(templates.AuthErrorResult{
|
|
|
|
|
Title: "Headscale - Error",
|
|
|
|
|
Heading: http.StatusText(code),
|
|
|
|
|
Message: userMsg,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
_, werr := w.Write([]byte(page.Render()))
|
|
|
|
|
if werr != nil {
|
|
|
|
|
log.Error().Err(werr).Msg("failed to write HTML error response")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func userMessageForStatusCode(code int) string {
|
|
|
|
|
switch {
|
|
|
|
|
case code == http.StatusUnauthorized || code == http.StatusForbidden:
|
|
|
|
|
return "You are not authorized. Please contact your administrator."
|
|
|
|
|
case code == http.StatusGone:
|
|
|
|
|
return "Your session has expired. Please try again."
|
|
|
|
|
case code >= 400 && code < 500:
|
|
|
|
|
return "The request could not be processed. Please try again."
|
|
|
|
|
default:
|
|
|
|
|
return "Something went wrong. Please try again later."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 15:25:18 +01:00
|
|
|
// HTTPError represents an error that is surfaced to the user via web.
|
|
|
|
|
type HTTPError struct {
|
|
|
|
|
Code int // HTTP response code to send to client; 0 means 500
|
|
|
|
|
Msg string // Response body to send to client
|
|
|
|
|
Err error // Detailed error to log on the server
|
2025-01-30 21:40:29 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-01 15:25:18 +01:00
|
|
|
func (e HTTPError) Error() string { return fmt.Sprintf("http error[%d]: %s, %s", e.Code, e.Msg, e.Err) }
|
|
|
|
|
func (e HTTPError) Unwrap() error { return e.Err }
|
|
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
// NewHTTPError returns an HTTPError containing the given information.
|
2025-02-01 15:25:18 +01:00
|
|
|
func NewHTTPError(code int, msg string, err error) HTTPError {
|
|
|
|
|
return HTTPError{Code: code, Msg: msg, Err: err}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var errMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed, "method not allowed", nil)
|
|
|
|
|
|
2023-05-11 09:09:18 +02:00
|
|
|
var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
|
2026-02-05 16:29:54 +00:00
|
|
|
"machines registered with CLI do not support expiry",
|
2021-11-18 08:49:55 +00:00
|
|
|
)
|
2023-09-28 12:33:53 -07:00
|
|
|
|
2025-07-04 09:40:29 +02:00
|
|
|
func parseCapabilityVersion(req *http.Request) (tailcfg.CapabilityVersion, error) {
|
2023-09-28 12:33:53 -07:00
|
|
|
clientCapabilityStr := req.URL.Query().Get("v")
|
|
|
|
|
|
|
|
|
|
if clientCapabilityStr == "" {
|
2025-02-01 15:25:18 +01:00
|
|
|
return 0, NewHTTPError(http.StatusBadRequest, "capability version must be set", nil)
|
2023-09-28 12:33:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
|
|
|
|
|
if err != nil {
|
2026-02-05 16:29:54 +00:00
|
|
|
return 0, NewHTTPError(http.StatusBadRequest, "invalid capability version", fmt.Errorf("parsing capability version: %w", err))
|
2023-09-28 12:33:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
|
|
|
|
}
|
2021-11-14 18:31:51 +01:00
|
|
|
|
2026-04-09 17:56:55 +00:00
|
|
|
// verifyBodyLimit caps the request body for /verify. The DERP verify
|
|
|
|
|
// protocol payload (tailcfg.DERPAdmitClientRequest) is a few hundred
|
|
|
|
|
// bytes; 4 KiB is generous and prevents an unauthenticated client from
|
|
|
|
|
// OOMing the public router with arbitrarily large POSTs.
|
|
|
|
|
const verifyBodyLimit int64 = 4 * 1024
|
|
|
|
|
|
2025-06-18 15:24:53 +08:00
|
|
|
func (h *Headscale) handleVerifyRequest(
|
2024-11-22 20:23:05 +08:00
|
|
|
req *http.Request,
|
2025-06-18 15:24:53 +08:00
|
|
|
writer io.Writer,
|
|
|
|
|
) error {
|
2024-11-22 20:23:05 +08:00
|
|
|
body, err := io.ReadAll(req.Body)
|
|
|
|
|
if err != nil {
|
2026-04-09 17:56:55 +00:00
|
|
|
return NewHTTPError(http.StatusRequestEntityTooLarge, "request body too large", fmt.Errorf("reading request body: %w", err))
|
2024-11-22 20:23:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
|
2026-02-06 21:45:32 +01:00
|
|
|
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil { //nolint:noinlineerr
|
2026-02-05 16:29:54 +00:00
|
|
|
return NewHTTPError(http.StatusBadRequest, "Bad Request: invalid JSON", fmt.Errorf("parsing DERP client request: %w", err))
|
2024-11-22 20:23:05 +08:00
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
nodes := h.state.ListNodes()
|
|
|
|
|
|
|
|
|
|
// Check if any node has the requested NodeKey
|
|
|
|
|
var nodeKeyFound bool
|
2025-11-11 22:46:57 -05:00
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
for _, node := range nodes.All() {
|
|
|
|
|
if node.NodeKey() == derpAdmitClientRequest.NodePublic {
|
|
|
|
|
nodeKeyFound = true
|
|
|
|
|
break
|
|
|
|
|
}
|
2024-11-22 20:23:05 +08:00
|
|
|
}
|
|
|
|
|
|
2025-06-18 15:24:53 +08:00
|
|
|
resp := &tailcfg.DERPAdmitClientResponse{
|
2025-07-05 23:30:47 +02:00
|
|
|
Allow: nodeKeyFound,
|
2025-06-18 15:24:53 +08:00
|
|
|
}
|
2025-07-10 23:38:55 +02:00
|
|
|
|
2025-06-18 15:24:53 +08:00
|
|
|
return json.NewEncoder(writer).Encode(resp)
|
2024-11-22 20:23:05 +08:00
|
|
|
}
|
|
|
|
|
|
2025-06-18 15:24:53 +08:00
|
|
|
// VerifyHandler see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159
|
|
|
|
|
// DERP use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.
|
2024-11-22 20:23:05 +08:00
|
|
|
func (h *Headscale) VerifyHandler(
|
|
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
|
|
|
|
) {
|
|
|
|
|
if req.Method != http.MethodPost {
|
2025-02-01 15:25:18 +01:00
|
|
|
httpError(writer, errMethodNotAllowed)
|
2024-11-22 20:23:05 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 17:56:55 +00:00
|
|
|
req.Body = http.MaxBytesReader(writer, req.Body, verifyBodyLimit)
|
|
|
|
|
|
2025-06-18 15:24:53 +08:00
|
|
|
err := h.handleVerifyRequest(req, writer)
|
2024-11-22 20:23:05 +08:00
|
|
|
if err != nil {
|
2025-02-01 15:25:18 +01:00
|
|
|
httpError(writer, err)
|
2025-01-30 21:40:29 +00:00
|
|
|
return
|
2024-11-22 20:23:05 +08:00
|
|
|
}
|
2025-11-11 22:46:57 -05:00
|
|
|
|
2024-11-22 20:23:05 +08:00
|
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-06 17:14:56 +02:00
|
|
|
// KeyHandler provides the Headscale pub key
|
|
|
|
|
// Listens in /key.
|
|
|
|
|
func (h *Headscale) KeyHandler(
|
|
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
|
|
|
|
) {
|
|
|
|
|
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
|
2025-07-04 09:40:29 +02:00
|
|
|
capVer, err := parseCapabilityVersion(req)
|
2023-09-28 12:33:53 -07:00
|
|
|
if err != nil {
|
2025-02-01 15:25:18 +01:00
|
|
|
httpError(writer, err)
|
2023-09-28 12:33:53 -07:00
|
|
|
return
|
2023-06-06 17:14:56 +02:00
|
|
|
}
|
2023-09-28 12:33:53 -07:00
|
|
|
|
|
|
|
|
// TS2021 (Tailscale v2 protocol) requires to have a different key
|
|
|
|
|
if capVer >= NoiseCapabilityVersion {
|
|
|
|
|
resp := tailcfg.OverTLSPublicKeyResponse{
|
2023-11-23 08:31:33 +01:00
|
|
|
PublicKey: h.noisePrivateKey.Public(),
|
2023-09-28 12:33:53 -07:00
|
|
|
}
|
2025-11-11 22:46:57 -05:00
|
|
|
|
2023-09-28 12:33:53 -07:00
|
|
|
writer.Header().Set("Content-Type", "application/json")
|
2026-02-06 21:45:32 +01:00
|
|
|
|
|
|
|
|
err := json.NewEncoder(writer).Encode(resp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to encode public key response")
|
|
|
|
|
}
|
2023-09-28 12:33:53 -07:00
|
|
|
|
|
|
|
|
return
|
2023-06-06 17:14:56 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-06 13:39:10 +02:00
|
|
|
func (h *Headscale) HealthHandler(
|
|
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
|
|
|
|
) {
|
|
|
|
|
respond := func(err error) {
|
|
|
|
|
writer.Header().Set("Content-Type", "application/health+json; charset=utf-8")
|
|
|
|
|
|
|
|
|
|
res := struct {
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
}{
|
|
|
|
|
Status: "pass",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
writer.WriteHeader(http.StatusInternalServerError)
|
2025-11-11 22:46:57 -05:00
|
|
|
|
2022-07-06 13:39:10 +02:00
|
|
|
res.Status = "fail"
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
encErr := json.NewEncoder(writer).Encode(res)
|
|
|
|
|
if encErr != nil {
|
|
|
|
|
log.Error().Err(encErr).Msg("failed to encode health response")
|
|
|
|
|
}
|
2022-07-06 13:39:10 +02:00
|
|
|
}
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-11-11 22:46:57 -05:00
|
|
|
err := h.state.PingDB(req.Context())
|
|
|
|
|
if err != nil {
|
2022-07-06 13:39:10 +02:00
|
|
|
respond(err)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
respond(nil)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:16:27 +02:00
|
|
|
func (h *Headscale) RobotsHandler(
|
|
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
|
|
|
|
) {
|
|
|
|
|
writer.Header().Set("Content-Type", "text/plain")
|
|
|
|
|
writer.WriteHeader(http.StatusOK)
|
2025-11-11 22:46:57 -05:00
|
|
|
|
2025-06-06 11:16:27 +02:00
|
|
|
_, err := writer.Write([]byte("User-agent: *\nDisallow: /"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().
|
|
|
|
|
Caller().
|
|
|
|
|
Err(err).
|
2025-09-05 16:32:46 +02:00
|
|
|
Msg("Failed to write HTTP response")
|
2025-06-06 11:16:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-27 10:41:34 +01:00
|
|
|
// VersionHandler returns version information about the Headscale server
|
|
|
|
|
// Listens in /version.
|
|
|
|
|
func (h *Headscale) VersionHandler(
|
|
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
|
|
|
|
) {
|
|
|
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
|
|
|
writer.WriteHeader(http.StatusOK)
|
|
|
|
|
|
|
|
|
|
versionInfo := types.GetVersionInfo()
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-11-11 22:46:57 -05:00
|
|
|
err := json.NewEncoder(writer).Encode(versionInfo)
|
|
|
|
|
if err != nil {
|
2025-10-27 10:41:34 +01:00
|
|
|
log.Error().
|
|
|
|
|
Caller().
|
|
|
|
|
Err(err).
|
|
|
|
|
Msg("Failed to write version response")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Redo OIDC configuration (#2020)
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>
2024-10-02 14:50:17 +02:00
|
|
|
type AuthProviderWeb struct {
|
|
|
|
|
serverURL string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewAuthProviderWeb(serverURL string) *AuthProviderWeb {
|
|
|
|
|
return &AuthProviderWeb{
|
|
|
|
|
serverURL: serverURL,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 18:48:57 +00:00
|
|
|
func (a *AuthProviderWeb) RegisterURL(authID types.AuthID) string {
|
Redo OIDC configuration (#2020)
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>
2024-10-02 14:50:17 +02:00
|
|
|
return fmt.Sprintf(
|
|
|
|
|
"%s/register/%s",
|
|
|
|
|
strings.TrimSuffix(a.serverURL, "/"),
|
2026-02-24 18:48:57 +00:00
|
|
|
authID.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *AuthProviderWeb) AuthURL(authID types.AuthID) string {
|
|
|
|
|
return fmt.Sprintf(
|
|
|
|
|
"%s/auth/%s",
|
|
|
|
|
strings.TrimSuffix(a.serverURL, "/"),
|
|
|
|
|
authID.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *AuthProviderWeb) AuthHandler(
|
|
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
|
|
|
|
) {
|
2026-02-24 18:49:18 +00:00
|
|
|
authID, err := authIDFromRequest(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
httpError(writer, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
writer.WriteHeader(http.StatusOK)
|
|
|
|
|
|
|
|
|
|
_, err = writer.Write([]byte(templates.AuthWeb(
|
|
|
|
|
"Authentication check",
|
|
|
|
|
"Run the command below in the headscale server to approve this authentication request:",
|
|
|
|
|
"headscale auth approve --auth-id "+authID.String(),
|
|
|
|
|
).Render()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to write auth response")
|
|
|
|
|
}
|
2026-02-24 18:48:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func authIDFromRequest(req *http.Request) (types.AuthID, error) {
|
2026-02-24 18:51:11 +00:00
|
|
|
raw, err := urlParam[string](req, "auth_id")
|
2026-02-24 18:48:57 +00:00
|
|
|
if err != nil {
|
2026-02-28 20:36:21 +01:00
|
|
|
return "", NewHTTPError(http.StatusBadRequest, "invalid auth id", fmt.Errorf("parsing auth_id from URL: %w", err))
|
2026-02-24 18:48:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to make sure we dont open for XSS style injections, if the parameter that
|
|
|
|
|
// is passed as a key is not parsable/validated as a NodePublic key, then fail to render
|
|
|
|
|
// the template and log an error.
|
2026-02-28 20:36:21 +01:00
|
|
|
authId, err := types.AuthIDFromString(raw)
|
2026-02-24 18:48:57 +00:00
|
|
|
if err != nil {
|
2026-02-28 20:36:21 +01:00
|
|
|
return "", NewHTTPError(http.StatusBadRequest, "invalid auth id", fmt.Errorf("parsing auth_id from URL: %w", err))
|
2026-02-24 18:48:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 20:36:21 +01:00
|
|
|
return authId, nil
|
Redo OIDC configuration (#2020)
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>
2024-10-02 14:50:17 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
// RegisterHandler shows a simple message in the browser to point to the CLI
|
2025-01-26 22:20:11 +01:00
|
|
|
// Listens in /register/:registration_id.
|
2022-08-11 12:16:50 +02:00
|
|
|
//
|
|
|
|
|
// This is not part of the Tailscale control API, as we could send whatever URL
|
|
|
|
|
// in the RegisterResponse.AuthURL field.
|
Redo OIDC configuration (#2020)
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>
2024-10-02 14:50:17 +02:00
|
|
|
func (a *AuthProviderWeb) RegisterHandler(
|
2022-06-26 11:55:37 +02:00
|
|
|
writer http.ResponseWriter,
|
|
|
|
|
req *http.Request,
|
2022-06-17 16:48:04 +02:00
|
|
|
) {
|
2026-02-28 20:36:21 +01:00
|
|
|
authId, err := authIDFromRequest(req)
|
2023-11-19 22:37:04 +01:00
|
|
|
if err != nil {
|
2026-02-24 18:48:57 +00:00
|
|
|
httpError(writer, err)
|
2021-12-22 19:43:53 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-04 04:39:24 -07:00
|
|
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
writer.WriteHeader(http.StatusOK)
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2026-02-24 18:49:18 +00:00
|
|
|
_, err = writer.Write([]byte(templates.AuthWeb(
|
|
|
|
|
"Node registration",
|
|
|
|
|
"Run the command below in the headscale server to add this node to your network:",
|
2026-02-28 20:36:21 +01:00
|
|
|
fmt.Sprintf("headscale auth register --auth-id %s --user USERNAME", authId.String()),
|
2026-02-24 18:49:18 +00:00
|
|
|
).Render()))
|
2026-02-06 21:45:32 +01:00
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("failed to write register response")
|
|
|
|
|
}
|
2021-02-28 00:58:09 +01:00
|
|
|
}
|
2025-11-11 22:46:57 -05:00
|
|
|
|
|
|
|
|
func FaviconHandler(writer http.ResponseWriter, req *http.Request) {
|
|
|
|
|
writer.Header().Set("Content-Type", "image/png")
|
2025-11-12 14:23:15 +01:00
|
|
|
http.ServeContent(writer, req, "favicon.ico", time.Unix(0, 0), bytes.NewReader(assets.Favicon))
|
2025-11-11 22:46:57 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 04:54:29 +01:00
|
|
|
// BlankHandler returns a blank page with favicon linked.
|
2025-11-11 22:46:57 -05:00
|
|
|
func BlankHandler(writer http.ResponseWriter, res *http.Request) {
|
|
|
|
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
2025-11-12 04:54:29 +01:00
|
|
|
writer.WriteHeader(http.StatusOK)
|
|
|
|
|
|
|
|
|
|
_, err := writer.Write([]byte(templates.BlankPage().Render()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().
|
|
|
|
|
Caller().
|
|
|
|
|
Err(err).
|
|
|
|
|
Msg("Failed to write HTTP response")
|
|
|
|
|
}
|
2025-11-11 22:46:57 -05:00
|
|
|
}
|