15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
21// GitHubProvider represents an GitHub based Identity Provider
22type GitHubProvider struct {
31var _ Provider = (*GitHubProvider)(nil)
34 githubProviderName = "GitHub"
35 githubDefaultScope = "user:email read:org"
36 orgTeamSeparator = ":"
40 // Default Login URL for GitHub.
41 // Pre-parsed URL of https://github.org/login/oauth/authorize.
42 githubDefaultLoginURL = &url.URL{
45 Path: "/login/oauth/authorize",
48 // Default Redeem URL for GitHub.
49 // Pre-parsed URL of https://github.org/login/oauth/access_token.
50 githubDefaultRedeemURL = &url.URL{
53 Path: "/login/oauth/access_token",
56 // Default Validation URL for GitHub.
57 // ValidationURL is the API Base URL.
58 // Other API requests are based off of this (eg to fetch users/groups).
59 // Pre-parsed URL of https://api.github.com/.
60 githubDefaultValidateURL = &url.URL{
62 Host: "api.github.com",
67// NewGitHubProvider initiates a new GitHubProvider
68func NewGitHubProvider(p *ProviderData, opts options.GitHubOptions) *GitHubProvider {
69 p.setProviderDefaults(providerDefaults{
70 name: githubProviderName,
71 loginURL: githubDefaultLoginURL,
72 redeemURL: githubDefaultRedeemURL,
74 validateURL: githubDefaultValidateURL,
75 scope: githubDefaultScope,
78 provider := &GitHubProvider{ProviderData: p}
80 provider.setOrgTeam(opts.Org, opts.Team)
81 provider.setRepo(opts.Repo, opts.Token)
82 provider.setUsers(opts.Users)
86func makeGitHubHeader(accessToken string) http.Header {
87 // extra headers required by the GitHub API when making authenticated requests
88 extraHeaders := map[string]string{
89 acceptHeader: "application/vnd.github.v3+json",
91 return makeAuthorizationHeader(tokenTypeToken, accessToken, extraHeaders)
94func (p *GitHubProvider) makeGitHubAPIEndpoint(endpoint string, params *url.Values) *url.URL {
95 basePath := p.ValidateURL.Path
97 re := regexp.MustCompile(`^/api/v\d+`)
98 match := re.FindString(p.ValidateURL.Path)
104 params = &url.Values{}
108 Scheme: p.ValidateURL.Scheme,
109 Host: p.ValidateURL.Host,
110 Path: path.Join(basePath, endpoint),
111 RawQuery: params.Encode(),
115// setOrgTeam adds GitHub org reading parameters to the OAuth2 scope
116func (p *GitHubProvider) setOrgTeam(org, team string) {
121// setRepo configures the target repository and optional token to use
122func (p *GitHubProvider) setRepo(repo, token string) {
127// setUsers configures allowed usernames
128func (p *GitHubProvider) setUsers(users []string) {
132// EnrichSession updates the User & Email after the initial Redeem
133func (p *GitHubProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
134 // Construct user info JSON from multiple GitHub API endpoints to have a more detailed session state
135 if err := p.getOrgAndTeam(ctx, s); err != nil {
139 if err := p.checkRestrictions(ctx, s); err != nil {
143 if err := p.getEmail(ctx, s); err != nil {
147 return p.getUser(ctx, s)
150// ValidateSession validates the AccessToken
151func (p *GitHubProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
152 return validateToken(ctx, p, s.AccessToken, makeGitHubHeader(s.AccessToken))
155func (p *GitHubProvider) hasOrg(s *sessions.SessionState) error {
156 // https://developer.github.com/v3/orgs/#list-your-organizations
159 for _, group := range s.Groups {
160 if !strings.Contains(group, ":") {
161 orgs = append(orgs, group)
165 presentOrgs := make([]string, 0, len(orgs))
166 for _, org := range orgs {
168 logger.Printf("Found Github Organization:%q", org)
171 presentOrgs = append(presentOrgs, org)
174 logger.Printf("Missing Organization:%q in %v", p.Org, presentOrgs)
175 return errors.New("user is missing required organization")
178func (p *GitHubProvider) hasOrgAndTeam(s *sessions.SessionState) error {
179 type orgTeam struct {
180 Org string `json:"org"`
181 Team string `json:"team"`
184 var presentOrgTeams []orgTeam
186 for _, group := range s.Groups {
187 if strings.Contains(group, orgTeamSeparator) {
188 ot := strings.Split(group, orgTeamSeparator)
189 presentOrgTeams = append(presentOrgTeams, orgTeam{ot[0], ot[1]})
195 presentOrgs := make(map[string]bool)
196 var presentTeams []string
198 for _, ot := range presentOrgTeams {
199 presentOrgs[ot.Org] = true
201 if strings.EqualFold(p.Org, ot.Org) {
204 teams := strings.Split(p.Team, ",")
205 for _, team := range teams {
206 if strings.EqualFold(strings.TrimSpace(team), ot.Team) {
207 logger.Printf("Found Github Organization/Team:%q/%q", ot.Org, ot.Team)
211 presentTeams = append(presentTeams, ot.Team)
216 logger.Printf("Missing Team:%q from Org:%q in teams: %v", p.Team, p.Org, presentTeams)
217 return errors.New("user is missing required team")
220 logger.Printf("Missing Organization:%q in %#v", p.Org, maps.Keys(presentOrgs))
221 return errors.New("user is missing required organization")
224func (p *GitHubProvider) hasTeam(s *sessions.SessionState) error {
227 for _, group := range s.Groups {
228 if strings.Contains(group, orgTeamSeparator) {
229 teams = append(teams, strings.TrimSpace(group))
233 var presentTeams = make([]string, 0, len(teams))
235 for _, ot := range teams {
236 allowedTeams := strings.Split(p.Team, ",")
237 for _, team := range allowedTeams {
238 if !strings.Contains(team, orgTeamSeparator) {
239 logger.Printf("Please use fully qualified team names (org:team-slug) if you omit the organisation. Current Team name: %s", team)
240 return errors.New("team name is invalid")
243 if strings.EqualFold(strings.TrimSpace(team), ot) {
244 logger.Printf("Found Github Organization/Team:%s", ot)
248 presentTeams = append(presentTeams, ot)
251 logger.Printf("Missing Team:%q in teams: %v", p.Team, presentTeams)
252 return errors.New("user is missing required team")
255func (p *GitHubProvider) hasRepoAccess(ctx context.Context, accessToken string) error {
256 // https://developer.github.com/v3/repos/#get-a-repository
258 type permissions struct {
259 Pull bool `json:"pull"`
260 Push bool `json:"push"`
263 type repository struct {
264 Permissions permissions `json:"permissions"`
265 Private bool `json:"private"`
268 endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo, nil)
271 err := requests.New(endpoint.String()).
273 WithHeaders(makeGitHubHeader(accessToken)).
281 // Every user can implicitly pull from a public repo, so only grant access
282 // if they have push access or the repo is private and they can pull
283 if repo.Permissions.Push || (repo.Private && repo.Permissions.Pull) {
287 return errors.New("user doesn't have repository access")
290func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool, error) {
291 // https://developer.github.com/v3/users/#get-the-authenticated-user
294 Login string `json:"login"`
295 Email string `json:"email"`
298 endpoint := p.makeGitHubAPIEndpoint("/user", nil)
300 err := requests.New(endpoint.String()).
302 WithHeaders(makeGitHubHeader(accessToken)).
309 if p.isVerifiedUser(user.Login) {
315func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
316 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
318 endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo+"/collaborators/"+username, nil)
319 result := requests.New(endpoint.String()).
321 WithHeaders(makeGitHubHeader(accessToken)).
323 if result.Error() != nil {
324 return false, result.Error()
327 if result.StatusCode() != 204 {
328 return false, fmt.Errorf("got %d from %q %s",
329 result.StatusCode(), endpoint.String(), result.Body())
332 logger.Printf("got %d from %q %s", result.StatusCode(), endpoint.String(), result.Body())
337// getEmail updates the SessionState Email
338func (p *GitHubProvider) getEmail(ctx context.Context, s *sessions.SessionState) error {
340 var emails []struct {
341 Email string `json:"email"`
342 Primary bool `json:"primary"`
343 Verified bool `json:"verified"`
346 endpoint := p.makeGitHubAPIEndpoint("/user/emails", nil)
348 err := requests.New(endpoint.String()).
350 WithHeaders(makeGitHubHeader(s.AccessToken)).
352 UnmarshalInto(&emails)
357 for _, email := range emails {
360 s.Email = email.Email
369// getUser updates the SessionState User
370func (p *GitHubProvider) getUser(ctx context.Context, s *sessions.SessionState) error {
372 Login string `json:"login"`
373 Email string `json:"email"`
376 endpoint := p.makeGitHubAPIEndpoint("/user", nil)
378 err := requests.New(endpoint.String()).
380 WithHeaders(makeGitHubHeader(s.AccessToken)).
387 // Now that we have the username we can check collaborator status
388 if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
389 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
398func (p *GitHubProvider) isVerifiedUser(username string) bool {
399 for _, u := range p.Users {
407func (p *GitHubProvider) checkRestrictions(ctx context.Context, s *sessions.SessionState) error {
408 // If a user is verified by username options, skip the following restrictions
409 if ok, err := p.checkUserRestriction(ctx, s); err != nil || ok {
415 case p.Org != "" && p.Team != "":
416 err = p.hasOrgAndTeam(s)
427 if p.Org == "" && p.Repo != "" && p.Token == "" {
428 // If we have a token we'll do the collaborator check
429 return p.hasRepoAccess(ctx, s.AccessToken)
435func (p *GitHubProvider) checkUserRestriction(ctx context.Context, s *sessions.SessionState) (bool, error) {
436 if len(p.Users) == 0 {
440 verifiedUser, err := p.hasUser(ctx, s.AccessToken)
442 return verifiedUser, err
445 // org and repository options are not configured
446 if !verifiedUser && p.Org == "" && p.Repo == "" {
447 return false, errors.New("missing github user")
450 return verifiedUser, nil
453func (p *GitHubProvider) getOrgAndTeam(ctx context.Context, s *sessions.SessionState) error {
454 err := p.getOrgs(ctx, s)
459 return p.getTeams(ctx, s)
462func (p *GitHubProvider) getOrgs(ctx context.Context, s *sessions.SessionState) error {
464 type Organization struct {
465 // Support for Github organizations
466 // https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-the-authenticated-user
467 Login string `json:"login,omitempty"`
468 // Support for Gitea organizations
469 // https://docs.gitea.com/api/1.24/#tag/organization/operation/orgGetAll
470 Name string `json:"name,omitempty"`
475 params := url.Values{
477 "page": {strconv.Itoa(pn)},
480 endpoint := p.makeGitHubAPIEndpoint("/user/orgs", ¶ms)
482 var orgs []Organization
483 err := requests.New(endpoint.String()).
485 WithHeaders(makeGitHubHeader(s.AccessToken)).
496 for _, org := range orgs {
498 if len(org.Login) > 0 {
500 logger.Printf("Member of Github Organization: %q", orgName)
503 logger.Printf("Member of Gitea Organization: %q", orgName)
506 s.Groups = append(s.Groups, orgName)
514func (p *GitHubProvider) getTeams(ctx context.Context, s *sessions.SessionState) error {
515 // https://docs.github.com/en/rest/teams/teams?#list-user-teams
517 Name string `json:"name"`
518 Slug string `json:"slug"`
520 Login string `json:"login"`
521 Name string `json:"name"`
522 } `json:"organization"`
527 params := url.Values{
529 "page": {strconv.Itoa(pn)},
532 endpoint := p.makeGitHubAPIEndpoint("/user/teams", ¶ms)
535 err := requests.New(endpoint.String()).
537 WithHeaders(makeGitHubHeader(s.AccessToken)).
539 UnmarshalInto(&teams)
548 for _, team := range teams {
549 var orgName, teamName string
551 if len(team.Org.Login) > 0 {
552 orgName = team.Org.Login
554 logger.Printf("Member of Github Organization/Team: %q/%q", orgName, teamName)
556 orgName = team.Org.Name
558 logger.Printf("Member of Gitea Organization/Team: %q/%q", orgName, teamName)
561 s.Groups = append(s.Groups, fmt.Sprintf("%s%s%s", orgName, orgTeamSeparator, teamName))
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