package handler import ( "context" "database/sql" _ "embed" "encoding/json" "fmt" "html/template" "net/http" "strings" "github.com/bluesky-social/indigo/api/agnostic" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/did-method-plc/go-didplc" "github.com/gorilla/sessions" "tangled.org/core/knot2/config" "tangled.org/core/knot2/db" "tangled.org/core/log" ) const ( // atproto serviceId = "tangled_knot" serviceType = "TangledKnot" // cookies sessionName = "oauth-demo" sessionId = "sessionId" sessionDid = "sessionDID" ) //go:embed "templates/register.html" var tmplRegisgerText string var tmplRegister = template.Must(template.New("register.html").Parse(tmplRegisgerText)) func Register(jar *sessions.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "Register") sess, _ := jar.Get(r, sessionName) var data map[string]any if !sess.IsNew { // render Register { Handle, Web: true } did := syntax.DID(sess.Values[sessionDid].(string)) plcop := did.Method() == "plc" && r.URL.Query().Get("method") != "web" data = map[string]any{ "Did": did, "PlcOp": plcop, } } err := tmplRegister.Execute(w, data) if err != nil { l.Error("failed to render", "err", err) } } } func OauthClientMetadata(cfg *config.Config, clientApp *oauth.ClientApp) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { doc := clientApp.Config.ClientMetadata() var ( clientName = cfg.HostName clientUri = cfg.Uri() jwksUri = clientUri + "/oauth/jwks.json" ) doc.ClientName = &clientName doc.ClientURI = &clientUri doc.JWKSURI = &jwksUri 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 OauthJwks(clientApp *oauth.ClientApp) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") body := clientApp.Config.PublicJWKS() if err := json.NewEncoder(w).Encode(body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } func OauthLoginPost(clientApp *oauth.ClientApp) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "OauthLoginPost") handle := r.FormValue("handle") handle = strings.TrimPrefix(handle, "\u202a") handle = strings.TrimSuffix(handle, "\u202c") // `@` is harmless handle = strings.TrimPrefix(handle, "@") redirectURL, err := clientApp.StartAuthFlow(ctx, handle) if err != nil { l.Error("failed to start auth flow", "err", err) panic(err) } w.Header().Set("HX-Redirect", redirectURL) w.WriteHeader(http.StatusOK) } } func OauthCallback(oauth *oauth.ClientApp, jar *sessions.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "OauthCallback") data, err := oauth.ProcessCallback(ctx, r.URL.Query()) if err != nil { l.Error("failed to process oauth callback", "err", err) panic(err) } // store session data to cookie jar sess, _ := jar.Get(r, sessionName) sess.Values[sessionDid] = data.AccountDID.String() sess.Values[sessionId] = data.SessionID if err = sess.Save(r, w); err != nil { l.Error("failed to save session", "err", err) panic(err) } if data.AccountDID.Method() == "plc" { sess, err := oauth.ResumeSession(ctx, data.AccountDID, data.SessionID) if err != nil { l.Error("failed to resume atproto session", "err", err) panic(err) } client := sess.APIClient() err = atproto.IdentityRequestPlcOperationSignature(ctx, client) if err != nil { l.Error("failed to request plc operation signature", "err", err) panic(err) } } http.Redirect(w, r, "/register", http.StatusSeeOther) } } func RegisterPost(cfg *config.Config, d *sql.DB, clientApp *oauth.ClientApp, jar *sessions.CookieStore) http.HandlerFunc { plcop := func(ctx context.Context, did syntax.DID, sessId, token string) error { sess, err := clientApp.ResumeSession(ctx, did, sessId) if err != nil { return fmt.Errorf("failed to resume atproto session: %w", err) } client := sess.APIClient() identity, err := clientApp.Dir.LookupDID(ctx, did) services := make(map[string]didplc.OpService) for id, service := range identity.Services { services[id] = didplc.OpService{ Type: service.Type, Endpoint: service.URL, } } services[serviceId] = didplc.OpService{ Type: serviceType, Endpoint: cfg.Uri(), } rawServices, err := json.Marshal(services) if err != nil { return fmt.Errorf("failed to marshal services map: %w", err) } raw := json.RawMessage(rawServices) signed, err := agnostic.IdentitySignPlcOperation(ctx, client, &agnostic.IdentitySignPlcOperation_Input{ Services: &raw, Token: &token, }) if err != nil { return fmt.Errorf("failed to sign plc operatino: %w", err) } err = agnostic.IdentitySubmitPlcOperation(ctx, client, &agnostic.IdentitySubmitPlcOperation_Input{ Operation: signed.Operation, }) if err != nil { return fmt.Errorf("failed to submit plc operatino: %w", err) } return nil } return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l := log.FromContext(ctx).With("handler", "RegisterPost") sess, _ := jar.Get(r, sessionName) var ( did = syntax.DID(sess.Values[sessionDid].(string)) sessId = sess.Values[sessionId].(string) token = r.FormValue("token") doPlcOp = r.FormValue("plcop") == "on" ) tx, err := d.BeginTx(ctx, nil) if err != nil { l.Error("failed to begin db tx", "err", err) panic(err) } defer tx.Rollback() if err := db.AddUser(tx, did); err != nil { l.Error("failed to add user", "err", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } if doPlcOp { l.Debug("performing plc op", "did", did, "token", token) if err := plcop(ctx, did, sessId, token); err != nil { l.Error("failed to perform plc op", "err", err) http.Error(w, err.Error(), http.StatusInternalServerError) } } else { // TODO: check if did doc already include the knot service tx.Rollback() panic("unimplemented") } if err := tx.Commit(); err != nil { l.Error("failed to commit tx", "err", err) http.Error(w, err.Error(), http.StatusInternalServerError) } } }