+196
appview/oauth/handler.go
+196
appview/oauth/handler.go
···
1
1
package oauth
2
2
3
3
import (
4
+
"bytes"
5
+
"context"
4
6
"encoding/json"
7
+
"fmt"
5
8
"log"
6
9
"net/http"
10
+
"slices"
11
+
"time"
7
12
8
13
"github.com/go-chi/chi/v5"
9
14
"github.com/lestrrat-go/jwx/v2/jwk"
10
15
"github.com/posthog/posthog-go"
16
+
"tangled.org/core/api/tangled"
17
+
"tangled.org/core/appview/db"
18
+
"tangled.org/core/consts"
19
+
"tangled.org/core/tid"
11
20
)
12
21
13
22
func (o *OAuth) Router() http.Handler {
···
62
71
return
63
72
}
64
73
74
+
log.Println("session saved successfully")
75
+
go o.addToDefaultKnot(sessData.AccountDID.String())
76
+
go o.addToDefaultSpindle(sessData.AccountDID.String())
77
+
65
78
if !o.Config.Core.Dev {
66
79
err = o.Posthog.Enqueue(posthog.Capture{
67
80
DistinctId: sessData.AccountDID.String(),
···
74
87
75
88
http.Redirect(w, r, "/", http.StatusFound)
76
89
}
90
+
91
+
func (o *OAuth) addToDefaultSpindle(did string) {
92
+
// use the tangled.sh app password to get an accessJwt
93
+
// and create an sh.tangled.spindle.member record with that
94
+
spindleMembers, err := db.GetSpindleMembers(
95
+
o.Db,
96
+
db.FilterEq("instance", "spindle.tangled.sh"),
97
+
db.FilterEq("subject", did),
98
+
)
99
+
if err != nil {
100
+
log.Printf("failed to get spindle members for did %s: %v", did, err)
101
+
return
102
+
}
103
+
104
+
if len(spindleMembers) != 0 {
105
+
log.Printf("did %s is already a member of the default spindle", did)
106
+
return
107
+
}
108
+
109
+
log.Printf("adding %s to default spindle", did)
110
+
session, err := o.createAppPasswordSession(o.Config.Core.AppPassword, consts.TangledDid)
111
+
if err != nil {
112
+
log.Printf("failed to create session: %s", err)
113
+
return
114
+
}
115
+
116
+
record := tangled.SpindleMember{
117
+
LexiconTypeID: "sh.tangled.spindle.member",
118
+
Subject: did,
119
+
Instance: consts.DefaultSpindle,
120
+
CreatedAt: time.Now().Format(time.RFC3339),
121
+
}
122
+
123
+
if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil {
124
+
log.Printf("failed to add member to default spindle: %s", err)
125
+
return
126
+
}
127
+
128
+
log.Printf("successfully added %s to default spindle", did)
129
+
}
130
+
131
+
func (o *OAuth) addToDefaultKnot(did string) {
132
+
// use the tangled.sh app password to get an accessJwt
133
+
// and create an sh.tangled.spindle.member record with that
134
+
135
+
allKnots, err := o.Enforcer.GetKnotsForUser(did)
136
+
if err != nil {
137
+
log.Printf("failed to get knot members for did %s: %v", did, err)
138
+
return
139
+
}
140
+
141
+
if slices.Contains(allKnots, consts.DefaultKnot) {
142
+
log.Printf("did %s is already a member of the default knot", did)
143
+
return
144
+
}
145
+
146
+
log.Printf("adding %s to default knot", did)
147
+
session, err := o.createAppPasswordSession(o.Config.Core.TmpAltAppPassword, consts.IcyDid)
148
+
if err != nil {
149
+
log.Printf("failed to create session: %s", err)
150
+
return
151
+
}
152
+
153
+
record := tangled.KnotMember{
154
+
LexiconTypeID: "sh.tangled.knot.member",
155
+
Subject: did,
156
+
Domain: consts.DefaultKnot,
157
+
CreatedAt: time.Now().Format(time.RFC3339),
158
+
}
159
+
160
+
if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil {
161
+
log.Printf("failed to add member to default knot: %s", err)
162
+
return
163
+
}
164
+
165
+
if err := o.Enforcer.AddKnotMember(consts.DefaultKnot, did); err != nil {
166
+
log.Printf("failed to set up enforcer rules: %s", err)
167
+
return
168
+
}
169
+
170
+
log.Printf("successfully added %s to default Knot", did)
171
+
}
172
+
173
+
// create a session using apppasswords
174
+
type session struct {
175
+
AccessJwt string `json:"accessJwt"`
176
+
PdsEndpoint string
177
+
Did string
178
+
}
179
+
180
+
func (o *OAuth) createAppPasswordSession(appPassword, did string) (*session, error) {
181
+
if appPassword == "" {
182
+
return nil, fmt.Errorf("no app password configured, skipping member addition")
183
+
}
184
+
185
+
resolved, err := o.IdResolver.ResolveIdent(context.Background(), did)
186
+
if err != nil {
187
+
return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", did, err)
188
+
}
189
+
190
+
pdsEndpoint := resolved.PDSEndpoint()
191
+
if pdsEndpoint == "" {
192
+
return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", did)
193
+
}
194
+
195
+
sessionPayload := map[string]string{
196
+
"identifier": did,
197
+
"password": appPassword,
198
+
}
199
+
sessionBytes, err := json.Marshal(sessionPayload)
200
+
if err != nil {
201
+
return nil, fmt.Errorf("failed to marshal session payload: %v", err)
202
+
}
203
+
204
+
sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession"
205
+
sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes))
206
+
if err != nil {
207
+
return nil, fmt.Errorf("failed to create session request: %v", err)
208
+
}
209
+
sessionReq.Header.Set("Content-Type", "application/json")
210
+
211
+
client := &http.Client{Timeout: 30 * time.Second}
212
+
sessionResp, err := client.Do(sessionReq)
213
+
if err != nil {
214
+
return nil, fmt.Errorf("failed to create session: %v", err)
215
+
}
216
+
defer sessionResp.Body.Close()
217
+
218
+
if sessionResp.StatusCode != http.StatusOK {
219
+
return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode)
220
+
}
221
+
222
+
var session session
223
+
if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil {
224
+
return nil, fmt.Errorf("failed to decode session response: %v", err)
225
+
}
226
+
227
+
session.PdsEndpoint = pdsEndpoint
228
+
session.Did = did
229
+
230
+
return &session, nil
231
+
}
232
+
233
+
func (s *session) putRecord(record any, collection string) error {
234
+
recordBytes, err := json.Marshal(record)
235
+
if err != nil {
236
+
return fmt.Errorf("failed to marshal knot member record: %w", err)
237
+
}
238
+
239
+
payload := map[string]any{
240
+
"repo": s.Did,
241
+
"collection": collection,
242
+
"rkey": tid.TID(),
243
+
"record": json.RawMessage(recordBytes),
244
+
}
245
+
246
+
payloadBytes, err := json.Marshal(payload)
247
+
if err != nil {
248
+
return fmt.Errorf("failed to marshal request payload: %w", err)
249
+
}
250
+
251
+
url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord"
252
+
req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes))
253
+
if err != nil {
254
+
return fmt.Errorf("failed to create HTTP request: %w", err)
255
+
}
256
+
257
+
req.Header.Set("Content-Type", "application/json")
258
+
req.Header.Set("Authorization", "Bearer "+s.AccessJwt)
259
+
260
+
client := &http.Client{Timeout: 30 * time.Second}
261
+
resp, err := client.Do(req)
262
+
if err != nil {
263
+
return fmt.Errorf("failed to add user to default service: %w", err)
264
+
}
265
+
defer resp.Body.Close()
266
+
267
+
if resp.StatusCode != http.StatusOK {
268
+
return fmt.Errorf("failed to add user to default service: HTTP %d", resp.StatusCode)
269
+
}
270
+
271
+
return nil
272
+
}
+20
-11
appview/oauth/oauth.go
+20
-11
appview/oauth/oauth.go
···
15
15
"github.com/lestrrat-go/jwx/v2/jwk"
16
16
"github.com/posthog/posthog-go"
17
17
"tangled.org/core/appview/config"
18
+
"tangled.org/core/appview/db"
19
+
"tangled.org/core/idresolver"
20
+
"tangled.org/core/rbac"
18
21
)
19
22
20
-
func New(config *config.Config, ph posthog.Client) (*OAuth, error) {
23
+
func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, res *idresolver.Resolver) (*OAuth, error) {
21
24
22
25
var oauthConfig oauth.ClientConfig
23
26
var clientUri string
···
43
46
sessStore := sessions.NewCookieStore([]byte(config.Core.CookieSecret))
44
47
45
48
return &OAuth{
46
-
ClientApp: oauth.NewClientApp(&oauthConfig, authStore),
47
-
Config: config,
48
-
SessStore: sessStore,
49
-
JwksUri: jwksUri,
50
-
Posthog: ph,
49
+
ClientApp: oauth.NewClientApp(&oauthConfig, authStore),
50
+
Config: config,
51
+
SessStore: sessStore,
52
+
JwksUri: jwksUri,
53
+
Posthog: ph,
54
+
Db: db,
55
+
Enforcer: enforcer,
56
+
IdResolver: res,
51
57
}, nil
52
58
}
53
59
54
60
type OAuth struct {
55
-
ClientApp *oauth.ClientApp
56
-
SessStore *sessions.CookieStore
57
-
Config *config.Config
58
-
JwksUri string
59
-
Posthog posthog.Client
61
+
ClientApp *oauth.ClientApp
62
+
SessStore *sessions.CookieStore
63
+
Config *config.Config
64
+
JwksUri string
65
+
Posthog posthog.Client
66
+
Db *db.DB
67
+
Enforcer *rbac.Enforcer
68
+
IdResolver *idresolver.Resolver
60
69
}
61
70
62
71
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error {
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
···
85
85
pages := pages.NewPages(config, res)
86
86
cache := cache.New(config.Redis.Addr)
87
87
sess := session.New(cache)
88
-
oauth2, err := oauth.New(config, posthog)
88
+
oauth2, err := oauth.New(config, posthog, d, enforcer, res)
89
89
if err != nil {
90
90
return nil, fmt.Errorf("failed to start oauth handler: %w", err)
91
91
}