package signin import ( "context" "errors" "net/http" "time" "tug/internal/platform/config" "tug/internal/service/identity" "tug/internal/service/session" "tug/internal/storage" "tug/internal/transport/forwardauth" "tug/internal/transport/google" "tug/internal/transport/idp" "tug/internal/transport/oidcrp" "tug/internal/transport/web" ) const ForwardTxnLimit = 4 type Wiring struct { Store *storage.Store Kit *web.Kit IDP *idp.IDP IDPHandler http.Handler Server config.Server PrimaryGroup string Apps []config.App MasterKey config.Secret SessionTTL time.Duration Now func() time.Time Issuer string AccountOrigin string AdminOrigin string AppOrigins map[string]string Refuse func(http.ResponseWriter, *http.Request, int, error) Google *google.RP Portdistrict bool ObserveAuthz func(outcome string) } type Composition struct { Sessions *session.Service PortalRP *oidcrp.RP Signin *Signin ForwardRP *oidcrp.RP ForwardAuth *forwardauth.ForwardAuth } func Compose(ctx context.Context, w Wiring) (*Composition, error) { if w.Store == nil || w.Kit == nil || w.IDP == nil { return nil, errors.New("signin: the store, the kit and the identity provider are required") } handler := w.IDPHandler if handler == nil { handler = w.IDP.Handler() } refuse := w.Refuse if refuse == nil { refuse = Refuse(w.Kit) } ids := identity.NewWithLockout(w.Store, w.PrimaryGroup, w.IDP.Lockout()) sessions, err := session.New(session.Config{Store: w.Store, Identity: ids.Observed(identity.PathPassword, w.IDP.ObserveChecks()), AdminGroup: w.Server.AdminGroup, TTL: w.SessionTTL, Now: w.Now, HostTTL: config.HostTTLs(w.Apps)}) if err != nil { return nil, err } var clients []oidcrp.Client for _, c := range []struct{ id, domain, origin string }{ {idp.PortalClientID, w.Server.DomainAccount, w.AccountOrigin}, {idp.AdminClientID, w.Server.DomainAdmin, w.AdminOrigin}, } { secret, err := idp.FirstPartySecret(w.MasterKey.Bytes(), c.id) if err != nil { return nil, err } clients = append(clients, oidcrp.Client{ID: c.id, Secret: config.NewSecret([]byte(secret)), Origins: []string{originOr(c.origin, c.domain)}, CallbackPath: CallbackPath}) } portal, err := oidcrp.New(ctx, oidcrp.Config{ Issuer: originOr(w.Issuer, w.Server.DomainIDP), IDP: handler, Hosts: []string{w.Server.DomainAccount, w.Server.DomainAdmin}, MasterKey: w.MasterKey, Clients: clients, ConnectorID: idp.ConnectorID, Sessions: Sessions(sessions), CodeEpoch: w.IDP.CodeEpoch, Now: w.Now, Refuse: refuse, Rotate: w.Kit.RotateCSRF, }) if err != nil { return nil, err } si, err := New(Config{ Kit: w.Kit, Sessions: sessions, Identity: ids.Observed(identity.PathSudo, w.IDP.ObserveChecks()), Verifier: w.IDP.Verifier(), RP: portal, AccountDomain: w.Server.DomainAccount, AdminOrigin: originOr(w.AdminOrigin, w.Server.DomainAdmin), PrimaryGroup: w.PrimaryGroup, Google: w.Google, Portdistrict: w.Portdistrict, }) if err != nil { return nil, err } comp := &Composition{Sessions: sessions, PortalRP: portal, Signin: si} if len(w.Apps) == 0 { return comp, nil } secret, err := idp.FirstPartySecret(w.MasterKey.Bytes(), idp.ForwardClientID) if err != nil { return nil, err } hosts := make([]string, 0, len(w.Apps)) origins := make([]string, 0, len(w.Apps)) for _, a := range w.Apps { hosts = append(hosts, a.Domain) origins = append(origins, originOr(w.AppOrigins[a.Domain], a.Domain)) } comp.ForwardRP, err = oidcrp.New(ctx, oidcrp.Config{ Issuer: originOr(w.Issuer, w.Server.DomainIDP), IDP: handler, Hosts: hosts, MasterKey: w.MasterKey, Clients: []oidcrp.Client{{ID: idp.ForwardClientID, Secret: config.NewSecret([]byte(secret)), Origins: origins, CallbackPath: forwardauth.CallbackPath}}, ConnectorID: idp.ConnectorID, Sessions: Sessions(sessions), CodeEpoch: w.IDP.CodeEpoch, TxnLimit: ForwardTxnLimit, Now: w.Now, Refuse: refuse, Rotate: w.Kit.RotateCSRF, }) if err != nil { return nil, err } guard, ender := AppSession(sessions, comp.ForwardRP) comp.ForwardAuth, err = forwardauth.New(forwardauth.Config{RP: comp.ForwardRP, Sessions: sessions, Kit: w.Kit, Guard: guard, Ender: ender, Observe: w.ObserveAuthz}) if err != nil { return nil, err } return comp, nil } func originOr(origin, domain string) string { if origin != "" { return origin } return "https://" + domain }