02 Nov 2016, 00:00

How to do Google Sign-In with Go - Part 2

Intro

Hi Folks.

This is a follow up on my previous post about Google Sign-In. In this post we will discover what to do with the information retrieved in the first encounter, which you can find here: Google Sign-In Part 1.

Forewords

The Project

Everything I did in the first post, and that I’m going to do in this example, can be found in this project: Google-OAuth-Go-Sample.

Just to recap, we left off previously on the point where we successfully obtained information about the user, with a secure token and a session initiated with them. Google nicely enough provided us with some details which we can use. This information was in JSON format and looked something like this:

{
  "sub": "1111111111111111111111",
  "name": "Your Name",
  "given_name": "Your",
  "family_name": "Name",
  "profile": "https://plus.google.com/1111111111111111111111",
  "picture": "https://lh3.googleusercontent.com/asdfadsf/AAAAAAAAAAI/Aasdfads/Xasdfasdfs/photo.jpg",
  "email": "your@gmail.com",
  "email_verified": true,
  "gender": "male"
}

In my example, to keep things simple, I will use the email address since that has to be unique in the land of Google. You could assign an ID to the user, and you could complicate things even further, but my goal is not to write an academic paper about cryptography here.

Implementation

Making something useful out of the data

In order for the app to recognise a user it must save some data about the user. I’m doing that in MongoDB right now, but that could be any form of persistence layer, like, SQLite3, BoltDB, PostgresDB, etc.

After successful user authorization

Once the user used google to provide us with sufficient information about him/herself, we can retrieve data about that user from our records. The data could be anything that is linked to our unique identifier like: Character Profile, Player Information, Status, Last Logged-In, etcetc. For this, there are two things that need to happen after authorization: Save/Load user information and initiate a session.

The session can be in the form of a cookie, or a Redis storage, or URL re-writing. I’m choosing a cookie here.

Save / Load user information

All I’m doing is a simple, returning / new user handling. The concept is simple. If the email isn’t saved, we save it. If it’s saved, we set a logic to our page render to greet the returning user.

In the AuthHandler I’m doing the following:

...
seen := false
db := database.MongoDBConnection{}
if _, mongoErr := db.LoadUser(u.Email); mongoErr == nil {
    seen = true
} else {
    err = db.SaveUser(&u)
    if err != nil {
        log.Println(err)
        c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": "Error while saving user. Please try again."})
        return
    }
}
c.HTML(http.StatusOK, "battle.tmpl", gin.H{"email": u.Email, "seen": seen})
...

Let’s break this down a bit. There is a db connection here, which calls a function that either returns an error, or it doesn’t. If it doesn’t, that means we have our user. If it does, it means we have to save the user. This is a very simple case (disregard for now, that the error could be something else as well (If you can’t get passed that, you could type check the error or check if the returned record contains the requested user information instead of checking for an error.)).

The template is than rendered depending on the seen boolean like this:

<!DOCTYPE html>
<link rel="icon"
      type="image/png"
      href="/img/favicon.ico" />
<html>
  <head>
    <link rel="stylesheet" href="/css/main.css">
  </head>
  <body>
    {{if .seen}}
        <h1>Welcome back to the battlefield '{{ .email }}'.</h1>
    {{else}}
        <h1>Welcome to the battlefield '{{ .email }}'.</h1>
    {{end}}
  </body>
</html>

You can see here, that if seen is true the header message will say: “Welcome back…“.

Initiating a session

When the user is successfully authenticated, we activate a session so that the user can access pages that require authorization. Here, I have to mention that I’m using Gin, so restricted end-points are made with groups which require a middleware.

As I mentioned earlier, I’m using cookies as session handlers. For this, a new session store has to be created with some secure token. This is achieved with the following code fragments ( note that I’m using a Gin session middleware which uses gorilla’s session handler located here: Gin-Gonic(Sessions)):

// RandToken in handlers.go:
// RandToken generates a random @l length token.
func RandToken(l int) string {
	b := make([]byte, l)
	rand.Read(b)
	return base64.StdEncoding.EncodeToString(b)
}

// quest.go:
// Create the cookie store in main.go.
store := sessions.NewCookieStore([]byte(handlers.RandToken(64)))
store.Options(sessions.Options{
    Path:   "/",
    MaxAge: 86400 * 7,
})

// using the cookie store:
router.Use(sessions.Sessions("goquestsession", store))

After this gin.Context lets us access this session store by doing session := sessions.Default(c). Now, create a session variable called user-id like this:

session.Set("user-id", u.Email)
err = session.Save()
if err != nil {
    log.Println(err)
    c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"message": "Error while saving session. Please try again."})
    return
}

Don’t forget to save the session. ;) That is it. If I restart the server, the cookie won’t be usable any longer, since it will generate a new token for the cookie store. The user will have to log in again. Note: It might be that you’ll see something like this, from session: [sessions] ERROR! securecookie: the value is not valid. You can ignore this error.

Restricting access to certain end-points with the auth Middlewareâ„¢

Now, that our session is alive, we can use it to restrict access to some part of the application. With Gin, it looks like this:

authorized := router.Group("/battle")
authorized.Use(middleware.AuthorizeRequest())
{
    authorized.GET("/field", handlers.FieldHandler)
}

This creates a grouping of end-points under /battle. Which means, everything under /battle will only be accessible if the middleware passed to the Use function calls the next handler in the chain. If it aborts the call chain, the end-point will not be accessible. My middleware is pretty simple, but it gets the job done:

// AuthorizeRequest is used to authorize a request for a certain end-point group.
func AuthorizeRequest() gin.HandlerFunc {
	return func(c *gin.Context) {
		session := sessions.Default(c)
		v := session.Get("user-id")
		if v == nil {
			c.HTML(http.StatusUnauthorized, "error.tmpl", gin.H{"message": "Please log in."})
			c.Abort()
		}
		c.Next()
	}
}

Note, that this only check if user-id is set or not. That’s certainly not enough for a secure application. Its only supposed to be a simple example of the mechanics of the auth middleware. Also, the session usually contains more than one parameter. It’s more likely that it contains several variables, which describe the user including a state for CORS protection. For CORS I’d recommend using rs/cors.

If you would try to access http://127.0.0.1:9090/battle/field without logging in, you’d be redirected to an error.tmpl with the message: Please log in..

Final Words

That’s pretty much it. Important parts are:

  • Saving the right information
  • Secure cookie store
  • CORS for sessions
  • Checks of the users details in the cookie
  • Authorised end-points
  • Session handling

Any questions, remarks, ideas, are very welcomed in the comment section. There are plenty of very nice Go frameworks which do Google OAuth2 out of the box. I recommend using them, as they save you a lot of legwork.

Thank you for reading! Gergely.

12 Jun 2016, 00:00

How to do Google sign-in with Go

Hi folks.

Today, I would like to write up a step - by - step guide with a sample web app on how to do Google Sign-In and authorization.

Let’s get started.

EDIT: A sample project of this, and Part 2, can be found here or here.

Setup

Google OAuth token

First what you need is, to register your application with Google, so you’ll get a Token that you can use to authorize later calls to Google services.

You can do that here: Google Developer Console. You’ll have to create a new project. Once it’s done, click on Credentials and create an OAuth token. You should see something like this: “To create an OAuth client ID, you must first set a product name on the consent screen.”. Go through the questions, like, what type your application is, and once you arrive at stage where it’s asking for your application’s name – there is a section asking for redirect URLs; there, write the url you wish to use when authorising your user. If you don’t know this yet, don’t fret, you can come back and change it later. Do NOT use localhost. If you are running on your own, use http://127.0.0.1:port/whatever.

This will get you a client ID and a client secret. I’m going to save these into a file which will sit next to my web app. It could be stored more securely, for example, in a database or a mounted secure, encrypted drive, and so and so forth.

Your application can now be identified through Google services.

The Application

Libraries

Google has a nice library to use with OAuth 2.0. The library is available here: Google OAth 2.0. It’s a bit cryptic at first, but not to worry. After a bit of fiddling you’ll understand fast what it does. I’m also using Gin, and Gin’s session handling middleware Gin-Session.

Setup - Credentials

Let’s create a setup which configures your credentials from the file you saved earlier. This is pretty straightforward.

// Credentials which stores google ids.
type Credentials struct {
    Cid string `json:"cid"`
    Csecret string `json:"csecret"`
}

func init() {
    var c Credentials
    file, err := ioutil.ReadFile("./creds.json")
    if err != nil {
        fmt.Printf("File error: %v\n", err)
        os.Exit(1)
    }
    json.Unmarshal(file, &c)
}

Once you have the creds loaded, you can now go on to construct the OAuth client.

Setup - OAuth client

Construct the OAuth config like this:

conf := &oauth2.Config{
  ClientID:     c.Cid,
  ClientSecret: c.Csecret,
  RedirectURL:  "http://localhost:9090/auth",
  Scopes: []string{
    "https://www.googleapis.com/auth/userinfo.email", // You have to select your own scope from here -> https://developers.google.com/identity/protocols/googlescopes#google_sign-in
  },
  Endpoint: google.Endpoint,
}

It will give you a struct which you can then use to Authorize the user in the google domain. Next, all you need to do is call AuthCodeURL on this config. It will give you a URL which redirects to a Google Sign-In form. Once the user fills that out and clicks ‘Allow’, you’ll get back a TOKEN in the code query parameter and a state which helps protect against CSRF attacks. Always check if the provided state is the same which you provided with AuthCodeURL. This will look something like this http://127.0.0.1:9090/auth?code=4FLKFskdjflf3343d4f&state=lhfu3f983j;asdf. Small function for this:

func getLoginURL(state string) string {
    // State can be some kind of random generated hash string.
    // See relevant RFC: http://tools.ietf.org/html/rfc6749#section-10.12
    return conf.AuthCodeURL(state)
}

Construct a button which the user can click and be redirected to the Google Sign-In form. When constructing the url, we must do one more thing. Create a secure state token and save it in the form of a cookie for the current user.

Random State and Button construction

Small piece of code random token:

func randToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return base64.StdEncoding.EncodeToString(b)
}

Storing it in a session and constructing the button:

func loginHandler(c *gin.Context) {
    state = randToken()
    session := sessions.Default(c)
    session.Set("state", state)
    session.Save()
    c.Writer.Write([]byte("<html><title>Golang Google</title> <body> <a href='" + getLoginURL() + "'><button>Login with Google!</button> </a> </body></html>"))
}

It’s not the nicest button I ever come up with, but it will have to do.

User Information

After you got the token, you can construct an authorised Google HTTP Client, which let’s you call Google related services and retrieve information about the user.

Getting the Client

Before we construct a client, we must check if the retrieved state is still the same compared to the one we provided. I’m doing this before constructing the client. Together this looks like this:

func authHandler(c *gin.Context) {
    // Check state validity.
    session := sessions.Default(c)
    retrievedState := session.Get("state")
    if retrievedState != c.Query("state") {
        c.AbortWithError(http.StatusUnauthorized, fmt.Errorf("Invalid session state: %s", retrievedState))
        return
    }
    // Handle the exchange code to initiate a transport.
  	tok, err := conf.Exchange(oauth2.NoContext, c.Query("code"))
  	if err != nil {
  		c.AbortWithError(http.StatusBadRequest, err)
          return
  	}
    // Construct the client.
    client := conf.Client(oauth2.NoContext, tok)
    ...

Obtaining information

Our next step is to retrieve information about the user. To achieve this, call Google’s API with the authorised client. The code for that is:

...
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
if err != nil {
    c.AbortWithError(http.StatusBadRequest, err)
    return
}
defer resp.Body.Close()
data, _ := ioutil.ReadAll(resp.Body)
log.Println("Resp body: ", string(data))
...

And this will yield a body like this:

{
  "sub": "1111111111111111111111",
  "name": "Your Name",
  "given_name": "Your",
  "family_name": "Name",
  "profile": "https://plus.google.com/1111111111111111111111",
  "picture": "https://lh3.googleusercontent.com/asdfadsf/AAAAAAAAAAI/Aasdfads/Xasdfasdfs/photo.jpg",
  "email": "your@gmail.com",
  "email_verified": true,
  "gender": "male"
}

Parse it, and you’ve got an email which you can store somewhere for registration purposes. At this point, your user is not yet Authenticated. For that, I’m going to post a second post, which describes how to go on. Retrieving the stored email address, and user session handling with Gin and MongoDB.

Putting it all together

package main

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "io/ioutil"
    "fmt"
    "log"
    "os"
    "net/http"

    "github.com/gin-gonic/contrib/sessions"
    "github.com/gin-gonic/gin"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

// Credentials which stores google ids.
type Credentials struct {
    Cid     string `json:"cid"`
    Csecret string `json:"csecret"`
}

// User is a retrieved and authentiacted user.
type User struct {
    Sub string `json:"sub"`
    Name string `json:"name"`
    GivenName string `json:"given_name"`
    FamilyName string `json:"family_name"`
    Profile string `json:"profile"`
    Picture string `json:"picture"`
    Email string `json:"email"`
    EmailVerified string `json:"email_verified"`
    Gender string `json:"gender"`
}

var cred Credentials
var conf *oauth2.Config
var state string
var store = sessions.NewCookieStore([]byte("secret"))

func randToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return base64.StdEncoding.EncodeToString(b)
}

func init() {
    file, err := ioutil.ReadFile("./creds.json")
    if err != nil {
        log.Printf("File error: %v\n", err)
        os.Exit(1)
    }
    json.Unmarshal(file, &cred)

    conf = &oauth2.Config{
        ClientID:     cred.Cid,
        ClientSecret: cred.Csecret,
        RedirectURL:  "http://127.0.0.1:9090/auth",
        Scopes: []string{
            "https://www.googleapis.com/auth/userinfo.email", // You have to select your own scope from here -> https://developers.google.com/identity/protocols/googlescopes#google_sign-in
        },
        Endpoint: google.Endpoint,
    }
}

func indexHandler(c *gin.Context) {
    c.HTML(http.StatusOK, "index.tmpl", gin.H{})
}

func getLoginURL(state string) string {
    return conf.AuthCodeURL(state)
}

func authHandler(c *gin.Context) {
    // Handle the exchange code to initiate a transport.
    session := sessions.Default(c)
    retrievedState := session.Get("state")
    if retrievedState != c.Query("state") {
        c.AbortWithError(http.StatusUnauthorized, fmt.Errorf("Invalid session state: %s", retrievedState))
        return
    }

	tok, err := conf.Exchange(oauth2.NoContext, c.Query("code"))
	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
        return
	}

	client := conf.Client(oauth2.NoContext, tok)
	email, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
    if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
        return
	}
    defer email.Body.Close()
    data, _ := ioutil.ReadAll(email.Body)
    log.Println("Email body: ", string(data))
    c.Status(http.StatusOK)
}

func loginHandler(c *gin.Context) {
    state = randToken()
    session := sessions.Default(c)
    session.Set("state", state)
    session.Save()
    c.Writer.Write([]byte("<html><title>Golang Google</title> <body> <a href='" + getLoginURL(state) + "'><button>Login with Google!</button> </a> </body></html>"))
}

func main() {
    router := gin.Default()
    router.Use(sessions.Sessions("goquestsession", store))
    router.Static("/css", "./static/css")
    router.Static("/img", "./static/img")
    router.LoadHTMLGlob("templates/*")

    router.GET("/", indexHandler)
    router.GET("/login", loginHandler)
    router.GET("/auth", authHandler)

    router.Run("127.0.0.1:9090")
}
}

This is it folks. I hope this helped. Any comments or advices are welcomed.

Google API Documentation

The documentation to this whole process, and MUCH more information can be found here: Google API Docs.

Thanks for reading, Gergely.