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)
|
||||||
|
86
providers/keycloak.go
Normal file
86
providers/keycloak.go
Normal file
@ -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()
|
||||||
|
}
|
148
providers/keycloak_test.go
Normal file
148
providers/keycloak_test.go
Normal file
@ -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
Block a user