git.iko.soy/typeable/oauth2-proxy
on master help sign in
Support new option "github-user" (#421)

* feat(github): support new option "github-user" * feat(github): rename github-user to github-users * feat(github): update docs for github-users option * feat(github): remove unneeded code * feat(github): remove logging * feat(github-user): use github-user as flagset options * feat(github-user): remove optionns.go * feat(github-user): add github-user flagset * feat(github): improve readability in the docs * feat(github-user): refactored SetUsers method * Update flag description Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commitd8d43bb51b7cc5c29966f43324a10fe286274629 authorYoshiki Nakagawa <yyoshiki41@gmail.com> dateTue 2 Jun 2020, 04:02 (6 years ago) parenta17c488
8 files changed+171−13
CHANGELOG.md+1 contrib/oauth2-proxy_autocomplete.sh+1−1 docs/2_auth.md+6 docs/configuration/configuration.md+1 pkg/apis/options/options.go+2 pkg/validation/options.go+1 providers/github.go+84−12 providers/github_test.go+75
CHANGELOG.md+1
@@ -99,6 +99,7 @@
9999- [#494](https://github.com/oauth2-proxy/oauth2-proxy/pull/494) Upstream websockets TLS certificate validation now depends on ssl-upstream-insecure-skip-verify (@yaroslavros)
100100- [#497](https://github.com/oauth2-proxy/oauth2-proxy/pull/497) Restrict access using Github collaborators (@jsclayton)
101101- [#414](https://github.com/oauth2-proxy/oauth2-proxy/pull/414) Always encrypt sessions regardless of config (@ti-mo)
102+- [#421](https://github.com/oauth2-proxy/oauth2-proxy/pull/421) Allow logins by usernames even if they do not belong to the specified org and team or collaborators (@yyoshiki41)
102103
103104# v5.1.1
104105
contrib/oauth2-proxy_autocomplete.sh+1−1
@@ -24,7 +24,7 @@ _oauth2_proxy() {
2424 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
2525 return 0
2626 ;;
27− --@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url))
27+ --@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url))
2828 return 0
2929 ;;
3030 esac
docs/2_auth.md+6
@@ -103,6 +103,8 @@ Note: When using the Azure Auth provider with nginx and the cookie session store
103103
104104The GitHub auth provider supports two additional ways to restrict authentication to either organization and optional team level access, or to collaborators of a repository. Restricting by these options is normally accompanied with `--email-domain=*`
105105
106+NOTE: When `--github-user` is set, the specified users are allowed to login even if they do not belong to the specified org and team or collaborators.
107+
106108To restrict by organization only, include the following flag:
107109
108110 -github-org="": restrict logins to members of this organisation
@@ -119,6 +121,10 @@ If you'd like to allow access to users with **read only** access to a **public**
119121
120122 -github-token="": the token to use when verifying repository collaborators
121123
124+To allow a user to login with their username even if they do not belong to the specified org and team or collaborators, separated by a comma
125+
126+ -github-user="": allow logins by username, separated by a comma
127+
122128If you are using GitHub enterprise, make sure you set the following to the appropriate url:
123129
124130 -login-url="http(s)://<enterprise github host>/login/oauth/authorize"
docs/configuration/configuration.md+1
@@ -56,6 +56,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
5656| `--github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
5757| `--github-repo` | string | restrict logins to collaborators of this repository formatted as `orgname/repo` | |
5858| `--github-token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | |
59+| `--github-user` | string \| list | To allow users to login by username even if they do not belong to the specified org and team or collaborators | |
5960| `--gitlab-group` | string | restrict logins to members of any of these groups (slug), separated by a comma | |
6061| `--google-admin-email` | string | the google admin to impersonate for api calls | |
6162| `--google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
pkg/apis/options/options.go+2
@@ -48,6 +48,7 @@ type Options struct {
4848 GitHubTeam string `flag:"github-team" cfg:"github_team"`
4949 GitHubRepo string `flag:"github-repo" cfg:"github_repo"`
5050 GitHubToken string `flag:"github-token" cfg:"github_token"`
51+ GitHubUsers []string `flag:"github-user" cfg:"github_users"`
5152 GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group"`
5253 GoogleGroups []string `flag:"google-group" cfg:"google_group"`
5354 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
@@ -228,6 +229,7 @@ func NewFlagSet() *pflag.FlagSet {
228229 flagSet.String("github-team", "", "restrict logins to members of this team")
229230 flagSet.String("github-repo", "", "restrict logins to collaborators of this repository")
230231 flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
232+ flagSet.StringSlice("github-user", []string{}, "allow users with these usernames to login even if they do not belong to the specified org and team or collaborators (may be given multiple times)")
231233 flagSet.String("gitlab-group", "", "restrict logins to members of this group")
232234 flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
233235 flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
pkg/validation/options.go+1
@@ -309,6 +309,7 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
309309 case *providers.GitHubProvider:
310310 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
311311 p.SetRepo(o.GitHubRepo, o.GitHubToken)
312+ p.SetUsers(o.GitHubUsers)
312313 case *providers.KeycloakProvider:
313314 p.SetGroup(o.KeycloakGroup)
314315 case *providers.GoogleProvider:
providers/github.go+84−12
@@ -3,6 +3,7 @@ package providers
33import (
44 "context"
55 "encoding/json"
6+ "errors"
67 "fmt"
78 "io/ioutil"
89 "net/http"
@@ -23,6 +24,7 @@ type GitHubProvider struct {
2324 Team string
2425 Repo string
2526 Token string
27+ Users []string
2628}
2729
2830var _ Provider = (*GitHubProvider)(nil)
@@ -80,6 +82,11 @@ func (p *GitHubProvider) SetRepo(repo, token string) {
8082 p.Token = token
8183}
8284
85+// SetUsers configures allowed usernames
86+func (p *GitHubProvider) SetUsers(users []string) {
87+ p.Users = users
88+}
89+
8390func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) {
8491 // https://developer.github.com/v3/orgs/#list-your-organizations
8592
@@ -317,6 +324,46 @@ func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool,
317324 return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil
318325}
319326
327+func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool, error) {
328+ // https://developer.github.com/v3/users/#get-the-authenticated-user
329+
330+ var user struct {
331+ Login string `json:"login"`
332+ Email string `json:"email"`
333+ }
334+
335+ endpoint := &url.URL{
336+ Scheme: p.ValidateURL.Scheme,
337+ Host: p.ValidateURL.Host,
338+ Path: path.Join(p.ValidateURL.Path, "/user"),
339+ }
340+ req, _ := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
341+ req.Header = getGitHubHeader(accessToken)
342+ resp, err := http.DefaultClient.Do(req)
343+ if err != nil {
344+ return false, err
345+ }
346+ defer resp.Body.Close()
347+
348+ body, err := ioutil.ReadAll(resp.Body)
349+ if err != nil {
350+ return false, err
351+ }
352+ if resp.StatusCode != 200 {
353+ return false, fmt.Errorf("got %d from %q %s",
354+ resp.StatusCode, stripToken(endpoint.String()), body)
355+ }
356+
357+ if err := json.Unmarshal(body, &user); err != nil {
358+ return false, err
359+ }
360+
361+ if p.isVerifiedUser(user.Login) {
362+ return true, nil
363+ }
364+ return false, nil
365+}
366+
320367func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
321368 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
322369
@@ -356,21 +403,36 @@ func (p *GitHubProvider) GetEmailAddress(ctx context.Context, s *sessions.Sessio
356403 Verified bool `json:"verified"`
357404 }
358405
359− // if we require an Org or Team, check that first
360− if p.Org != "" {
361− if p.Team != "" {
362− if ok, err := p.hasOrgAndTeam(ctx, s.AccessToken); err != nil || !ok {
363− return "", err
406+ // If usernames are set, check that first
407+ verifiedUser := false
408+ if len(p.Users) > 0 {
409+ var err error
410+ verifiedUser, err = p.hasUser(ctx, s.AccessToken)
411+ if err != nil {
412+ return "", err
413+ }
414+ // org and repository options are not configured
415+ if !verifiedUser && p.Org == "" && p.Repo == "" {
416+ return "", errors.New("missing github user")
417+ }
418+ }
419+ // If a user is verified by username options, skip the following restrictions
420+ if !verifiedUser {
421+ if p.Org != "" {
422+ if p.Team != "" {
423+ if ok, err := p.hasOrgAndTeam(ctx, s.AccessToken); err != nil || !ok {
424+ return "", err
425+ }
426+ } else {
427+ if ok, err := p.hasOrg(ctx, s.AccessToken); err != nil || !ok {
428+ return "", err
429+ }
364430 }
365− } else {
366− if ok, err := p.hasOrg(ctx, s.AccessToken); err != nil || !ok {
431+ } else if p.Repo != "" && p.Token == "" { // If we have a token we'll do the collaborator check in GetUserName
432+ if ok, err := p.hasRepo(ctx, s.AccessToken); err != nil || !ok {
367433 return "", err
368434 }
369435 }
370− } else if p.Repo != "" && p.Token == "" { // If we have a token we'll do the collaborator check in GetUserName
371− if ok, err := p.hasRepo(ctx, s.AccessToken); err != nil || !ok {
372− return "", err
373− }
374436 }
375437
376438 endpoint := &url.URL{
@@ -456,7 +518,7 @@ func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionSta
456518 }
457519
458520 // Now that we have the username we can check collaborator status
459− if p.Org == "" && p.Repo != "" && p.Token != "" {
521+ if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
460522 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
461523 return "", err
462524 }
@@ -469,3 +531,13 @@ func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionSta
469531func (p *GitHubProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
470532 return validateToken(ctx, p, s.AccessToken, getGitHubHeader(s.AccessToken))
471533}
534+
535+// isVerifiedUser
536+func (p *GitHubProvider) isVerifiedUser(username string) bool {
537+ for _, u := range p.Users {
538+ if username == u {
539+ return true
540+ }
541+ }
542+ return false
543+}
providers/github_test.go+75
@@ -318,3 +318,78 @@ func TestGitHubProviderGetUserNameWithRepoAndTokenWithoutPushAccess(t *testing.T
318318 assert.NotEqual(t, nil, err)
319319 assert.Equal(t, "", email)
320320}
321+
322+func TestGitHubProviderGetEmailAddressWithUsername(t *testing.T) {
323+ b := testGitHubBackend(map[string][]string{
324+ "/user": {`{"email": "michael.bland@gsa.gov", "login": "mbland"}`},
325+ "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`},
326+ })
327+ defer b.Close()
328+
329+ bURL, _ := url.Parse(b.URL)
330+ p := testGitHubProvider(bURL.Host)
331+ p.SetUsers([]string{"mbland", "octocat"})
332+
333+ session := CreateAuthorizedSession()
334+ email, err := p.GetEmailAddress(context.Background(), session)
335+ assert.Equal(t, nil, err)
336+ assert.Equal(t, "michael.bland@gsa.gov", email)
337+}
338+
339+func TestGitHubProviderGetEmailAddressWithNotAllowedUsername(t *testing.T) {
340+ b := testGitHubBackend(map[string][]string{
341+ "/user": {`{"email": "michael.bland@gsa.gov", "login": "mbland"}`},
342+ "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`},
343+ })
344+ defer b.Close()
345+
346+ bURL, _ := url.Parse(b.URL)
347+ p := testGitHubProvider(bURL.Host)
348+ p.SetUsers([]string{"octocat"})
349+
350+ session := CreateAuthorizedSession()
351+ email, err := p.GetEmailAddress(context.Background(), session)
352+ assert.NotEqual(t, nil, err)
353+ assert.Equal(t, "", email)
354+}
355+
356+func TestGitHubProviderGetEmailAddressWithUsernameAndNotBelongToOrg(t *testing.T) {
357+ b := testGitHubBackend(map[string][]string{
358+ "/user": {`{"email": "michael.bland@gsa.gov", "login": "mbland"}`},
359+ "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`},
360+ "/user/orgs": {
361+ `[ {"login":"testorg"} ]`,
362+ `[ ]`,
363+ },
364+ })
365+ defer b.Close()
366+
367+ bURL, _ := url.Parse(b.URL)
368+ p := testGitHubProvider(bURL.Host)
369+ p.SetOrgTeam("not_belong_to", "")
370+ p.SetUsers([]string{"mbland"})
371+
372+ session := CreateAuthorizedSession()
373+ email, err := p.GetEmailAddress(context.Background(), session)
374+ assert.Equal(t, nil, err)
375+ assert.Equal(t, "michael.bland@gsa.gov", email)
376+}
377+
378+func TestGitHubProviderGetEmailAddressWithUsernameAndNoAccessToPrivateRepo(t *testing.T) {
379+ b := testGitHubBackend(map[string][]string{
380+ "/user": {`{"email": "michael.bland@gsa.gov", "login": "mbland"}`},
381+ "/user/emails": {`[ {"email": "michael.bland@gsa.gov", "verified": true, "primary": true} ]`},
382+ "/repo/oauth2-proxy/oauth2-proxy": {},
383+ })
384+ defer b.Close()
385+
386+ bURL, _ := url.Parse(b.URL)
387+ p := testGitHubProvider(bURL.Host)
388+ p.SetRepo("oauth2-proxy/oauth2-proxy", "")
389+ p.SetUsers([]string{"mbland"})
390+
391+ session := CreateAuthorizedSession()
392+ email, err := p.GetEmailAddress(context.Background(), session)
393+ assert.Equal(t, nil, err)
394+ assert.Equal(t, "michael.bland@gsa.gov", email)
395+}