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