this repo has no description
1package main
2
3import (
4 "fmt"
5 "log/slog"
6 "net/http"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/crypto"
12 "github.com/bluesky-social/indigo/atproto/identity"
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "github.com/golang-jwt/jwt/v5"
15 "github.com/willdot/bskyfeedgen/frontend"
16)
17
18// The contents of this file have been borrowed from here: https://github.com/orthanc/bluesky-go-feeds/blob/f719f113f1afc9080e50b4b1f5ca239aa3073c79/web/auth.go#L20-L46
19// It essentially allows the signing method that atproto uses for JWT to be used when verifying the JWT that they send in requests
20
21const (
22 ES256K = "ES256K"
23 ES256 = "ES256"
24)
25
26type AtProtoSigningMethod struct {
27 alg string
28}
29
30func (m *AtProtoSigningMethod) Alg() string {
31 return m.alg
32}
33
34func (m *AtProtoSigningMethod) Verify(signingString string, signature []byte, key interface{}) error {
35 err := key.(crypto.PublicKey).HashAndVerifyLenient([]byte(signingString), signature)
36 return err
37}
38
39func (m *AtProtoSigningMethod) Sign(signingString string, key interface{}) ([]byte, error) {
40 return nil, fmt.Errorf("unimplemented")
41}
42
43func init() {
44 ES256K := AtProtoSigningMethod{alg: "ES256K"}
45 jwt.RegisterSigningMethod(ES256K.Alg(), func() jwt.SigningMethod {
46 return &ES256K
47 })
48
49 ES256 := AtProtoSigningMethod{alg: "ES256"}
50 jwt.RegisterSigningMethod(ES256.Alg(), func() jwt.SigningMethod {
51 return &ES256
52 })
53
54}
55
56var directory = identity.DefaultDirectory()
57
58func getRequestUserDID(r *http.Request) (string, error) {
59 headerValues := r.Header["Authorization"]
60
61 if len(headerValues) != 1 {
62 return "", fmt.Errorf("missing authorization header")
63 }
64 token := strings.TrimSpace(strings.Replace(headerValues[0], "Bearer ", "", 1))
65
66 keyfunc := func(token *jwt.Token) (interface{}, error) {
67 did := syntax.DID(token.Claims.(jwt.MapClaims)["iss"].(string))
68 identity, err := directory.LookupDID(r.Context(), did)
69 if err != nil {
70 return nil, fmt.Errorf("unable to resolve did %s: %s", did, err)
71 }
72 key, err := identity.PublicKey()
73 if err != nil {
74 return nil, fmt.Errorf("signing key not found for did %s: %s", did, err)
75 }
76 return key, nil
77 }
78
79 validMethods := jwt.WithValidMethods([]string{ES256, ES256K})
80
81 parsedToken, err := jwt.ParseWithClaims(token, jwt.MapClaims{}, keyfunc, validMethods)
82 if err != nil {
83 return "", fmt.Errorf("invalid token: %s", err)
84 }
85
86 claims, ok := parsedToken.Claims.(jwt.MapClaims)
87 if !ok {
88 return "", fmt.Errorf("token contained no claims")
89 }
90
91 issVal, ok := claims["iss"].(string)
92 if !ok {
93 return "", fmt.Errorf("iss claim missing")
94 }
95
96 return string(syntax.DID(issVal)), nil
97}
98
99const (
100 jwtCookieName = "JWT"
101 didCookieName = "DID"
102)
103
104func (s *Server) authMiddleware(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
105 return func(w http.ResponseWriter, r *http.Request) {
106 jwtCookie, err := r.Cookie(jwtCookieName)
107 if err != nil {
108 slog.Error("read JWT cookie", "error", err)
109 frontend.Login("", "").Render(r.Context(), w)
110 return
111 }
112 if jwtCookie == nil {
113 slog.Error("missing JWT cookie")
114 frontend.Login("", "").Render(r.Context(), w)
115 return
116 }
117
118 didCookie, err := r.Cookie(didCookieName)
119 if err != nil {
120 slog.Error("read DID cookie", "error", err)
121 frontend.Login("", "").Render(r.Context(), w)
122 return
123 }
124 if didCookie == nil {
125 slog.Error("missing DID cookie")
126 frontend.Login("", "").Render(r.Context(), w)
127 return
128 }
129
130 claims := jwt.MapClaims{}
131 _, _, err = jwt.NewParser().ParseUnverified(jwtCookie.Value, &claims)
132 if err != nil {
133 slog.Error("parsing JWT", "error", err)
134 frontend.Login("", "").Render(r.Context(), w)
135 return
136 }
137
138 if expiry, ok := claims["exp"].(string); ok {
139 expiryInt, err := strconv.Atoi(expiry)
140 if err != nil {
141 slog.Error("invalid claims from token", "error", err)
142 frontend.Login("", "").Render(r.Context(), w)
143 return
144 }
145
146 if time.Now().Unix() > int64(expiryInt) {
147 frontend.Login("", "").Render(r.Context(), w)
148 return
149 }
150 }
151 next(w, r)
152 }
153}