git.iko.soy/typeable/oauth2-proxy
on master help sign in
Support new option "github-user" (#421)
d8d43bbYoshiki Nakagawa
CHANGELOG.md+1
@@ -99,6 +99,7 @@
99- [#494](https://github.com/oauth2-proxy/oauth2-proxy/pull/494) Upstream websockets TLS certificate validation now depends on ssl-upstream-insecure-skip-verify (@yaroslavros)
99- [#494](https://github.com/oauth2-proxy/oauth2-proxy/pull/494) Upstream websockets TLS certificate validation now depends on ssl-upstream-insecure-skip-verify (@yaroslavros)
100- [#497](https://github.com/oauth2-proxy/oauth2-proxy/pull/497) Restrict access using Github collaborators (@jsclayton)
100- [#497](https://github.com/oauth2-proxy/oauth2-proxy/pull/497) Restrict access using Github collaborators (@jsclayton)
101- [#414](https://github.com/oauth2-proxy/oauth2-proxy/pull/414) Always encrypt sessions regardless of config (@ti-mo)
101- [#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)
102
103
103# v5.1.1
104# v5.1.1
104
105
contrib/oauth2-proxy_autocomplete.sh+1−1
@@ -24,7 +24,7 @@ _oauth2_proxy() {
24 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
24 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
25 return 0
25 return 0
26 ;;
26 ;;
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))
28 return 0
28 return 0
29 ;;
29 ;;
30 esac
30 esac
docs/2_auth.md+6
@@ -103,6 +103,8 @@ Note: When using the Azure Auth provider with nginx and the cookie session store
103
103
104The 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=*`
104The 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=*`
105
105
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+
106To restrict by organization only, include the following flag:
108To restrict by organization only, include the following flag:
107
109
108 -github-org="": restrict logins to members of this organisation
110 -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**
119
121
120 -github-token="": the token to use when verifying repository collaborators
122 -github-token="": the token to use when verifying repository collaborators
121
123
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+
122If you are using GitHub enterprise, make sure you set the following to the appropriate url:
128If you are using GitHub enterprise, make sure you set the following to the appropriate url:
123
129
124 -login-url="http(s)://<enterprise github host>/login/oauth/authorize"
130 -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
56| `--github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
56| `--github-team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
57| `--github-repo` | string | restrict logins to collaborators of this repository formatted as `orgname/repo` | |
57| `--github-repo` | string | restrict logins to collaborators of this repository formatted as `orgname/repo` | |
58| `--github-token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | |
58| `--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 | |
59| `--gitlab-group` | string | restrict logins to members of any of these groups (slug), separated by a comma | |
60| `--gitlab-group` | string | restrict logins to members of any of these groups (slug), separated by a comma | |
60| `--google-admin-email` | string | the google admin to impersonate for api calls | |
61| `--google-admin-email` | string | the google admin to impersonate for api calls | |
61| `--google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
62| `--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 {
48 GitHubTeam string `flag:"github-team" cfg:"github_team"`
48 GitHubTeam string `flag:"github-team" cfg:"github_team"`
49 GitHubRepo string `flag:"github-repo" cfg:"github_repo"`
49 GitHubRepo string `flag:"github-repo" cfg:"github_repo"`
50 GitHubToken string `flag:"github-token" cfg:"github_token"`
50 GitHubToken string `flag:"github-token" cfg:"github_token"`
51+ GitHubUsers []string `flag:"github-user" cfg:"github_users"`
51 GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group"`
52 GitLabGroup string `flag:"gitlab-group" cfg:"gitlab_group"`
52 GoogleGroups []string `flag:"google-group" cfg:"google_group"`
53 GoogleGroups []string `flag:"google-group" cfg:"google_group"`
53 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
54 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
@@ -228,6 +229,7 @@ func NewFlagSet() *pflag.FlagSet {
228 flagSet.String("github-team", "", "restrict logins to members of this team")
229 flagSet.String("github-team", "", "restrict logins to members of this team")
229 flagSet.String("github-repo", "", "restrict logins to collaborators of this repository")
230 flagSet.String("github-repo", "", "restrict logins to collaborators of this repository")
230 flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
231 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)")
231 flagSet.String("gitlab-group", "", "restrict logins to members of this group")
233 flagSet.String("gitlab-group", "", "restrict logins to members of this group")
232 flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
234 flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
233 flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
235 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 {
309 case *providers.GitHubProvider:
309 case *providers.GitHubProvider:
310 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
310 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
311 p.SetRepo(o.GitHubRepo, o.GitHubToken)
311 p.SetRepo(o.GitHubRepo, o.GitHubToken)
312+ p.SetUsers(o.GitHubUsers)
312 case *providers.KeycloakProvider:
313 case *providers.KeycloakProvider:
313 p.SetGroup(o.KeycloakGroup)
314 p.SetGroup(o.KeycloakGroup)
314 case *providers.GoogleProvider:
315 case *providers.GoogleProvider:
providers/github.go+84−12
@@ -3,6 +3,7 @@ package providers
3import (
3import (
4 "context"
4 "context"
5 "encoding/json"
5 "encoding/json"
6+ "errors"
6 "fmt"
7 "fmt"
7 "io/ioutil"
8 "io/ioutil"
8 "net/http"
9 "net/http"
@@ -23,6 +24,7 @@ type GitHubProvider struct {
23 Team string
24 Team string
24 Repo string
25 Repo string
25 Token string
26 Token string
27+ Users []string
26}
28}
27
29
28var _ Provider = (*GitHubProvider)(nil)
30var _ Provider = (*GitHubProvider)(nil)
@@ -80,6 +82,11 @@ func (p *GitHubProvider) SetRepo(repo, token string) {
80 p.Token = token
82 p.Token = token
81}
83}
82
84
85+// SetUsers configures allowed usernames
86+func (p *GitHubProvider) SetUsers(users []string) {
87+ p.Users = users
88+}
89+
83func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) {
90func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) {
84 // https://developer.github.com/v3/orgs/#list-your-organizations
91 // https://developer.github.com/v3/orgs/#list-your-organizations
85
92
@@ -317,6 +324,46 @@ func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool,
317 return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil
324 return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil
318}
325}
319
326
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+
320func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
367func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
321 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
368 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
322
369
@@ -356,21 +403,36 @@ func (p *GitHubProvider) GetEmailAddress(ctx context.Context, s *sessions.Sessio
356 Verified bool `json:"verified"`
403 Verified bool `json:"verified"`
357 }
404 }
358
405
359− // if we require an Org or Team, check that first
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 {
360− if p.Org != "" {
421+ if p.Org != "" {
361− if p.Team != "" {
422+ if p.Team != "" {
362− if ok, err := p.hasOrgAndTeam(ctx, s.AccessToken); err != nil || !ok {
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 {
363− return "", err
428+ return "", err
429+ }
364 }
430 }
365− } else {
431+ } else if p.Repo != "" && p.Token == "" { // If we have a token we'll do the collaborator check in GetUserName
366− if ok, err := p.hasOrg(ctx, s.AccessToken); err != nil || !ok {
432+ if ok, err := p.hasRepo(ctx, s.AccessToken); err != nil || !ok {
367 return "", err
433 return "", err
368 }
434 }
369 }
435 }
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− }
374 }
436 }
375
437
376 endpoint := &url.URL{
438 endpoint := &url.URL{
@@ -456,7 +518,7 @@ func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionSta
456 }
518 }
457
519
458 // Now that we have the username we can check collaborator status
520 // 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 != "" {
460 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
522 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
461 return "", err
523 return "", err
462 }
524 }
@@ -469,3 +531,13 @@ func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionSta
469func (p *GitHubProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
531func (p *GitHubProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
470 return validateToken(ctx, p, s.AccessToken, getGitHubHeader(s.AccessToken))
532 return validateToken(ctx, p, s.AccessToken, getGitHubHeader(s.AccessToken))
471}
533}
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
318 assert.NotEqual(t, nil, err)
318 assert.NotEqual(t, nil, err)
319 assert.Equal(t, "", email)
319 assert.Equal(t, "", email)
320}
320}
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+}