DEV Community

Cover image for Authorizing Your App With GitHub in Go
hiro
hiro

Posted on • Originally published at hiro.one on

Authorizing Your App With GitHub in 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 register application form image

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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 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, "/")
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

Thanks for reading ✌️

Top comments (0)