14 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
16 "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
19// GitHubProvider represents an GitHub based Identity Provider
20type GitHubProvider struct {
29var _ Provider = (*GitHubProvider)(nil)
31// NewGitHubProvider initiates a new GitHubProvider
32func NewGitHubProvider(p *ProviderData) *GitHubProvider {
33 p.ProviderName = "GitHub"
34 if p.LoginURL == nil || p.LoginURL.String() == "" {
35 p.LoginURL = &url.URL{
38 Path: "/login/oauth/authorize",
41 if p.RedeemURL == nil || p.RedeemURL.String() == "" {
42 p.RedeemURL = &url.URL{
45 Path: "/login/oauth/access_token",
48 // ValidationURL is the API Base URL
49 if p.ValidateURL == nil || p.ValidateURL.String() == "" {
50 p.ValidateURL = &url.URL{
52 Host: "api.github.com",
57 p.Scope = "user:email"
59 return &GitHubProvider{ProviderData: p}
62func getGitHubHeader(accessToken string) http.Header {
63 header := make(http.Header)
64 header.Set("Accept", "application/vnd.github.v3+json")
65 header.Set("Authorization", fmt.Sprintf("token %s", accessToken))
69// SetOrgTeam adds GitHub org reading parameters to the OAuth2 scope
70func (p *GitHubProvider) SetOrgTeam(org, team string) {
73 if org != "" || team != "" {
74 p.Scope += " read:org"
78// SetRepo configures the target repository and optional token to use
79func (p *GitHubProvider) SetRepo(repo, token string) {
84// SetUsers configures allowed usernames
85func (p *GitHubProvider) SetUsers(users []string) {
89func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) {
90 // https://developer.github.com/v3/orgs/#list-your-organizations
93 Login string `json:"login"`
96 type orgsPage []struct {
97 Login string `json:"login"`
102 params := url.Values{
104 "page": {strconv.Itoa(pn)},
107 endpoint := &url.URL{
108 Scheme: p.ValidateURL.Scheme,
109 Host: p.ValidateURL.Host,
110 Path: path.Join(p.ValidateURL.Path, "/user/orgs"),
111 RawQuery: params.Encode(),
115 err := requests.New(endpoint.String()).
117 WithHeaders(getGitHubHeader(accessToken)).
128 orgs = append(orgs, op...)
132 presentOrgs := make([]string, 0, len(orgs))
133 for _, org := range orgs {
134 if p.Org == org.Login {
135 logger.Printf("Found Github Organization: %q", org.Login)
138 presentOrgs = append(presentOrgs, org.Login)
141 logger.Printf("Missing Organization:%q in %v", p.Org, presentOrgs)
145func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string) (bool, error) {
146 // https://developer.github.com/v3/orgs/teams/#list-user-teams
149 Name string `json:"name"`
150 Slug string `json:"slug"`
152 Login string `json:"login"`
153 } `json:"organization"`
156 type teamsPage []struct {
157 Name string `json:"name"`
158 Slug string `json:"slug"`
160 Login string `json:"login"`
161 } `json:"organization"`
167 params := url.Values{
169 "page": {strconv.Itoa(pn)},
172 endpoint := &url.URL{
173 Scheme: p.ValidateURL.Scheme,
174 Host: p.ValidateURL.Host,
175 Path: path.Join(p.ValidateURL.Path, "/user/teams"),
176 RawQuery: params.Encode(),
179 // bodyclose cannot detect that the body is being closed later in requests.Into,
180 // so have to skip the linting for the next line.
182 result := requests.New(endpoint.String()).
184 WithHeaders(getGitHubHeader(accessToken)).
186 if result.Error() != nil {
187 return false, result.Error()
191 // link header may not be obtained
192 // When paging is not required and all data can be retrieved with a single call
194 // Conditions for obtaining the link header.
195 // 1. When paging is required (Example: When the data size is 100 and the page size is 99 or less)
196 // 2. When it exceeds the paging frame (Example: When there is only 10 records but the second page is called with a page size of 100)
198 // link header at not last page
199 // <https://api.github.com/user/teams?page=1&per_page=100>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=100>; rel="last", <https://api.github.com/user/teams?page=1&per_page=100>; rel="first"
200 // link header at last page (doesn't exist last info)
201 // <https://api.github.com/user/teams?page=3&per_page=10>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=10>; rel="first"
203 link := result.Headers().Get("Link")
204 rep1 := regexp.MustCompile(`(?s).*\<https://api.github.com/user/teams\?page=(.)&per_page=[0-9]+\>; rel="last".*`)
205 i, converr := strconv.Atoi(rep1.ReplaceAllString(link, "$1"))
207 // If the last page cannot be taken from the link in the http header, the last variable remains zero
214 if err := result.UnmarshalInto(&tp); err != nil {
221 teams = append(teams, tp...)
234 presentOrgs := make(map[string]bool)
235 var presentTeams []string
236 for _, team := range teams {
237 presentOrgs[team.Org.Login] = true
238 if p.Org == team.Org.Login {
240 ts := strings.Split(p.Team, ",")
241 for _, t := range ts {
243 logger.Printf("Found Github Organization:%q Team:%q (Name:%q)", team.Org.Login, team.Slug, team.Name)
247 presentTeams = append(presentTeams, team.Slug)
251 logger.Printf("Missing Team:%q from Org:%q in teams: %v", p.Team, p.Org, presentTeams)
254 for org := range presentOrgs {
255 allOrgs = append(allOrgs, org)
257 logger.Printf("Missing Organization:%q in %#v", p.Org, allOrgs)
262func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool, error) {
263 // https://developer.github.com/v3/repos/#get-a-repository
265 type permissions struct {
266 Pull bool `json:"pull"`
267 Push bool `json:"push"`
270 type repository struct {
271 Permissions permissions `json:"permissions"`
272 Private bool `json:"private"`
275 endpoint := &url.URL{
276 Scheme: p.ValidateURL.Scheme,
277 Host: p.ValidateURL.Host,
278 Path: path.Join(p.ValidateURL.Path, "/repo/", p.Repo),
282 err := requests.New(endpoint.String()).
284 WithHeaders(getGitHubHeader(accessToken)).
291 // Every user can implicitly pull from a public repo, so only grant access
292 // if they have push access or the repo is private and they can pull
293 return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil
296func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool, error) {
297 // https://developer.github.com/v3/users/#get-the-authenticated-user
300 Login string `json:"login"`
301 Email string `json:"email"`
304 endpoint := &url.URL{
305 Scheme: p.ValidateURL.Scheme,
306 Host: p.ValidateURL.Host,
307 Path: path.Join(p.ValidateURL.Path, "/user"),
310 err := requests.New(endpoint.String()).
312 WithHeaders(getGitHubHeader(accessToken)).
319 if p.isVerifiedUser(user.Login) {
325func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
326 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
328 endpoint := &url.URL{
329 Scheme: p.ValidateURL.Scheme,
330 Host: p.ValidateURL.Host,
331 Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "/collaborators/", username),
333 result := requests.New(endpoint.String()).
335 WithHeaders(getGitHubHeader(accessToken)).
337 if result.Error() != nil {
338 return false, result.Error()
341 if result.StatusCode() != 204 {
342 return false, fmt.Errorf("got %d from %q %s",
343 result.StatusCode(), endpoint.String(), result.Body())
346 logger.Printf("got %d from %q %s", result.StatusCode(), endpoint.String(), result.Body())
351// GetEmailAddress returns the Account email address
352func (p *GitHubProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
354 var emails []struct {
355 Email string `json:"email"`
356 Primary bool `json:"primary"`
357 Verified bool `json:"verified"`
360 // If usernames are set, check that first
361 verifiedUser := false
362 if len(p.Users) > 0 {
364 verifiedUser, err = p.hasUser(ctx, s.AccessToken)
368 // org and repository options are not configured
369 if !verifiedUser && p.Org == "" && p.Repo == "" {
370 return "", errors.New("missing github user")
373 // If a user is verified by username options, skip the following restrictions
377 if ok, err := p.hasOrgAndTeam(ctx, s.AccessToken); err != nil || !ok {
381 if ok, err := p.hasOrg(ctx, s.AccessToken); err != nil || !ok {
385 } else if p.Repo != "" && p.Token == "" { // If we have a token we'll do the collaborator check in GetUserName
386 if ok, err := p.hasRepo(ctx, s.AccessToken); err != nil || !ok {
392 endpoint := &url.URL{
393 Scheme: p.ValidateURL.Scheme,
394 Host: p.ValidateURL.Host,
395 Path: path.Join(p.ValidateURL.Path, "/user/emails"),
397 err := requests.New(endpoint.String()).
399 WithHeaders(getGitHubHeader(s.AccessToken)).
401 UnmarshalInto(&emails)
407 for _, email := range emails {
409 returnEmail = email.Email
411 return returnEmail, nil
416 return returnEmail, nil
419// GetUserName returns the Account user name
420func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) {
422 Login string `json:"login"`
423 Email string `json:"email"`
426 endpoint := &url.URL{
427 Scheme: p.ValidateURL.Scheme,
428 Host: p.ValidateURL.Host,
429 Path: path.Join(p.ValidateURL.Path, "/user"),
432 err := requests.New(endpoint.String()).
434 WithHeaders(getGitHubHeader(s.AccessToken)).
441 // Now that we have the username we can check collaborator status
442 if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
443 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
448 return user.Login, nil
451// ValidateSessionState validates the AccessToken
452func (p *GitHubProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
453 return validateToken(ctx, p, s.AccessToken, getGitHubHeader(s.AccessToken))
457func (p *GitHubProvider) isVerifiedUser(username string) bool {
458 for _, u := range p.Users {
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list