Add keycloak provider
This commit is contained in:
		
							parent
							
								
									3f219bd85c
								
							
						
					
					
						commit
						583ec18fa2
					
				
							
								
								
									
										1
									
								
								main.go
								
								
								
								
							
							
						
						
									
										1
									
								
								main.go
								
								
								
								
							|  | @ -54,6 +54,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(&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.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("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") | ||||||
| 	flagSet.String("github-org", "", "restrict logins to members of this organisation") | 	flagSet.String("github-org", "", "restrict logins to members of this organisation") | ||||||
| 	flagSet.String("github-team", "", "restrict logins to members of this team") | 	flagSet.String("github-team", "", "restrict logins to members of this team") | ||||||
|  |  | ||||||
|  | @ -41,6 +41,7 @@ type Options struct { | ||||||
| 	TLSKeyFile      string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"` | 	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"` | 	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"` | ||||||
|  | 	KeycloakGroup            string   `flag:"keycloak-group" cfg:"keycloak_group"` | ||||||
| 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` | 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant" env:"OAUTH2_PROXY_AZURE_TENANT"` | ||||||
| 	EmailDomains             []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"` | 	EmailDomains             []string `flag:"email-domain" cfg:"email_domains" env:"OAUTH2_PROXY_EMAIL_DOMAINS"` | ||||||
| 	WhitelistDomains         []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"` | 	WhitelistDomains         []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"` | ||||||
|  | @ -394,6 +395,8 @@ func parseProviderInfo(o *Options, msgs []string) []string { | ||||||
| 		p.Configure(o.AzureTenant) | 		p.Configure(o.AzureTenant) | ||||||
| 	case *providers.GitHubProvider: | 	case *providers.GitHubProvider: | ||||||
| 		p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) | 		p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) | ||||||
|  | 	case *providers.KeycloakProvider: | ||||||
|  | 		p.SetGroup(o.KeycloakGroup) | ||||||
| 	case *providers.GoogleProvider: | 	case *providers.GoogleProvider: | ||||||
| 		if o.GoogleServiceAccountJSON != "" { | 		if o.GoogleServiceAccountJSON != "" { | ||||||
| 			file, err := os.Open(o.GoogleServiceAccountJSON) | 			file, err := os.Open(o.GoogleServiceAccountJSON) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | package providers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/bitly/oauth2_proxy/api" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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 { | ||||||
|  | 		log.Printf("failed building request %s", err) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	json, err := api.Request(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("failed making request %s", err) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if p.Group != "" { | ||||||
|  | 		var groups, err = json.Get("groups").Array() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.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 { | ||||||
|  | 			log.Printf("group not found, access denied") | ||||||
|  | 			return "", nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return json.Get("email").String() | ||||||
|  | } | ||||||
|  | @ -0,0 +1,148 @@ | ||||||
|  | package providers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/bmizerany/assert" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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") != "Bearer imaginary_access_token" { | ||||||
|  | 				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() | ||||||
|  | 
 | ||||||
|  | 	b_url, _ := url.Parse(b.URL) | ||||||
|  | 	p := testKeycloakProvider(b_url.Host, "") | ||||||
|  | 
 | ||||||
|  | 	session := &sessions.SessionState{AccessToken: "imaginary_access_token"} | ||||||
|  | 	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() | ||||||
|  | 
 | ||||||
|  | 	b_url, _ := url.Parse(b.URL) | ||||||
|  | 	p := testKeycloakProvider(b_url.Host, "test-grp1") | ||||||
|  | 
 | ||||||
|  | 	session := &sessions.SessionState{AccessToken: "imaginary_access_token"} | ||||||
|  | 	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() | ||||||
|  | 
 | ||||||
|  | 	b_url, _ := url.Parse(b.URL) | ||||||
|  | 	p := testKeycloakProvider(b_url.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() | ||||||
|  | 
 | ||||||
|  | 	b_url, _ := url.Parse(b.URL) | ||||||
|  | 	p := testKeycloakProvider(b_url.Host, "") | ||||||
|  | 
 | ||||||
|  | 	session := &sessions.SessionState{AccessToken: "imaginary_access_token"} | ||||||
|  | 	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) | 		return NewFacebookProvider(p) | ||||||
| 	case "github": | 	case "github": | ||||||
| 		return NewGitHubProvider(p) | 		return NewGitHubProvider(p) | ||||||
|  | 	case "keycloak": | ||||||
|  | 		return NewKeycloakProvider(p) | ||||||
| 	case "azure": | 	case "azure": | ||||||
| 		return NewAzureProvider(p) | 		return NewAzureProvider(p) | ||||||
| 	case "gitlab": | 	case "gitlab": | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue