github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/login.go (about) 1 // Copyright 2019 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package webapp 6 7 import ( 8 "context" 9 "crypto/rand" 10 "encoding/base64" 11 "fmt" 12 "net/http" 13 "net/url" 14 15 "github.com/web-platform-tests/wpt.fyi/shared" 16 "golang.org/x/oauth2" 17 ) 18 19 func loginHandler(w http.ResponseWriter, r *http.Request) { 20 ctx := r.Context() 21 aeAPI := shared.NewAppEngineAPI(ctx) 22 if !aeAPI.IsFeatureEnabled("githubLogin") { 23 http.Error(w, "Feature not enabled", http.StatusNotImplemented) 24 return 25 } 26 27 githubOauthImp, err := shared.NewGitHubOAuth(ctx) 28 if err != nil { 29 http.Error(w, "Error creating githuboauthImp", http.StatusInternalServerError) 30 return 31 } 32 handleLogin(githubOauthImp, w, r) 33 } 34 35 func handleLogin(g shared.GitHubOAuth, w http.ResponseWriter, r *http.Request) { 36 ctx := g.Context() 37 ds := g.Datastore() 38 user, _ := shared.GetUserFromCookie(ctx, ds, r) 39 returnURL := r.FormValue("return") 40 if returnURL == "" { 41 returnURL = "/" 42 } 43 44 redirect := "" 45 log := shared.GetLogger(ctx) 46 if user == nil { 47 log.Infof("Initiating a new user login.") 48 g.SetRedirectURL(getCallbackURI(returnURL, r)) 49 state, err := generateRandomState(32) 50 if err != nil { 51 log.Errorf("Failed to generate a random state for OAuth: %v", err) 52 http.Error(w, "Failed to generate a random state for OAuth", http.StatusInternalServerError) 53 return 54 } 55 56 redirect = g.GetAuthCodeURL(state, oauth2.AccessTypeOnline) 57 err = setState(ctx, ds, state, w) 58 if err != nil { 59 log.Errorf("Failed to set state cookie for OAuth: %v", err) 60 http.Error(w, "Failed to set state cookie for OAuth", http.StatusInternalServerError) 61 return 62 } 63 64 log.Infof("OAuthing with github and returning to %s", returnURL) 65 } else { 66 if redirect == "" { 67 redirect = "/" 68 } 69 log.Infof("User %s is logged in", user.GitHubHandle) 70 } 71 72 http.Redirect(w, r, redirect, http.StatusTemporaryRedirect) 73 } 74 75 func oauthHandler(w http.ResponseWriter, r *http.Request) { 76 ctx := r.Context() 77 githuboauthImp, err := shared.NewGitHubOAuth(ctx) 78 if err != nil { 79 http.Error(w, "Error creating githuboauthImp", http.StatusInternalServerError) 80 return 81 } 82 handleOauth(githuboauthImp, w, r) 83 } 84 85 func handleOauth(g shared.GitHubOAuth, w http.ResponseWriter, r *http.Request) { 86 ctx := g.Context() 87 log := shared.GetLogger(ctx) 88 ds := g.Datastore() 89 90 encodedState := r.FormValue("state") 91 if encodedState == "" { 92 http.Error(w, "Missing URL param \"state\"", http.StatusBadRequest) 93 return 94 } 95 96 encryptedState, err := r.Cookie("state") 97 if err != nil || encryptedState == nil { 98 http.Error(w, "Missing cookie \"state\"", http.StatusBadRequest) 99 return 100 } 101 stateFromCookie, err := decodeState(ctx, ds, encryptedState) 102 if err != nil { 103 log.Errorf(err.Error()) 104 http.Error(w, "Failed to decode state from cookies", http.StatusBadRequest) 105 return 106 } 107 108 if stateFromCookie == "" { 109 http.Error(w, "Failed to get state cookie", http.StatusBadRequest) 110 return 111 } 112 113 if encodedState != stateFromCookie { 114 http.Error(w, "Failed to verify encoded state", http.StatusBadRequest) 115 return 116 } 117 118 oauthCode := r.FormValue("code") 119 if oauthCode == "" { 120 http.Error(w, "No OAuth code provided", http.StatusBadRequest) 121 return 122 } 123 124 client, err := g.NewClient(oauthCode) 125 if err != nil { 126 log.Errorf("Error creating GitHub client using OAuth code: %v", err) 127 http.Error(w, "Error creating GitHub client using OAuth code", http.StatusBadRequest) 128 return 129 } 130 131 ghUser, err := g.GetUser(client) 132 if err != nil || ghUser == nil { 133 log.Errorf("Failed to get authenticated user: %v", err) 134 http.Error(w, "Failed to get authenticated user", http.StatusBadRequest) 135 return 136 } 137 138 user := &shared.User{ 139 GitHubHandle: ghUser.GetLogin(), 140 GitHubEmail: ghUser.GetEmail(), 141 } 142 token := g.GetAccessToken() 143 if token == "" { 144 http.Error(w, "Got empty OAuth access token", http.StatusBadRequest) 145 return 146 } 147 setSession(ctx, ds, user, token, w) 148 if err != nil { 149 http.Error(w, "Failed to set credential cookie", http.StatusInternalServerError) 150 return 151 } 152 log.Infof("User %s logged in", user.GitHubHandle) 153 154 ret := r.FormValue("return") 155 http.Redirect(w, r, ret, http.StatusTemporaryRedirect) 156 } 157 158 func logoutHandler(response http.ResponseWriter, r *http.Request) { 159 ctx := r.Context() 160 log := shared.GetLogger(ctx) 161 clearSession(response) 162 163 log.Infof("User logged out") 164 http.Redirect(response, r, "/", http.StatusFound) 165 } 166 167 func setSession(ctx context.Context, ds shared.Datastore, user *shared.User, token string, response http.ResponseWriter) error { 168 var err error 169 value := map[string]interface{}{ 170 "user": *user, 171 "token": token, 172 } 173 174 sc, err := shared.NewSecureCookie(ds) 175 if err != nil { 176 return fmt.Errorf("failed to create SecureCookie: %v", err) 177 } 178 179 if encoded, err := sc.Encode("session", value); err == nil { 180 cookie := &http.Cookie{ 181 Name: "session", 182 Value: encoded, 183 Path: "/", 184 MaxAge: 2592000, 185 HttpOnly: true, 186 Secure: true, 187 } 188 189 // SameSite=None for http.Cookie is only available in Go.113; 190 // see https://github.com/golang/go/issues/32546. 191 if v := cookie.String(); v != "" { 192 response.Header().Add("Set-Cookie", v+"; SameSite=None") 193 } 194 } else { 195 log := shared.GetLogger(ctx) 196 log.Errorf("Failed to set session cookie: %v", err) 197 } 198 199 return err 200 } 201 202 func setState(ctx context.Context, ds shared.Datastore, state string, response http.ResponseWriter) error { 203 var err error 204 sc, err := shared.NewSecureCookie(ds) 205 if err != nil { 206 return fmt.Errorf("failed to create SecureCookie: %v", err) 207 } 208 209 if encoded, err := sc.Encode("state", state); err == nil { 210 cookie := &http.Cookie{ 211 Name: "state", 212 Value: encoded, 213 Path: "/", 214 MaxAge: 600, 215 Secure: true, 216 HttpOnly: true, 217 SameSite: http.SameSiteLaxMode, 218 } 219 http.SetCookie(response, cookie) 220 } 221 222 return err 223 } 224 225 func decodeState(ctx context.Context, ds shared.Datastore, encryptedState *http.Cookie) (string, error) { 226 cookieValue := "" 227 sc, err := shared.NewSecureCookie(ds) 228 if err != nil { 229 return "", fmt.Errorf("failed to create SecureCookie: %v", err) 230 } 231 232 if err := sc.Decode("state", encryptedState.Value, &cookieValue); err != nil { 233 return "", fmt.Errorf("failed to decode state cookie: %v", err) 234 } 235 return cookieValue, nil 236 } 237 238 func clearSession(response http.ResponseWriter) { 239 cookie := &http.Cookie{ 240 Name: "session", 241 Value: "", 242 Path: "/", 243 MaxAge: -1, 244 } 245 http.SetCookie(response, cookie) 246 } 247 248 func generateRandomState(size int) (string, error) { 249 byteArray := make([]byte, size) 250 _, err := rand.Read(byteArray) 251 if err != nil { 252 return "", err 253 } 254 255 return base64.URLEncoding.EncodeToString(byteArray), nil 256 } 257 258 func getCallbackURI(ret string, r *http.Request) string { 259 callback := url.URL{Scheme: "https", Host: r.Host, Path: "oauth"} 260 q := callback.Query() 261 q.Set("return", ret) 262 callback.RawQuery = q.Encode() 263 return callback.String() 264 }