auth.go

  1package github
  2
  3import (
  4	"bufio"
  5	"bytes"
  6	"encoding/json"
  7	"fmt"
  8	"io"
  9	"io/ioutil"
 10	"math/rand"
 11	"net/http"
 12	"os"
 13	"strings"
 14	"syscall"
 15	"time"
 16
 17	"golang.org/x/crypto/ssh/terminal"
 18)
 19
 20const githubV3Url = "https://api.github.com"
 21
 22func Configure() (map[string]string, error) {
 23	fmt.Println("git-bug will generate an access token in your Github profile.")
 24	// fmt.Println("The token will have the \"repo\" permission, giving it read/write access to your repositories and issues. There is no narrower scope available, sorry :-|")
 25	fmt.Println()
 26
 27	tokenName, err := promptTokenName()
 28	if err != nil {
 29		return nil, err
 30	}
 31
 32	fmt.Println()
 33
 34	username, err := promptUsername()
 35	if err != nil {
 36		return nil, err
 37	}
 38
 39	fmt.Println()
 40
 41	password, err := promptPassword()
 42	if err != nil {
 43		return nil, err
 44	}
 45
 46	fmt.Println()
 47
 48	// Attempt to authenticate and create a token
 49
 50	var note string
 51	if tokenName == "" {
 52		note = "git-bug"
 53	} else {
 54		note = fmt.Sprintf("git-bug - %s", tokenName)
 55	}
 56
 57	resp, err := requestToken(note, username, password)
 58	if err != nil {
 59		return nil, err
 60	}
 61
 62	defer resp.Body.Close()
 63
 64	if resp.StatusCode == http.StatusCreated {
 65		return decodeBody(resp.Body)
 66	}
 67
 68	// Handle 2FA is needed
 69	OTPHeader := resp.Header.Get("X-GitHub-OTP")
 70	if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
 71		otpCode, err := prompt2FA()
 72		if err != nil {
 73			return nil, err
 74		}
 75
 76		resp, err = requestTokenWith2FA(note, username, password, otpCode)
 77		if err != nil {
 78			return nil, err
 79		}
 80
 81		defer resp.Body.Close()
 82
 83		if resp.StatusCode == http.StatusCreated {
 84			return decodeBody(resp.Body)
 85		}
 86	}
 87
 88	b, _ := ioutil.ReadAll(resp.Body)
 89	fmt.Printf("Error %v: %v\n", resp.StatusCode, string(b))
 90
 91	return nil, nil
 92}
 93
 94func requestToken(note, username, password string) (*http.Response, error) {
 95	return requestTokenWith2FA(note, username, password, "")
 96}
 97
 98func requestTokenWith2FA(note, username, password, otpCode string) (*http.Response, error) {
 99	url := fmt.Sprintf("%s/authorizations", githubV3Url)
100	params := struct {
101		Scopes      []string `json:"scopes"`
102		Note        string   `json:"note"`
103		Fingerprint string   `json:"fingerprint"`
104	}{
105		// Scopes:      []string{"repo"},
106		Note:        note,
107		Fingerprint: randomFingerprint(),
108	}
109
110	data, err := json.Marshal(params)
111	if err != nil {
112		return nil, err
113	}
114
115	req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
116	if err != nil {
117		return nil, err
118	}
119
120	req.SetBasicAuth(username, password)
121	req.Header.Set("Content-Type", "application/json")
122
123	if otpCode != "" {
124		req.Header.Set("X-GitHub-OTP", otpCode)
125	}
126
127	client := http.Client{}
128
129	return client.Do(req)
130}
131
132func decodeBody(body io.ReadCloser) (map[string]string, error) {
133	data, _ := ioutil.ReadAll(body)
134
135	aux := struct {
136		Token string `json:"token"`
137	}{}
138
139	err := json.Unmarshal(data, &aux)
140	if err != nil {
141		return nil, err
142	}
143
144	return map[string]string{
145		"token": aux.Token,
146	}, nil
147}
148
149func randomFingerprint() string {
150	// Doesn't have to be crypto secure, it's just to avoid token collision
151	rand.Seed(time.Now().UnixNano())
152	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
153	b := make([]rune, 32)
154	for i := range b {
155		b[i] = letterRunes[rand.Intn(len(letterRunes))]
156	}
157	return string(b)
158}
159
160func promptUsername() (string, error) {
161	for {
162		fmt.Println("username:")
163
164		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
165		if err != nil {
166			return "", err
167		}
168
169		line = strings.TrimRight(line, "\n")
170
171		ok, err := validateUsername(line)
172		if err != nil {
173			return "", err
174		}
175		if ok {
176			return line, nil
177		}
178
179		fmt.Println("invalid username")
180	}
181}
182
183func promptTokenName() (string, error) {
184	fmt.Println("To help distinguish the token, you can optionally provide a description")
185	fmt.Println("The token will be named \"git-bug - <description>\"")
186	fmt.Println("description:")
187
188	line, err := bufio.NewReader(os.Stdin).ReadString('\n')
189	if err != nil {
190		return "", err
191	}
192
193	return strings.TrimRight(line, "\n"), nil
194}
195
196func validateUsername(username string) (bool, error) {
197	url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
198
199	resp, err := http.Get(url)
200	if err != nil {
201		return false, err
202	}
203
204	err = resp.Body.Close()
205	if err != nil {
206		return false, err
207	}
208
209	return resp.StatusCode == http.StatusOK, nil
210}
211
212func promptPassword() (string, error) {
213	for {
214		fmt.Println("password:")
215
216		bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
217		if err != nil {
218			return "", err
219		}
220
221		if len(bytePassword) > 0 {
222			return string(bytePassword), nil
223		}
224
225		fmt.Println("password is empty")
226	}
227}
228
229func prompt2FA() (string, error) {
230	for {
231		fmt.Println("two-factor authentication code:")
232
233		byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
234		if err != nil {
235			return "", err
236		}
237
238		if len(byte2fa) > 0 {
239			return string(byte2fa), nil
240		}
241
242		fmt.Println("code is empty")
243	}
244}