Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280package oauth
import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "slices" "time"
"github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/go-chi/chi/v5" "github.com/posthog/posthog-go" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" "tangled.org/core/consts" "tangled.org/core/orm" "tangled.org/core/tid")
func (o *OAuth) Router() http.Handler { r := chi.NewRouter()
r.Get("/oauth/client-metadata.json", o.clientMetadata) r.Get("/oauth/jwks.json", o.jwks) r.Get("/oauth/callback", o.callback) return r}
func (o *OAuth) clientMetadata(w http.ResponseWriter, r *http.Request) { doc := o.ClientApp.Config.ClientMetadata() doc.JWKSURI = &o.JwksUri doc.ClientName = &o.ClientName doc.ClientURI = &o.ClientUri
w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(doc); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }}
func (o *OAuth) jwks(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") body := o.ClientApp.Config.PublicJWKS() if err := json.NewEncoder(w).Encode(body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }}
func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := o.Logger.With("query", r.URL.Query())
sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) if err != nil { var callbackErr *oauth.AuthRequestCallbackError if errors.As(err, &callbackErr) { l.Debug("callback error", "err", callbackErr) http.Redirect(w, r, fmt.Sprintf("/login?error=%s", callbackErr.ErrorCode), http.StatusFound) return } l.Error("failed to process callback", "err", err) http.Redirect(w, r, "/login?error=oauth", http.StatusFound) return }
if err := o.SaveSession(w, r, sessData); err != nil { l.Error("failed to save session", "data", sessData, "err", err) http.Redirect(w, r, "/login?error=session", http.StatusFound) return }
o.Logger.Debug("session saved successfully") go o.addToDefaultKnot(sessData.AccountDID.String()) go o.addToDefaultSpindle(sessData.AccountDID.String())
if !o.Config.Core.Dev { err = o.Posthog.Enqueue(posthog.Capture{ DistinctId: sessData.AccountDID.String(), Event: "signin", }) if err != nil { o.Logger.Error("failed to enqueue posthog event", "err", err) } }
http.Redirect(w, r, "/", http.StatusFound)}
func (o *OAuth) addToDefaultSpindle(did string) { l := o.Logger.With("subject", did)
// use the tangled.sh app password to get an accessJwt // and create an sh.tangled.spindle.member record with that spindleMembers, err := db.GetSpindleMembers( o.Db, orm.FilterEq("instance", "spindle.tangled.sh"), orm.FilterEq("subject", did), ) if err != nil { l.Error("failed to get spindle members", "err", err) return }
if len(spindleMembers) != 0 { l.Warn("already a member of the default spindle") return }
l.Debug("adding to default spindle") session, err := o.createAppPasswordSession(o.Config.Core.AppPassword, consts.TangledDid) if err != nil { l.Error("failed to create session", "err", err) return }
record := tangled.SpindleMember{ LexiconTypeID: "sh.tangled.spindle.member", Subject: did, Instance: consts.DefaultSpindle, CreatedAt: time.Now().Format(time.RFC3339), }
if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { l.Error("failed to add to default spindle", "err", err) return }
l.Debug("successfully added to default spindle", "did", did)}
func (o *OAuth) addToDefaultKnot(did string) { l := o.Logger.With("subject", did)
// use the tangled.sh app password to get an accessJwt // and create an sh.tangled.spindle.member record with that
allKnots, err := o.Enforcer.GetKnotsForUser(did) if err != nil { l.Error("failed to get knot members for did", "err", err) return }
if slices.Contains(allKnots, consts.DefaultKnot) { l.Warn("already a member of the default knot") return }
l.Debug("addings to default knot") session, err := o.createAppPasswordSession(o.Config.Core.TmpAltAppPassword, consts.IcyDid) if err != nil { l.Error("failed to create session", "err", err) return }
record := tangled.KnotMember{ LexiconTypeID: "sh.tangled.knot.member", Subject: did, Domain: consts.DefaultKnot, CreatedAt: time.Now().Format(time.RFC3339), }
if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { l.Error("failed to add to default knot", "err", err) return }
if err := o.Enforcer.AddKnotMember(consts.DefaultKnot, did); err != nil { l.Error("failed to set up enforcer rules", "err", err) return }
l.Debug("successfully addeds to default Knot")}
// create a session using apppasswordstype session struct { AccessJwt string `json:"accessJwt"` PdsEndpoint string Did string}
func (o *OAuth) createAppPasswordSession(appPassword, did string) (*session, error) { if appPassword == "" { return nil, fmt.Errorf("no app password configured, skipping member addition") }
resolved, err := o.IdResolver.ResolveIdent(context.Background(), did) if err != nil { return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", did, err) }
pdsEndpoint := resolved.PDSEndpoint() if pdsEndpoint == "" { return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", did) }
sessionPayload := map[string]string{ "identifier": did, "password": appPassword, } sessionBytes, err := json.Marshal(sessionPayload) if err != nil { return nil, fmt.Errorf("failed to marshal session payload: %v", err) }
sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession" sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes)) if err != nil { return nil, fmt.Errorf("failed to create session request: %v", err) } sessionReq.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second} sessionResp, err := client.Do(sessionReq) if err != nil { return nil, fmt.Errorf("failed to create session: %v", err) } defer sessionResp.Body.Close()
if sessionResp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode) }
var session session if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil { return nil, fmt.Errorf("failed to decode session response: %v", err) }
session.PdsEndpoint = pdsEndpoint session.Did = did
return &session, nil}
func (s *session) putRecord(record any, collection string) error { recordBytes, err := json.Marshal(record) if err != nil { return fmt.Errorf("failed to marshal knot member record: %w", err) }
payload := map[string]any{ "repo": s.Did, "collection": collection, "rkey": tid.TID(), "record": json.RawMessage(recordBytes), }
payloadBytes, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal request payload: %w", err) }
url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord" req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes)) if err != nil { return fmt.Errorf("failed to create HTTP request: %w", err) }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+s.AccessJwt)
client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to add user to default service: %w", err) } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to add user to default service: HTTP %d", resp.StatusCode) }
return nil}