+90
-14
appview/auth/auth.go
+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
+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
+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(®isteredBy, ®istratedAt)
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
+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
+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
+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
+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=