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}