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