From 6dfce113520026448779501becc9e2b99d5fd7e8 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 3 Apr 2021 04:43:34 +0100 Subject: [PATCH 1/2] Allow for arbitrary OAuth hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not all OAuth hosts use the same routes as GitHub, for example: - Microsoft use /oauth2/v2.0/devicecode - Google use /device/code - Auth0 use /oauth/device/code Similar differences are present for the authorise and access token routes too. This commit introduces a concept of a Host, which is a container for the endpoints that the library uses. After this, the Hostname field is deprecated. Co-authored-by: Mislav Marohnić --- examples_test.go | 2 +- go.sum | 1 + oauth.go | 36 +++++++++++++++++++++++------------- oauth_device.go | 8 ++++++-- oauth_webapp.go | 9 +++++++-- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/examples_test.go b/examples_test.go index f5f155d..18e794c 100644 --- a/examples_test.go +++ b/examples_test.go @@ -10,7 +10,7 @@ import ( // flow support is globally available, but enables logging in to hosted GitHub instances as well. func Example() { flow := &Flow{ - Hostname: "github.com", + Host: GitHubHost("https://github.com"), ClientID: os.Getenv("OAUTH_CLIENT_ID"), ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"), // only applicable to web app flow CallbackURI: "http://127.0.0.1/callback", // only applicable to web app flow diff --git a/go.sum b/go.sum index 3f7b215..cb4bc70 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ github.com/cli/browser v1.0.0 h1:RIleZgXrhdiCVgFBSjtWwkLPUCWyhhhN5k5HGSBt1js= github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= diff --git a/oauth.go b/oauth.go index ed34dbb..6439782 100644 --- a/oauth.go +++ b/oauth.go @@ -17,10 +17,32 @@ type httpClient interface { PostForm(string, url.Values) (*http.Response, error) } +// Host defines the endpoints used to authorize against an OAuth server. +type Host struct { + DeviceCodeURL string + AuthorizeURL string + TokenURL string +} + +// GitHubHost constructs a Host from the given URL to a GitHub instance. +func GitHubHost(hostURL string) *Host { + u, _ := url.Parse(hostURL) + + return &Host{ + DeviceCodeURL: fmt.Sprintf("%s://%s/login/device/code", u.Scheme, u.Host), + AuthorizeURL: fmt.Sprintf("%s://%s/login/oauth/authorize", u.Scheme, u.Host), + TokenURL: fmt.Sprintf("%s://%s/login/oauth/access_token", u.Scheme, u.Host), + } +} + // Flow facilitates a single OAuth authorization flow. type Flow struct { - // The host to authorize the app with. + // The hostname to authorize the app with. + // + // Deprecated: Use Host instead. Hostname string + // Host configuration to authorize the app with. + Host *Host // OAuth scopes to request from the user. Scopes []string // OAuth application ID. @@ -47,18 +69,6 @@ type Flow struct { Stdout io.Writer } -func deviceInitURL(host string) string { - return fmt.Sprintf("https://%s/login/device/code", host) -} - -func webappInitURL(host string) string { - return fmt.Sprintf("https://%s/login/oauth/authorize", host) -} - -func tokenURL(host string) string { - return fmt.Sprintf("https://%s/login/oauth/access_token", host) -} - // DetectFlow tries to perform Device flow first and falls back to Web application flow. func (oa *Flow) DetectFlow() (*api.AccessToken, error) { accessToken, err := oa.DeviceFlow() diff --git a/oauth_device.go b/oauth_device.go index 19fd344..4faa4d6 100644 --- a/oauth_device.go +++ b/oauth_device.go @@ -28,8 +28,12 @@ func (oa *Flow) DeviceFlow() (*api.AccessToken, error) { if stdout == nil { stdout = os.Stdout } + host := oa.Host + if host == nil { + host = GitHubHost("https://" + oa.Hostname) + } - code, err := device.RequestCode(httpClient, deviceInitURL(oa.Hostname), oa.ClientID, oa.Scopes) + code, err := device.RequestCode(httpClient, host.DeviceCodeURL, oa.ClientID, oa.Scopes) if err != nil { return nil, err } @@ -54,7 +58,7 @@ func (oa *Flow) DeviceFlow() (*api.AccessToken, error) { return nil, fmt.Errorf("error opening the web browser: %w", err) } - return device.PollToken(httpClient, tokenURL(oa.Hostname), oa.ClientID, code) + return device.PollToken(httpClient, host.TokenURL, oa.ClientID, code) } func waitForEnter(r io.Reader) error { diff --git a/oauth_webapp.go b/oauth_webapp.go index 723fe87..5484aee 100644 --- a/oauth_webapp.go +++ b/oauth_webapp.go @@ -12,6 +12,11 @@ import ( // WebAppFlow starts a local HTTP server, opens the web browser to initiate the OAuth Web application // flow, blocks until the user completes authorization and is redirected back, and returns the access token. func (oa *Flow) WebAppFlow() (*api.AccessToken, error) { + host := oa.Host + if host == nil { + host = GitHubHost("https://" + oa.Hostname) + } + flow, err := webapp.InitFlow() if err != nil { return nil, err @@ -23,7 +28,7 @@ func (oa *Flow) WebAppFlow() (*api.AccessToken, error) { Scopes: oa.Scopes, AllowSignup: true, } - browserURL, err := flow.BrowserURL(webappInitURL(oa.Hostname), params) + browserURL, err := flow.BrowserURL(host.AuthorizeURL, params) if err != nil { return nil, err } @@ -47,5 +52,5 @@ func (oa *Flow) WebAppFlow() (*api.AccessToken, error) { httpClient = http.DefaultClient } - return flow.AccessToken(httpClient, tokenURL(oa.Hostname), oa.ClientSecret) + return flow.AccessToken(httpClient, host.TokenURL, oa.ClientSecret) } From 5b3cff987108f0b962e07384af4f50ed23b4205a Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Wed, 7 Apr 2021 12:22:00 +0100 Subject: [PATCH 2/2] Support OAuth servers using standard responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mislav Marohnić --- api/form.go | 36 ++++++++++++++++++++++++------------ api/form_test.go | 22 +++++++++++++++++++++- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/api/form.go b/api/form.go index 2b370a4..abcd7ad 100644 --- a/api/form.go +++ b/api/form.go @@ -1,12 +1,14 @@ package api import ( + "encoding/json" "fmt" "io" "io/ioutil" + "mime" "net/http" "net/url" - "strings" + "strconv" ) type httpClient interface { @@ -71,7 +73,9 @@ func PostForm(c httpClient, u string, params url.Values) (*FormResponse, error) requestURI: u, } - if contentType(resp.Header.Get("Content-Type")) == formType { + mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + switch mediaType { + case "application/x-www-form-urlencoded": var bb []byte bb, err = ioutil.ReadAll(resp.Body) if err != nil { @@ -82,7 +86,24 @@ func PostForm(c httpClient, u string, params url.Values) (*FormResponse, error) if err != nil { return r, err } - } else { + case "application/json": + var values map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&values); err != nil { + return r, err + } + + r.values = make(url.Values) + for key, value := range values { + switch v := value.(type) { + case string: + r.values.Set(key, v) + case int64: + r.values.Set(key, strconv.FormatInt(v, 10)) + case float64: + r.values.Set(key, strconv.FormatFloat(v, 'f', -1, 64)) + } + } + default: _, err = io.Copy(ioutil.Discard, resp.Body) if err != nil { return r, err @@ -91,12 +112,3 @@ func PostForm(c httpClient, u string, params url.Values) (*FormResponse, error) return r, nil } - -const formType = "application/x-www-form-urlencoded" - -func contentType(t string) string { - if i := strings.IndexRune(t, ';'); i >= 0 { - return t[0:i] - } - return t -} diff --git a/api/form_test.go b/api/form_test.go index 1da72f3..291fb0d 100644 --- a/api/form_test.go +++ b/api/form_test.go @@ -141,7 +141,7 @@ func TestPostForm(t *testing.T) { wantErr bool }{ { - name: "success", + name: "success urlencoded", args: args{ url: "https://github.com/oauth", }, @@ -160,6 +160,26 @@ func TestPostForm(t *testing.T) { }, wantErr: false, }, + { + name: "success JSON", + args: args{ + url: "https://github.com/oauth", + }, + http: apiClient{ + body: `{"access_token":"123abc", "scopes":"repo gist"}`, + status: 200, + contentType: "application/json; charset=utf-8", + }, + want: &FormResponse{ + StatusCode: 200, + requestURI: "https://github.com/oauth", + values: url.Values{ + "access_token": {"123abc"}, + "scopes": {"repo gist"}, + }, + }, + wantErr: false, + }, { name: "HTML response", args: args{