1
0
mirror of https://github.com/smallstep/cli.git synced 2025-08-09 03:22:43 +03:00

Add support for OAuth using GitHub

It adds the header "Accept: application/json" so OAuth services like
GitHub returns the data in the appropriate form instead of using
application/x-www-form-urlencoded. It also configures GitHub as a new
provider as it does not have a well-known url.

This header does not cause any issues on Google or Microsoft.

Fixes #740
This commit is contained in:
Mariano Cano
2022-11-10 18:55:39 -08:00
parent ab89c80b1b
commit a769a24744

View File

@@ -328,8 +328,8 @@ type options struct {
// Validate validates the options. // Validate validates the options.
func (o *options) Validate() error { func (o *options) Validate() error {
if o.Provider != "google" && !strings.HasPrefix(o.Provider, "https://") { if o.Provider != "google" && o.Provider != "github" && !strings.HasPrefix(o.Provider, "https://") {
return errors.New("use a valid provider: google") return errors.New("use a valid provider: google or github")
} }
if o.CallbackListener != "" { if o.CallbackListener != "" {
if _, _, err := net.SplitHostPort(o.CallbackListener); err != nil { if _, _, err := net.SplitHostPort(o.CallbackListener); err != nil {
@@ -563,6 +563,28 @@ type oauth struct {
tokCh chan *token tokCh chan *token
} }
type endpoint struct {
authorization string
deviceAuthorization string
token string
userInfo string
}
var knownProviders = map[string]endpoint{
"google": {
authorization: "https://accounts.google.com/o/oauth2/v2/auth",
deviceAuthorization: "https://oauth2.googleapis.com/device/code",
token: "https://www.googleapis.com/oauth2/v4/token",
userInfo: "https://www.googleapis.com/oauth2/v3/userinfo",
},
"github": {
authorization: "https://github.com/login/oauth/authorize",
deviceAuthorization: "https://github.com/login/device/code",
token: "https://github.com/login/oauth/access_token",
userInfo: "https://api.github.com/user",
},
}
func newOauth(provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp, scope, prompt string, authParams url.Values, opts *options) (*oauth, error) { func newOauth(provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp, scope, prompt string, authParams url.Values, opts *options) (*oauth, error) {
state, err := randutil.Alphanumeric(32) state, err := randutil.Alphanumeric(32)
if err != nil { if err != nil {
@@ -579,18 +601,18 @@ func newOauth(provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp,
return nil, err return nil, err
} }
switch provider { // Check known providers
case "google": if p, ok := knownProviders[provider]; ok {
return &oauth{ return &oauth{
provider: provider, provider: provider,
clientID: clientID, clientID: clientID,
clientSecret: clientSecret, clientSecret: clientSecret,
scope: scope, scope: scope,
prompt: prompt, prompt: prompt,
authzEndpoint: "https://accounts.google.com/o/oauth2/v2/auth", authzEndpoint: p.authorization,
deviceAuthzEndpoint: "https://oauth2.googleapis.com/device/code", deviceAuthzEndpoint: p.deviceAuthorization,
tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token", tokenEndpoint: p.token,
userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo", userInfoEndpoint: p.userInfo,
loginHint: opts.Email, loginHint: opts.Email,
state: state, state: state,
codeChallenge: challenge, codeChallenge: challenge,
@@ -605,13 +627,12 @@ func newOauth(provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp,
errCh: make(chan error), errCh: make(chan error),
tokCh: make(chan *token), tokCh: make(chan *token),
}, nil }, nil
default: }
userinfoEp := ""
userinfoEp := ""
isDeviceFlow := opts.Console && opts.ConsoleFlow == deviceConsoleFlow isDeviceFlow := opts.Console && opts.ConsoleFlow == deviceConsoleFlow
if (isDeviceFlow && deviceAuthzEp == "" && tokenEp == "") || if (isDeviceFlow && deviceAuthzEp == "" && tokenEp == "") || (!isDeviceFlow && authzEp == "" && tokenEp == "") {
(!isDeviceFlow && authzEp == "" && tokenEp == "") {
d, err := disco(provider) d, err := disco(provider)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -658,7 +679,6 @@ func newOauth(provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp,
errCh: make(chan error), errCh: make(chan error),
tokCh: make(chan *token), tokCh: make(chan *token),
}, nil }, nil
}
} }
func disco(provider string) (map[string]interface{}, error) { func disco(provider string) (map[string]interface{}, error) {
@@ -688,6 +708,19 @@ func disco(provider string) (map[string]interface{}, error) {
return details, err return details, err
} }
// postForm simulates http.PostForm but adds the header "Accept:
// application/json", without this header GitHub will use
// application/x-www-form-urlencoded.
func postForm(url string, data url.Values) (*http.Response, error) {
req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode()))
if err != nil {
return nil, fmt.Errorf("create POST %s request failed: %w", url, err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
return http.DefaultClient.Do(req)
}
// NewServer creates http server // NewServer creates http server
func (o *oauth) NewServer() (*httptest.Server, error) { func (o *oauth) NewServer() (*httptest.Server, error) {
if o.CallbackListener == "" { if o.CallbackListener == "" {
@@ -826,7 +859,8 @@ func (o *oauth) DoDeviceAuthorization() (*token, error) {
data.Set("client_secret", o.clientSecret) data.Set("client_secret", o.clientSecret)
data.Set("scope", o.scope) data.Set("scope", o.scope)
resp, err := http.PostForm(o.deviceAuthzEndpoint, data) resp, err := postForm(o.deviceAuthzEndpoint, data)
// resp, err := http.PostForm(o.deviceAuthzEndpoint, data)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "http failure to identify device") return nil, errors.Wrap(err, "http failure to identify device")
} }
@@ -913,7 +947,8 @@ func openBrowserIfAsked(o *oauth, u string) {
var errHTTPToken = errors.New("bad request; token not returned") var errHTTPToken = errors.New("bad request; token not returned")
func (o *oauth) deviceAuthzTokenPoll(data url.Values) (*token, error) { func (o *oauth) deviceAuthzTokenPoll(data url.Values) (*token, error) {
resp, err := http.PostForm(o.tokenEndpoint, data) resp, err := postForm(o.tokenEndpoint, data)
// resp, err := http.PostForm(o.tokenEndpoint, data)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error doing POST to /token endpoint") return nil, errors.Wrap(err, "error doing POST to /token endpoint")
} }
@@ -986,7 +1021,8 @@ func (o *oauth) DoTwoLeggedAuthorization(issuer string) (*token, error) {
} }
// Send the POST request and return token. // Send the POST request and return token.
resp, err := http.PostForm(o.tokenEndpoint, params) resp, err := postForm(o.tokenEndpoint, params)
// resp, err := http.PostForm(o.tokenEndpoint, params)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "error from token endpoint") return nil, errors.Wrapf(err, "error from token endpoint")
} }
@@ -1195,7 +1231,8 @@ func (o *oauth) Exchange(tokenEndpoint, code string) (*token, error) {
//nolint:gosec // Tainted url deemed acceptable. Not used to store any //nolint:gosec // Tainted url deemed acceptable. Not used to store any
// backend data. // backend data.
resp, err := http.PostForm(tokenEndpoint, data) resp, err := postForm(tokenEndpoint, data)
// resp, err := http.PostForm(tokenEndpoint, data)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }