A fork of the Cocoon PDS but being made more distributed.
11

Configure Feed

Select the types of activity you want to include in your feed.

Support service auth for create account (#34)

* Handle service auth and existing users on create account

* Remove duplicate registration of create account

authored by

Ed Costello and committed by
GitHub
(Oct 27, 2025, 7:28 AM -0700) 89013a78 28876f4e

+159 -37
+1 -1
identity/types.go
··· 4 4 Context []string `json:"@context"` 5 5 Id string `json:"id"` 6 6 AlsoKnownAs []string `json:"alsoKnownAs"` 7 - VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"` 7 + VerificationMethods []DidDocVerificationMethod `json:"verificationMethod"` 8 8 Service []DidDocService `json:"service"` 9 9 } 10 10
+6
internal/db/db.go
··· 25 25 return db.cli.Clauses(clauses...).Create(value) 26 26 } 27 27 28 + func (db *DB) Save(value any, clauses []clause.Expression) *gorm.DB { 29 + db.mu.Lock() 30 + defer db.mu.Unlock() 31 + return db.cli.Clauses(clauses...).Save(value) 32 + } 33 + 28 34 func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB { 29 35 db.mu.Lock() 30 36 defer db.mu.Unlock()
+16
internal/helpers/helpers.go
··· 32 32 return genericError(e, 400, msg) 33 33 } 34 34 35 + func UnauthorizedError(e echo.Context, suffix *string) error { 36 + msg := "Unauthorized" 37 + if suffix != nil { 38 + msg += ". " + *suffix 39 + } 40 + return genericError(e, 401, msg) 41 + } 42 + 43 + func ForbiddenError(e echo.Context, suffix *string) error { 44 + msg := "Forbidden" 45 + if suffix != nil { 46 + msg += ". " + *suffix 47 + } 48 + return genericError(e, 403, msg) 49 + } 50 + 35 51 func InvalidTokenError(e echo.Context) error { 36 52 return InputError(e, to.StringPtr("InvalidToken")) 37 53 }
+45 -35
server/handle_server_create_account.go
··· 10 10 "github.com/Azure/go-autorest/autorest/to" 11 11 "github.com/bluesky-social/indigo/api/atproto" 12 12 "github.com/bluesky-social/indigo/atproto/atcrypto" 13 - "github.com/bluesky-social/indigo/atproto/syntax" 14 13 "github.com/bluesky-social/indigo/events" 15 14 "github.com/bluesky-social/indigo/repo" 16 15 "github.com/bluesky-social/indigo/util" ··· 39 38 func (s *Server) handleCreateAccount(e echo.Context) error { 40 39 var request ComAtprotoServerCreateAccountRequest 41 40 42 - var signupDid string 43 - customDidHeader := e.Request().Header.Get("authorization") 44 - if customDidHeader != "" { 45 - pts := strings.Split(customDidHeader, " ") 46 - if len(pts) != 2 { 47 - return helpers.InputError(e, to.StringPtr("InvalidDid")) 48 - } 49 - 50 - _, err := syntax.ParseDID(pts[1]) 51 - if err != nil { 52 - return helpers.InputError(e, to.StringPtr("InvalidDid")) 53 - } 54 - 55 - signupDid = pts[1] 56 - } 57 - 58 41 if err := e.Bind(&request); err != nil { 59 42 s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) 60 43 return helpers.ServerError(e, nil) ··· 85 68 } 86 69 } 87 70 } 71 + 72 + var signupDid string 73 + if request.Did != nil { 74 + signupDid = *request.Did; 75 + 76 + token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1)) 77 + if token == "" { 78 + return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did")) 79 + } 80 + authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount") 81 + 82 + if err != nil { 83 + s.logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err) 84 + return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token")) 85 + } 86 + 87 + if authDid != signupDid { 88 + return helpers.ForbiddenError(e, to.StringPtr("auth did did not match signup did")) 89 + } 90 + } 88 91 89 92 // see if the handle is already taken 90 - _, err := s.getActorByHandle(request.Handle) 93 + actor, err := s.getActorByHandle(request.Handle) 91 94 if err != nil && err != gorm.ErrRecordNotFound { 92 95 s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err) 93 96 return helpers.ServerError(e, nil) 94 97 } 95 - if err == nil { 98 + if err == nil && actor.Did != signupDid { 96 99 return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 97 100 } 98 101 99 - if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != "" { 102 + if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != signupDid { 100 103 return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 101 104 } 102 105 ··· 114 117 } 115 118 116 119 // see if the email is already taken 117 - _, err = s.getRepoByEmail(request.Email) 120 + existingRepo, err := s.getRepoByEmail(request.Email) 118 121 if err != nil && err != gorm.ErrRecordNotFound { 119 122 s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err) 120 123 return helpers.ServerError(e, nil) 121 124 } 122 - if err == nil { 125 + if err == nil && existingRepo.Did != signupDid { 123 126 return helpers.InputError(e, to.StringPtr("EmailNotAvailable")) 124 127 } 125 128 ··· 160 163 SigningKey: k.Bytes(), 161 164 } 162 165 163 - actor := models.Actor{ 164 - Did: signupDid, 165 - Handle: request.Handle, 166 - } 167 - 168 - if err := s.db.Create(&urepo, nil).Error; err != nil { 169 - s.logger.Error("error inserting new repo", "error", err) 170 - return helpers.ServerError(e, nil) 171 - } 166 + if actor == nil { 167 + actor = &models.Actor{ 168 + Did: signupDid, 169 + Handle: request.Handle, 170 + } 172 171 173 - if err := s.db.Create(&actor, nil).Error; err != nil { 174 - s.logger.Error("error inserting new actor", "error", err) 175 - return helpers.ServerError(e, nil) 172 + if err := s.db.Create(&urepo, nil).Error; err != nil { 173 + s.logger.Error("error inserting new repo", "error", err) 174 + return helpers.ServerError(e, nil) 175 + } 176 + 177 + if err := s.db.Create(&actor, nil).Error; err != nil { 178 + s.logger.Error("error inserting new actor", "error", err) 179 + return helpers.ServerError(e, nil) 180 + } 181 + } else { 182 + if err := s.db.Save(&actor, nil).Error; err != nil { 183 + s.logger.Error("error inserting new actor", "error", err) 184 + return helpers.ServerError(e, nil) 185 + } 176 186 } 177 187 178 - if customDidHeader == "" { 188 + if request.Did == nil || *request.Did == "" { 179 189 bs := s.getBlockstore(signupDid) 180 190 r := repo.NewRepo(context.TODO(), signupDid, bs) 181 191
-1
server/server.go
··· 423 423 // public 424 424 s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle) 425 425 s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 426 - s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 427 426 s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession) 428 427 s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer) 429 428
+91
server/service_auth.go
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + 8 + "github.com/bluesky-social/indigo/atproto/atcrypto" 9 + "github.com/bluesky-social/indigo/atproto/identity" 10 + atproto_identity "github.com/bluesky-social/indigo/atproto/identity" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/golang-jwt/jwt/v4" 13 + ) 14 + 15 + type ES256KSigningMethod struct { 16 + alg string 17 + } 18 + 19 + func (m *ES256KSigningMethod) Alg() string { 20 + return m.alg 21 + } 22 + 23 + func (m *ES256KSigningMethod) Verify(signingString string, signature string, key interface{}) error { 24 + signatureBytes, err := jwt.DecodeSegment(signature) 25 + if err != nil { 26 + return err 27 + } 28 + return key.(atcrypto.PublicKey).HashAndVerifyLenient([]byte(signingString), signatureBytes) 29 + } 30 + 31 + func (m *ES256KSigningMethod) Sign(signingString string, key interface{}) (string, error) { 32 + return "", fmt.Errorf("unimplemented") 33 + } 34 + 35 + func init() { 36 + ES256K := ES256KSigningMethod{alg: "ES256K"} 37 + jwt.RegisterSigningMethod(ES256K.Alg(), func() jwt.SigningMethod { 38 + return &ES256K 39 + }) 40 + } 41 + 42 + func (s *Server) validateServiceAuth(ctx context.Context, rawToken string, nsid string) (string, error) { 43 + token := strings.TrimSpace(rawToken) 44 + 45 + parsedToken, err := jwt.ParseWithClaims(token, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { 46 + did := syntax.DID(token.Claims.(jwt.MapClaims)["iss"].(string)) 47 + didDoc, err := s.passport.FetchDoc(ctx, did.String()); 48 + if err != nil { 49 + return nil, fmt.Errorf("unable to resolve did %s: %s", did, err) 50 + } 51 + 52 + verificationMethods := make([]atproto_identity.DocVerificationMethod, len(didDoc.VerificationMethods)) 53 + for i, verificationMethod := range didDoc.VerificationMethods { 54 + verificationMethods[i] = atproto_identity.DocVerificationMethod{ 55 + ID: verificationMethod.Id, 56 + Type: verificationMethod.Type, 57 + PublicKeyMultibase: verificationMethod.PublicKeyMultibase, 58 + Controller: verificationMethod.Controller, 59 + } 60 + } 61 + services := make([]atproto_identity.DocService, len(didDoc.Service)) 62 + for i, service := range didDoc.Service { 63 + services[i] = atproto_identity.DocService{ 64 + ID: service.Id, 65 + Type: service.Type, 66 + ServiceEndpoint: service.ServiceEndpoint, 67 + } 68 + } 69 + parsedIdentity := atproto_identity.ParseIdentity(&identity.DIDDocument{ 70 + DID: did, 71 + AlsoKnownAs: didDoc.AlsoKnownAs, 72 + VerificationMethod: verificationMethods, 73 + Service: services, 74 + }) 75 + 76 + key, err := parsedIdentity.PublicKey() 77 + if err != nil { 78 + return nil, fmt.Errorf("signing key not found for did %s: %s", did, err) 79 + } 80 + return key, nil 81 + }) 82 + if err != nil { 83 + return "", fmt.Errorf("invalid token: %s", err) 84 + } 85 + 86 + claims := parsedToken.Claims.(jwt.MapClaims) 87 + if claims["lxm"] != nsid { 88 + return "", fmt.Errorf("bad jwt lexicon method (\"lxm\"). must match: %s", nsid) 89 + } 90 + return claims["iss"].(string), nil 91 + }