Skip to content

Instantly share code, notes, and snippets.

@brydavis
Forked from Integralist/OAuth.md
Created October 7, 2018 08:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brydavis/329dc8bff0b9bd8e45585b84375964a3 to your computer and use it in GitHub Desktop.
Save brydavis/329dc8bff0b9bd8e45585b84375964a3 to your computer and use it in GitHub Desktop.
OAuth

OAuth is a mechanism that allows a user to authorize your application to access his/her data from another service without giving you their authentication details.

For a banking monitoring application (e.g. your application reads the user's banking data and displays it within some useful diagrams), the steps could look something like the following:

  1. User requests API action from your application (e.g. show me my spending graph)
  2. Your application requests access to your bank
  3. User's bank service tells the users your application would like access to it
  4. User accepts or declines the request to access your banking data
  5. The bank provides a temporary code to your application
  6. Your app requests a token while passing back the code it was given from the bank
  7. Banking service verifies the code is a match/valid and provides a token
  8. Your app can now request data from the bank using the provided token

GitHub API

  • User is on your website and clicks “login with GitHub” link
  • Your app redirects user to GitHub’s authorization page
  • In that url you specify desired access level and a random secret
  • The user authorizes your app by clicking on a link
  • GitHub redirects to a callback url on your website (which you provided when registering the app with GitHub)
  • In the url handler, extract "secret" and "code" arguments
  • Your app checks that the secret is the same as the one you sent to GitHub
  • Your app calls another GitHub url to exchange code for access token
  • Your app uses the access token to authenticate your API calls

Steps for Application

  • Register your app with GitHub (https://github.com/settings/applications/new)
  • Set Homepage url to http://127.0.0.1:5000/
  • Set authorization callback url to http://127.0.0.1:5000/github_oauth_cb
  • In your code specify your Client ID and Client Secret (provided by GitHub when registering your app)

Steps for User

  • User should be logged into GitHub (if not they'll have to enter their username/password when asked by GitHub)
  • User visits your application (which in this example is a locally running Go application)
  • User clicks on "Login with GitHub"
  • User is directed to a GitHub page that asks them to confirm your registered app is OK to access their data
  • User is asked by GitHub to confirm access by entering their password
  • Your application has the token it needs and redirects the user back to the login page (but change this behaviour in your application accordingly)

Notes

If you restart your application (see below for code), and the user goes back to the /login page, then clicking on the login button will attempt to access the token and realise it already exists and so redirects back to the login page again. Check the console/terminal output for user details that have been printed.

The user should now be able to see your application listed in their authorized applications: https://github.com/settings/applications

Application Code

https://blog.kowalczyk.info/article/f/Accessing-GitHub-API-from-Go.html

package main

import (
	"fmt"
	"net/http"

	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
	githuboauth "golang.org/x/oauth2/github"
)

var (
	// You must register the app at https://github.com/settings/applications
	// Set callback to http://127.0.0.1:5000/github_oauth_cb
	// Set ClientId and ClientSecret values taken from GitHub registration page
	// Note: revoked the already details
	oauthConf = &oauth2.Config{
		ClientID:     "x",
		ClientSecret: "x",
		Scopes:       []string{"user:email", "repo"},
		Endpoint:     githuboauth.Endpoint,
	}
	// random string for oauth2 API calls to protect against CSRF
	oauthStateString = "thisshouldberandom"
)

const htmlIndex = `<html><body>
Login with <a href="/login">GitHub</a>
</body></html>
`

// /
func handleMain(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(htmlIndex))
}

// /login redirects to GitHub’s authorization page:
// GitHub will show authorization page to your user
// If the user authorizes your app, GitHub will re-direct to OAuth callback
func handleGitHubLogin(w http.ResponseWriter, r *http.Request) {
	url := oauthConf.AuthCodeURL(oauthStateString, oauth2.AccessTypeOnline)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

// /github_oauth_cb. Called by GitHub after authorization is granted
// This handler accesses the GitHub token, and converts the token into a http client
// We then use that http client to list GitHub information about the user
func handleGitHubCallback(w http.ResponseWriter, r *http.Request) {
	state := r.FormValue("state")
	if state != oauthStateString {
		fmt.Printf("invalid oauth state, expected '%s', got '%s'\n", oauthStateString, state)
		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
		return
	}

	code := r.FormValue("code")
	token, err := oauthConf.Exchange(oauth2.NoContext, code)
	if err != nil {
		fmt.Printf("oauthConf.Exchange() failed with '%s'\n", err)
		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
		return
	}

	oauthClient := oauthConf.Client(oauth2.NoContext, token)
	client := github.NewClient(oauthClient)
	user, _, err := client.Users.Get("")
	if err != nil {
		fmt.Printf("client.Users.Get() faled with '%s'\n", err)
		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
		return
	}
	fmt.Printf("Logged in as GitHub user: %s\n", *user.Login)
	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}

func main() {
	http.HandleFunc("/", handleMain)
	http.HandleFunc("/login", handleGitHubLogin)
	http.HandleFunc("/github_oauth_cb", handleGitHubCallback)
	fmt.Print("Started running on http://127.0.0.1:5000\n")
	fmt.Println(http.ListenAndServe(":5000", nil))
}

Handling State

If you want your user state to be persistent, then you would need to store it in a permanent/persistent store; there are lots of options for that (e.g. a distributed redis cluster - so the data is available across machines and allows your web servers to scale horizontally)

This also means you’ll need to handle your own authentication and / or authorization in your web app to use that persistent data in order to make those upstream requests on behalf of a specific user.

You could also keep a cookie in the browser (encrypted and hashed) with the token and some basic user info. But I'm not overly confident with using cookies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment