Authenticating Users with Go

This part of the Go Bookshelf tutorial shows how to create a sign-in flow for users and how to use profile information to provide users with personalized functionality.

By using Google Identity Platform, you can easily access information about your users while ensuring their sign-in credentials are safely managed by Google. OAuth 2.0 makes it easy to provide a sign-in flow for all users of your app and provides your application with access to basic profile information about authenticated users.

This page is part of a multi-page tutorial. To start from the beginning and see instructions for setting up, go to Go Bookshelf App.

Creating a web application client ID

A web application client ID allows your application to authorize users and access Google APIs on behalf of your users.

  1. Go to the credentials section in the Google Cloud Platform Console.

  2. Click OAuth consent screen. For the the product name, enter Go Bookshelf App. Fill in any relevant optional fields. Click Save.

  3. Click Create credentials > OAuth client ID.

  4. Under Application type, select Web Application.

  5. Under Name, enter Go Bookshelf Client.

  6. Under Authorized redirect URIs enter the following URLs, one at a time. Replace [YOUR_PROJECT_ID] with your project ID:

    http://localhost:8080/oauth2callback
    http://[YOUR_PROJECT_ID].appspot.com/oauth2callback
    https://[YOUR_PROJECT_ID].appspot.com/oauth2callback
    http://[YOUR_PROJECT_ID].appspot-preview.com/oauth2callback
    https://[YOUR_PROJECT_ID].appspot-preview.com/oauth2callback

  7. Click Create.

  8. Copy the client ID and client secret and save them for later use.

Configuring settings

  1. Go to the directory that contains the sample code:

    Linux/Mac OS X

    cd $GOPATH/src/github.com/GoogleCloudPlatform/golang-samples/getting-started/bookshelf
    

    Windows

    cd %GOPATH%\src\github.com\GoogleCloudPlatform\golang-samples\getting-started\bookshelf
    

  2. Open config.go for editing.

  3. Uncomment this line:

    // oauthConfig = configureOAuthClient("clientid", "clientsecret")
    
  4. Replace "clientid" and "clientsecret" with the client ID and client secret you created previously.

  5. Save and close config.go.

  6. Edit app/app.yaml. In the OAUTH2_CALLBACK environment variable, replace <your-project-id> with your project ID.

Running the app on your local machine

  1. Run the app to start a local web server:

    cd app
    go run app.go auth.go template.go
    
  2. In your web browser, enter this address:

    http://localhost:8080

Now you can browse the app's web pages, sign in using your Google account, add books, and see the books you've added using the My Books link in the top navigation bar.

Press Control+C to exit the local web server.

Deploying the app to the App Engine flexible environment

  1. In the app directory, enter this command to deploy the sample:

    aedeploy gcloud app deploy
    
  2. In your web browser, enter this address. Replace [YOUR_PROJECT_ID] with your project ID:

    https://[YOUR_PROJECT_ID].appspot.com
    

If you update your app, you can deploy the updated version by entering the same command you used to deploy the app the first time. The new deployment creates a new version of your app and promotes it to the default version. The older versions of your app remain, as do their associated VM instances. Be aware that all of these app versions and VM instances are billable resources.

You can reduce costs by deleting the non-default versions of your app.

To delete an app version:

  1. In the Cloud Platform Console, go to the App Engine Versions page.

    Go to the Versions page

  2. Click the checkbox next to the non-default app version you want to delete.
  3. Click the Delete button at the top of the page to delete the app version.

For complete information about cleaning up billable resources, see the Cleaning up section in the final step of this tutorial.

Application structure

The following diagram shows the application's components and how they connect to one another.

Auth sample structure

Understanding the code

This section walks you through the application code and explains how it works.

About sessions

Before your application can authenticate users, you need a way to store information about the current user in a session. The Gorilla web toolkit includes a Store interface, which includes several implementations. The CookieStore implementation stores session data in a secure cookie, and the FilesystemStore implementation stores session data on the file system. Storing session data on the file system is unsuitable for an application that can be served from multiple VM instances, as the session that is recorded in one instance might differ from other instances. For this reason, the Bookshelf sample uses encrypted cookies (CookieStore). The encrypted session data is stored in the user's browser and can be decrypted by any instance running the application code. The CookieStore implementation can be used in production environments where you have deployed multiple instances of your application, but be aware that users will lose their session information when they close the browser.

// Configure storage method for session-wide information.
// Update "something-very-secret" with a hard to guess string or byte sequence.
cookieStore := sessions.NewCookieStore([]byte("something-very-secret"))
cookieStore.Options = &sessions.Options{
	HttpOnly: true,
}
SessionStore = cookieStore

Authenticating users

Authenticating the user involves two basic steps, which together are called the web service flow.

  1. Redirect the user to Google’s authorization service.
  2. Process the response when Google redirects the user back to your application.

Here we describe the two steps in detail:

  1. Redirect to Google.

    Your application sends the user to Google’s authorization service. The application generates a URL by using your client ID and the OAuth scopes that the application uses. Scopes indicate which items of the user’s information your application is allowed to access. There are multiple Google services, with different scopes for each service. For example, there is a scope that allows read-only access to Google Drive and another scope that allows read/write access. This sample requires only the basic scopes email and profile, which grant the application access to the user's email address and basic profile information:

    func configureOAuthClient(clientID, clientSecret string) *oauth2.Config {
    	redirectURL := os.Getenv("OAUTH2_CALLBACK")
    	if redirectURL == "" {
    		redirectURL = "http://localhost:8080/oauth2callback"
    	}
    	return &oauth2.Config{
    		ClientID:     clientID,
    		ClientSecret: clientSecret,
    		RedirectURL:  redirectURL,
    		Scopes:       []string{"email", "profile"},
    		Endpoint:     google.Endpoint,
    	}
    }

    The loginHandler function redirects the /login URL path to the Google authorization service:

    // loginHandler initiates an OAuth flow to authenticate the user.
    func loginHandler(w http.ResponseWriter, r *http.Request) *appError {
    	sessionID := uuid.NewV4().String()
    
    	oauthFlowSession, err := bookshelf.SessionStore.New(r, sessionID)
    	if err != nil {
    		return appErrorf(err, "could not create oauth session: %v", err)
    	}
    	oauthFlowSession.Options.MaxAge = 10 * 60 // 10 minutes
    
    	redirectURL, err := validateRedirectURL(r.FormValue("redirect"))
    	if err != nil {
    		return appErrorf(err, "invalid redirect URL: %v", err)
    	}
    	oauthFlowSession.Values[oauthFlowRedirectKey] = redirectURL
    
    	if err := oauthFlowSession.Save(r, w); err != nil {
    		return appErrorf(err, "could not save session: %v", err)
    	}
    
    	// Use the session ID for the "state" parameter.
    	// This protects against CSRF (cross-site request forgery).
    	// See https://godoc.org/golang.org/x/oauth2#Config.AuthCodeURL for more detail.
    	url := bookshelf.OAuthConfig.AuthCodeURL(sessionID, oauth2.ApprovalForce,
    		oauth2.AccessTypeOnline)
    	http.Redirect(w, r, url, http.StatusFound)
    	return nil
    }

    The first parameter of the AuthCodeURL function is the state parameter. This is used to keep the sign-in process secure by preventing cross-site request forgery.

    Your application redirects the user to the generated URL. The user is prompted to allow your application to access the scopes you specified. After the user accepts, they are redirected back to your application.

  2. Process the authorization response.

    The Google authorization service sends the user back to your application via the /oauth2callback URL path along with the authorization code specified by the code form value. This code can be exchanged, along with your client secrets, to obtain the user’s credentials and information about the user. This step of the flow happens entirely server-side without having to redirect the user. This extra step is required to verify the authenticity of the authorization code and that your application is the one requesting credentials:

    // oauthCallbackHandler completes the OAuth flow, retreives the user's profile
    // information and stores it in a session.
    func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) *appError {
    	oauthFlowSession, err := bookshelf.SessionStore.Get(r, r.FormValue("state"))
    	if err != nil {
    		return appErrorf(err, "invalid state parameter. try logging in again.")
    	}
    
    	redirectURL, ok := oauthFlowSession.Values[oauthFlowRedirectKey].(string)
    	// Validate this callback request came from the app.
    	if !ok {
    		return appErrorf(err, "invalid state parameter. try logging in again.")
    	}
    
    	code := r.FormValue("code")
    	tok, err := bookshelf.OAuthConfig.Exchange(context.Background(), code)
    	if err != nil {
    		return appErrorf(err, "could not get auth token: %v", err)
    	}
    
    	session, err := bookshelf.SessionStore.New(r, defaultSessionID)
    	if err != nil {
    		return appErrorf(err, "could not get default session: %v", err)
    	}
    
    	ctx := context.Background()
    	profile, err := fetchProfile(ctx, tok)
    	if err != nil {
    		return appErrorf(err, "could not fetch Google profile: %v", err)
    	}
    
    	session.Values[oauthTokenSessionKey] = tok
    	// Strip the profile to only the fields we need. Otherwise the struct is too big.
    	session.Values[googleProfileSessionKey] = stripProfile(profile)
    	if err := session.Save(r, w); err != nil {
    		return appErrorf(err, "could not save session: %v", err)
    	}
    
    	http.Redirect(w, r, redirectURL, http.StatusFound)
    	return nil
    }

    After you obtain the user's credentials, you can retrieve their profile information via the Google+ API. The call to plusService.People.Get("me") retrieves the profile for the currently authenticated user. The profile is stored in the session against the googleProfileSessionKey (encoded using encoding/gob):

    // fetchProfile retrieves the Google+ profile of the user associated with the
    // provided OAuth token.
    func fetchProfile(ctx context.Context, tok *oauth2.Token) (*plus.Person, error) {
    	client := oauth2.NewClient(ctx, bookshelf.OAuthConfig.TokenSource(ctx, tok))
    	plusService, err := plus.New(client)
    	if err != nil {
    		return nil, err
    	}
    	return plusService.People.Get("me").Do()
    }

    Because the profile information is stored in the session, it can be retrieved by the application without fetching it again from the Google+ API.

    // profileFromSession retreives the Google+ profile from the default session.
    // Returns nil if the profile cannot be retreived (e.g. user is logged out).
    func profileFromSession(r *http.Request) *Profile {
    	session, err := bookshelf.SessionStore.Get(r, defaultSessionID)
    	if err != nil {
    		return nil
    	}
    	tok, ok := session.Values[oauthTokenSessionKey].(*oauth2.Token)
    	if !ok || !tok.Valid() {
    		return nil
    	}
    	profile, ok := session.Values[googleProfileSessionKey].(*Profile)
    	if !ok {
    		return nil
    	}
    	return profile
    }

    For example, the profile is passed into the base template to render the signed-in user's name and profile photo on every page:

    // Execute writes the template using the provided data, adding login and user
    // information to the base template.
    func (tmpl *appTemplate) Execute(w http.ResponseWriter, r *http.Request, data interface{}) *appError {
    	d := struct {
    		Data        interface{}
    		AuthEnabled bool
    		Profile     *Profile
    		LoginURL    string
    		LogoutURL   string
    	}{
    		Data:        data,
    		AuthEnabled: bookshelf.OAuthConfig != nil,
    		LoginURL:    "/login?redirect=" + r.URL.RequestURI(),
    		LogoutURL:   "/logout?redirect=" + r.URL.RequestURI(),
    	}
    
    	if d.AuthEnabled {
    		// Ignore any errors.
    		d.Profile = profileFromSession(r)
    	}
    
    	if err := tmpl.t.Execute(w, d); err != nil {
    		return appErrorf(err, "could not write template: %v")
    	}
    	return nil
    }

    {{if .AuthEnabled}}
      {{if .Profile}}
      <form method="post" action="{{.LogoutURL}}" class="navbar-form navbar-right">
        <button class="btn btn-default">Log out</button>
      </form>
      <div class="navbar-text navbar-right">
        {{if .Profile.ImageURL}}
          <img class="img-circle" width="24" src="{{.Profile.ImageURL}}">
        {{end}}
        <span>{{.Profile.DisplayName}}</span>
      </div>
      {{else}}
      <div class="navbar-text navbar-right">
        <a href="{{.LoginURL}}">Log in</a>
      </div>
      {{end}}
    {{end}}

Personalization

Now that you have the signed-in user's information available through middleware, you can keep track of which user added which book to the database.

// If the form didn't carry the user information for the creator, populate it
// from the currently logged in user (or mark as anonymous).
if book.CreatedByID == "" {
	user := profileFromSession(r)
	if user != nil {
		// Logged in.
		book.CreatedBy = user.DisplayName
		book.CreatedByID = user.ID
	} else {
		// Not logged in.
		book.SetCreatorAnonymous()
	}
}

And because that information is stored in the database, you can show the user which books they have personally added.

// listMineHandler displays a list of books created by the currently
// authenticated user.
func listMineHandler(w http.ResponseWriter, r *http.Request) *appError {
	user := profileFromSession(r)
	if user == nil {
		http.Redirect(w, r, "/login?redirect=/books/mine", http.StatusFound)
		return nil
	}

	books, err := bookshelf.DB.ListBooksCreatedBy(user.ID)
	if err != nil {
		return appErrorf(err, "could not list books: %v", err)
	}

	return listTmpl.Execute(w, r, books)
}

This code uses a database method called ListBooksCreatedBy. The implementation depends on which database backend you chose.

Datastore

In Cloud Datastore, you need to create additional indexes for a query that filters over more than one field. This query, in the ListBooksCreatedBy function, filters over both CreatedByID and Title.

// ListBooksCreatedBy returns a list of books, ordered by title, filtered by
// the user who created the book entry.
func (db *datastoreDB) ListBooksCreatedBy(userID string) ([]*Book, error) {
	ctx := context.Background()
	if userID == "" {
		return db.ListBooks()
	}

	books := make([]*Book, 0)
	q := datastore.NewQuery("Book").
		Filter("CreatedByID =", userID).
		Order("Title")

	keys, err := db.client.GetAll(ctx, q, &books)

	if err != nil {
		return nil, fmt.Errorf("datastoredb: could not list books: %v", err)
	}

	for i, k := range keys {
		books[i].ID = k.ID
	}

	return books, nil
}

To create the indexes, enter these commands:

Linux/Mac OS X

cd $GOPATH/src/github.com/GoogleCloudPlatform/golang-samples/getting-started/bookshelf
cd app
gcloud datastore create-indexes index.yaml

Windows

cd %GOPATH%\src\github.com\GoogleCloudPlatform\golang-samples\getting-started\bookshelf
cd app
gcloud datastore create-indexes index.yaml
indexes:

# This index enables filtering by "CreatedByID" and sort by "Title".
- kind: Book
  properties:
  - name: CreatedByID
    direction: asc
  - name: Title
    direction: asc

Cloud SQL

const listByStatement = `
  SELECT * FROM books
  WHERE createdById = ? ORDER BY title`

// ListBooksCreatedBy returns a list of books, ordered by title, filtered by
// the user who created the book entry.
func (db *mysqlDB) ListBooksCreatedBy(userID string) ([]*Book, error) {
	if userID == "" {
		return db.ListBooks()
	}

	rows, err := db.listBy.Query(userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var books []*Book
	for rows.Next() {
		book, err := scanBook(rows)
		if err != nil {
			return nil, fmt.Errorf("mysql: could not read row: %v", err)
		}

		books = append(books, book)
	}

	return books, nil
}

MongoDB

// ListBooksCreatedBy returns a list of books, ordered by title, filtered by
// the user who created the book entry.
func (db *mongoDB) ListBooksCreatedBy(userID string) ([]*Book, error) {
	var result []*Book
	if err := db.c.Find(bson.D{{Name: "createdbyid", Value: userID}}).Sort("title").All(&result); err != nil {
		return nil, err
	}
	return result, nil
}

Send feedback about...