forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 7.8 kB view raw
1package oauth 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net/http" 10 "slices" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/auth/oauth" 14 "github.com/go-chi/chi/v5" 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/orm" 20 "tangled.org/core/tid" 21) 22 23func (o *OAuth) Router() http.Handler { 24 r := chi.NewRouter() 25 26 r.Get("/oauth/client-metadata.json", o.clientMetadata) 27 r.Get("/oauth/jwks.json", o.jwks) 28 r.Get("/oauth/callback", o.callback) 29 return r 30} 31 32func (o *OAuth) clientMetadata(w http.ResponseWriter, r *http.Request) { 33 doc := o.ClientApp.Config.ClientMetadata() 34 doc.JWKSURI = &o.JwksUri 35 doc.ClientName = &o.ClientName 36 doc.ClientURI = &o.ClientUri 37 38 w.Header().Set("Content-Type", "application/json") 39 if err := json.NewEncoder(w).Encode(doc); err != nil { 40 http.Error(w, err.Error(), http.StatusInternalServerError) 41 return 42 } 43} 44 45func (o *OAuth) jwks(w http.ResponseWriter, r *http.Request) { 46 w.Header().Set("Content-Type", "application/json") 47 body := o.ClientApp.Config.PublicJWKS() 48 if err := json.NewEncoder(w).Encode(body); err != nil { 49 http.Error(w, err.Error(), http.StatusInternalServerError) 50 return 51 } 52} 53 54func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 55 ctx := r.Context() 56 l := o.Logger.With("query", r.URL.Query()) 57 58 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 59 if err != nil { 60 var callbackErr *oauth.AuthRequestCallbackError 61 if errors.As(err, &callbackErr) { 62 l.Debug("callback error", "err", callbackErr) 63 http.Redirect(w, r, fmt.Sprintf("/login?error=%s", callbackErr.ErrorCode), http.StatusFound) 64 return 65 } 66 l.Error("failed to process callback", "err", err) 67 http.Redirect(w, r, "/login?error=oauth", http.StatusFound) 68 return 69 } 70 71 if err := o.SaveSession(w, r, sessData); err != nil { 72 l.Error("failed to save session", "data", sessData, "err", err) 73 http.Redirect(w, r, "/login?error=session", http.StatusFound) 74 return 75 } 76 77 o.Logger.Debug("session saved successfully") 78 go o.addToDefaultKnot(sessData.AccountDID.String()) 79 go o.addToDefaultSpindle(sessData.AccountDID.String()) 80 81 if !o.Config.Core.Dev { 82 err = o.Posthog.Enqueue(posthog.Capture{ 83 DistinctId: sessData.AccountDID.String(), 84 Event: "signin", 85 }) 86 if err != nil { 87 o.Logger.Error("failed to enqueue posthog event", "err", err) 88 } 89 } 90 91 http.Redirect(w, r, "/", http.StatusFound) 92} 93 94func (o *OAuth) addToDefaultSpindle(did string) { 95 l := o.Logger.With("subject", did) 96 97 // use the tangled.sh app password to get an accessJwt 98 // and create an sh.tangled.spindle.member record with that 99 spindleMembers, err := db.GetSpindleMembers( 100 o.Db, 101 orm.FilterEq("instance", "spindle.tangled.sh"), 102 orm.FilterEq("subject", did), 103 ) 104 if err != nil { 105 l.Error("failed to get spindle members", "err", err) 106 return 107 } 108 109 if len(spindleMembers) != 0 { 110 l.Warn("already a member of the default spindle") 111 return 112 } 113 114 l.Debug("adding to default spindle") 115 session, err := o.createAppPasswordSession(o.Config.Core.AppPassword, consts.TangledDid) 116 if err != nil { 117 l.Error("failed to create session", "err", err) 118 return 119 } 120 121 record := tangled.SpindleMember{ 122 LexiconTypeID: "sh.tangled.spindle.member", 123 Subject: did, 124 Instance: consts.DefaultSpindle, 125 CreatedAt: time.Now().Format(time.RFC3339), 126 } 127 128 if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { 129 l.Error("failed to add to default spindle", "err", err) 130 return 131 } 132 133 l.Debug("successfully added to default spindle", "did", did) 134} 135 136func (o *OAuth) addToDefaultKnot(did string) { 137 l := o.Logger.With("subject", did) 138 139 // use the tangled.sh app password to get an accessJwt 140 // and create an sh.tangled.spindle.member record with that 141 142 allKnots, err := o.Enforcer.GetKnotsForUser(did) 143 if err != nil { 144 l.Error("failed to get knot members for did", "err", err) 145 return 146 } 147 148 if slices.Contains(allKnots, consts.DefaultKnot) { 149 l.Warn("already a member of the default knot") 150 return 151 } 152 153 l.Debug("addings to default knot") 154 session, err := o.createAppPasswordSession(o.Config.Core.TmpAltAppPassword, consts.IcyDid) 155 if err != nil { 156 l.Error("failed to create session", "err", err) 157 return 158 } 159 160 record := tangled.KnotMember{ 161 LexiconTypeID: "sh.tangled.knot.member", 162 Subject: did, 163 Domain: consts.DefaultKnot, 164 CreatedAt: time.Now().Format(time.RFC3339), 165 } 166 167 if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { 168 l.Error("failed to add to default knot", "err", err) 169 return 170 } 171 172 if err := o.Enforcer.AddKnotMember(consts.DefaultKnot, did); err != nil { 173 l.Error("failed to set up enforcer rules", "err", err) 174 return 175 } 176 177 l.Debug("successfully addeds to default Knot") 178} 179 180// create a session using apppasswords 181type session struct { 182 AccessJwt string `json:"accessJwt"` 183 PdsEndpoint string 184 Did string 185} 186 187func (o *OAuth) createAppPasswordSession(appPassword, did string) (*session, error) { 188 if appPassword == "" { 189 return nil, fmt.Errorf("no app password configured, skipping member addition") 190 } 191 192 resolved, err := o.IdResolver.ResolveIdent(context.Background(), did) 193 if err != nil { 194 return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", did, err) 195 } 196 197 pdsEndpoint := resolved.PDSEndpoint() 198 if pdsEndpoint == "" { 199 return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", did) 200 } 201 202 sessionPayload := map[string]string{ 203 "identifier": did, 204 "password": appPassword, 205 } 206 sessionBytes, err := json.Marshal(sessionPayload) 207 if err != nil { 208 return nil, fmt.Errorf("failed to marshal session payload: %v", err) 209 } 210 211 sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession" 212 sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes)) 213 if err != nil { 214 return nil, fmt.Errorf("failed to create session request: %v", err) 215 } 216 sessionReq.Header.Set("Content-Type", "application/json") 217 218 client := &http.Client{Timeout: 30 * time.Second} 219 sessionResp, err := client.Do(sessionReq) 220 if err != nil { 221 return nil, fmt.Errorf("failed to create session: %v", err) 222 } 223 defer sessionResp.Body.Close() 224 225 if sessionResp.StatusCode != http.StatusOK { 226 return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode) 227 } 228 229 var session session 230 if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil { 231 return nil, fmt.Errorf("failed to decode session response: %v", err) 232 } 233 234 session.PdsEndpoint = pdsEndpoint 235 session.Did = did 236 237 return &session, nil 238} 239 240func (s *session) putRecord(record any, collection string) error { 241 recordBytes, err := json.Marshal(record) 242 if err != nil { 243 return fmt.Errorf("failed to marshal knot member record: %w", err) 244 } 245 246 payload := map[string]any{ 247 "repo": s.Did, 248 "collection": collection, 249 "rkey": tid.TID(), 250 "record": json.RawMessage(recordBytes), 251 } 252 253 payloadBytes, err := json.Marshal(payload) 254 if err != nil { 255 return fmt.Errorf("failed to marshal request payload: %w", err) 256 } 257 258 url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 259 req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes)) 260 if err != nil { 261 return fmt.Errorf("failed to create HTTP request: %w", err) 262 } 263 264 req.Header.Set("Content-Type", "application/json") 265 req.Header.Set("Authorization", "Bearer "+s.AccessJwt) 266 267 client := &http.Client{Timeout: 30 * time.Second} 268 resp, err := client.Do(req) 269 if err != nil { 270 return fmt.Errorf("failed to add user to default service: %w", err) 271 } 272 defer resp.Body.Close() 273 274 if resp.StatusCode != http.StatusOK { 275 return fmt.Errorf("failed to add user to default service: HTTP %d", resp.StatusCode) 276 } 277 278 return nil 279}