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("TODO: describe token")
 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	url := fmt.Sprintf("%s/authorizations", githubV3Url)
 58	params := struct {
 59		Scopes      []string `json:"scopes"`
 60		Note        string   `json:"note"`
 61		Fingerprint string   `json:"fingerprint"`
 62	}{
 63		Scopes:      []string{"repo"},
 64		Note:        note,
 65		Fingerprint: randomFingerprint(),
 66	}
 67
 68	data, err := json.Marshal(params)
 69	if err != nil {
 70		return nil, err
 71	}
 72
 73	req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
 74	if err != nil {
 75		return nil, err
 76	}
 77	req.SetBasicAuth(username, password)
 78	req.Header.Set("Content-Type", "application/json")
 79	client := http.Client{}
 80
 81	resp, err := client.Do(req)
 82	if err != nil {
 83		return nil, err
 84	}
 85
 86	defer resp.Body.Close()
 87
 88	if resp.StatusCode == http.StatusCreated {
 89		return decodeBody(resp.Body)
 90	}
 91
 92	// Handle 2FA is needed
 93	OTPHeader := resp.Header.Get("X-GitHub-OTP")
 94	if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
 95		otpCode, err := prompt2FA()
 96		if err != nil {
 97			return nil, err
 98		}
 99
100		req.Header.Set("X-GitHub-OTP", otpCode)
101
102		resp2, err := client.Do(req)
103		if err != nil {
104			return nil, err
105		}
106
107		defer resp2.Body.Close()
108
109		if resp2.StatusCode == http.StatusCreated {
110			return decodeBody(resp.Body)
111		}
112	}
113
114	b, _ := ioutil.ReadAll(resp.Body)
115	fmt.Printf("Error %v: %v\n", resp.StatusCode, string(b))
116
117	return nil, nil
118}
119
120func decodeBody(body io.ReadCloser) (map[string]string, error) {
121	data, _ := ioutil.ReadAll(body)
122
123	aux := struct {
124		Token string `json:"token"`
125	}{}
126
127	err := json.Unmarshal(data, &aux)
128	if err != nil {
129		return nil, err
130	}
131
132	return map[string]string{
133		"token": aux.Token,
134	}, nil
135}
136
137func randomFingerprint() string {
138	// Doesn't have to be crypto secure, it's just to avoid token collision
139	rand.Seed(time.Now().UnixNano())
140	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
141	b := make([]rune, 32)
142	for i := range b {
143		b[i] = letterRunes[rand.Intn(len(letterRunes))]
144	}
145	return string(b)
146}
147
148func promptUsername() (string, error) {
149	for {
150		fmt.Println("Username:")
151
152		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
153		if err != nil {
154			return "", err
155		}
156
157		line = strings.TrimRight(line, "\n")
158
159		ok, err := validateUsername(line)
160		if err != nil {
161			return "", err
162		}
163		if ok {
164			return line, nil
165		}
166
167		fmt.Println("invalid username")
168	}
169}
170
171func promptTokenName() (string, error) {
172	fmt.Println("To help distinguish the token, you can optionally provide a description")
173	fmt.Println("Token name:")
174
175	line, err := bufio.NewReader(os.Stdin).ReadString('\n')
176	if err != nil {
177		return "", err
178	}
179
180	return strings.TrimRight(line, "\n"), nil
181}
182
183func validateUsername(username string) (bool, error) {
184	url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
185
186	resp, err := http.Get(url)
187	if err != nil {
188		return false, err
189	}
190
191	err = resp.Body.Close()
192	if err != nil {
193		return false, err
194	}
195
196	return resp.StatusCode == http.StatusOK, nil
197}
198
199func promptPassword() (string, error) {
200	for {
201		fmt.Println("Password:")
202
203		bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
204		if err != nil {
205			return "", err
206		}
207
208		if len(bytePassword) > 0 {
209			return string(bytePassword), nil
210		}
211
212		fmt.Println("password is empty")
213	}
214}
215
216func prompt2FA() (string, error) {
217	for {
218		fmt.Println("Two-factor authentication code:")
219
220		byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
221		if err != nil {
222			return "", err
223		}
224
225		if len(byte2fa) > 0 {
226			return string(byte2fa), nil
227		}
228
229		fmt.Println("code is empty")
230	}
231}