+59
-1
appview/auth/auth.go
+59
-1
appview/auth/auth.go
···
13
13
"github.com/sotangled/tangled/appview"
14
14
)
15
15
16
+
const ExpiryDuration = 15 * time.Minute
17
+
16
18
type Auth struct {
17
19
Store *sessions.CookieStore
18
20
}
···
61
63
GetStatus() *string
62
64
}
63
65
66
+
type ClientSessionish struct {
67
+
sessions.Session
68
+
}
69
+
70
+
func (c *ClientSessionish) GetAccessJwt() string {
71
+
return c.Values[appview.SessionAccessJwt].(string)
72
+
}
73
+
74
+
func (c *ClientSessionish) GetActive() *bool {
75
+
return c.Values[appview.SessionAuthenticated].(*bool)
76
+
}
77
+
78
+
func (c *ClientSessionish) GetDid() string {
79
+
return c.Values[appview.SessionDid].(string)
80
+
}
81
+
82
+
func (c *ClientSessionish) GetDidDoc() *interface{} {
83
+
return nil
84
+
}
85
+
86
+
func (c *ClientSessionish) GetHandle() string {
87
+
return c.Values[appview.SessionHandle].(string)
88
+
}
89
+
90
+
func (c *ClientSessionish) GetRefreshJwt() string {
91
+
return c.Values[appview.SessionRefreshJwt].(string)
92
+
}
93
+
94
+
func (c *ClientSessionish) GetStatus() *string {
95
+
return nil
96
+
}
97
+
64
98
// Create a wrapper type for ServerRefreshSession_Output
65
99
type RefreshSessionWrapper struct {
66
100
*comatproto.ServerRefreshSession_Output
···
140
174
clientSession.Values[appview.SessionPds] = pdsEndpoint
141
175
clientSession.Values[appview.SessionAccessJwt] = atSessionish.GetAccessJwt()
142
176
clientSession.Values[appview.SessionRefreshJwt] = atSessionish.GetRefreshJwt()
143
-
clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Minute * 15).Format(time.RFC3339)
177
+
clientSession.Values[appview.SessionExpiry] = time.Now().Add(ExpiryDuration).Format(time.RFC3339)
144
178
clientSession.Values[appview.SessionAuthenticated] = true
145
179
return clientSession.Save(r, w)
180
+
}
181
+
182
+
func (a *Auth) RefreshSession(ctx context.Context, r *http.Request, w http.ResponseWriter, atSessionish Sessionish, pdsEndpoint string) error {
183
+
client := xrpc.Client{
184
+
Host: pdsEndpoint,
185
+
Auth: &xrpc.AuthInfo{
186
+
Did: atSessionish.GetDid(),
187
+
AccessJwt: atSessionish.GetRefreshJwt(),
188
+
RefreshJwt: atSessionish.GetRefreshJwt(),
189
+
},
190
+
}
191
+
192
+
atSession, err := comatproto.ServerRefreshSession(ctx, &client)
193
+
if err != nil {
194
+
return fmt.Errorf("failed to refresh session: %w", err)
195
+
}
196
+
197
+
newAtSessionish := &RefreshSessionWrapper{atSession}
198
+
err = a.StoreSession(r, w, newAtSessionish, pdsEndpoint)
199
+
if err != nil {
200
+
return fmt.Errorf("failed to store refreshed session: %w", err)
201
+
}
202
+
203
+
return nil
146
204
}
147
205
148
206
func (a *Auth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) {
+9
-44
appview/state/middleware.go
+9
-44
appview/state/middleware.go
···
5
5
"log"
6
6
"net/http"
7
7
"strings"
8
-
"time"
9
8
10
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
9
"github.com/bluesky-social/indigo/atproto/identity"
12
-
"github.com/bluesky-social/indigo/xrpc"
13
10
"github.com/go-chi/chi/v5"
14
11
"github.com/sotangled/tangled/appview"
15
-
"github.com/sotangled/tangled/appview/auth"
16
12
"github.com/sotangled/tangled/appview/db"
17
13
)
18
14
···
21
17
func AuthMiddleware(s *State) Middleware {
22
18
return func(next http.Handler) http.Handler {
23
19
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24
-
session, _ := s.auth.Store.Get(r, appview.SessionName)
25
-
authorized, ok := session.Values[appview.SessionAuthenticated].(bool)
26
-
if !ok || !authorized {
27
-
log.Printf("not logged in, redirecting")
20
+
if s.auth == nil {
28
21
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
29
22
return
30
23
}
31
-
32
-
// refresh if nearing expiry
33
-
// TODO: dedup with /login
34
-
expiryStr := session.Values[appview.SessionExpiry].(string)
35
-
expiry, err := time.Parse(time.RFC3339, expiryStr)
24
+
err := s.RestoreSessionIfNeeded(r, w)
36
25
if err != nil {
37
-
log.Println("invalid expiry time", err)
38
26
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
39
27
return
40
28
}
41
-
pdsUrl := session.Values[appview.SessionPds].(string)
42
-
did := session.Values[appview.SessionDid].(string)
43
-
refreshJwt := session.Values[appview.SessionRefreshJwt].(string)
44
29
45
-
if time.Now().After(expiry) {
46
-
log.Println("token expired, refreshing ...")
47
-
48
-
client := xrpc.Client{
49
-
Host: pdsUrl,
50
-
Auth: &xrpc.AuthInfo{
51
-
Did: did,
52
-
AccessJwt: refreshJwt,
53
-
RefreshJwt: refreshJwt,
54
-
},
55
-
}
56
-
atSession, err := comatproto.ServerRefreshSession(r.Context(), &client)
57
-
if err != nil {
58
-
log.Println("failed to refresh session", err)
59
-
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
60
-
return
61
-
}
62
-
63
-
sessionish := auth.RefreshSessionWrapper{atSession}
64
-
65
-
err = s.auth.StoreSession(r, w, &sessionish, pdsUrl)
66
-
if err != nil {
67
-
log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err)
68
-
return
69
-
}
70
-
71
-
log.Println("successfully refreshed token")
30
+
session, _ := s.auth.Store.Get(r, appview.SessionName)
31
+
authorized, ok := session.Values[appview.SessionAuthenticated].(bool)
32
+
if !ok || !authorized {
33
+
log.Printf("not logged in, redirecting")
34
+
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
35
+
return
72
36
}
73
37
38
+
// refresh if nearing expiry
74
39
next.ServeHTTP(w, r)
75
40
})
76
41
}
+72
appview/state/session.go
+72
appview/state/session.go
···
1
+
package state
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"log"
7
+
"net/http"
8
+
"time"
9
+
10
+
"github.com/gorilla/sessions"
11
+
"github.com/sotangled/tangled/appview"
12
+
"github.com/sotangled/tangled/appview/auth"
13
+
)
14
+
15
+
func (s *State) StartTokenRefresher(
16
+
ctx context.Context,
17
+
refreshInterval time.Duration,
18
+
r *http.Request,
19
+
w http.ResponseWriter,
20
+
atSessionish auth.Sessionish,
21
+
pdsEndpoint string,
22
+
) {
23
+
go func() {
24
+
ticker := time.NewTicker(refreshInterval)
25
+
defer ticker.Stop()
26
+
27
+
for {
28
+
select {
29
+
case <-ticker.C:
30
+
err := s.auth.RefreshSession(ctx, r, w, atSessionish, pdsEndpoint)
31
+
if err != nil {
32
+
log.Printf("token refresh failed: %v", err)
33
+
} else {
34
+
log.Println("token refreshed successfully")
35
+
}
36
+
case <-ctx.Done():
37
+
log.Println("stopping token refresher")
38
+
return
39
+
}
40
+
}
41
+
}()
42
+
}
43
+
44
+
// RestoreSessionIfNeeded checks if a session exists in the request and starts a
45
+
// token refresher if it doesn't have one running already.
46
+
func (s *State) RestoreSessionIfNeeded(r *http.Request, w http.ResponseWriter) error {
47
+
var session *sessions.Session
48
+
var err error
49
+
session, err = s.auth.GetSession(r)
50
+
if err != nil {
51
+
fmt.Errorf("error getting session: %w", err)
52
+
}
53
+
54
+
did, ok := session.Values[appview.SessionDid].(string)
55
+
if !ok {
56
+
return fmt.Errorf("session did not contain a did")
57
+
}
58
+
sessionish := auth.ClientSessionish{Session: *session}
59
+
pdsEndpoint := session.Values[appview.SessionPds].(string)
60
+
61
+
// If no refresher is running for this session, start one
62
+
if _, exists := s.sessionCancelFuncs[did]; !exists {
63
+
sessionCtx, cancel := context.WithCancel(context.Background())
64
+
s.sessionCancelFuncs[did] = cancel
65
+
66
+
s.StartTokenRefresher(sessionCtx, auth.ExpiryDuration, r, w, &sessionish, pdsEndpoint)
67
+
68
+
log.Printf("restored session refresher for %s", did)
69
+
}
70
+
71
+
return nil
72
+
}
+27
-1
appview/state/state.go
+27
-1
appview/state/state.go
···
36
36
resolver *appview.Resolver
37
37
jc *jetstream.JetstreamClient
38
38
config *appview.Config
39
+
40
+
sessionCancelFuncs map[string]context.CancelFunc
39
41
}
40
42
41
43
func Make(config *appview.Config) (*State, error) {
···
70
72
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
71
73
}
72
74
75
+
sessionCancelFuncs := make(map[string]context.CancelFunc)
76
+
73
77
state := &State{
74
78
d,
75
79
auth,
···
79
83
resolver,
80
84
jc,
81
85
config,
86
+
sessionCancelFuncs,
82
87
}
83
88
84
89
return state, nil
···
123
128
}
124
129
125
130
log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
131
+
132
+
sessionCtx, cancel := context.WithCancel(context.Background())
133
+
s.sessionCancelFuncs[sessionish.GetDid()] = cancel
134
+
expiry := auth.ExpiryDuration
135
+
136
+
go s.StartTokenRefresher(sessionCtx, expiry, r, w, &sessionish, resolved.PDSEndpoint())
137
+
126
138
s.pages.HxRedirect(w, "/")
127
139
return
128
140
}
129
141
}
130
142
131
143
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
144
+
session, err := s.auth.GetSession(r)
145
+
did := session.Values[appview.SessionDid].(string)
146
+
if err == nil {
147
+
if cancel, exists := s.sessionCancelFuncs[did]; exists {
148
+
cancel()
149
+
delete(s.sessionCancelFuncs, did)
150
+
}
151
+
}
152
+
132
153
s.auth.ClearSession(r, w)
133
154
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
134
155
}
···
512
533
switch r.Method {
513
534
case http.MethodGet:
514
535
user := s.auth.GetUser(r)
536
+
err := s.enforcer.AddMember("knot1.tangled.sh", user.Did)
537
+
if err != nil {
538
+
log.Println("failed to add user to knot1.tangled.sh: ", err)
539
+
s.pages.Notice(w, "repo", "Failed to add user to knot1.tangled.sh. You should be able to use your own knot however.")
540
+
}
541
+
515
542
knots, err := s.enforcer.GetDomainsForUser(user.Did)
516
-
517
543
if err != nil {
518
544
s.pages.Notice(w, "repo", "Invalid user account.")
519
545
return