OAuth2 with Heroku & Go
Last updated January 26, 2022
Table of Contents
This tutorial demonstrates how to create a web application on Heroku that lets users authorize using the Heroku platform’s OAuth API, and then perform API calls to api.heroku.com. It can serve as the basis of a more complex integration scenario.
The tutorial uses the Go language, but the main concepts, configuration and architecture can be just as easily applied to any other language supported by Heroku.
Sample code for the demo application is available on GitHub. Edits and enhancements are welcome. Just fork the repository, make your changes and send us a pull request.
Prerequisites
- A Heroku user account. Signup is free and instant.
- If you haven’t already, follow Getting Started with Go on Heroku to familiarize yourself with Heroku.
The sample application
The sample app demonstrates the main components of authorization and making API calls. The rest of this tutorial will highlight various parts of the app.
To get started, clone the application:
$ go get -u github.com/heroku-examples/heroku-oauth-example-go
$ cd $GOPATH/src/github.com/heroku-examples/heroku-oauth-example-go
OAuth Client Setup
$ heroku clients:create "Go OAuth Example ($USER)" https://go-heroku-oauth-example-$USER.herokuapp.com/auth/heroku/callback
Note the provided HEROKU_OAUTH_ID
and HEROKU_OAUTH_SECRET
output, this is used below. These are used to identify and authenticate the sample application to heroku.
App Setup
$ heroku create go-heroku-oauth-example-$USER
$ heroku labs:enable runtime-dyno-metadata
$ heroku config:add HEROKU_OAUTH_ID=<value from above>
$ heroku config:add HEROKU_OAUTH_SECRET=<value from above>
$ heroku config:add COOKIE_SECRET=`openssl rand -hex 32`
$ heroku config:add COOKIE_ENCRYPT=`openssl rand -hex 16`
$ git push heroku master
$ heroku open
The COOKIE_*
config vars are used to secure the cookies created by the sample app.
At this point you should have a working application and your web browser open to a page that says “Sign in with Heroku”.
Before continuing let’s look at the code behind the sample application.
Global Variables
Since this is a simple example application it uses a few global variables. In a normal application these would be defined on a struct implementing http.Handler or a similar, framework specific struct.
var (
store = sessions.NewCookieStore([]byte(os.Getenv("COOKIE_SECRET")), []byte(os.Getenv("COOKIE_ENCRYPT")))
oauthConfig = &oauth2.Config{
ClientID: os.Getenv("HEROKU_OAUTH_ID"),
ClientSecret: os.Getenv("HEROKU_OAUTH_SECRET"),
Endpoint: heroku.Endpoint,
Scopes: []string{"identity"},
RedirectURL: "http://" + os.Getenv("HEROKU_APP_NAME") + "herokuapp.com/auth/heroku/callback",
}
stateToken = os.Getenv("HEROKU_APP_NAME")
)
func init() {
gob.Register(&oauth2.Token{})
store.MaxAge(60 * 60 * 8)
store.Options.Secure = true
}
store
is a cookie based session store. The sample app saves it’s intermediate data in a secure cookie.
oauthConfig
is an oauth2.Config setup for your instance of the sample application. You can find more information about supported scopes here.
stateToken
is used during the ouath2 handshake.This would normally be set to an opaque value that can be checked by the applications during the oauth2 handshake.
init function
The init function:
- Registers the
oauth2.Token
type withgob
so the token we receive can be serialized into a cookie later. - Sets up the
store
to expire in 8 hours and ensures that cookies are only sent via https.
Handlers
func handleRoot(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `<html><body><a href="/auth/heroku">Sign in with Heroku</a></body></html>`)
}
The handleRoot
function displays our simple index page.
func handleAuth(w http.ResponseWriter, r *http.Request) {
url := oauthConfig.AuthCodeURL(stateToken)
http.Redirect(w, r, url, http.StatusFound)
}
The handleAuth
function uses oauthConfig
to create an url pointing to Heroku’s consent page and then redirects the user to this page. This is the first part of the OAuth interaction.
func handleAuthCallback(w http.ResponseWriter, r *http.Request) {
if v := r.FormValue("state"); v != stateToken {
http.Error(w, "Invalid State token", http.StatusBadRequest)
return
}
ctx := context.Background()
token, err := oauthConfig.Exchange(ctx, r.FormValue("code"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := store.Get(r, "heroku-oauth-example-go")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["heroku-oauth-token"] = token
if err := session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/user", http.StatusFound)
}
The handleAuthCallback
is the second and core part of the OAuth interaction. First the state
form value is retrieved and checked against the set stateToken
. If it’s not the same, the request is bad. Next the app uses the background context (In a real application an application specific context would be used.) to perform the OAuth2 exchange, converting the provided code
to a token. Next the app stores the token in session and save the session into the cookie. Last we’re redirected to the /user
route.
func handleUser(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "heroku-oauth-example-go")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
token, ok := session.Values["heroku-oauth-token"].(*oauth2.Token)
if !ok {
http.Error(w, "Unable to assert token", http.StatusInternalServerError)
return
}
client := oauthConfig.Client(context.Background(), token)
resp, err := client.Get("https://api.heroku.com/account")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
d := json.NewDecoder(resp.Body)
var account struct { // See https://devcenter.heroku.com/articles/platform-api-reference#account
Email string `json:"email"`
}
if err := d.Decode(&account); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, `<html><body><h1>Hello %s</h1></body></html>`, account.Email)
}
The handleUser
function shows how to use the oauth2.Client
method paired with a token to call the Heroku API. The function starts out by retrieving the token from the session. Next it creates a http.Client using the [oauth2.Client](https://godoc.org/golang.org/x/oauth2#Config.Client)
method to fetch the user’s account information from the Heroku API. Last it says hello to the user.
func main() {
http.HandleFunc("/", handleRoot)
http.HandleFunc("/auth/heroku", handleAuth)
http.HandleFunc("/auth/heroku/callback", handleAuthCallback)
http.HandleFunc("/user", handleUser)
http.ListenAndServe(":"+os.Getenv("PORT"), nil)
main function
The main
function is your entry point into the program and wires up the above handlers into the default http Mux and starts a http server on $PORT
.
Visiting the App
When you click on Sign in with Heroku
, your browser requests /auth/heroku
, which generates the oauth2 provider URL and redirects you to https://id.heroku.com
for login. Once you have authorized your instance of the sample app, id.heroku.com
redirects you back to your sample app’s /auth/heroku/callback
with the appropriate oauth2 state
and code
parameters. And finally this redirects you to a page that says Hello <your email address>
.
Summary
This tutorial demonstrates how to create a Heroku web application that uses Heroku’s OAuth2 API for authentication and how to then use the provided OAuth2 token to authenticate to Heroku’s API.