Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to ensure user belongs in required groups when using the OIDC provider #616

Merged
merged 1 commit into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

## Important Notes

- [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Ensure you have configured oauth2-proxy to use the `groups` scope. The user may be logged out initially as they may not currently have the `groups` claim however after going back through login process wil be authenticated.

## Breaking Changes

## Changes since v6.1.1

- [#764](https://github.com/oauth2-proxy/oauth2-proxy/pull/764) Document bcrypt encryption for htpasswd (and hide SHA) (@lentzi90)
- [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Add support to ensure user belongs in required groups when using the OIDC provider (@stefansedich)

# v6.1.1

Expand Down
6 changes: 4 additions & 2 deletions docs/configuration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
| `--insecure-oidc-skip-issuer-verification` | bool | allow the OIDC issuer URL to differ from the expected (currently required for Azure multi-tenant compatibility) | false |
| `--oidc-issuer-url` | string | the OpenID Connect issuer URL, e.g. `"https://accounts.google.com"` | |
| `--oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
| `--oidc-groups-claim` | string | which claim contains the user groups | `"groups"` |
| `--pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header | false |
| `--pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
| `--pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
| `--prefer-email-to-user` | bool | Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, e.g. htaccess authentication. Used in conjunction with `--pass-basic-auth` and `--pass-user-headers` | false |
| `--pass-host-header` | bool | pass the request Host Header to upstream | true |
| `--pass-user-headers` | bool | pass X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
| `--pass-user-headers` | bool | pass X-Forwarded-User, X-Forwarded-Groups, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
| `--profile-url` | string | Profile access endpoint | |
| `--prompt` | string | [OIDC prompt](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest); if present, `approval-prompt` is ignored | `""` |
| `--provider` | string | OAuth provider | google |
Expand Down Expand Up @@ -112,7 +113,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
| `--scope` | string | OAuth scope specification | |
| `--session-cookie-minimal` | bool | strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only) | false |
| `--session-store-type` | string | [Session data storage backend](configuration/sessions); redis or cookie | cookie |
| `--set-xauthrequest` | bool | set X-Auth-Request-User, X-Auth-Request-Email and X-Auth-Request-Preferred-Username response headers (useful in Nginx auth_request mode) | false |
| `--set-xauthrequest` | bool | set X-Auth-Request-User, X-Auth-Request-Groups, X-Auth-Request-Email and X-Auth-Request-Preferred-Username response headers (useful in Nginx auth_request mode) | false |
| `--set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
| `--set-basic-auth` | bool | set HTTP Basic Auth information in response (useful in Nginx auth_request mode) | false |
| `--signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
Expand All @@ -131,6 +132,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
| `--tls-key-file` | string | path to private key file | |
| `--upstream` | string \| list | the http url(s) of the upstream endpoint, file:// paths for static files or `static://<status_code>` for static response. Routing is based on the path | |
| `--user-id-claim` | string | which claim contains the user ID | \["email"\] |
| `--allowed-group` | string \| list | restrict logins to members of this group (may be given multiple times) | |
| `--validate-url` | string | Access token validation endpoint | |
| `--version` | n/a | print version string | |
| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (e.g. `.example.com`)&nbsp;\[[2](#footnote2)\] | |
Expand Down
45 changes: 44 additions & 1 deletion oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type OAuthProxy struct {
trustedIPs *ip.NetSet
Banner string
Footer string
AllowedGroups []string

sessionChain alice.Chain
}
Expand Down Expand Up @@ -215,6 +216,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
Banner: opts.Banner,
Footer: opts.Footer,
SignInMessage: buildSignInMessage(opts),
AllowedGroups: opts.AllowedGroups,

basicAuthValidator: basicAuthValidator,
displayHtpasswdForm: basicAuthValidator != nil,
Expand Down Expand Up @@ -888,7 +890,10 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
return nil, ErrNeedsLogin
}

if session != nil && session.Email != "" && !p.Validator(session.Email) {
invalidEmail := session != nil && session.Email != "" && !p.Validator(session.Email)
invalidGroups := session != nil && !p.validateGroups(session.Groups)

if invalidEmail || invalidGroups {
logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
// Invalid session, clear it
err := p.ClearSessionCookie(rw, req)
Expand Down Expand Up @@ -942,6 +947,14 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
} else {
req.Header.Del("X-Forwarded-Preferred-Username")
}

if len(session.Groups) > 0 {
for _, group := range session.Groups {
req.Header.Add("X-Forwarded-Groups", group)
}
} else {
req.Header.Del("X-Forwarded-Groups")
stefansedich marked this conversation as resolved.
Show resolved Hide resolved
}
}

if p.SetXAuthRequest {
Expand All @@ -964,6 +977,14 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
rw.Header().Del("X-Auth-Request-Access-Token")
}
}

if len(session.Groups) > 0 {
for _, group := range session.Groups {
rw.Header().Add("X-Auth-Request-Groups", group)
}
} else {
rw.Header().Del("X-Auth-Request-Groups")
}
}

if p.PassAccessToken {
Expand Down Expand Up @@ -1012,13 +1033,15 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
if p.PassBasicAuth {
req.Header.Del("X-Forwarded-User")
req.Header.Del("X-Forwarded-Groups")
req.Header.Del("X-Forwarded-Email")
req.Header.Del("X-Forwarded-Preferred-Username")
req.Header.Del("Authorization")
}

if p.PassUserHeaders {
req.Header.Del("X-Forwarded-User")
req.Header.Del("X-Forwarded-Groups")
stefansedich marked this conversation as resolved.
Show resolved Hide resolved
req.Header.Del("X-Forwarded-Email")
req.Header.Del("X-Forwarded-Preferred-Username")
}
Expand Down Expand Up @@ -1049,3 +1072,23 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
rw.Header().Set("Content-Type", applicationJSON)
rw.WriteHeader(code)
}

func (p *OAuthProxy) validateGroups(groups []string) bool {
if len(p.AllowedGroups) == 0 {
return true
}

allowedGroups := map[string]struct{}{}

for _, group := range p.AllowedGroups {
allowedGroups[group] = struct{}{}
}

for _, group := range groups {
if _, ok := allowedGroups[group]; ok {
return true
}
}

return false
}
147 changes: 146 additions & 1 deletion oauthproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,37 @@ func TestPassUserHeadersWithEmail(t *testing.T) {
}
}

func TestPassGroupsHeadersWithGroups(t *testing.T) {
opts := baseTestOptions()
err := validation.Validate(opts)
assert.NoError(t, err)

const emailAddress = "john.doe@example.com"
const userName = "9fcab5c9b889a557"

groups := []string{"a", "b"}
created := time.Now()
session := &sessions.SessionState{
User: userName,
Groups: groups,
Email: emailAddress,
AccessToken: "oauth_token",
CreatedAt: &created,
}
{
rw := httptest.NewRecorder()
req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase0", nil)
proxy, err := NewOAuthProxy(opts, func(email string) bool {
return email == emailAddress
})
if err != nil {
t.Fatal(err)
}
proxy.addHeadersForProxying(rw, req, session)
assert.Equal(t, groups, req.Header["X-Forwarded-Groups"])
}
}

func TestStripAuthHeaders(t *testing.T) {
testCases := map[string]struct {
SkipAuthStripHeaders bool
Expand All @@ -609,6 +640,7 @@ func TestStripAuthHeaders(t *testing.T) {
PassAuthorization: false,
StrippedHeaders: map[string]bool{
"X-Forwarded-User": true,
"X-Forwared-Groups": true,
"X-Forwarded-Email": true,
"X-Forwarded-Preferred-Username": true,
"X-Forwarded-Access-Token": false,
Expand All @@ -623,6 +655,7 @@ func TestStripAuthHeaders(t *testing.T) {
PassAuthorization: false,
StrippedHeaders: map[string]bool{
"X-Forwarded-User": true,
"X-Forwared-Groups": true,
"X-Forwarded-Email": true,
"X-Forwarded-Preferred-Username": true,
"X-Forwarded-Access-Token": true,
Expand All @@ -637,6 +670,7 @@ func TestStripAuthHeaders(t *testing.T) {
PassAuthorization: false,
StrippedHeaders: map[string]bool{
"X-Forwarded-User": true,
"X-Forwared-Groups": true,
"X-Forwarded-Email": true,
"X-Forwarded-Preferred-Username": true,
"X-Forwarded-Access-Token": true,
Expand All @@ -651,6 +685,7 @@ func TestStripAuthHeaders(t *testing.T) {
PassAuthorization: true,
StrippedHeaders: map[string]bool{
"X-Forwarded-User": false,
"X-Forwared-Groups": false,
"X-Forwarded-Email": false,
"X-Forwarded-Preferred-Username": false,
"X-Forwarded-Access-Token": false,
Expand All @@ -665,6 +700,7 @@ func TestStripAuthHeaders(t *testing.T) {
PassAuthorization: false,
StrippedHeaders: map[string]bool{
"X-Forwarded-User": false,
"X-Forwared-Groups": false,
"X-Forwarded-Email": false,
"X-Forwarded-Preferred-Username": false,
"X-Forwarded-Access-Token": false,
Expand All @@ -679,6 +715,7 @@ func TestStripAuthHeaders(t *testing.T) {
PassAuthorization: false,
StrippedHeaders: map[string]bool{
"X-Forwarded-User": false,
"X-Forwared-Groups": false,
"X-Forwarded-Email": false,
"X-Forwarded-Preferred-Username": false,
"X-Forwarded-Access-Token": false,
Expand All @@ -690,6 +727,7 @@ func TestStripAuthHeaders(t *testing.T) {
initialHeaders := map[string]string{
"X-Forwarded-User": "9fcab5c9b889a557",
"X-Forwarded-Email": "john.doe@example.com",
"X-Forwarded-Groups": "a,b,c",
"X-Forwarded-Preferred-Username": "john.doe",
"X-Forwarded-Access-Token": "AccessToken",
"Authorization": "bearer IDToken",
Expand Down Expand Up @@ -1333,6 +1371,7 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {

pcTest.opts = baseTestOptions()
pcTest.opts.SetXAuthRequest = true
pcTest.opts.AllowedGroups = []string{"oauth_groups"}
err := validation.Validate(pcTest.opts)
assert.NoError(t, err)

Expand All @@ -1354,13 +1393,14 @@ func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {

created := time.Now()
startSession := &sessions.SessionState{
User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
User: "oauth_user", Groups: []string{"oauth_groups"}, Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created}
err = pcTest.SaveSession(startSession)
assert.NoError(t, err)

pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req)
assert.Equal(t, http.StatusAccepted, pcTest.rw.Code)
assert.Equal(t, "oauth_user", pcTest.rw.Header().Get("X-Auth-Request-User"))
assert.Equal(t, startSession.Groups, pcTest.rw.Header().Values("X-Auth-Request-Groups"))
assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Get("X-Auth-Request-Email"))
}

Expand Down Expand Up @@ -2199,3 +2239,108 @@ func TestTrustedIPs(t *testing.T) {
})
}
}

func TestProxyAllowedGroups(t *testing.T) {
tests := []struct {
name string
allowedGroups []string
groups []string
expectUnauthorized bool
}{
{"NoAllowedGroups", []string{}, []string{}, false},
{"NoAllowedGroupsUserHasGroups", []string{}, []string{"a", "b"}, false},
{"UserInAllowedGroup", []string{"a"}, []string{"a", "b"}, false},
{"UserNotInAllowedGroup", []string{"a"}, []string{"c"}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
emailAddress := "test"
created := time.Now()

session := &sessions.SessionState{
Groups: tt.groups,
Email: emailAddress,
AccessToken: "oauth_token",
CreatedAt: &created,
}

upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
t.Cleanup(upstream.Close)

test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
opts.AllowedGroups = tt.allowedGroups
opts.UpstreamServers = options.Upstreams{
{
ID: upstream.URL,
Path: "/",
URI: upstream.URL,
},
}
})
if err != nil {
t.Fatal(err)
}

test.req, _ = http.NewRequest("GET", "/", nil)

test.req.Header.Add("accept", applicationJSON)
test.SaveSession(session)
test.proxy.ServeHTTP(test.rw, test.req)

if tt.expectUnauthorized {
assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
} else {
assert.Equal(t, http.StatusOK, test.rw.Code)
}
})
}
}

func TestAuthOnlyAllowedGroups(t *testing.T) {
tests := []struct {
name string
allowedGroups []string
groups []string
expectUnauthorized bool
}{
{"NoAllowedGroups", []string{}, []string{}, false},
{"NoAllowedGroupsUserHasGroups", []string{}, []string{"a", "b"}, false},
{"UserInAllowedGroup", []string{"a"}, []string{"a", "b"}, false},
{"UserNotInAllowedGroup", []string{"a"}, []string{"c"}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
emailAddress := "test"
created := time.Now()

session := &sessions.SessionState{
Groups: tt.groups,
Email: emailAddress,
AccessToken: "oauth_token",
CreatedAt: &created,
}

test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
opts.AllowedGroups = tt.allowedGroups
})
if err != nil {
t.Fatal(err)
}

err = test.SaveSession(session)
assert.NoError(t, err)

test.proxy.ServeHTTP(test.rw, test.req)

if tt.expectUnauthorized {
assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
} else {
assert.Equal(t, http.StatusAccepted, test.rw.Code)
}
})
}
}
Loading