github.com/aarzilli/tools@v0.0.0-20151123112009-0d27094f75e0/appengine/login/gitkit/gitkit.go (about) 1 // package gitkit expends the google identity toolkit; 2 // wrapping a user inside cookie SESSIONID; 3 // as opposed to appengine login cookie SACSID. 4 package gitkit 5 6 // Code taken from 7 // https://github.com/googlesamples/identity-toolkit-go/tree/master/favweekday 8 // 9 // The complete concept is expained here: 10 // https://developers.google.com/identity/choose-auth 11 // https://developers.google.com/identity/toolkit/web/federated-login 12 // 13 // https://developers.google.com/identity/toolkit/web/configure-service 14 // https://developers.google.com/identity/toolkit/web/setup-frontend 15 // 16 // 17 // Remove apps: 18 // https://security.google.com/settings/security/permissions 19 // https://www.facebook.com/settings?tab=applications 20 21 import ( 22 "fmt" 23 "html/template" 24 "io/ioutil" 25 "net/http" 26 "net/url" 27 "strconv" 28 "time" 29 30 "github.com/adg/xsrftoken" 31 "github.com/pbberlin/tools/net/http/tplx" // issues certificates (tokens) for possible http requests, making other requests impossible 32 33 "github.com/google/identity-toolkit-go-client/gitkit" 34 "github.com/gorilla/sessions" 35 36 "google.golang.org/appengine" 37 aelog "google.golang.org/appengine/log" 38 ) 39 40 // Action URLs. 41 // These need to be updated 42 // https://console.developers.google.com/project/tec-news/apiui/credential 43 // https://console.developers.google.com/project/tec-news/apiui/apiview/identitytoolkit/identity_toolkit 44 // https://developers.facebook.com/apps/942324259171809/settings/advanced/ 45 46 const ( 47 homeURL = "/auth" 48 49 widgetSigninAuthorizedRedirectURL = "/auth/authorized-redirect" // THIS one needs to be registered all over 50 successLandingURL_X = "SHOULD BE DYNAMIC" 51 signOutURL = "/auth/signout" 52 updateURL = "/auth/update" 53 accountChooserBrandingURL = "/auth/accountChooserBranding.html" 54 ) 55 56 var ( 57 successLandingURL = "/auth/signin-landing" 58 signoutLandingURL = "/auth/signout-landing" 59 ) 60 61 // Identity toolkit configurations. 62 const ( 63 serverAPIKey = "AIzaSyCnFQTG9WlS-y-eDpv3GtCUQhsUy61q8B8" 64 browserAPIKey = "AIzaSyAnarmnl8f0nHkGSqyU6CUdZxeN9e_5LhM" 65 66 clientID = "153437159745-cong6hlqenujf9o8fvl0gvum5gb9np1t.apps.googleusercontent.com" 67 serviceAccount = "153437159745-c79ndj0k7csi118tj489v14jkm7iln1f@developer.gserviceaccount.com" 68 ) 69 70 // The pseudo absolute path to the pem keyfile 71 var CodeBaseDirectory = "/not-initialized" 72 var privateKeyPath = "[CodeBaseDirectory]appaccess-only/tec-news-49bc2267287d.pem" 73 74 // Cookie/Form input names. 75 const ( 76 77 // contains jws from appengine/user.CurrentUser() ...; 78 // not used here 79 aeUserSessName = "SACSID" 80 81 // = cookie name; 82 // contains jwt from google/facebook/twitter; 83 // remains, even when "signed out" 84 // remains, even when logging out of google/twitter 85 // cannot be overwritten by "eraser" 86 sessionName = "SESSIONID" 87 88 // Created on top of sessionName on "signin" 89 // Remains 90 gtokenCookieName = "gtoken" 91 92 xsrfTokenName = "xsrftoken" 93 fieldNameFavWeekDay = "favorite" 94 95 maxTokenAge = 1200 // 20 minutes 96 97 maxSessionIDAge = 1800 98 ) 99 100 var ( 101 xsrfKey string 102 cookieStore *sessions.CookieStore 103 gitkitClient *gitkit.Client 104 ) 105 106 // User information. 107 type User struct { 108 ID string 109 Email string 110 Name string 111 EmailVerified bool 112 } 113 114 // Key used to store the user information in the current session. 115 type SessionUserKey int 116 117 const sessionUserKey SessionUserKey = 0 118 119 func IsSignedIn(r *http.Request) bool { 120 121 signedIn := false 122 cks := r.Cookies() 123 for _, ck := range cks { 124 if ck.Name == gtokenCookieName { 125 signedIn = true 126 break 127 } 128 } 129 130 return signedIn 131 132 } 133 134 // 135 // CurrentUser extracts the user information stored in current session. 136 // 137 // If there is no existing session, identity toolkit token is checked. 138 // If the token is valid, a new session is created. 139 // 140 // If any error happens, nil is returned. 141 func CurrentUser(r *http.Request) *User { 142 c := appengine.NewContext(r) 143 sess, _ := cookieStore.Get(r, sessionName) 144 if sess.IsNew { 145 // Create an identity toolkit client associated with the GAE context. 146 client, err := gitkit.NewWithContext(c, gitkitClient) 147 if err != nil { 148 aelog.Errorf(c, "Failed to create a gitkit.Client with a context: %s", err) 149 return nil 150 } 151 // Extract the token string from request. 152 ts := client.TokenFromRequest(r) 153 if ts == "" { 154 return nil 155 } 156 // Check the token issue time. Only accept token that is no more than 15 157 // minitues old even if it's still valid. 158 token, err := client.ValidateToken(ts) 159 if err != nil { 160 aelog.Errorf(c, "Invalid token %s: %s", ts, err) 161 return nil 162 } 163 if time.Now().Sub(token.IssueAt) > maxTokenAge*time.Second { 164 aelog.Infof(c, "Token %s is too old. Issused at: %s", ts, token.IssueAt) 165 return nil 166 } 167 168 // Fetch user info. 169 u, err := client.UserByLocalID(token.LocalID) 170 if err != nil { 171 aelog.Errorf(c, "Failed to fetch user info for %s[%s]: %s", token.Email, token.LocalID, err) 172 return nil 173 } 174 return &User{ 175 ID: u.LocalID, 176 Email: u.Email, 177 Name: u.DisplayName, 178 EmailVerified: u.EmailVerified, 179 } 180 } else { 181 // Extracts user from current session. 182 v, ok := sess.Values[sessionUserKey] 183 if !ok { 184 aelog.Errorf(c, "no user found in current session") 185 } 186 return v.(*User) 187 } 188 } 189 190 // saveCurrentUser stores the user information in current session. 191 func saveCurrentUser(r *http.Request, w http.ResponseWriter, u *User) { 192 if u == nil { 193 return 194 } 195 sess, _ := cookieStore.Get(r, sessionName) 196 sess.Values[sessionUserKey] = *u 197 err := sess.Save(r, w) 198 if err != nil { 199 aelog.Errorf(appengine.NewContext(r), "Cannot save session: %s", err) 200 } 201 } 202 203 func f_HANDLERS() { 204 205 } 206 207 func handleHome(w http.ResponseWriter, r *http.Request) { 208 209 format := ` 210 <a href='%v?mode=select'>Signin with Redirect (Widget)</a><br> 211 <a href='%v'>Signin Success Landing</a><br> 212 <a href='%v'>Signout </a><br> 213 <a href='%v'>Signout Landing</a><br> 214 <a href='%v'>Update</a><br> 215 <a href='%v'>Branding for Account Chooser</a><br> 216 ` 217 218 str := fmt.Sprintf(format, 219 widgetSigninAuthorizedRedirectURL, 220 successLandingURL, 221 signOutURL, 222 signoutLandingURL, 223 updateURL, 224 accountChooserBrandingURL, 225 ) 226 227 bstpl := tplx.TemplateFromHugoPage(w, r) // the jQuery irritates 228 fmt.Fprintf(w, tplx.ExecTplHelper(bstpl, map[string]interface{}{ 229 "HtmlTitle": "Google Identity Toolkit Overview", 230 "HtmlDescription": "", // reminder 231 "HtmlContent": template.HTML(str), 232 })) 233 234 } 235 236 func HandleWidget(w http.ResponseWriter, r *http.Request) { 237 238 defer r.Body.Close() 239 // Extract the POST body if any. 240 b, _ := ioutil.ReadAll(r.Body) 241 body, _ := url.QueryUnescape(string(b)) 242 243 gitkitTemplate := getWidgetTpl(w, r) 244 245 gitkitTemplate.Execute(w, map[string]interface{}{ 246 "BrowserAPIKey": browserAPIKey, 247 "SignInSuccessUrl": successLandingURL, 248 "SignOutURL": signOutURL, 249 "OOBActionURL": oobActionURL, // unnecessary, since we don't offer "home account", but kept 250 "POSTBody": body, 251 }) 252 253 } 254 255 func HandleSuccess(w http.ResponseWriter, r *http.Request) { 256 257 u := CurrentUser(r) 258 259 if ok := IsSignedIn(r); !ok { 260 u = nil 261 } 262 263 if u == nil { 264 http.Redirect(w, r, widgetSigninAuthorizedRedirectURL+"?mode=select&user=wasNil", http.StatusFound) 265 } 266 267 saveCurrentUser(r, w, u) 268 var xf string 269 if u != nil { 270 xf = xsrftoken.Generate(xsrfKey, u.ID, updateURL) 271 } 272 273 // 274 var d time.Weekday 275 if u != nil { 276 d = weekdayForUser(r, u) 277 } 278 279 homeTemplate := getHomeTpl(w, r) 280 homeTemplate.Execute(w, map[string]interface{}{ 281 "WidgetURL": widgetSigninAuthorizedRedirectURL, 282 "SignOutURL": signOutURL, 283 "User": u, 284 "WeekdayIndex": d, 285 "Weekdays": weekdays, 286 "UpdateWeekdayURL": updateURL, 287 "UpdateWeekdayXSRFToken": xf, 288 // "CookieDump": template.HTML(htmlfrag.CookieDump(r)), 289 }) 290 } 291 292 func handleSignOut(w http.ResponseWriter, r *http.Request) { 293 294 sess, _ := cookieStore.Get(r, sessionName) 295 sess.Options = &sessions.Options{MaxAge: -1} // MaxAge<0 means delete session cookie. 296 err := sess.Save(r, w) 297 if err != nil { 298 aelog.Errorf(appengine.NewContext(r), "Cannot save session: %s", err) 299 } 300 301 // NONE of this has any effect 302 if false { 303 304 w.Header().Del("Set-Cookie") 305 306 // The above deletion does not remove SESSIONID cookie. 307 // This also does not remove SESSIONID. 308 eraser := &http.Cookie{Name: sessionName, MaxAge: -1, Value: "erased", 309 Expires: time.Now().Add(-240 * time.Hour), HttpOnly: true} 310 http.SetCookie(w, eraser) 311 eraser.Name = "SESSIONID" 312 http.SetCookie(w, eraser) 313 314 // 315 w.Header().Del("Set-Cookie") 316 ck := `set-cookie: SESSIONID=doesnthelp; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=1800; HttpOnly` 317 ck = `set-cookie: SESSIONID=doesnthelp; expires=Thu, 01 Jan 1970 00:00:00 GMT` 318 w.Header().Add("Set-Cookie", ck) 319 320 } 321 322 // Also clear identity toolkit token. 323 http.SetCookie(w, &http.Cookie{Name: gtokenCookieName, MaxAge: -1}) 324 325 // Redirect to home page for sign in again. 326 http.Redirect(w, r, signoutLandingURL+"?logout=true", http.StatusFound) 327 // w.Write([]byte("<a href='" + signoutLandingURL + "'>Home<a>")) 328 329 } 330 331 func handleSignoutLanding(w http.ResponseWriter, r *http.Request) { 332 333 format := ` 334 Signed out<br> 335 <a href='%v'>Home</a><br> 336 ` 337 338 str := fmt.Sprintf(format, homeURL) 339 340 bstpl := tplx.TemplateFromHugoPage(w, r) // the jQuery irritates 341 fmt.Fprintf(w, tplx.ExecTplHelper(bstpl, map[string]interface{}{ 342 "HtmlTitle": "Google Identity Toolkit Overview", 343 "HtmlDescription": "", // reminder 344 "HtmlContent": template.HTML(str), 345 })) 346 347 } 348 349 func handleUpdate(w http.ResponseWriter, r *http.Request) { 350 351 operationResult := "failure" 352 outFunc := func() { 353 http.Redirect(w, r, successLandingURL+"?update="+operationResult, http.StatusFound) 354 } 355 356 var ( 357 d int 358 day time.Weekday 359 err error 360 ) 361 362 // Generic 363 c := appengine.NewContext(r) 364 // Check if there is a signed in user. 365 u := CurrentUser(r) 366 if u == nil { 367 aelog.Errorf(c, "No signed in user for updating") 368 outFunc() 369 goto out 370 } 371 // Validate XSRF token first. 372 if !xsrftoken.Valid(r.PostFormValue(xsrfTokenName), xsrfKey, u.ID, updateURL) { 373 aelog.Errorf(c, "XSRF token validation failed") 374 outFunc() 375 goto out 376 } 377 378 // 379 // Specific 380 // Extract the new favorite weekday. 381 d, err = strconv.Atoi(r.PostFormValue(fieldNameFavWeekDay)) 382 if err != nil { 383 aelog.Errorf(c, "Failed to extract new favoriate weekday: %s", err) 384 outFunc() 385 goto out 386 } 387 day = time.Weekday(d) 388 if day < time.Sunday || day > time.Saturday { 389 aelog.Errorf(c, "Got wrong value for favorite weekday: %d", d) 390 outFunc() 391 goto out 392 } 393 // Update the favorite weekday. 394 updateWeekdayForUser(r, u, day) 395 operationResult = "success" 396 397 out: 398 outFunc() 399 } 400 401 // Is called by AccountChooser to retrieve some layout. 402 // Dynamic execution required because of Access-Control header ... 403 func accountChooserBranding(w http.ResponseWriter, r *http.Request) { 404 w.Header().Set("Access-Control-Allow-Origin", "*") 405 str := `<!DOCTYPE html> 406 <html> 407 <head> 408 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 409 </head> 410 <body> 411 <div style="width:256px;margin:auto"> 412 <img src="/img/house-of-cards-mousepointer-03-04.gif" 413 style="display:block;height:120px;margin:auto"> 414 <p style="font-size:14px;opacity:.54;margin-top:20px;text-align:center"> 415 Welcome to tec-news insights. 416 </p> 417 </div> 418 </body> 419 </html>` 420 421 w.Write([]byte(str)) 422 423 }