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}