-
-
Save ogazitt/f749dad9cca8d0ac6607f93a42adf322 to your computer and use it in GitHub Desktop.
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() | |
} |
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.
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.
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.
The pull request from @jimlambrt was merged on May 9, 2022.
I believe this should be safe[r] to use now...
Just to update, go 1.21 now has pkce support with
verifier = oauth2.GenerateVerifier()
authCodeURL := oauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
You are a scholar and a gentleman. You just saved me hours of hacking around.