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  }