1package signin
2
3import (
4 "context"
5 "errors"
6 "net/http"
7 "time"
8
9 "tug/internal/platform/config"
10 "tug/internal/service/identity"
11 "tug/internal/service/session"
12 "tug/internal/storage"
13 "tug/internal/transport/forwardauth"
14 "tug/internal/transport/google"
15 "tug/internal/transport/idp"
16 "tug/internal/transport/oidcrp"
17 "tug/internal/transport/web"
18)
19
20const ForwardTxnLimit = 4
21
22type Wiring struct {
23 Store *storage.Store
24 Kit *web.Kit
25 IDP *idp.IDP
26 IDPHandler http.Handler
27 Server config.Server
28 PrimaryGroup string
29 Apps []config.App
30 MasterKey config.Secret
31 SessionTTL time.Duration
32 Now func() time.Time
33 Issuer string
34 AccountOrigin string
35 AdminOrigin string
36 AppOrigins map[string]string
37 Refuse func(http.ResponseWriter, *http.Request, int, error)
38 Google *google.RP
39 Portdistrict bool
40 ObserveAuthz func(outcome string)
41}
42
43type Composition struct {
44 Sessions *session.Service
45 PortalRP *oidcrp.RP
46 Signin *Signin
47 ForwardRP *oidcrp.RP
48 ForwardAuth *forwardauth.ForwardAuth
49}
50
51func Compose(ctx context.Context, w Wiring) (*Composition, error) {
52 if w.Store == nil || w.Kit == nil || w.IDP == nil {
53 return nil, errors.New("signin: the store, the kit and the identity provider are required")
54 }
55 handler := w.IDPHandler
56 if handler == nil {
57 handler = w.IDP.Handler()
58 }
59 refuse := w.Refuse
60 if refuse == nil {
61 refuse = Refuse(w.Kit)
62 }
63 ids := identity.NewWithLockout(w.Store, w.PrimaryGroup, w.IDP.Lockout())
64 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)})
65 if err != nil {
66 return nil, err
67 }
68 var clients []oidcrp.Client
69 for _, c := range []struct{ id, domain, origin string }{
70 {idp.PortalClientID, w.Server.DomainAccount, w.AccountOrigin},
71 {idp.AdminClientID, w.Server.DomainAdmin, w.AdminOrigin},
72 } {
73 secret, err := idp.FirstPartySecret(w.MasterKey.Bytes(), c.id)
74 if err != nil {
75 return nil, err
76 }
77 clients = append(clients, oidcrp.Client{ID: c.id, Secret: config.NewSecret([]byte(secret)), Origins: []string{originOr(c.origin, c.domain)}, CallbackPath: CallbackPath})
78 }
79 portal, err := oidcrp.New(ctx, oidcrp.Config{
80 Issuer: originOr(w.Issuer, w.Server.DomainIDP),
81 IDP: handler,
82 Hosts: []string{w.Server.DomainAccount, w.Server.DomainAdmin},
83 MasterKey: w.MasterKey,
84 Clients: clients,
85 ConnectorID: idp.ConnectorID,
86 Sessions: Sessions(sessions),
87 CodeEpoch: w.IDP.CodeEpoch,
88 Now: w.Now,
89 Refuse: refuse,
90 Rotate: w.Kit.RotateCSRF,
91 })
92 if err != nil {
93 return nil, err
94 }
95 si, err := New(Config{
96 Kit: w.Kit, Sessions: sessions, Identity: ids.Observed(identity.PathSudo, w.IDP.ObserveChecks()), Verifier: w.IDP.Verifier(), RP: portal, AccountDomain: w.Server.DomainAccount,
97 AdminOrigin: originOr(w.AdminOrigin, w.Server.DomainAdmin), PrimaryGroup: w.PrimaryGroup, Google: w.Google, Portdistrict: w.Portdistrict,
98 })
99 if err != nil {
100 return nil, err
101 }
102 comp := &Composition{Sessions: sessions, PortalRP: portal, Signin: si}
103 if len(w.Apps) == 0 {
104 return comp, nil
105 }
106 secret, err := idp.FirstPartySecret(w.MasterKey.Bytes(), idp.ForwardClientID)
107 if err != nil {
108 return nil, err
109 }
110 hosts := make([]string, 0, len(w.Apps))
111 origins := make([]string, 0, len(w.Apps))
112 for _, a := range w.Apps {
113 hosts = append(hosts, a.Domain)
114 origins = append(origins, originOr(w.AppOrigins[a.Domain], a.Domain))
115 }
116 comp.ForwardRP, err = oidcrp.New(ctx, oidcrp.Config{
117 Issuer: originOr(w.Issuer, w.Server.DomainIDP),
118 IDP: handler,
119 Hosts: hosts,
120 MasterKey: w.MasterKey,
121 Clients: []oidcrp.Client{{ID: idp.ForwardClientID, Secret: config.NewSecret([]byte(secret)), Origins: origins, CallbackPath: forwardauth.CallbackPath}},
122 ConnectorID: idp.ConnectorID,
123 Sessions: Sessions(sessions),
124 CodeEpoch: w.IDP.CodeEpoch,
125 TxnLimit: ForwardTxnLimit,
126 Now: w.Now,
127 Refuse: refuse,
128 Rotate: w.Kit.RotateCSRF,
129 })
130 if err != nil {
131 return nil, err
132 }
133 guard, ender := AppSession(sessions, comp.ForwardRP)
134 comp.ForwardAuth, err = forwardauth.New(forwardauth.Config{RP: comp.ForwardRP, Sessions: sessions, Kit: w.Kit, Guard: guard, Ender: ender, Observe: w.ObserveAuthz})
135 if err != nil {
136 return nil, err
137 }
138 return comp, nil
139}
140
141func originOr(origin, domain string) string {
142 if origin != "" {
143 return origin
144 }
145 return "https://" + domain
146}