Raw
  1package idp
  2
  3import (
  4	"context"
  5	"crypto/rand"
  6	"encoding/base64"
  7	"encoding/json"
  8	"errors"
  9	"fmt"
 10	"log/slog"
 11	"net/http"
 12	"net/url"
 13	"strings"
 14	"time"
 15
 16	gosundheit "github.com/AppsFlyer/go-sundheit"
 17	"github.com/dexidp/dex/server"
 18	"github.com/dexidp/dex/server/authflow"
 19	"github.com/dexidp/dex/server/signer"
 20	"github.com/dexidp/dex/server/tokens"
 21	dexstorage "github.com/dexidp/dex/storage"
 22	"github.com/prometheus/client_golang/prometheus"
 23
 24	"tug/internal/platform/backoff"
 25	"tug/internal/platform/config"
 26	"tug/internal/service/identity"
 27	"tug/internal/service/refusalcap"
 28	"tug/internal/storage"
 29	"tug/internal/storage/dexstore"
 30	transporthttp "tug/internal/transport/http"
 31	"tug/internal/transport/idp/theme"
 32)
 33
 34var (
 35	dexStorageHook    = func(s dexstorage.Storage) dexstorage.Storage { return s }
 36	authenticatedHook = func(ctx context.Context, userID string) {}
 37	resumeHook        = func(ctx context.Context, userID string) {}
 38)
 39
 40const (
 41	IDTokensValidFor        = time.Hour
 42	AuthRequestsValidFor    = time.Hour
 43	RefreshAbsoluteLifetime = 720 * time.Hour
 44	RefreshIdleLifetime     = 168 * time.Hour
 45	SigningKeyRotation      = "6h"
 46	ValueTTL                = 5 * time.Minute
 47)
 48
 49type Config struct {
 50	Issuer        string
 51	AccountOrigin string
 52	AdminOrigin   string
 53	AccountDomain string
 54	AdminDomain   string
 55	Apps          []config.App
 56	AppOrigins    map[string]string
 57	Clients       []config.Client
 58	ClientSecrets map[string]config.Secret
 59	MasterKey     config.Secret
 60	PrimaryGroup  string
 61	ErrorPage     transporthttp.ErrorPage
 62	Now           func() time.Time
 63	Logger        *slog.Logger
 64	Metrics       *prometheus.Registry
 65	ObserveChecks func(path, result string)
 66	Refusals      *refusalcap.Cap
 67}
 68
 69type IDP struct {
 70	handler  http.Handler
 71	verifier *Verifier
 72	dex      dexstorage.Storage
 73	key      string
 74	cancel   context.CancelFunc
 75}
 76
 77func Start(ctx context.Context, store *storage.Store, cfg Config) (*IDP, error) {
 78	issuer, err := url.Parse(cfg.Issuer)
 79	if err != nil || issuer.Host == "" || (issuer.Scheme != "https" && issuer.Scheme != "http") || issuer.Path != "" {
 80		return nil, fmt.Errorf("idp: issuer %q is not scheme://host", cfg.Issuer)
 81	}
 82	if cfg.AccountOrigin == "" || cfg.AccountDomain == "" || cfg.AdminDomain == "" {
 83		return nil, errors.New("idp: the account origin and the account and admin domains are required")
 84	}
 85	if _, err := transporthttp.ParseOrigin(cfg.AccountOrigin, issuer.Scheme, cfg.AccountDomain); err != nil {
 86		return nil, fmt.Errorf("idp: account origin: %w", err)
 87	}
 88	if _, err := transporthttp.ParseOrigin(cfg.AdminOrigin, issuer.Scheme, cfg.AdminDomain); err != nil {
 89		return nil, fmt.Errorf("idp: admin origin: %w", err)
 90	}
 91	apps := make(map[string]bool, len(cfg.Apps))
 92	for _, a := range cfg.Apps {
 93		apps[a.Domain] = true
 94	}
 95	for domain, origin := range cfg.AppOrigins {
 96		if !apps[domain] {
 97			return nil, fmt.Errorf("idp: app origin %q: %q is not an app", origin, domain)
 98		}
 99		if _, err := transporthttp.ParseOrigin(origin, issuer.Scheme, domain); err != nil {
100			return nil, fmt.Errorf("idp: app origin: %w", err)
101		}
102	}
103	if cfg.ErrorPage == nil {
104		return nil, errors.New("idp: an error page is required")
105	}
106	now := cfg.Now
107	if now == nil {
108		now = time.Now
109	}
110	logger := cfg.Logger
111	if logger == nil {
112		logger = slog.Default()
113	}
114	clients, err := staticClients(cfg)
115	if err != nil {
116		return nil, err
117	}
118
119	view, err := dexstore.Attach(ctx, store)
120	if err != nil {
121		return nil, fmt.Errorf("idp: attach dex storage: %w", err)
122	}
123	if err := deleteStoredClients(ctx, view); err != nil {
124		return nil, err
125	}
126
127	registerConnectorType()
128	hooked := guardEpochs(dexStorageHook(view), store)
129	keyBytes := make([]byte, 16)
130	rand.Read(keyBytes)
131	key := base64.RawURLEncoding.EncodeToString(keyBytes)
132	d := &deps{
133		identity:      identity.NewWithLockout(store, cfg.PrimaryGroup, backoff.New(now, backoff.Entries)).Observed(identity.PathLogin, cfg.ObserveChecks),
134		store:         store,
135		dex:           hooked,
136		now:           now,
137		issuer:        strings.TrimSuffix(cfg.Issuer, "/"),
138		accountOrigin: cfg.AccountOrigin,
139		accountDomain: cfg.AccountDomain,
140		refusals:      refusalsOrOwn(cfg.Refusals, store, now, logger),
141	}
142	registry.Store(key, d)
143	fail := func(err error) (*IDP, error) {
144		registry.Delete(key)
145		return nil, err
146	}
147	if err := writeConnectorRow(ctx, view, key); err != nil {
148		return fail(err)
149	}
150
151	sig, err := (&signer.LocalConfig{KeysRotationPeriod: SigningKeyRotation}).Open(ctx, view, IDTokensValidFor, now, logger)
152	if err != nil {
153		return fail(fmt.Errorf("idp: open signer: %w", err))
154	}
155	srvCtx, cancel := context.WithCancel(context.Background())
156	srv, err := server.NewServer(srvCtx, server.Config{
157		Issuer:                 cfg.Issuer,
158		Storage:                dexstorage.WithStaticClients(hooked, clients),
159		AllowedGrantTypes:      []string{"authorization_code", "refresh_token"},
160		SupportedResponseTypes: []string{"code"},
161		SkipApprovalScreen:     true,
162		IDTokensValidFor:       IDTokensValidFor,
163		AuthRequestsValidFor:   AuthRequestsValidFor,
164		RefreshTokenPolicy:     tokens.NewRefreshStrategy(true, RefreshAbsoluteLifetime, RefreshIdleLifetime, 0, now),
165		PKCE:                   authflow.PKCEConfig{CodeChallengeMethodsSupported: []string{"S256"}},
166		Now:                    now,
167		Logger:                 logger,
168		Signer:                 sig,
169		HealthChecker:          gosundheit.New(),
170		Web:                    server.WebConfig{WebFS: theme.FS(), Issuer: theme.Issuer, Theme: theme.Name},
171		PrometheusRegistry:     cfg.Metrics,
172	})
173	if err != nil {
174		cancel()
175		return fail(fmt.Errorf("idp: start dex: %w", err))
176	}
177	public := make(map[string]bool)
178	for _, c := range cfg.Clients {
179		if c.Public {
180			public[c.ID] = true
181		}
182	}
183	h, err := newHandler(revokeOnReuse(srv, hooked, clients, store, logger), public, cfg.ErrorPage)
184	if err != nil {
185		cancel()
186		return fail(err)
187	}
188	verifier := &Verifier{deps: d}
189	return &IDP{handler: silent{next: h, v: verifier, page: cfg.ErrorPage}, verifier: verifier, dex: view, key: key, cancel: cancel}, nil
190}
191
192func (p *IDP) Handler() http.Handler {
193	return p.handler
194}
195
196func (p *IDP) Verifier() *Verifier {
197	return p.verifier
198}
199
200func (p *IDP) Ready(ctx context.Context) error {
201	keys, err := p.dex.GetKeys(ctx)
202	if err != nil {
203		return fmt.Errorf("idp: read signing keys: %w", err)
204	}
205	if keys.SigningKey == nil {
206		return errors.New("idp: no signing key yet")
207	}
208	return nil
209}
210
211func (p *IDP) Stop() {
212	p.cancel()
213	registry.Delete(p.key)
214}
215
216func writeConnectorRow(ctx context.Context, dex dexstorage.Storage, key string) error {
217	cfg, err := json.Marshal(connectorConfig{Key: key})
218	if err != nil {
219		return err
220	}
221	existing, err := dex.ListConnectors(ctx)
222	if err != nil {
223		return fmt.Errorf("idp: list connectors: %w", err)
224	}
225	have := false
226	for _, c := range existing {
227		if c.ID == ConnectorID {
228			have = true
229			continue
230		}
231		if err := dex.DeleteConnector(ctx, c.ID); err != nil {
232			return fmt.Errorf("idp: delete connector %q: %w", c.ID, err)
233		}
234	}
235	row := dexstorage.Connector{ID: ConnectorID, Type: ConnectorType, Name: "tug", Config: cfg}
236	if !have {
237		return dex.CreateConnector(ctx, row)
238	}
239	return dex.UpdateConnector(ctx, ConnectorID, func(dexstorage.Connector) (dexstorage.Connector, error) { return row, nil })
240}