14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
18 "golang.org/x/exp/maps"
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) {
203 teams := strings.Split(p.Team, ",")
204 for _, team := range teams {
205 if strings.EqualFold(strings.TrimSpace(team), ot.Team) {
206 logger.Printf("Found Github Organization/Team:%q/%q", ot.Org, ot.Team)
210 presentTeams = append(presentTeams, ot.Team)
215 logger.Printf("Missing Team:%q from Org:%q in teams: %v", p.Team, p.Org, presentTeams)
216 return errors.New("user is missing required team")
219 logger.Printf("Missing Organization:%q in %#v", p.Org, maps.Keys(presentOrgs))
220 return errors.New("user is missing required organization")
223func (p *GitHubProvider) hasRepoAccess(ctx context.Context, accessToken string) error {
224 // https://developer.github.com/v3/repos/#get-a-repository
226 type permissions struct {
227 Pull bool `json:"pull"`
228 Push bool `json:"push"`
231 type repository struct {
232 Permissions permissions `json:"permissions"`
233 Private bool `json:"private"`
236 endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo, nil)
239 err := requests.New(endpoint.String()).
241 WithHeaders(makeGitHubHeader(accessToken)).
249 // Every user can implicitly pull from a public repo, so only grant access
250 // if they have push access or the repo is private and they can pull
251 if repo.Permissions.Push || (repo.Private && repo.Permissions.Pull) {
255 return errors.New("user doesn't have repository access")
258func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool, error) {
259 // https://developer.github.com/v3/users/#get-the-authenticated-user
262 Login string `json:"login"`
263 Email string `json:"email"`
266 endpoint := p.makeGitHubAPIEndpoint("/user", nil)
268 err := requests.New(endpoint.String()).
270 WithHeaders(makeGitHubHeader(accessToken)).
277 if p.isVerifiedUser(user.Login) {
283func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
284 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
286 endpoint := p.makeGitHubAPIEndpoint("/repos/"+p.Repo+"/collaborators/"+username, nil)
287 result := requests.New(endpoint.String()).
289 WithHeaders(makeGitHubHeader(accessToken)).
291 if result.Error() != nil {
292 return false, result.Error()
295 if result.StatusCode() != 204 {
296 return false, fmt.Errorf("got %d from %q %s",
297 result.StatusCode(), endpoint.String(), result.Body())
300 logger.Printf("got %d from %q %s", result.StatusCode(), endpoint.String(), result.Body())
305// getEmail updates the SessionState Email
306func (p *GitHubProvider) getEmail(ctx context.Context, s *sessions.SessionState) error {
308 var emails []struct {
309 Email string `json:"email"`
310 Primary bool `json:"primary"`
311 Verified bool `json:"verified"`
314 endpoint := p.makeGitHubAPIEndpoint("/user/emails", nil)
316 err := requests.New(endpoint.String()).
318 WithHeaders(makeGitHubHeader(s.AccessToken)).
320 UnmarshalInto(&emails)
325 for _, email := range emails {
328 s.Email = email.Email
337// getUser updates the SessionState User
338func (p *GitHubProvider) getUser(ctx context.Context, s *sessions.SessionState) error {
340 Login string `json:"login"`
341 Email string `json:"email"`
344 endpoint := p.makeGitHubAPIEndpoint("/user", nil)
346 err := requests.New(endpoint.String()).
348 WithHeaders(makeGitHubHeader(s.AccessToken)).
355 // Now that we have the username we can check collaborator status
356 if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
357 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
366func (p *GitHubProvider) isVerifiedUser(username string) bool {
367 for _, u := range p.Users {
375func (p *GitHubProvider) checkRestrictions(ctx context.Context, s *sessions.SessionState) error {
376 // If a user is verified by username options, skip the following restrictions
377 if ok, err := p.checkUserRestriction(ctx, s); err != nil || ok {
381 if err := p.hasOrgAndTeamAccess(s); err != nil {
385 if p.Org == "" && p.Repo != "" && p.Token == "" {
386 // If we have a token we'll do the collaborator check in GetUserName
387 return p.hasRepoAccess(ctx, s.AccessToken)
393func (p *GitHubProvider) checkUserRestriction(ctx context.Context, s *sessions.SessionState) (bool, error) {
394 if len(p.Users) == 0 {
398 verifiedUser, err := p.hasUser(ctx, s.AccessToken)
400 return verifiedUser, err
403 // org and repository options are not configured
404 if !verifiedUser && p.Org == "" && p.Repo == "" {
405 return false, errors.New("missing github user")
408 return verifiedUser, nil
411func (p *GitHubProvider) hasOrgAndTeamAccess(s *sessions.SessionState) error {
412 if p.Org != "" && p.Team != "" {
413 return p.hasOrgAndTeam(s)
423func (p *GitHubProvider) getOrgAndTeam(ctx context.Context, s *sessions.SessionState) error {
424 err := p.getOrgs(ctx, s)
429 return p.getTeams(ctx, s)
432func (p *GitHubProvider) getOrgs(ctx context.Context, s *sessions.SessionState) error {
433 // https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-the-authenticated-user
435 type Organization struct {
436 Login string `json:"login"`
441 params := url.Values{
443 "page": {strconv.Itoa(pn)},
446 endpoint := p.makeGitHubAPIEndpoint("/user/orgs", ¶ms)
448 var orgs []Organization
449 err := requests.New(endpoint.String()).
451 WithHeaders(makeGitHubHeader(s.AccessToken)).
462 for _, org := range orgs {
463 logger.Printf("Member of Github Organization:%q", org.Login)
464 s.Groups = append(s.Groups, org.Login)
472func (p *GitHubProvider) getTeams(ctx context.Context, s *sessions.SessionState) error {
473 // https://docs.github.com/en/rest/teams/teams?#list-user-teams
475 Name string `json:"name"`
476 Slug string `json:"slug"`
478 Login string `json:"login"`
479 } `json:"organization"`
484 params := url.Values{
486 "page": {strconv.Itoa(pn)},
489 endpoint := p.makeGitHubAPIEndpoint("/user/teams", ¶ms)
492 err := requests.New(endpoint.String()).
494 WithHeaders(makeGitHubHeader(s.AccessToken)).
496 UnmarshalInto(&teams)
505 for _, team := range teams {
506 logger.Printf("Member of Github Organization/Team:%q/%q", team.Org.Login, team.Slug)
507 s.Groups = append(s.Groups, team.Org.Login+orgTeamSeparator+team.Slug)
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