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 r.Get("/login", o.login) 39 r.Post("/login", o.login) 40 41 r.Get("/oauth/client-metadata.json", o.clientMetadata) 42 r.Get("/oauth/jwks.json", o.jwks) 43 r.Get("/oauth/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 87func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { 88 switch r.Method { 89 case http.MethodGet: 90 o.Pages.Login(w, pages.LoginParams{}) 91 case http.MethodPost: 92 handle := strings.TrimPrefix(r.FormValue("handle"), "@") 93 94 resolved, err := o.Resolver.ResolveIdent(r.Context(), handle) 95 if err != nil { 96 log.Println("failed to resolve handle:", err) 97 o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 98 return 99 } 100 oauthClient, err := client.NewClient( 101 o.Config.OAuth.ServerMetadataUrl, 102 o.Config.OAuth.Jwks, 103 fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) 104 105 if err != nil { 106 log.Println("failed to create oauth client:", err) 107 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 108 return 109 } 110 111 authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint()) 112 if err != nil { 113 log.Println("failed to resolve auth server:", err) 114 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 115 return 116 } 117 118 authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer) 119 if err != nil { 120 log.Println("failed to fetch auth server metadata:", err) 121 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 122 return 123 } 124 125 dpopKey, err := helpers.GenerateKey(nil) 126 if err != nil { 127 log.Println("failed to generate dpop key:", err) 128 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 129 return 130 } 131 132 dpopKeyJson, err := json.Marshal(dpopKey) 133 if err != nil { 134 log.Println("failed to marshal dpop key:", err) 135 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 136 return 137 } 138 139 parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey) 140 if err != nil { 141 log.Println("failed to send par auth request:", err) 142 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 143 return 144 } 145 146 err = db.SaveOAuthRequest(o.Db, db.OAuthRequest{ 147 Did: resolved.DID.String(), 148 PdsUrl: resolved.PDSEndpoint(), 149 Handle: handle, 150 AuthserverIss: authMeta.Issuer, 151 PkceVerifier: parResp.PkceVerifier, 152 DpopAuthserverNonce: parResp.DpopAuthserverNonce, 153 DpopPrivateJwk: string(dpopKeyJson), 154 State: parResp.State, 155 }) 156 if err != nil { 157 log.Println("failed to save oauth request:", err) 158 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 159 return 160 } 161 162 u, _ := url.Parse(authMeta.AuthorizationEndpoint) 163 u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(o.Config.OAuth.ServerMetadataUrl), parResp.RequestUri) 164 o.Pages.HxRedirect(w, u.String()) 165 } 166} 167 168func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { 169 state := r.FormValue("state") 170 171 oauthRequest, err := db.GetOAuthRequestByState(o.Db, state) 172 if err != nil { 173 log.Println("failed to get oauth request:", err) 174 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 175 return 176 } 177 178 defer func() { 179 err := db.DeleteOAuthRequestByState(o.Db, state) 180 if err != nil { 181 log.Println("failed to delete oauth request for state:", state, err) 182 } 183 }() 184 185 code := r.FormValue("code") 186 if code == "" { 187 log.Println("missing code for state: ", state) 188 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 189 return 190 } 191 192 iss := r.FormValue("iss") 193 if iss == "" { 194 log.Println("missing iss for state: ", state) 195 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 196 return 197 } 198 199 oauthClient, err := client.NewClient( 200 o.Config.OAuth.ServerMetadataUrl, 201 o.Config.OAuth.Jwks, 202 fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) 203 204 if err != nil { 205 log.Println("failed to create oauth client:", err) 206 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 207 return 208 } 209 210 jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk)) 211 if err != nil { 212 log.Println("failed to parse jwk:", err) 213 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 214 return 215 } 216 217 tokenResp, err := oauthClient.InitialTokenRequest( 218 r.Context(), 219 code, 220 oauthRequest.AuthserverIss, 221 oauthRequest.PkceVerifier, 222 oauthRequest.DpopAuthserverNonce, 223 jwk, 224 ) 225 if err != nil { 226 log.Println("failed to get token:", err) 227 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 228 return 229 } 230 231 if tokenResp.Scope != oauthScope { 232 log.Println("scope doesn't match:", tokenResp.Scope) 233 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 234 return 235 } 236 237 err = o.OAuth.SaveSession(w, r, oauthRequest, tokenResp) 238 if err != nil { 239 log.Println("failed to save session:", err) 240 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 241 return 242 } 243 244 log.Println("session saved successfully") 245 246 http.Redirect(w, r, "/", http.StatusFound) 247} 248 249func pubKeyFromJwk(jwks string) (jwk.Key, error) { 250 k, err := helpers.ParseJWKFromBytes([]byte(jwks)) 251 if err != nil { 252 return nil, err 253 } 254 pubKey, err := k.PublicKey() 255 if err != nil { 256 return nil, err 257 } 258 return pubKey, nil 259}