Skip to content

Instantly share code, notes, and snippets.

@ogazitt
Created April 14, 2020 05:53
  • Star 47 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ogazitt/f749dad9cca8d0ac6607f93a42adf322 to your computer and use it in GitHub Desktop.
Auth0 PKCE flow for a CLI built in golang
package auth
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"strings"
cv "github.com/nirasan/go-oauth-pkce-code-verifier"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/viper"
)
// AuthorizeUser implements the PKCE OAuth2 flow.
func AuthorizeUser(clientID string, authDomain string, redirectURL string) {
// initialize the code verifier
var CodeVerifier, _ = cv.CreateCodeVerifier()
// Create code_challenge with S256 method
codeChallenge := CodeVerifier.CodeChallengeS256()
// construct the authorization URL (with Auth0 as the authorization provider)
authorizationURL := fmt.Sprintf(
"https://%s/authorize?audience=https://api.snapmaster.io"+
"&scope=openid"+
"&response_type=code&client_id=%s"+
"&code_challenge=%s"+
"&code_challenge_method=S256&redirect_uri=%s",
authDomain, clientID, codeChallenge, redirectURL)
// start a web server to listen on a callback URL
server := &http.Server{Addr: redirectURL}
// define a handler that will get the authorization code, call the token endpoint, and close the HTTP server
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// get the authorization code
code := r.URL.Query().Get("code")
if code == "" {
fmt.Println("snap: Url Param 'code' is missing")
io.WriteString(w, "Error: could not find 'code' URL parameter\n")
// close the HTTP server and return
cleanup(server)
return
}
// trade the authorization code and the code verifier for an access token
codeVerifier := CodeVerifier.String()
token, err := getAccessToken(clientID, codeVerifier, code, redirectURL)
if err != nil {
fmt.Println("snap: could not get access token")
io.WriteString(w, "Error: could not retrieve access token\n")
// close the HTTP server and return
cleanup(server)
return
}
viper.Set("AccessToken", token)
err = viper.WriteConfig()
//_, err = config.WriteConfigFile("auth.json", token)
if err != nil {
fmt.Println("snap: could not write config file")
io.WriteString(w, "Error: could not store access token\n")
// close the HTTP server and return
cleanup(server)
return
}
// return an indication of success to the caller
io.WriteString(w, `
<html>
<body>
<h1>Login successful!</h1>
<h2>You can close this window and return to the snap CLI.</h2>
</body>
</html>`)
fmt.Println("Successfully logged into snapmaster API.")
// close the HTTP server
cleanup(server)
})
// parse the redirect URL for the port number
u, err := url.Parse(redirectURL)
if err != nil {
fmt.Printf("snap: bad redirect URL: %s\n", err)
os.Exit(1)
}
// set up a listener on the redirect port
port := fmt.Sprintf(":%s", u.Port())
l, err := net.Listen("tcp", port)
if err != nil {
fmt.Printf("snap: can't listen to port %s: %s\n", port, err)
os.Exit(1)
}
// open a browser window to the authorizationURL
err = open.Start(authorizationURL)
if err != nil {
fmt.Printf("snap: can't open browser to URL %s: %s\n", authorizationURL, err)
os.Exit(1)
}
// start the blocking web server loop
// this will exit when the handler gets fired and calls server.Close()
server.Serve(l)
}
// getAccessToken trades the authorization code retrieved from the first OAuth2 leg for an access token
func getAccessToken(clientID string, codeVerifier string, authorizationCode string, callbackURL string) (string, error) {
// set the url and form-encoded data for the POST to the access token endpoint
url := "https://snapmaster-dev.auth0.com/oauth/token"
data := fmt.Sprintf(
"grant_type=authorization_code&client_id=%s"+
"&code_verifier=%s"+
"&code=%s"+
"&redirect_uri=%s",
clientID, codeVerifier, authorizationCode, callbackURL)
payload := strings.NewReader(data)
// create the request and execute it
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("content-type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("snap: HTTP error: %s", err)
return "", err
}
// process the response
defer res.Body.Close()
var responseData map[string]interface{}
body, _ := ioutil.ReadAll(res.Body)
// unmarshal the json into a string map
err = json.Unmarshal(body, &responseData)
if err != nil {
fmt.Printf("snap: JSON error: %s", err)
return "", err
}
// retrieve the access token out of the map, and return to caller
accessToken := responseData["access_token"].(string)
return accessToken, nil
}
// cleanup closes the HTTP server
func cleanup(server *http.Server) {
// we run this as a goroutine so that this function falls through and
// the socket to the browser gets flushed/closed before the server goes away
go server.Close()
}
@cadethacker
Copy link

You are a scholar and a gentleman. You just saved me hours of hacking around.

@cadethacker
Copy link

cadethacker commented Aug 25, 2020

In looking at the google code, I see that they try a bunch of ports until they find one. That means they setup all those as valid redirects. They did 100, but might be worth adding 10 valid redirects. Anybody using a CLI is probably also running a bunch of test code :D

port := 8000
	foundOpenPort := false

	for port < 8010 {

		host := fmt.Sprintf("localhost:%d", port)

		fmt.Printf("Trying %s", host)
		ln, err := net.Listen("tcp", host)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Can't listen on port %d: %s", port, err)
			// move to next port
			port = port + 1
			continue
		}

		_ = ln.Close()
		foundOpenPort = true
		break
	}

	fmt.Printf("TCP Port %d is available\n", port)

	redirectURL := fmt.Sprintf("http://localhost:%d/identity/callback", port)

	if !foundOpenPort {
		err := fmt.Errorf("Unable to find an open port, failing")
		return err
	}

I found this somewhere, and added it near the top. Just a passing gift.

@ogazitt
Copy link
Author

ogazitt commented Aug 26, 2020

Thanks @cadethacker! Glad it was helpful. Also, good find on the code that finds an open port - that's a great practical addition to the gist.

@jimlambrt
Copy link

jimlambrt commented Dec 20, 2020

FYI: github.com/nirasan/go-oauth-pkce-code-verifier uses math/rand to generate a non-cryptographic random string for the verifier which means it has a timing vulnerability.

I've opened a PR to address it: nirasan/go-oauth-pkce-code-verifier#1

Until the PR is merged, I would suggest not using that package for PKCE implementations.

@oogali
Copy link

oogali commented Sep 13, 2022

The pull request from @jimlambrt was merged on May 9, 2022.

I believe this should be safe[r] to use now...

@smitt04
Copy link

smitt04 commented Oct 13, 2023

Just to update, go 1.21 now has pkce support with

verifier = oauth2.GenerateVerifier()
authCodeURL := oauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))

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