mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-06-28 22:30:17 +00:00
feat: tailscale integration (#847)
This commit is contained in:
@@ -78,6 +78,7 @@ type AuthService struct {
|
||||
ldap *LdapService
|
||||
queries repository.Store
|
||||
oauthBroker *OAuthBrokerService
|
||||
tailscale *TailscaleService
|
||||
|
||||
loginAttempts map[string]*LoginAttempt
|
||||
ldapGroupsCache map[string]*LdapGroupsCache
|
||||
@@ -99,6 +100,7 @@ func NewAuthService(
|
||||
ldap *LdapService,
|
||||
queries repository.Store,
|
||||
oauthBroker *OAuthBrokerService,
|
||||
tailscale *TailscaleService,
|
||||
) *AuthService {
|
||||
service := &AuthService{
|
||||
log: log,
|
||||
@@ -111,6 +113,7 @@ func NewAuthService(
|
||||
ldap: ldap,
|
||||
queries: queries,
|
||||
oauthBroker: oauthBroker,
|
||||
tailscale: tailscale,
|
||||
}
|
||||
|
||||
wg.Go(service.CleanupOAuthSessionsRoutine)
|
||||
@@ -292,6 +295,10 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||
}
|
||||
|
||||
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
|
||||
if data.Provider == "tailscale" && auth.tailscale == nil {
|
||||
return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user")
|
||||
}
|
||||
|
||||
uuid, err := uuid.NewRandom()
|
||||
|
||||
if err != nil {
|
||||
@@ -328,6 +335,28 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
|
||||
return nil, fmt.Errorf("failed to create session entry: %w", err)
|
||||
}
|
||||
|
||||
if data.Provider == "tailscale" {
|
||||
auth.log.App.Trace().Str("url", fmt.Sprintf("https://%s", auth.tailscale.GetHostname())).Msg("Extracting root domain from Tailscale hostname")
|
||||
|
||||
tsCookieDomain, err := utils.GetCookieDomain(fmt.Sprintf("https://%s", auth.tailscale.GetHostname()))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cookie domain for tailscale user: %w", err)
|
||||
}
|
||||
|
||||
return &http.Cookie{
|
||||
Name: auth.runtime.SessionCookieName,
|
||||
Value: session.UUID,
|
||||
Path: "/",
|
||||
Domain: fmt.Sprintf(".%s", tsCookieDomain),
|
||||
Expires: expiresAt,
|
||||
MaxAge: int(time.Until(expiresAt).Seconds()),
|
||||
Secure: auth.config.Auth.SecureCookie,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &http.Cookie{
|
||||
Name: auth.runtime.SessionCookieName,
|
||||
Value: session.UUID,
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
type TailscaleWhoisResponse struct {
|
||||
UserID string
|
||||
LoginName string
|
||||
DisplayName string
|
||||
NodeName string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type TailscaleService struct {
|
||||
log *logger.Logger
|
||||
wg *sync.WaitGroup
|
||||
config model.Config
|
||||
ctx context.Context
|
||||
|
||||
srv *tsnet.Server
|
||||
lc *local.Client
|
||||
ln *net.Listener
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, wg *sync.WaitGroup) (*TailscaleService, error) {
|
||||
if !config.Tailscale.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
srv := new(tsnet.Server)
|
||||
|
||||
// node options
|
||||
srv.Dir = config.Tailscale.Dir
|
||||
srv.Hostname = config.Tailscale.Hostname
|
||||
srv.AuthKey = config.Tailscale.AuthKey
|
||||
srv.Ephemeral = config.Tailscale.Ephemeral
|
||||
|
||||
// redirect logs to zerolog
|
||||
srv.Logf = log.App.Printf
|
||||
srv.UserLogf = log.App.Printf
|
||||
|
||||
err := srv.Start()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start tailscale server: %w", err)
|
||||
}
|
||||
|
||||
lc, err := srv.LocalClient()
|
||||
|
||||
if err != nil {
|
||||
_ = srv.Close()
|
||||
return nil, fmt.Errorf("failed to get tailscale local client: %w", err)
|
||||
}
|
||||
|
||||
service := &TailscaleService{
|
||||
log: log,
|
||||
wg: wg,
|
||||
config: config,
|
||||
ctx: ctx,
|
||||
srv: srv,
|
||||
lc: lc,
|
||||
}
|
||||
|
||||
connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed
|
||||
defer cancel()
|
||||
|
||||
err = service.waitForConn(connectCtx)
|
||||
|
||||
if err != nil {
|
||||
_ = srv.Close()
|
||||
return nil, fmt.Errorf("failed to connect to tailscale network: %w", err)
|
||||
}
|
||||
|
||||
wg.Go(service.watchAndClose)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) watchAndClose() {
|
||||
<-ts.ctx.Done()
|
||||
ts.log.App.Debug().Msg("Shutting down Tailscale service")
|
||||
ts.mu.Lock()
|
||||
srv := ts.srv
|
||||
ln := ts.ln
|
||||
ts.ln = nil
|
||||
ts.srv = nil
|
||||
ts.mu.Unlock()
|
||||
if ln != nil {
|
||||
(*ts.ln).Close()
|
||||
}
|
||||
if srv != nil {
|
||||
ts.srv.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) Whois(ctx context.Context, addr string) (*TailscaleWhoisResponse, error) {
|
||||
who, err := ts.lc.WhoIs(ctx, addr)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, local.ErrPeerNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get client whois: %w", err)
|
||||
}
|
||||
|
||||
res := TailscaleWhoisResponse{
|
||||
UserID: who.UserProfile.ID.String(),
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
DisplayName: who.UserProfile.DisplayName,
|
||||
NodeName: strings.TrimSuffix(who.Node.Name, "."),
|
||||
Tags: who.Node.Tags,
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) CreateListener() (net.Listener, error) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
|
||||
if ts.ln != nil {
|
||||
return *ts.ln, nil
|
||||
}
|
||||
ln, err := ts.srv.ListenTLS("tcp", ":443")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts.ln = &ln
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) GetHostname() string {
|
||||
status, err := ts.lc.Status(ts.ctx)
|
||||
|
||||
if err != nil {
|
||||
ts.log.App.Error().Err(err).Msg("Failed to get Tailscale status")
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(status.Self.DNSName, ".")
|
||||
}
|
||||
|
||||
func (ts *TailscaleService) waitForConn(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("timed out waiting for tailscale connection")
|
||||
default:
|
||||
ip4, _ := ts.srv.TailscaleIPs()
|
||||
if !ip4.IsValid() {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user