Merge branch 'master' into support-x-auth-request-redirect
This commit is contained in:
		
						commit
						ac0d010371
					
				|  | @ -2,6 +2,8 @@ | |||
| 
 | ||||
| ## Changes since v4.0.0 | ||||
| 
 | ||||
| - [#227](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka) | ||||
| 
 | ||||
| # v4.0.0 | ||||
| 
 | ||||
| - [#248](https://github.com/pusher/oauth2_proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ Valid providers are : | |||
| - [Azure](#azure-auth-provider) | ||||
| - [Facebook](#facebook-auth-provider) | ||||
| - [GitHub](#github-auth-provider) | ||||
| - [Keycloak](#keycloak-auth-provider) | ||||
| - [GitLab](#gitlab-auth-provider) | ||||
| - [LinkedIn](#linkedin-auth-provider) | ||||
| - [login.gov](#logingov-provider) | ||||
|  | @ -101,6 +102,20 @@ If you are using GitHub enterprise, make sure you set the following to the appro | |||
|     -redeem-url="http(s)://<enterprise github host>/login/oauth/access_token" | ||||
|     -validate-url="http(s)://<enterprise github host>/api/v3" | ||||
| 
 | ||||
| ### Keycloak Auth Provider | ||||
| 
 | ||||
| 1.  Create new client in your Keycloak with **Access Type** 'confidental'. | ||||
| 2.  Create a mapper with **Mapper Type** 'Group Membership'. | ||||
| 
 | ||||
| Make sure you set the following to the appropriate url: | ||||
| 
 | ||||
|     -provider=keycloak | ||||
|     -client-id=<client you have created> | ||||
|     -client-secret=<your client's secret> | ||||
|     -login-url="http(s)://<keycloak host>/realms/<your realm>/protocol/openid-connect/auth" | ||||
|     -redeem-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/auth/token" | ||||
|     -validate-url="http(s)://<keycloak host>/realms/master/<your realm>/openid-connect/userinfo" | ||||
| 
 | ||||
| ### GitLab Auth Provider | ||||
| 
 | ||||
| Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes. | ||||
|  |  | |||
							
								
								
									
										1
									
								
								main.go
								
								
								
								
							
							
						
						
									
										1
									
								
								main.go
								
								
								
								
							|  | @ -55,6 +55,7 @@ func main() { | |||
| 
 | ||||
| 	flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") | ||||
| 	flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)") | ||||
| 	flagSet.String("keycloak-group", "", "restrict login to members of this group.") | ||||
| 	flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") | ||||
| 	flagSet.String("bitbucket-team", "", "restrict logins to members of this team") | ||||
| 	flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository") | ||||
|  |  | |||
|  | @ -41,6 +41,7 @@ type Options struct { | |||
| 	TLSKeyFile      string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` | ||||
| 
 | ||||
| 	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"` | ||||
| 	KeycloakGroup            string   `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"` | ||||
| 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` | ||||
| 	BitbucketTeam            string   `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"` | ||||
| 	BitbucketRepository      string   `flag:"bitbucket-repository" cfg:"bitbucket_repository" env:"OAUTH2_PROXY_BITBUCKET_REPOSITORY"` | ||||
|  | @ -398,6 +399,8 @@ func parseProviderInfo(o *Options, msgs []string) []string { | |||
| 		p.Configure(o.AzureTenant) | ||||
| 	case *providers.GitHubProvider: | ||||
| 		p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) | ||||
| 	case *providers.KeycloakProvider: | ||||
| 		p.SetGroup(o.KeycloakGroup) | ||||
| 	case *providers.GoogleProvider: | ||||
| 		if o.GoogleServiceAccountJSON != "" { | ||||
| 			file, err := os.Open(o.GoogleServiceAccountJSON) | ||||
|  |  | |||
|  | @ -0,0 +1,86 @@ | |||
| package providers | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions" | ||||
| 	"github.com/pusher/oauth2_proxy/pkg/logger" | ||||
| 	"github.com/pusher/oauth2_proxy/pkg/requests" | ||||
| ) | ||||
| 
 | ||||
| type KeycloakProvider struct { | ||||
| 	*ProviderData | ||||
| 	Group string | ||||
| } | ||||
| 
 | ||||
| func NewKeycloakProvider(p *ProviderData) *KeycloakProvider { | ||||
| 	p.ProviderName = "Keycloak" | ||||
| 	if p.LoginURL == nil || p.LoginURL.String() == "" { | ||||
| 		p.LoginURL = &url.URL{ | ||||
| 			Scheme: "https", | ||||
| 			Host:   "keycloak.org", | ||||
| 			Path:   "/oauth/authorize", | ||||
| 		} | ||||
| 	} | ||||
| 	if p.RedeemURL == nil || p.RedeemURL.String() == "" { | ||||
| 		p.RedeemURL = &url.URL{ | ||||
| 			Scheme: "https", | ||||
| 			Host:   "keycloak.org", | ||||
| 			Path:   "/oauth/token", | ||||
| 		} | ||||
| 	} | ||||
| 	if p.ValidateURL == nil || p.ValidateURL.String() == "" { | ||||
| 		p.ValidateURL = &url.URL{ | ||||
| 			Scheme: "https", | ||||
| 			Host:   "keycloak.org", | ||||
| 			Path:   "/api/v3/user", | ||||
| 		} | ||||
| 	} | ||||
| 	if p.Scope == "" { | ||||
| 		p.Scope = "api" | ||||
| 	} | ||||
| 	return &KeycloakProvider{ProviderData: p} | ||||
| } | ||||
| 
 | ||||
| func (p *KeycloakProvider) SetGroup(group string) { | ||||
| 	p.Group = group | ||||
| } | ||||
| 
 | ||||
| func (p *KeycloakProvider) GetEmailAddress(s *sessions.SessionState) (string, error) { | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", p.ValidateURL.String(), nil) | ||||
| 	req.Header.Set("Authorization", "Bearer "+s.AccessToken) | ||||
| 	if err != nil { | ||||
| 		logger.Printf("failed building request %s", err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 	json, err := requests.Request(req) | ||||
| 	if err != nil { | ||||
| 		logger.Printf("failed making request %s", err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	if p.Group != "" { | ||||
| 		var groups, err = json.Get("groups").Array() | ||||
| 		if err != nil { | ||||
| 			logger.Printf("groups not found %s", err) | ||||
| 			return "", err | ||||
| 		} | ||||
| 
 | ||||
| 		var found = false | ||||
| 		for i := range groups { | ||||
| 			if groups[i].(string) == p.Group { | ||||
| 				found = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if found != true { | ||||
| 			logger.Printf("group not found, access denied") | ||||
| 			return "", nil | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return json.Get("email").String() | ||||
| } | ||||
|  | @ -0,0 +1,151 @@ | |||
| package providers | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/bmizerany/assert" | ||||
| 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions" | ||||
| ) | ||||
| 
 | ||||
| const imaginaryAccessToken = "imaginary_access_token" | ||||
| const bearerAccessToken = "Bearer " + imaginaryAccessToken | ||||
| 
 | ||||
| func testKeycloakProvider(hostname, group string) *KeycloakProvider { | ||||
| 	p := NewKeycloakProvider( | ||||
| 		&ProviderData{ | ||||
| 			ProviderName: "", | ||||
| 			LoginURL:     &url.URL{}, | ||||
| 			RedeemURL:    &url.URL{}, | ||||
| 			ProfileURL:   &url.URL{}, | ||||
| 			ValidateURL:  &url.URL{}, | ||||
| 			Scope:        ""}) | ||||
| 
 | ||||
| 	if group != "" { | ||||
| 		p.SetGroup(group) | ||||
| 	} | ||||
| 
 | ||||
| 	if hostname != "" { | ||||
| 		updateURL(p.Data().LoginURL, hostname) | ||||
| 		updateURL(p.Data().RedeemURL, hostname) | ||||
| 		updateURL(p.Data().ProfileURL, hostname) | ||||
| 		updateURL(p.Data().ValidateURL, hostname) | ||||
| 	} | ||||
| 	return p | ||||
| } | ||||
| 
 | ||||
| func testKeycloakBackend(payload string) *httptest.Server { | ||||
| 	path := "/api/v3/user" | ||||
| 
 | ||||
| 	return httptest.NewServer(http.HandlerFunc( | ||||
| 		func(w http.ResponseWriter, r *http.Request) { | ||||
| 			url := r.URL | ||||
| 			if url.Path != path { | ||||
| 				w.WriteHeader(404) | ||||
| 			} else if r.Header.Get("Authorization") != bearerAccessToken { | ||||
| 				w.WriteHeader(403) | ||||
| 			} else { | ||||
| 				w.WriteHeader(200) | ||||
| 				w.Write([]byte(payload)) | ||||
| 			} | ||||
| 		})) | ||||
| } | ||||
| 
 | ||||
| func TestKeycloakProviderDefaults(t *testing.T) { | ||||
| 	p := testKeycloakProvider("", "") | ||||
| 	assert.NotEqual(t, nil, p) | ||||
| 	assert.Equal(t, "Keycloak", p.Data().ProviderName) | ||||
| 	assert.Equal(t, "https://keycloak.org/oauth/authorize", | ||||
| 		p.Data().LoginURL.String()) | ||||
| 	assert.Equal(t, "https://keycloak.org/oauth/token", | ||||
| 		p.Data().RedeemURL.String()) | ||||
| 	assert.Equal(t, "https://keycloak.org/api/v3/user", | ||||
| 		p.Data().ValidateURL.String()) | ||||
| 	assert.Equal(t, "api", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
| func TestKeycloakProviderOverrides(t *testing.T) { | ||||
| 	p := NewKeycloakProvider( | ||||
| 		&ProviderData{ | ||||
| 			LoginURL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/auth"}, | ||||
| 			RedeemURL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/token"}, | ||||
| 			ValidateURL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/api/v3/user"}, | ||||
| 			Scope: "profile"}) | ||||
| 	assert.NotEqual(t, nil, p) | ||||
| 	assert.Equal(t, "Keycloak", p.Data().ProviderName) | ||||
| 	assert.Equal(t, "https://example.com/oauth/auth", | ||||
| 		p.Data().LoginURL.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/token", | ||||
| 		p.Data().RedeemURL.String()) | ||||
| 	assert.Equal(t, "https://example.com/api/v3/user", | ||||
| 		p.Data().ValidateURL.String()) | ||||
| 	assert.Equal(t, "profile", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
| func TestKeycloakProviderGetEmailAddress(t *testing.T) { | ||||
| 	b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\"}") | ||||
| 	defer b.Close() | ||||
| 
 | ||||
| 	bURL, _ := url.Parse(b.URL) | ||||
| 	p := testKeycloakProvider(bURL.Host, "") | ||||
| 
 | ||||
| 	session := &sessions.SessionState{AccessToken: imaginaryAccessToken} | ||||
| 	email, err := p.GetEmailAddress(session) | ||||
| 	assert.Equal(t, nil, err) | ||||
| 	assert.Equal(t, "michael.bland@gsa.gov", email) | ||||
| } | ||||
| 
 | ||||
| func TestKeycloakProviderGetEmailAddressAndGroup(t *testing.T) { | ||||
| 	b := testKeycloakBackend("{\"email\": \"michael.bland@gsa.gov\", \"groups\": [\"test-grp1\", \"test-grp2\"]}") | ||||
| 	defer b.Close() | ||||
| 
 | ||||
| 	bURL, _ := url.Parse(b.URL) | ||||
| 	p := testKeycloakProvider(bURL.Host, "test-grp1") | ||||
| 
 | ||||
| 	session := &sessions.SessionState{AccessToken: imaginaryAccessToken} | ||||
| 	email, err := p.GetEmailAddress(session) | ||||
| 	assert.Equal(t, nil, err) | ||||
| 	assert.Equal(t, "michael.bland@gsa.gov", email) | ||||
| } | ||||
| 
 | ||||
| // Note that trying to trigger the "failed building request" case is not
 | ||||
| // practical, since the only way it can fail is if the URL fails to parse.
 | ||||
| func TestKeycloakProviderGetEmailAddressFailedRequest(t *testing.T) { | ||||
| 	b := testKeycloakBackend("unused payload") | ||||
| 	defer b.Close() | ||||
| 
 | ||||
| 	bURL, _ := url.Parse(b.URL) | ||||
| 	p := testKeycloakProvider(bURL.Host, "") | ||||
| 
 | ||||
| 	// We'll trigger a request failure by using an unexpected access
 | ||||
| 	// token. Alternatively, we could allow the parsing of the payload as
 | ||||
| 	// JSON to fail.
 | ||||
| 	session := &sessions.SessionState{AccessToken: "unexpected_access_token"} | ||||
| 	email, err := p.GetEmailAddress(session) | ||||
| 	assert.NotEqual(t, nil, err) | ||||
| 	assert.Equal(t, "", email) | ||||
| } | ||||
| 
 | ||||
| func TestKeycloakProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) { | ||||
| 	b := testKeycloakBackend("{\"foo\": \"bar\"}") | ||||
| 	defer b.Close() | ||||
| 
 | ||||
| 	bURL, _ := url.Parse(b.URL) | ||||
| 	p := testKeycloakProvider(bURL.Host, "") | ||||
| 
 | ||||
| 	session := &sessions.SessionState{AccessToken: imaginaryAccessToken} | ||||
| 	email, err := p.GetEmailAddress(session) | ||||
| 	assert.NotEqual(t, nil, err) | ||||
| 	assert.Equal(t, "", email) | ||||
| } | ||||
|  | @ -28,6 +28,8 @@ func New(provider string, p *ProviderData) Provider { | |||
| 		return NewFacebookProvider(p) | ||||
| 	case "github": | ||||
| 		return NewGitHubProvider(p) | ||||
| 	case "keycloak": | ||||
| 		return NewKeycloakProvider(p) | ||||
| 	case "azure": | ||||
| 		return NewAzureProvider(p) | ||||
| 	case "gitlab": | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue