Browse Source

add support for HTTP Basic Authentication

Add a new error type, TwoFactorAuthError, which identifies the need to
include a one-time password when using Basic Auth for a user with
two-factor auth enabled.

An example of using both the new transport and error type can be seen in
examples/basicauth/main.go.

fixes #258
Will Norris 10 years ago
parent
commit
8f3e741cad
4 changed files with 157 additions and 4 deletions
  1. +3
    -0
      README.md
  2. +53
    -0
      examples/basicauth/main.go
  3. +52
    -4
      github/github.go
  4. +49
    -0
      github/github_test.go

+ 3
- 0
README.md View File

@ -58,6 +58,9 @@ func main() {
See the [oauth2 docs][] for complete instructions on using that library.
For API methods that require HTTP Basic Authentication, use the
[`BasicAuthTransport`](https://godoc.org/github.com/google/go-github/github#BasicAuthTransport).
### Pagination ###
All requests for resource collections (repos, pull requests, issues, etc)


+ 53
- 0
examples/basicauth/main.go View File

@ -0,0 +1,53 @@
// Copyright 2015 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The basicauth command demonstrates using the github.BasicAuthTransport,
// including handling two-factor authentication. This won't currently work for
// accounts that use SMS to receive one-time passwords.
package main
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"github.com/google/go-github/github"
"golang.org/x/crypto/ssh/terminal"
)
func main() {
r := bufio.NewReader(os.Stdin)
fmt.Print("GitHub Username: ")
username, _ := r.ReadString('\n')
fmt.Print("GitHub Password: ")
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
password := string(bytePassword)
tp := github.BasicAuthTransport{
Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password),
}
client := github.NewClient(tp.Client())
user, _, err := client.Users.Get("")
// Is this a two-factor auth error? If so, prompt for OTP and try again.
if _, ok := err.(*github.TwoFactorAuthError); err != nil && ok {
fmt.Print("\nGitHub OTP: ")
otp, _ := r.ReadString('\n')
tp.OTP = strings.TrimSpace(otp)
user, _, err = client.Users.Get("")
}
if err != nil {
fmt.Printf("\nerror: %v\n", err)
return
}
fmt.Printf("\n%v\n", github.Stringify(user))
}

+ 52
- 4
github/github.go View File

@ -32,6 +32,7 @@ const (
headerRateLimit = "X-RateLimit-Limit"
headerRateRemaining = "X-RateLimit-Remaining"
headerRateReset = "X-RateLimit-Reset"
headerOTP = "X-GitHub-OTP"
mediaTypeV3 = "application/vnd.github.v3+json"
defaultMediaType = "application/octet-stream"
@ -357,8 +358,15 @@ func (r *ErrorResponse) Error() string {
r.Response.StatusCode, r.Message, r.Errors)
}
// sanitizeURL redacts the client_id and client_secret tokens from the URL which
// may be exposed to the user, specifically in the ErrorResponse error message.
// TwoFactorAuthError occurs when using HTTP Basic Authentication for a user
// that has two-factor authentication enabled. The request can be reattempted
// by providing a one-time password in the request.
type TwoFactorAuthError ErrorResponse
func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() }
// sanitizeURL redacts the client_secret parameter from the URL which may be
// exposed to the user, specifically in the ErrorResponse error message.
func sanitizeURL(uri *url.URL) *url.URL {
if uri == nil {
return nil
@ -411,6 +419,9 @@ func CheckResponse(r *http.Response) error {
if err == nil && data != nil {
json.Unmarshal(data, errorResponse)
}
if r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required") {
return (*TwoFactorAuthError)(errorResponse)
}
return errorResponse
}
@ -562,6 +573,43 @@ func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper {
return http.DefaultTransport
}
// BasicAuthTransport is an http.RoundTripper that authenticates all requests
// using HTTP Basic Authentication with the provided username and password. It
// additionally supports users who have two-factor authentication enabled on
// their GitHub account.
type BasicAuthTransport struct {
Username string // GitHub username
Password string // GitHub password
OTP string // one-time password for users with two-factor auth enabled
// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}
// RoundTrip implements the RoundTripper interface.
func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = cloneRequest(req) // per RoundTrip contract
req.SetBasicAuth(t.Username, t.Password)
if t.OTP != "" {
req.Header.Add(headerOTP, t.OTP)
}
return t.transport().RoundTrip(req)
}
// Client returns an *http.Client that makes requests that are authenticated
// using HTTP Basic Authentication.
func (t *BasicAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}
func (t *BasicAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
// cloneRequest returns a clone of the provided *http.Request. The clone is a
// shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
@ -569,9 +617,9 @@ func cloneRequest(r *http.Request) *http.Request {
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header)
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = s
r2.Header[k] = append([]string(nil), s...)
}
return r2
}


+ 49
- 0
github/github_test.go View File

@ -695,3 +695,52 @@ func TestUnauthenticatedRateLimitedTransport_transport(t *testing.T) {
t.Errorf("Expected custom transport to be used.")
}
}
func TestBasicAuthTransport(t *testing.T) {
setup()
defer teardown()
username, password, otp := "u", "p", "123456"
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok {
t.Errorf("request does not contain basic auth credentials")
}
if u != username {
t.Errorf("request contained basic auth username %q, want %q", u, username)
}
if p != password {
t.Errorf("request contained basic auth password %q, want %q", p, password)
}
if got, want := r.Header.Get(headerOTP), otp; got != want {
t.Errorf("request contained OTP %q, want %q", got, want)
}
})
tp := &BasicAuthTransport{
Username: username,
Password: password,
OTP: otp,
}
basicAuthClient := NewClient(tp.Client())
basicAuthClient.BaseURL = client.BaseURL
req, _ := basicAuthClient.NewRequest("GET", "/", nil)
basicAuthClient.Do(req, nil)
}
func TestBasicAuthTransport_transport(t *testing.T) {
// default transport
tp := &BasicAuthTransport{}
if tp.transport() != http.DefaultTransport {
t.Errorf("Expected http.DefaultTransport to be used.")
}
// custom transport
tp = &BasicAuthTransport{
Transport: &http.Transport{},
}
if tp.transport() == http.DefaultTransport {
t.Errorf("Expected custom transport to be used.")
}
}

Loading…
Cancel
Save