Merge branch 'master' into feature/azure-idtoken

This commit is contained in:
leyshon 2019-10-02 10:33:08 +01:00 committed by GitHub
commit d8d4c687df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 269 additions and 3 deletions

View File

@ -2,11 +2,14 @@
## Changes since v4.0.0 ## Changes since v4.0.0
[#258](https://github.com/pusher/oauth2_proxy/pull/258) Add IDToken for Azure provider - [#227](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka)
- [#258](https://github.com/pusher/oauth2_proxy/pull/258) Add IDToken for Azure provider
- This PR adds the IDToken into the session for the Azure provider allowing requests to a backend to be identified as a specific user. As a consequence, if you are using a cookie to store the session the cookie will now exceed the 4kb size limit and be split into multiple cookies. This can cause problems when using nginx as a proxy, resulting in no cookie being passed at all. Either increase the proxy_buffer_size in nginx or implement the redis session storage (see https://pusher.github.io/oauth2_proxy/configuration#redis-storage) - This PR adds the IDToken into the session for the Azure provider allowing requests to a backend to be identified as a specific user. As a consequence, if you are using a cookie to store the session the cookie will now exceed the 4kb size limit and be split into multiple cookies. This can cause problems when using nginx as a proxy, resulting in no cookie being passed at all. Either increase the proxy_buffer_size in nginx or implement the redis session storage (see https://pusher.github.io/oauth2_proxy/configuration#redis-storage)
# v4.0.0 # v4.0.0
- [#248](https://github.com/pusher/oauth2_proxy/pull/248) Fix issue with X-Auth-Request-Redirect header being ignored
## Release Highlights ## Release Highlights
- Documentation is now on a [microsite](https://pusher.github.io/oauth2_proxy/) - Documentation is now on a [microsite](https://pusher.github.io/oauth2_proxy/)
- Health check logging can now be disabled for quieter logs - Health check logging can now be disabled for quieter logs

View File

@ -15,6 +15,7 @@ Valid providers are :
- [Azure](#azure-auth-provider) - [Azure](#azure-auth-provider)
- [Facebook](#facebook-auth-provider) - [Facebook](#facebook-auth-provider)
- [GitHub](#github-auth-provider) - [GitHub](#github-auth-provider)
- [Keycloak](#keycloak-auth-provider)
- [GitLab](#gitlab-auth-provider) - [GitLab](#gitlab-auth-provider)
- [LinkedIn](#linkedin-auth-provider) - [LinkedIn](#linkedin-auth-provider)
- [login.gov](#logingov-provider) - [login.gov](#logingov-provider)
@ -103,6 +104,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" -redeem-url="http(s)://<enterprise github host>/login/oauth/access_token"
-validate-url="http(s)://<enterprise github host>/api/v3" -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 ### 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. 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.

View File

@ -249,6 +249,8 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme; proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri; proxy_set_header X-Auth-Request-Redirect $request_uri;
# or, if you are handling multiple domains:
# proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
} }
location = /oauth2/auth { location = /oauth2/auth {
proxy_pass http://127.0.0.1:4180; proxy_pass http://127.0.0.1:4180;
@ -308,7 +310,7 @@ If you use ingress-nginx in Kubernetes (which includes the Lua module), you also
```yaml ```yaml
nginx.ingress.kubernetes.io/auth-response-headers: Authorization nginx.ingress.kubernetes.io/auth-response-headers: Authorization
nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/start?rd=$request_uri nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/start?rd=$escaped_request_uri
nginx.ingress.kubernetes.io/auth-url: https://$host/oauth2/auth nginx.ingress.kubernetes.io/auth-url: https://$host/oauth2/auth
nginx.ingress.kubernetes.io/configuration-snippet: | nginx.ingress.kubernetes.io/configuration-snippet: |
auth_request_set $name_upstream_1 $upstream_cookie_name_1; auth_request_set $name_upstream_1 $upstream_cookie_name_1;

View File

@ -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(&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("bitbucket-team", "", "restrict logins to members of this team") flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository") flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")

View File

@ -480,7 +480,10 @@ func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error)
return return
} }
redirect = req.Form.Get("rd") redirect = req.Header.Get("X-Auth-Request-Redirect")
if req.Form.Get("rd") != "" {
redirect = req.Form.Get("rd")
}
if !p.IsValidRedirect(redirect) { if !p.IsValidRedirect(redirect) {
redirect = req.URL.Path redirect = req.URL.Path
if strings.HasPrefix(redirect, p.ProxyPrefix) { if strings.HasPrefix(redirect, p.ProxyPrefix) {

View File

@ -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" env:"OAUTH2_PROXY_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"`
BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team" env:"OAUTH2_PROXY_BITBUCKET_TEAM"` 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"` 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) 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
View File

@ -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()
}

151
providers/keycloak_test.go Normal file
View File

@ -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)
}

View File

@ -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":