git.iko.soy/typeable/oauth2-proxy
on v3.0.0 help sign in
301 lines 7.1 kB history raw
1package providers
3import (
4 "encoding/json"
5 "fmt"
6 "io/ioutil"
7 "log"
8 "net/http"
9 "net/url"
10 "path"
11 "strconv"
12 "strings"
15// GitHubProvider represents an GitHub based Identity Provider
16type GitHubProvider struct {
17 *ProviderData
18 Org string
19 Team string
22// NewGitHubProvider initiates a new GitHubProvider
23func NewGitHubProvider(p *ProviderData) *GitHubProvider {
24 p.ProviderName = "GitHub"
25 if p.LoginURL == nil || p.LoginURL.String() == "" {
26 p.LoginURL = &url.URL{
27 Scheme: "https",
28 Host: "github.com",
29 Path: "/login/oauth/authorize",
30 }
31 }
32 if p.RedeemURL == nil || p.RedeemURL.String() == "" {
33 p.RedeemURL = &url.URL{
34 Scheme: "https",
35 Host: "github.com",
36 Path: "/login/oauth/access_token",
37 }
38 }
39 // ValidationURL is the API Base URL
40 if p.ValidateURL == nil || p.ValidateURL.String() == "" {
41 p.ValidateURL = &url.URL{
42 Scheme: "https",
43 Host: "api.github.com",
44 Path: "/",
45 }
46 }
47 if p.Scope == "" {
48 p.Scope = "user:email"
49 }
50 return &GitHubProvider{ProviderData: p}
53// SetOrgTeam adds GitHub org reading parameters to the OAuth2 scope
54func (p *GitHubProvider) SetOrgTeam(org, team string) {
55 p.Org = org
56 p.Team = team
57 if org != "" || team != "" {
58 p.Scope += " read:org"
59 }
62func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) {
63 // https://developer.github.com/v3/orgs/#list-your-organizations
65 var orgs []struct {
66 Login string `json:"login"`
67 }
69 type orgsPage []struct {
70 Login string `json:"login"`
71 }
73 pn := 1
74 for {
75 params := url.Values{
76 "limit": {"200"},
77 "page": {strconv.Itoa(pn)},
78 }
80 endpoint := &url.URL{
81 Scheme: p.ValidateURL.Scheme,
82 Host: p.ValidateURL.Host,
83 Path: path.Join(p.ValidateURL.Path, "/user/orgs"),
84 RawQuery: params.Encode(),
85 }
86 req, _ := http.NewRequest("GET", endpoint.String(), nil)
87 req.Header.Set("Accept", "application/vnd.github.v3+json")
88 req.Header.Set("Authorization", fmt.Sprintf("token %s", accessToken))
89 resp, err := http.DefaultClient.Do(req)
90 if err != nil {
91 return false, err
92 }
94 body, err := ioutil.ReadAll(resp.Body)
95 resp.Body.Close()
96 if err != nil {
97 return false, err
98 }
99 if resp.StatusCode != 200 {
100 return false, fmt.Errorf(
101 "got %d from %q %s", resp.StatusCode, endpoint.String(), body)
102 }
104 var op orgsPage
105 if err := json.Unmarshal(body, &op); err != nil {
106 return false, err
107 }
108 if len(op) == 0 {
109 break
110 }
112 orgs = append(orgs, op...)
113 pn++
114 }
116 var presentOrgs []string
117 for _, org := range orgs {
118 if p.Org == org.Login {
119 log.Printf("Found Github Organization: %q", org.Login)
120 return true, nil
121 }
122 presentOrgs = append(presentOrgs, org.Login)
123 }
125 log.Printf("Missing Organization:%q in %v", p.Org, presentOrgs)
126 return false, nil
129func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
130 // https://developer.github.com/v3/orgs/teams/#list-user-teams
132 var teams []struct {
133 Name string `json:"name"`
134 Slug string `json:"slug"`
135 Org struct {
136 Login string `json:"login"`
137 } `json:"organization"`
138 }
140 params := url.Values{
141 "limit": {"200"},
142 }
144 endpoint := &url.URL{
145 Scheme: p.ValidateURL.Scheme,
146 Host: p.ValidateURL.Host,
147 Path: path.Join(p.ValidateURL.Path, "/user/teams"),
148 RawQuery: params.Encode(),
149 }
150 req, _ := http.NewRequest("GET", endpoint.String(), nil)
151 req.Header.Set("Accept", "application/vnd.github.v3+json")
152 req.Header.Set("Authorization", fmt.Sprintf("token %s", accessToken))
153 resp, err := http.DefaultClient.Do(req)
154 if err != nil {
155 return false, err
156 }
158 body, err := ioutil.ReadAll(resp.Body)
159 resp.Body.Close()
160 if err != nil {
161 return false, err
162 }
163 if resp.StatusCode != 200 {
164 return false, fmt.Errorf(
165 "got %d from %q %s", resp.StatusCode, endpoint.String(), body)
166 }
168 if err := json.Unmarshal(body, &teams); err != nil {
169 return false, fmt.Errorf("%s unmarshaling %s", err, body)
170 }
172 var hasOrg bool
173 presentOrgs := make(map[string]bool)
174 var presentTeams []string
175 for _, team := range teams {
176 presentOrgs[team.Org.Login] = true
177 if p.Org == team.Org.Login {
178 hasOrg = true
179 ts := strings.Split(p.Team, ",")
180 for _, t := range ts {
181 if t == team.Slug {
182 log.Printf("Found Github Organization:%q Team:%q (Name:%q)", team.Org.Login, team.Slug, team.Name)
183 return true, nil
184 }
185 }
186 presentTeams = append(presentTeams, team.Slug)
187 }
188 }
189 if hasOrg {
190 log.Printf("Missing Team:%q from Org:%q in teams: %v", p.Team, p.Org, presentTeams)
191 } else {
192 var allOrgs []string
193 for org := range presentOrgs {
194 allOrgs = append(allOrgs, org)
195 }
196 log.Printf("Missing Organization:%q in %#v", p.Org, allOrgs)
197 }
198 return false, nil
201// GetEmailAddress returns the Account email address
202func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
204 var emails []struct {
205 Email string `json:"email"`
206 Primary bool `json:"primary"`
207 }
209 // if we require an Org or Team, check that first
210 if p.Org != "" {
211 if p.Team != "" {
212 if ok, err := p.hasOrgAndTeam(s.AccessToken); err != nil || !ok {
213 return "", err
214 }
215 } else {
216 if ok, err := p.hasOrg(s.AccessToken); err != nil || !ok {
217 return "", err
218 }
219 }
220 }
222 endpoint := &url.URL{
223 Scheme: p.ValidateURL.Scheme,
224 Host: p.ValidateURL.Host,
225 Path: path.Join(p.ValidateURL.Path, "/user/emails"),
226 }
227 req, _ := http.NewRequest("GET", endpoint.String(), nil)
228 req.Header.Set("Authorization", fmt.Sprintf("token %s", s.AccessToken))
229 resp, err := http.DefaultClient.Do(req)
230 if err != nil {
231 return "", err
232 }
233 body, err := ioutil.ReadAll(resp.Body)
234 resp.Body.Close()
235 if err != nil {
236 return "", err
237 }
239 if resp.StatusCode != 200 {
240 return "", fmt.Errorf("got %d from %q %s",
241 resp.StatusCode, endpoint.String(), body)
242 }
244 log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
246 if err := json.Unmarshal(body, &emails); err != nil {
247 return "", fmt.Errorf("%s unmarshaling %s", err, body)
248 }
250 for _, email := range emails {
251 if email.Primary {
252 return email.Email, nil
253 }
254 }
256 return "", nil
259// GetUserName returns the Account user name
260func (p *GitHubProvider) GetUserName(s *SessionState) (string, error) {
261 var user struct {
262 Login string `json:"login"`
263 Email string `json:"email"`
264 }
266 endpoint := &url.URL{
267 Scheme: p.ValidateURL.Scheme,
268 Host: p.ValidateURL.Host,
269 Path: path.Join(p.ValidateURL.Path, "/user"),
270 }
272 req, err := http.NewRequest("GET", endpoint.String(), nil)
273 if err != nil {
274 return "", fmt.Errorf("could not create new GET request: %v", err)
275 }
277 req.Header.Set("Authorization", fmt.Sprintf("token %s", s.AccessToken))
278 resp, err := http.DefaultClient.Do(req)
279 if err != nil {
280 return "", err
281 }
283 body, err := ioutil.ReadAll(resp.Body)
284 defer resp.Body.Close()
285 if err != nil {
286 return "", err
287 }
289 if resp.StatusCode != 200 {
290 return "", fmt.Errorf("got %d from %q %s",
291 resp.StatusCode, endpoint.String(), body)
292 }
294 log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
296 if err := json.Unmarshal(body, &user); err != nil {
297 return "", fmt.Errorf("%s unmarshaling %s", err, body)
298 }
300 return user.Login, nil