Authorizing Your App With GitHub in Go
Tags: go, oauth
Published: 2024-03-25 08:09:05

---
title: Authorizing Your App With GitHub in Go
published: true
date: 2024-03-25 00:00:00 UTC
tags: Go, oauth
canonical_url: https://hiro.one/blog/2024-03-25/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yhsxel7x6nz6az7m8zpt.jpg
---

It had been a long time since my last OAuth 2.0 DynamicsCompressorNode, so I decided to revisit it with GitHub's OAuth as the partner. They have [an excellent documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps), which is always a great start. 

GitHub offers two app flavors: `GitHub app` and `OAuth app`. While they recommend GitHub apps, OAuth apps were perfectly fine for my needs. And I'm going to leverage the authozation code grant type for my web app OAuth 2.0 integration. If you know how the grant type works this should be easy for you to read :)

### Project Set Up

For the very first step, we need to register our app in GitHub.com and get client ID and secret respectively. Go ahead [this page](https://github.com/settings/developers) and hit `New OAuth app`. Fill in the blank and click `Register application`.

![GitHub register application form image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkshbmpcakgtx3texpg6.png)

Let's put a login button in our web page. Your HTTP request to GitHub should be GET request and at least include the following parameters:

|parameter|description|
|---|---|
|`client_id`|The client ID you get from GitHub.| 
|`redirect_uri`|The URL in your application where users will be sent after authorization. |
|`scope`|A list of scope (user's information) that you want to get from GitHub.|
|`state`|A random string used for security purpose (in this blog I'm going to use a fixed value, which is "abcdefgh").|

Here is how it looked in my Go project using template standard library:

```html
<body>
  <h3>Login Page</h3>
  <button>
      <a id="login" href="https://github.com/login/oauth/authorize?client_id={{.}}&redirect_uri=http://localhost:3000/callback/&scope=read:user&state=abcdefgh">
          login with GitHub
      </a>
  </button>
</body>
```

### Callback Endpoint

Now for the `/callback` endpoint. Here's a simplified breakdown of our OAuth 2.0 dance moves:

1. Grab the authorization code from the callback URL.
2. Verify the state value in the URL.
3. Request user info to GitHub using the code we just got.
4. Set the user info to a cookie and redirect user back to our web app (`/`).

Here's the snippet of my Go code:

```go
func (h *GitHubAuthHandler) HandleCallback(c echo.Context) error {
	// get authorization code in the query params
	code := c.Request().URL.Query().Get("code")
	if len(code) == 0 {
		return c.String(http.StatusBadRequest, "bad request")
	}
	// check state value to make sure the request was initiated by the server itself.
	state := c.Request().URL.Query().Get("state")
	// TODO: state must be generated by server each time
	if state != "abcdefgh" {
		return c.String(http.StatusBadRequest, "unexpected state value. The authorization request could have been malformed.")
	}
	// get user info from GitHub using the authorization code we just received
	userInfo, err := h.svc.GetUserInfo(code)
	if err != nil {
		return c.String(http.StatusInternalServerError, err.Error())
	}
	// add the data to cookie should be enough for this project
	cookie := new(http.Cookie)
	cookie.Name = "session"
	cookie.Value = userInfo.Login
	cookie.Path = "/"
	cookie.Expires = time.Now().Add(10 * time.Minute)
	c.SetCookie(cookie)
	return c.Redirect(http.StatusTemporaryRedirect, "/")
}
```

And here is the code for my `GitHubAuthService`:

```go
func (g *GitHubAuthService) GetUserInfo(code string) (*UserInfo, error) {
  // generate URL
	url := fmt.Sprintf("%s?client_id=%s&client_secret=%s&code=%s",
		githubAccessTokenUrl, g.clientId, g.clientSecret, code)
  // get authorization request (simply, an access (bearer) token)
	ar, err := getAuthRequest(g.logger, url)
	if err != nil {
		return nil, err
	}
  // use the access token to get user info
	ui, err := getUserInfo(g.logger, *ar)
	if err != nil {
		return nil, err
	}
	return ui, nil
}

func getUserInfo(logger echo.Logger, ar AuthRequest) (*UserInfo, error) {
	req, err := http.NewRequest("GET", githubUserApi, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ar.Token))
	hc := &http.Client{}
	res, err := hc.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		return nil, err
	}
	var ui UserInfo
	err = json.NewDecoder(res.Body).Decode(&ui)
	if err != nil {
		return nil, err
	}
	logger.Info("Got user info: ", fmt.Sprintf("%+v", ui))
	return &ui, nil
}

func getAuthRequest(logger echo.Logger, url string) (*AuthRequest, error) {
	req, err := http.NewRequest("POST", url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "application/json")
	hc := &http.Client{}
	res, err := hc.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		return nil, err
	}
	var ar AuthRequest
	err = json.NewDecoder(res.Body).Decode(&ar)
	if err != nil {
		return nil, err
	}
	if len(ar.Token) == 0 {
		return nil, errors.New("no token")
	}
	logger.Info("auth request: ", fmt.Sprintf("%+v", ar))
	return &ar, nil
}

```

Thanks for reading ✌️