You've already forked step-ca-cli
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:
@@ -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
|
||||||
@@ -659,7 +680,6 @@ func newOauth(provider, clientID, clientSecret, authzEp, deviceAuthzEp, tokenEp,
|
|||||||
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) {
|
||||||
u, err := url.Parse(provider)
|
u, err := url.Parse(provider)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user