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