this repo has no description

add /node/generate-key to appview

Akshay 39240ac4 6988ef13

Changed files
+372 -24
appview
+90 -14
appview/auth/auth.go
··· 4 4 "context" 5 5 "encoding/json" 6 6 "fmt" 7 - "log" 8 7 "net/http" 9 8 "time" 10 9 ··· 13 12 "github.com/bluesky-social/indigo/atproto/syntax" 14 13 "github.com/bluesky-social/indigo/xrpc" 15 14 "github.com/gorilla/sessions" 15 + "github.com/icyphox/bild/appview" 16 16 ) 17 17 18 18 type Auth struct { 19 - store sessions.Store 19 + Store *sessions.CookieStore 20 20 } 21 21 22 22 type AtSessionCreate struct { ··· 30 30 } 31 31 32 32 func Make() (*Auth, error) { 33 - store := sessions.NewCookieStore([]byte("TODO_CHANGE_ME")) 33 + store := sessions.NewCookieStore([]byte(appview.SESSION_COOKIE_SECRET)) 34 34 return &Auth{store}, nil 35 35 } 36 36 ··· 66 66 return atSession, nil 67 67 } 68 68 69 - func (a *Auth) StoreSession(r *http.Request, w http.ResponseWriter, atSession *comatproto.ServerCreateSession_Output) error { 69 + // Sessionish is an interface that provides access to the common fields of both types. 70 + type Sessionish interface { 71 + GetAccessJwt() string 72 + GetActive() *bool 73 + GetDid() string 74 + GetDidDoc() *interface{} 75 + GetHandle() string 76 + GetRefreshJwt() string 77 + GetStatus() *string 78 + } 79 + 80 + // Create a wrapper type for ServerRefreshSession_Output 81 + type RefreshSessionWrapper struct { 82 + *comatproto.ServerRefreshSession_Output 83 + } 84 + 85 + func (s *RefreshSessionWrapper) GetAccessJwt() string { 86 + return s.AccessJwt 87 + } 88 + 89 + func (s *RefreshSessionWrapper) GetActive() *bool { 90 + return s.Active 91 + } 92 + 93 + func (s *RefreshSessionWrapper) GetDid() string { 94 + return s.Did 95 + } 96 + 97 + func (s *RefreshSessionWrapper) GetDidDoc() *interface{} { 98 + return s.DidDoc 99 + } 100 + 101 + func (s *RefreshSessionWrapper) GetHandle() string { 102 + return s.Handle 103 + } 104 + 105 + func (s *RefreshSessionWrapper) GetRefreshJwt() string { 106 + return s.RefreshJwt 107 + } 108 + 109 + func (s *RefreshSessionWrapper) GetStatus() *string { 110 + return s.Status 111 + } 112 + 113 + // Create a wrapper type for ServerRefreshSession_Output 114 + type CreateSessionWrapper struct { 115 + *comatproto.ServerCreateSession_Output 116 + } 117 + 118 + func (s *CreateSessionWrapper) GetAccessJwt() string { 119 + return s.AccessJwt 120 + } 121 + 122 + func (s *CreateSessionWrapper) GetActive() *bool { 123 + return s.Active 124 + } 125 + 126 + func (s *CreateSessionWrapper) GetDid() string { 127 + return s.Did 128 + } 129 + 130 + func (s *CreateSessionWrapper) GetDidDoc() *interface{} { 131 + return s.DidDoc 132 + } 133 + 134 + func (s *CreateSessionWrapper) GetHandle() string { 135 + return s.Handle 136 + } 137 + 138 + func (s *CreateSessionWrapper) GetRefreshJwt() string { 139 + return s.RefreshJwt 140 + } 141 + 142 + func (s *CreateSessionWrapper) GetStatus() *string { 143 + return s.Status 144 + } 145 + 146 + func (a *Auth) StoreSession(r *http.Request, w http.ResponseWriter, atSessionish Sessionish) error { 70 147 var didDoc identity.DIDDocument 71 148 72 - bytes, _ := json.Marshal(atSession.DidDoc) 149 + bytes, _ := json.Marshal(atSessionish.GetDidDoc()) 73 150 err := json.Unmarshal(bytes, &didDoc) 74 151 if err != nil { 75 - log.Printf("did: %+v", *atSession.DidDoc) 76 152 return fmt.Errorf("invalid did document for session") 77 153 } 78 154 ··· 83 159 return fmt.Errorf("no pds endpoint found") 84 160 } 85 161 86 - clientSession, _ := a.store.Get(r, "appview-session") 87 - clientSession.Values["handle"] = atSession.Handle 88 - clientSession.Values["did"] = atSession.Did 89 - clientSession.Values["pds"] = pdsEndpoint 90 - clientSession.Values["accessJwt"] = atSession.AccessJwt 91 - clientSession.Values["refreshJwt"] = atSession.RefreshJwt 92 - clientSession.Values["expiry"] = time.Now().Add(time.Hour).String() 93 - clientSession.Values["authenticated"] = true 162 + clientSession, _ := a.Store.Get(r, appview.SESSION_NAME) 163 + clientSession.Values[appview.SESSION_HANDLE] = atSessionish.GetHandle() 164 + clientSession.Values[appview.SESSION_DID] = atSessionish.GetDid() 165 + clientSession.Values[appview.SESSION_PDS] = pdsEndpoint 166 + clientSession.Values[appview.SESSION_ACCESSJWT] = atSessionish.GetAccessJwt() 167 + clientSession.Values[appview.SESSION_REFRESHJWT] = atSessionish.GetRefreshJwt() 168 + clientSession.Values[appview.SESSION_EXPIRY] = time.Now().Add(time.Hour).Format(appview.TIME_LAYOUT) 169 + clientSession.Values[appview.SESSION_AUTHENTICATED] = true 94 170 95 171 return clientSession.Save(r, w) 96 172 }
+16
appview/consts.go
··· 1 + package appview 2 + 3 + const ( 4 + SESSION_COOKIE_SECRET = "TODO_CHANGE_ME" 5 + SESSION_NAME = "appview-session" 6 + SESSION_HANDLE = "handle" 7 + SESSION_DID = "did" 8 + SESSION_PDS = "pds" 9 + SESSION_ACCESSJWT = "accessJwt" 10 + SESSION_REFRESHJWT = "refreshJwt" 11 + SESSION_EXPIRY = "expiry" 12 + SESSION_AUTHENTICATED = "authenticated" 13 + 14 + SALT = "TODO_RANDOM_SALT" 15 + TIME_LAYOUT = "2006-01-02 15:04:05.999999999 -0700 MST" 16 + )
+127 -1
appview/db/db.go
··· 1 1 package db 2 2 3 3 import ( 4 + "context" 4 5 "database/sql" 6 + "fmt" 7 + "log" 8 + 9 + "github.com/google/uuid" 5 10 _ "github.com/mattn/go-sqlite3" 11 + "golang.org/x/crypto/bcrypt" 6 12 ) 7 13 8 14 type DB struct { ··· 14 20 if err != nil { 15 21 return nil, err 16 22 } 17 - 23 + _, err = db.Exec(` 24 + create table if not exists registrations ( 25 + id integer primary key autoincrement, 26 + domain text not null unique, 27 + did text not null, 28 + secret text not null, 29 + created integer default (strftime('%s', 'now')), 30 + registered integer 31 + ); 32 + `) 33 + if err != nil { 34 + return nil, err 35 + } 18 36 return &DB{db: db}, nil 19 37 } 38 + 39 + type RegStatus uint32 40 + 41 + const ( 42 + Registered RegStatus = iota 43 + Unregistered 44 + Pending 45 + ) 46 + 47 + // returns registered status, did of owner, error 48 + func (d *DB) RegistrationStatus(domain string) (RegStatus, string, error) { 49 + var registeredBy string 50 + var registratedAt *uint64 51 + err := d.db.QueryRow(` 52 + select did, registered from registrations 53 + where domain = ? 54 + `, domain).Scan(&registeredBy, &registratedAt) 55 + if err != nil { 56 + if err == sql.ErrNoRows { 57 + return Unregistered, "", nil 58 + } else { 59 + return Unregistered, "", err 60 + } 61 + } 62 + 63 + if registratedAt != nil { 64 + return Registered, registeredBy, nil 65 + } else { 66 + return Pending, registeredBy, nil 67 + } 68 + } 69 + 70 + func (d *DB) GenerateRegistrationKey(domain, did string) (string, error) { 71 + // sanity check: does this domain already have a registration? 72 + status, owner, err := d.RegistrationStatus(domain) 73 + if err != nil { 74 + return "", err 75 + } 76 + switch status { 77 + case Registered: 78 + // already registered by `owner` 79 + return "", fmt.Errorf("%s already registered by %s", domain, owner) 80 + case Pending: 81 + log.Printf("%s registered by %s, status pending", domain, owner) 82 + // TODO: provide a warning here, and allow the current user to overwrite 83 + // the registration, this prevents users from registering domains that they 84 + // do not own 85 + default: 86 + // ok, we can register this domain 87 + } 88 + 89 + secret := uuid.New().String() 90 + hashedSecret, err := bcrypt.GenerateFromPassword([]byte(secret), 3) 91 + 92 + if err != nil { 93 + return "", err 94 + } 95 + 96 + _, err = d.db.Exec(` 97 + insert into registrations (domain, did, secret) 98 + values (?, ?, ?) 99 + on conflict(domain) do update set did = excluded.did, secret = excluded.secret 100 + `, domain, did, fmt.Sprintf("%x", hashedSecret)) 101 + 102 + if err != nil { 103 + return "", err 104 + } 105 + 106 + return secret, nil 107 + } 108 + 109 + func (d *DB) Register(domain, secret string) error { 110 + ctx := context.TODO() 111 + 112 + tx, err := d.db.BeginTx(ctx, nil) 113 + if err != nil { 114 + return err 115 + } 116 + 117 + res := tx.QueryRow(`select secret from registrations where domain = ?`, domain) 118 + 119 + var storedSecret string 120 + err = res.Scan(&storedSecret) 121 + if err != nil { 122 + return err 123 + } 124 + 125 + err = bcrypt.CompareHashAndPassword([]byte(storedSecret), []byte(secret)) 126 + if err != nil { 127 + return err 128 + } 129 + 130 + _, err = tx.Exec(` 131 + update registrations 132 + set registered = strftime('%s', 'now') 133 + where domain = ?; 134 + `, domain) 135 + if err != nil { 136 + return err 137 + } 138 + 139 + err = tx.Commit() 140 + if err != nil { 141 + return err 142 + } 143 + 144 + return nil 145 + }
+71
appview/state/middleware.go
··· 1 + package state 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "time" 7 + 8 + comatproto "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/xrpc" 10 + "github.com/icyphox/bild/appview" 11 + "github.com/icyphox/bild/appview/auth" 12 + ) 13 + 14 + type Middleware func(http.Handler) http.Handler 15 + 16 + func AuthMiddleware(s *State) Middleware { 17 + return func(next http.Handler) http.Handler { 18 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 + session, _ := s.Auth.Store.Get(r, appview.SESSION_NAME) 20 + authorized, ok := session.Values[appview.SESSION_AUTHENTICATED].(bool) 21 + 22 + if !ok || !authorized { 23 + log.Printf("not logged in, redirecting") 24 + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 25 + return 26 + } 27 + 28 + // refresh if nearing expiry 29 + // TODO: dedup with /login 30 + expiryStr := session.Values[appview.SESSION_EXPIRY].(string) 31 + expiry, err := time.Parse(appview.TIME_LAYOUT, expiryStr) 32 + if err != nil { 33 + log.Println("invalid expiry time", err) 34 + return 35 + } 36 + pdsUrl := session.Values[appview.SESSION_PDS].(string) 37 + did := session.Values[appview.SESSION_DID].(string) 38 + refreshJwt := session.Values[appview.SESSION_REFRESHJWT].(string) 39 + 40 + if time.Now().After(expiry) { 41 + log.Println("token expired, refreshing ...") 42 + 43 + client := xrpc.Client{ 44 + Host: pdsUrl, 45 + Auth: &xrpc.AuthInfo{ 46 + Did: did, 47 + AccessJwt: refreshJwt, 48 + RefreshJwt: refreshJwt, 49 + }, 50 + } 51 + atSession, err := comatproto.ServerRefreshSession(r.Context(), &client) 52 + if err != nil { 53 + log.Println(err) 54 + return 55 + } 56 + 57 + sessionish := auth.RefreshSessionWrapper{atSession} 58 + 59 + err = s.Auth.StoreSession(r, w, &sessionish) 60 + if err != nil { 61 + log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err) 62 + return 63 + } 64 + 65 + log.Println("successfully refreshed token") 66 + } 67 + 68 + next.ServeHTTP(w, r) 69 + }) 70 + } 71 + }
+60 -1
appview/state/state.go
··· 1 1 package state 2 2 3 3 import ( 4 + "encoding/json" 4 5 "log" 5 6 "net/http" 6 7 7 8 "github.com/go-chi/chi/v5" 9 + "github.com/icyphox/bild/appview" 8 10 "github.com/icyphox/bild/appview/auth" 9 11 "github.com/icyphox/bild/appview/db" 10 12 ) ··· 44 46 log.Printf("creating initial session: %s", err) 45 47 return 46 48 } 49 + sessionish := auth.CreateSessionWrapper{atSession} 47 50 48 - err = s.Auth.StoreSession(r, w, atSession) 51 + err = s.Auth.StoreSession(r, w, &sessionish) 49 52 if err != nil { 50 53 log.Printf("storing session: %s", err) 51 54 return ··· 57 60 } 58 61 } 59 62 63 + // requires auth 64 + func (s *State) GenerateRegistrationKey(w http.ResponseWriter, r *http.Request) { 65 + session, err := s.Auth.Store.Get(r, appview.SESSION_NAME) 66 + if err != nil || session.IsNew { 67 + log.Println("unauthorized attempt to generate registration key") 68 + http.Error(w, "Forbidden", http.StatusUnauthorized) 69 + return 70 + } 71 + 72 + did := session.Values[appview.SESSION_DID].(string) 73 + domain := r.FormValue("domain") 74 + if domain == "" { 75 + http.Error(w, "Invalid form", http.StatusBadRequest) 76 + return 77 + } 78 + 79 + key, err := s.Db.GenerateRegistrationKey(domain, did) 80 + 81 + if err != nil { 82 + log.Println(err) 83 + http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 84 + return 85 + } 86 + 87 + w.Write([]byte(key)) 88 + return 89 + } 90 + 91 + type RegisterRequest struct { 92 + Domain string `json:"domain"` 93 + Secret string `json:"secret"` 94 + } 95 + 96 + func (s *State) Register(w http.ResponseWriter, r *http.Request) { 97 + switch r.Method { 98 + case http.MethodGet: 99 + log.Println("unimplemented") 100 + return 101 + case http.MethodPost: 102 + var req RegisterRequest 103 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 104 + http.Error(w, "invalid request body", http.StatusBadRequest) 105 + return 106 + } 107 + 108 + domain := req.Domain 109 + secret := req.Secret 110 + 111 + log.Printf("Registered domain: %s with secret: %s", domain, secret) 112 + } 113 + } 114 + 60 115 func (s *State) Router() http.Handler { 61 116 r := chi.NewRouter() 62 117 63 118 r.Post("/login", s.Login) 119 + r.Group(func(r chi.Router) { 120 + r.Use(AuthMiddleware(s)) 121 + r.Post("/node/generate-key", s.GenerateRegistrationKey) 122 + }) 64 123 65 124 return r 66 125 }
+2 -2
go.mod
··· 19 19 github.com/russross/blackfriday/v2 v2.1.0 20 20 github.com/sethvargo/go-envconfig v1.1.0 21 21 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 22 - golang.org/x/crypto v0.31.0 22 + golang.org/x/crypto v0.32.0 23 23 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 24 24 gopkg.in/yaml.v3 v3.0.1 25 25 ) ··· 97 97 go.uber.org/multierr v1.11.0 // indirect 98 98 go.uber.org/zap v1.26.0 // indirect 99 99 golang.org/x/net v0.33.0 // indirect 100 - golang.org/x/sys v0.28.0 // indirect 100 + golang.org/x/sys v0.29.0 // indirect 101 101 golang.org/x/time v0.3.0 // indirect 102 102 google.golang.org/protobuf v1.33.0 // indirect 103 103 gopkg.in/warnings.v0 v0.1.2 // indirect
+6 -6
go.sum
··· 282 282 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 283 283 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 284 284 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 285 - golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 286 - golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 285 + golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 286 + golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 287 287 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 288 288 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 289 289 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= ··· 339 339 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 340 340 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 341 341 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 342 - golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 343 - golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 342 + golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 343 + golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 344 344 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 345 345 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 346 346 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= ··· 348 348 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 349 349 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 350 350 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 351 - golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 352 - golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 351 + golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 352 + golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 353 353 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 354 354 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 355 355 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=