Authorizing Your App With GitHub

Authorizing Your App With GitHub


Go

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, 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 and hit New OAuth app. Fill in the blank and click Register application.

GitHub Page

Login Button

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:

parameterdescription
client_idThe client ID you get from GitHub.
redirect_uriThe URL in your application where users will be sent after authorization.
scopeA list of scope (user’s information) that you want to get from GitHub.
stateA 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:

<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:

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
	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:

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 ✌️

© 2024 Hiro