forked from hailey.at/cocoon
An atproto PDS written in Go

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 89013a78 28876f4e

Changed files
+159 -37
identity
internal
db
helpers
server
+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 + }