OAuth 2.0 authorization for desktop applications using Google API

Posted: , Modified:   Go Google Cloud Platform OAuth2

Summary

This blog post introduces how to run OAuth protocol to Google API from a desktop application written in Go. Go has oauth2 package and it runs most processes; but we need to setup a local web server to receive a authorization code and calculate a code verifier to prevent man-in-the-middle attacks.

Application registration

At first, we need to register our application and obtain a pair of client ID and client secret.

Open credentials page in Google Cloud Console, and creates credentials with OAuth client ID. In create client ID page, there are several application types. Since our application is a desktop application, choose other.

Code verifier

In Google’s OAuth protocol, we can use code verifiers to prevent man-in-the-middle attacks. A code verifier is a random string. A requester computes a hash of the verifier and sends it to a server when requesting an authorization code. Even if an attacker steals the access token, the attacker doesn’t know the plain code verifier and thus the server can rejects any requests from the attacker.

A code verifier is a string of 43-128 characters from [A-Z] / [a-z] / [0-9] / - / . / _ / ~. The following is a function generating a code verifier:

import (
  "crypto/rand"
  "math/big"
)

var (
  CodeVerifierChars []byte
)

func init() {
  for b := byte('a'); b <= byte('z'); b++ {
    CodeVerifierChars = append(CodeVerifierChars, b)
  }
  for b := byte('A'); b <= byte('Z'); b++ {
    CodeVerifierChars = append(CodeVerifierChars, b)
  }
  for b := byte('0'); b <= byte('9'); b++ {
    CodeVerifierChars = append(CodeVerifierChars, b)
  }
  CodeVerifierChars = append(CodeVerifierChars, byte('-'), byte('.'), byte('_'), byte('~'))
}

func GenerateCodeVerifier() (codeVerifier []byte, err error) {
  max := big.NewInt(int64(len(CodeVerifierChars)))
  var n *big.Int
  for i := 0; i != 128; i++ {
    n, err = rand.Int(rand.Reader, max)
    if err != nil {
      return
    }
    codeVerifier = append(codeVerifier, CodeVerifierChars[n.Int64()])
  }
  return
}

Go has math/rand package to generate random values but the above code uses more secure crypto/rand package.

A requester sends a hash value of a code verifier to a server when requesting an authorization code. The hash value is called Code challenge. More precisely, a code challenge is a padding-less base64 encoded SHA256 hash value of a code verifier.

The following code calculates a code challenge:

codeVerifier := GenerateCodeVerifier()
sum := sha256.Sum256(codeVerifier)
codeChallenge := strings.TrimRight(base64.URLEncoding.EncodeToString(sum[:]), "=")

Local web server to receive authorization codes

Next, we implement a web server to receive an authorization code. The following structure defines a set of values the web server receives:

type authorizationCode struct {
  // Authorization code.
  Code  string
  // State of a request.
  State string
}

The web server runs as a goroutine, we defines channels to receive values from the goroutine.

type codeReceiver struct {
  // Channel to receive an authorization code.
  Result chan *authorizationCode
  // Channle to receive an error message.
  Error  chan error
}

// newCodeReceiver creates a set of channels.
func newCodeReceiver() *codeReceiver {
  return &codeReceiver{
    Result: make(chan *authorizationCode, 1),
    Error:  make(chan error, 1),
  }
}

The following code defines a handler of processing a response which has an authorization code as a URL query:

const (
  AuthSucceedURL = "https://jkawamoto.github.io/roadie/auth/succeed/"
  AuthErrorURL = "https://jkawamoto.github.io/roadie/auth/error/"
)

func (r *codeReceiver) ServeHTTP(res http.ResponseWriter, req *http.Request) {
  queries := req.URL.Query()
  if errCode := queries.Get("error"); errCode != "" {
    res.Header().Add("Location", AuthErrorURL)
    r.Error <- fmt.Errorf("Failed authorization: %v", errCode)

  } else if code := queries.Get("code"); code == "" {
    res.Header().Add("Location", AuthErrorURL)
    r.Error <- fmt.Errorf("Failed authorization")

  } else {
    res.Header().Add("Location", AuthSucceedURL)
    r.Result <- &authorizationCode{
      Code:  queries.Get("code"),
      State: queries.Get("state"),
    }

  }
  res.WriteHeader(http.StatusFound)
}

I prepare web pages showing succeeded message and error message; and the above code redirect those pages. Of course, we can send any HTML documents via res.Write().

Finally, the following code starts this web server:

port := 18029
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", port))
if err != nil {
  return
}
defer listener.Close()

receiver := newCodeReceiver()
go http.Serve(listener, receiver)

Requesting an access token

To request an access token, we at first need to calculates a code verifier and a code challenge, and runs a web server; and then AuthCodeURL and Exchenge of Config in oauth2 package runs OAuth protocol.

Config object takes a client ID, a client secret (both are obtained from Google Cloud Console as we show above), endpoint URLs, redirect URL i.e. the address of the local web server, and scopes which are chosen from the scope list.

The following core creates a Config object:

const (
  authorizeEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"
  tokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"
  gcpScope = "https://www.googleapis.com/auth/cloud-platform"
)

cfg := &oauth2.Config{
  ClientID:     ClientID,
  ClientSecret: ClientSecret,
  Endpoint: oauth2.Endpoint{
    AuthURL:  authorizeEndpoint,
    TokenURL: tokenEndpoint,
  },
  RedirectURL: fmt.Sprintf("http://127.0.0.1:%v", port),
  Scopes:      []string{gcpScope},
}

Then, we obtain a URL of requesting a authorization code to Google by AuthCodeURL function. We need to give a code challenge and a string representing a state to the AuthCodeURL function, too.

The following code shows how to attach a code challenge and a state string to AuthCodeURL:

state := fmt.Sprintf("%v", time.Now().Unix())
url := cfg.AuthCodeURL(
  state,
  oauth2.SetAuthURLParam("code_challenge_method", "S256"),
  oauth2.SetAuthURLParam("code_challenge", codeChallenge))

Our application shows this URL and asks users to access it and authorize our request. If the users accept our request, the web server receives an authorization code with the state string, otherwise receives an error.

The following code shows handling a response:

var code *authorizationCode
select {
case code = <-receiver.Result:
  // Check received state matches the one sent as a request.
  if code.State != state {
    err = fmt.Errorf("Received state dosen't match")
    return
  }

case err = <-receiver.Error:
  return

case <-ctx.Done():
  return nil, ctx.Err()

}

Finally, we request an access token by Config’s Exchange function. In this time, we also need to send the code verifier to proof the original requester as shown in the following code:

cfg.Endpoint.TokenURL = fmt.Sprintf(
  "%v?code_verifier=%v", cfg.Endpoint.TokenURL, string(codeVerifier))

token, err := cfg.Exchange(ctx, code.Code)

Summarizing above codes, we now define RequestToken function:

func RequestToken(ctx context.Context) (token *oauth2.Token, err error) {

  // Calculate a code verifier and associated code challenge
  codeVerifier, err := GenerateCodeVerifier()
  if err != nil {
    return
  }
  sum := sha256.Sum256(codeVerifier)
  codeChallenge := strings.TrimRight(base64.URLEncoding.EncodeToString(sum[:]), "=")

  // Start a web server
  port := 18029
  listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", port))
  if err != nil {
    return
  }
  defer listener.Close()

  receiver := newCodeReceiver()
  go http.Serve(listener, receiver)

	// Prepare oauth2.Config
  cfg := &oauth2.Config{
    ClientID:     ClientID,
    ClientSecret: ClientSecret,
    Endpoint: oauth2.Endpoint{
      AuthURL:  authorizeEndpoint,
      TokenURL: tokenEndpoint,
    },
    RedirectURL: fmt.Sprintf("http://127.0.0.1:%v", port),
    Scopes:      []string{gcpScope},
  }

  // Obtain a URL to ask users authorization.
  state := fmt.Sprintf("%v", time.Now().Unix())
  url := cfg.AuthCodeURL(
    state,
    oauth2.SetAuthURLParam("code_challenge_method", "S256"),
    oauth2.SetAuthURLParam("code_challenge", codeChallenge))

  // Show the above URL to users
  fmt.Println(url)

  // Receive an authorization code
  var code *authorizationCode
  select {
  case code = <-receiver.Result:
    if code.State != state {
      err = fmt.Errorf("Received state dosen't match")
      return
    }

  case err = <-receiver.Error:
    return
  case <-ctx.Done():
    return nil, ctx.Err()
  }

  // Exchange the authorization code to an access token.
  cfg.Endpoint.TokenURL = fmt.Sprintf(
    "%v?code_verifier=%v", cfg.Endpoint.TokenURL, string(codeVerifier))
  return cfg.Exchange(ctx, code.Code)

}

Access token usage

To use Google API with a obtained access token, we need to create an instance of http.Client which uses the access token. oauth2.Config has a function Client which creates the instance. This client renews the access token when it is expired.

The following is a sample code to create an http.Client with an access token:

ctx := context.Background()
cfg := &oauth2.Config{
  ClientID:     ClientID,
  ClientSecret: ClientSecret,
  Endpoint: oauth2.Endpoint{
    AuthURL:  authorizeEndpoint,
    TokenURL: tokenEndpoint,
  },
  Scopes:      []string{gcpScope},
}

client := cfg.Client(ctx, token)

and the following example retrieves available zones with the client:

import	(
  "context"
  "fmt"

  "google.golang.org/api/compute/v1"
)

func Zone(ctx context.Context, project string, token *oauth2.Token) (err error){

  cfg := &oauth2.Config{
    ClientID:     ClientID,
    ClientSecret: ClientSecret,
    Endpoint: oauth2.Endpoint{
      AuthURL:  authorizeEndpoint,
      TokenURL: tokenEndpoint,
    },
    Scopes:      []string{gcpScope},
  }

  client := cfg.Client(ctx, token)
  service, err := compute.New(client).newService(ctx)
  if err != nil {
    return
  }

  res, err := service.Zones.List(project).Do()
  if err != nil {
    return
  }

  for _, v := range res.Items {
    fmt.Println(v.Name, v.Status)
  }
  return

}

References