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
.
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>
Callback Endpoint
Now for the /callback
endpoint. Here's a simplified breakdown of our OAuth 2.0 dance moves:
- Grab the authorization code from the callback URL.
- Verify the state value in the URL.
- Request user info to GitHub using the code we just got.
- 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, "/")
}
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 ✌️