this repo has no description
1package oauth
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "net/http"
8 "net/url"
9 "strings"
10
11 "github.com/go-chi/chi/v5"
12 "github.com/gorilla/sessions"
13 "github.com/lestrrat-go/jwx/v2/jwk"
14 "github.com/posthog/posthog-go"
15 "tangled.sh/icyphox.sh/atproto-oauth/helpers"
16 sessioncache "tangled.sh/tangled.sh/core/appview/cache/session"
17 "tangled.sh/tangled.sh/core/appview/config"
18 "tangled.sh/tangled.sh/core/appview/db"
19 "tangled.sh/tangled.sh/core/appview/idresolver"
20 "tangled.sh/tangled.sh/core/appview/middleware"
21 "tangled.sh/tangled.sh/core/appview/oauth"
22 "tangled.sh/tangled.sh/core/appview/oauth/client"
23 "tangled.sh/tangled.sh/core/appview/pages"
24 "tangled.sh/tangled.sh/core/knotclient"
25 "tangled.sh/tangled.sh/core/rbac"
26)
27
28const (
29 oauthScope = "atproto transition:generic"
30)
31
32type OAuthHandler struct {
33 config *config.Config
34 pages *pages.Pages
35 idResolver *idresolver.Resolver
36 sess *sessioncache.SessionStore
37 db *db.DB
38 store *sessions.CookieStore
39 oauth *oauth.OAuth
40 enforcer *rbac.Enforcer
41 posthog posthog.Client
42}
43
44func New(
45 config *config.Config,
46 pages *pages.Pages,
47 idResolver *idresolver.Resolver,
48 db *db.DB,
49 sess *sessioncache.SessionStore,
50 store *sessions.CookieStore,
51 oauth *oauth.OAuth,
52 enforcer *rbac.Enforcer,
53 posthog posthog.Client,
54) *OAuthHandler {
55 return &OAuthHandler{
56 config: config,
57 pages: pages,
58 idResolver: idResolver,
59 db: db,
60 sess: sess,
61 store: store,
62 oauth: oauth,
63 enforcer: enforcer,
64 posthog: posthog,
65 }
66}
67
68func (o *OAuthHandler) Router() http.Handler {
69 r := chi.NewRouter()
70
71 r.Get("/login", o.login)
72 r.Post("/login", o.login)
73
74 r.With(middleware.AuthMiddleware(o.oauth)).Post("/logout", o.logout)
75
76 r.Get("/oauth/client-metadata.json", o.clientMetadata)
77 r.Get("/oauth/jwks.json", o.jwks)
78 r.Get("/oauth/callback", o.callback)
79 return r
80}
81
82func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) {
83 w.Header().Set("Content-Type", "application/json")
84 w.WriteHeader(http.StatusOK)
85 json.NewEncoder(w).Encode(o.oauth.ClientMetadata())
86}
87
88func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) {
89 jwks := o.config.OAuth.Jwks
90 pubKey, err := pubKeyFromJwk(jwks)
91 if err != nil {
92 log.Printf("error parsing public key: %v", err)
93 http.Error(w, err.Error(), http.StatusInternalServerError)
94 return
95 }
96
97 response := helpers.CreateJwksResponseObject(pubKey)
98
99 w.Header().Set("Content-Type", "application/json")
100 w.WriteHeader(http.StatusOK)
101 json.NewEncoder(w).Encode(response)
102}
103
104func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) {
105 switch r.Method {
106 case http.MethodGet:
107 o.pages.Login(w, pages.LoginParams{})
108 case http.MethodPost:
109 handle := strings.TrimPrefix(r.FormValue("handle"), "@")
110
111 resolved, err := o.idResolver.ResolveIdent(r.Context(), handle)
112 if err != nil {
113 log.Println("failed to resolve handle:", err)
114 o.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
115 return
116 }
117 self := o.oauth.ClientMetadata()
118 oauthClient, err := client.NewClient(
119 self.ClientID,
120 o.config.OAuth.Jwks,
121 self.RedirectURIs[0],
122 )
123
124 if err != nil {
125 log.Println("failed to create oauth client:", err)
126 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
127 return
128 }
129
130 authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint())
131 if err != nil {
132 log.Println("failed to resolve auth server:", err)
133 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
134 return
135 }
136
137 authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer)
138 if err != nil {
139 log.Println("failed to fetch auth server metadata:", err)
140 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
141 return
142 }
143
144 dpopKey, err := helpers.GenerateKey(nil)
145 if err != nil {
146 log.Println("failed to generate dpop key:", err)
147 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
148 return
149 }
150
151 dpopKeyJson, err := json.Marshal(dpopKey)
152 if err != nil {
153 log.Println("failed to marshal dpop key:", err)
154 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
155 return
156 }
157
158 parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey)
159 if err != nil {
160 log.Println("failed to send par auth request:", err)
161 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
162 return
163 }
164
165 err = o.sess.SaveRequest(r.Context(), sessioncache.OAuthRequest{
166 Did: resolved.DID.String(),
167 PdsUrl: resolved.PDSEndpoint(),
168 Handle: handle,
169 AuthserverIss: authMeta.Issuer,
170 PkceVerifier: parResp.PkceVerifier,
171 DpopAuthserverNonce: parResp.DpopAuthserverNonce,
172 DpopPrivateJwk: string(dpopKeyJson),
173 State: parResp.State,
174 })
175 if err != nil {
176 log.Println("failed to save oauth request:", err)
177 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
178 return
179 }
180
181 u, _ := url.Parse(authMeta.AuthorizationEndpoint)
182 query := url.Values{}
183 query.Add("client_id", self.ClientID)
184 query.Add("request_uri", parResp.RequestUri)
185 u.RawQuery = query.Encode()
186 o.pages.HxRedirect(w, u.String())
187 }
188}
189
190func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
191 state := r.FormValue("state")
192
193 oauthRequest, err := o.sess.GetRequestByState(r.Context(), state)
194 if err != nil {
195 log.Println("failed to get oauth request:", err)
196 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
197 return
198 }
199
200 defer func() {
201 err := o.sess.DeleteRequestByState(r.Context(), state)
202 if err != nil {
203 log.Println("failed to delete oauth request for state:", state, err)
204 }
205 }()
206
207 error := r.FormValue("error")
208 errorDescription := r.FormValue("error_description")
209 if error != "" || errorDescription != "" {
210 log.Printf("error: %s, %s", error, errorDescription)
211 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
212 return
213 }
214
215 code := r.FormValue("code")
216 if code == "" {
217 log.Println("missing code for state: ", state)
218 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
219 return
220 }
221
222 iss := r.FormValue("iss")
223 if iss == "" {
224 log.Println("missing iss for state: ", state)
225 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
226 return
227 }
228
229 self := o.oauth.ClientMetadata()
230
231 oauthClient, err := client.NewClient(
232 self.ClientID,
233 o.config.OAuth.Jwks,
234 self.RedirectURIs[0],
235 )
236
237 if err != nil {
238 log.Println("failed to create oauth client:", err)
239 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
240 return
241 }
242
243 jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
244 if err != nil {
245 log.Println("failed to parse jwk:", err)
246 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
247 return
248 }
249
250 tokenResp, err := oauthClient.InitialTokenRequest(
251 r.Context(),
252 code,
253 oauthRequest.AuthserverIss,
254 oauthRequest.PkceVerifier,
255 oauthRequest.DpopAuthserverNonce,
256 jwk,
257 )
258 if err != nil {
259 log.Println("failed to get token:", err)
260 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
261 return
262 }
263
264 if tokenResp.Scope != oauthScope {
265 log.Println("scope doesn't match:", tokenResp.Scope)
266 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
267 return
268 }
269
270 err = o.oauth.SaveSession(w, r, *oauthRequest, tokenResp)
271 if err != nil {
272 log.Println("failed to save session:", err)
273 o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
274 return
275 }
276
277 log.Println("session saved successfully")
278 go o.addToDefaultKnot(oauthRequest.Did)
279
280 if !o.config.Core.Dev {
281 err = o.posthog.Enqueue(posthog.Capture{
282 DistinctId: oauthRequest.Did,
283 Event: "signin",
284 })
285 if err != nil {
286 log.Println("failed to enqueue posthog event:", err)
287 }
288 }
289
290 http.Redirect(w, r, "/", http.StatusFound)
291}
292
293func (o *OAuthHandler) logout(w http.ResponseWriter, r *http.Request) {
294 err := o.oauth.ClearSession(r, w)
295 if err != nil {
296 log.Println("failed to clear session:", err)
297 http.Redirect(w, r, "/", http.StatusFound)
298 return
299 }
300
301 log.Println("session cleared successfully")
302 o.pages.HxRedirect(w, "/login")
303}
304
305func pubKeyFromJwk(jwks string) (jwk.Key, error) {
306 k, err := helpers.ParseJWKFromBytes([]byte(jwks))
307 if err != nil {
308 return nil, err
309 }
310 pubKey, err := k.PublicKey()
311 if err != nil {
312 return nil, err
313 }
314 return pubKey, nil
315}
316
317func (o *OAuthHandler) addToDefaultKnot(did string) {
318 defaultKnot := "knot1.tangled.sh"
319
320 log.Printf("adding %s to default knot", did)
321 err := o.enforcer.AddMember(defaultKnot, did)
322 if err != nil {
323 log.Println("failed to add user to knot1.tangled.sh: ", err)
324 return
325 }
326 err = o.enforcer.E.SavePolicy()
327 if err != nil {
328 log.Println("failed to add user to knot1.tangled.sh: ", err)
329 return
330 }
331
332 secret, err := db.GetRegistrationKey(o.db, defaultKnot)
333 if err != nil {
334 log.Println("failed to get registration key for knot1.tangled.sh")
335 return
336 }
337 signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.config.Core.Dev)
338 resp, err := signedClient.AddMember(did)
339 if err != nil {
340 log.Println("failed to add user to knot1.tangled.sh: ", err)
341 return
342 }
343
344 if resp.StatusCode != http.StatusNoContent {
345 log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
346 return
347 }
348}