git.iko.soy/typeable/oauth2-proxy
on master help sign in
Allow group authZ in AuthOnly endpoint via Querystring
23b2355Nick Meves
CHANGELOG.md+5
@@ -6,6 +6,10 @@
6
6
7- [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936) `--user-id-claim` option is deprecated and replaced by `--oidc-email-claim`
7- [#936](https://github.com/oauth2-proxy/oauth2-proxy/pull/936) `--user-id-claim` option is deprecated and replaced by `--oidc-email-claim`
8- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled
8- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled
9+- [#849](https://github.com/oauth2-proxy/oauth2-proxy/pull/849) `/oauth2/auth` `allowed_groups` querystring parameter can be paired with the `allowed-groups` configuration option.
10+ - In this scenario, the user's group must be in both lists to not get a 401 response code.
11+ - The `allowed_group` querystring parameter can be specified multiple times to support multiple groups.
12+ - The `allowed_groups` querystring parameter can specify multiple comma delimited groups.
9- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Existing sessions from v6.0.0 or earlier are no longer valid. They will trigger a reauthentication.
13- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Existing sessions from v6.0.0 or earlier are no longer valid. They will trigger a reauthentication.
10- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) `skip-auth-strip-headers` now applies to all requests, not just those where authentication would be skipped.
14- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) `skip-auth-strip-headers` now applies to all requests, not just those where authentication would be skipped.
11- [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) The behavior of the Google provider Groups restriction changes with this
15- [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) The behavior of the Google provider Groups restriction changes with this
@@ -56,6 +60,7 @@
56- [#918](https://github.com/oauth2-proxy/oauth2-proxy/pull/918) Fix log header output (@JoelSpeed)
60- [#918](https://github.com/oauth2-proxy/oauth2-proxy/pull/918) Fix log header output (@JoelSpeed)
57- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Validate provider type on startup.
61- [#911](https://github.com/oauth2-proxy/oauth2-proxy/pull/911) Validate provider type on startup.
58- [#869](https://github.com/oauth2-proxy/oauth2-proxy/pull/869) Streamline provider interface method names and signatures (@NickMeves)
62- [#869](https://github.com/oauth2-proxy/oauth2-proxy/pull/869) Streamline provider interface method names and signatures (@NickMeves)
63+- [#849](https://github.com/oauth2-proxy/oauth2-proxy/pull/849) Support group authorization on `oauth2/auth` endpoint via `allowed_group` & `allowed_groups` querystring parameters (@NickMeves)
59- [#906](https://github.com/oauth2-proxy/oauth2-proxy/pull/906) Set up v6.1.x versioned documentation as default documentation (@JoelSpeed)
64- [#906](https://github.com/oauth2-proxy/oauth2-proxy/pull/906) Set up v6.1.x versioned documentation as default documentation (@JoelSpeed)
60- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Remove v5 legacy sessions support (@NickMeves)
65- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Remove v5 legacy sessions support (@NickMeves)
61- [#904](https://github.com/oauth2-proxy/oauth2-proxy/pull/904) Set `skip-auth-strip-headers` to `true` by default (@NickMeves)
66- [#904](https://github.com/oauth2-proxy/oauth2-proxy/pull/904) Set `skip-auth-strip-headers` to `true` by default (@NickMeves)
oauthproxy.go+47−3
@@ -744,7 +744,7 @@ func (p *OAuthProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
744 case path == p.OAuthCallbackPath:
744 case path == p.OAuthCallbackPath:
745 p.OAuthCallback(rw, req)
745 p.OAuthCallback(rw, req)
746 case path == p.AuthOnlyPath:
746 case path == p.AuthOnlyPath:
747− p.AuthenticateOnly(rw, req)
747+ p.AuthOnly(rw, req)
748 case path == p.UserInfoPath:
748 case path == p.UserInfoPath:
749 p.UserInfo(rw, req)
749 p.UserInfo(rw, req)
750 default:
750 default:
@@ -925,14 +925,22 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
925 }
925 }
926}
926}
927
927
928−// AuthenticateOnly checks whether the user is currently logged in
928+// AuthOnly checks whether the user is currently logged in (both authentication
929+// and optional authorization via `allowed_groups` querystring).
929−func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
930+func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) {
930 session, err := p.getAuthenticatedSession(rw, req)
931 session, err := p.getAuthenticatedSession(rw, req)
931 if err != nil {
932 if err != nil {
932 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
933 http.Error(rw, "unauthorized request", http.StatusUnauthorized)
933 return
934 return
934 }
935 }
935
936
937+ // Allow secondary group restrictions based on the `allowed_group` or
938+ // `allowed_groups` querystring parameter
939+ if !checkAllowedGroups(req, session) {
940+ http.Error(rw, "unauthorized request", http.StatusUnauthorized)
941+ return
942+ }
943+
936 // we are authenticated
944 // we are authenticated
937 p.addHeadersForProxying(rw, req, session)
945 p.addHeadersForProxying(rw, req, session)
938 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
946 p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -1016,6 +1024,42 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
1016 return session, nil
1024 return session, nil
1017}
1025}
1018
1026
1027+func checkAllowedGroups(req *http.Request, session *sessionsapi.SessionState) bool {
1028+ allowedGroups := extractAllowedGroups(req)
1029+ if len(allowedGroups) == 0 {
1030+ return true
1031+ }
1032+
1033+ for _, group := range session.Groups {
1034+ if _, ok := allowedGroups[group]; ok {
1035+ return true
1036+ }
1037+ }
1038+
1039+ return false
1040+}
1041+
1042+func extractAllowedGroups(req *http.Request) map[string]struct{} {
1043+ groups := map[string]struct{}{}
1044+ query := req.URL.Query()
1045+
1046+ // multi-key singular support
1047+ if multiGroups, ok := query["allowed_group"]; ok {
1048+ for _, group := range multiGroups {
1049+ groups[group] = struct{}{}
1050+ }
1051+ }
1052+
1053+ // single key plural comma delimited support
1054+ for _, group := range strings.Split(query.Get("allowed_groups"), ",") {
1055+ if group != "" {
1056+ groups[group] = struct{}{}
1057+ }
1058+ }
1059+
1060+ return groups
1061+}
1062+
1019// addHeadersForProxying adds the appropriate headers the request / response for proxying
1063// addHeadersForProxying adds the appropriate headers the request / response for proxying
1020func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1064func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
1021 if session.Email == "" {
1065 if session.Email == "" {
oauthproxy_test.go+95−20
@@ -1197,18 +1197,20 @@ func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1197 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1197 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
1198}
1198}
1199
1199
1200−func NewAuthOnlyEndpointTest(modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
1200+func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
1201 pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...)
1201 pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...)
1202 if err != nil {
1202 if err != nil {
1203 return nil, err
1203 return nil, err
1204 }
1204 }
1205− pcTest.req, _ = http.NewRequest("GET",
1205+ pcTest.req, _ = http.NewRequest(
1206+ "GET",
1206− pcTest.opts.ProxyPrefix+"/auth", nil)
1207+ fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring),
1208+ nil)
1207 return pcTest, nil
1209 return pcTest, nil
1208}
1210}
1209
1211
1210func TestAuthOnlyEndpointAccepted(t *testing.T) {
1212func TestAuthOnlyEndpointAccepted(t *testing.T) {
1211− test, err := NewAuthOnlyEndpointTest()
1213+ test, err := NewAuthOnlyEndpointTest("")
1212 if err != nil {
1214 if err != nil {
1213 t.Fatal(err)
1215 t.Fatal(err)
1214 }
1216 }
@@ -1226,7 +1228,7 @@ func TestAuthOnlyEndpointAccepted(t *testing.T) {
1226}
1228}
1227
1229
1228func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1230func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1229− test, err := NewAuthOnlyEndpointTest()
1231+ test, err := NewAuthOnlyEndpointTest("")
1230 if err != nil {
1232 if err != nil {
1231 t.Fatal(err)
1233 t.Fatal(err)
1232 }
1234 }
@@ -1238,7 +1240,7 @@ func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
1238}
1240}
1239
1241
1240func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1242func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1241− test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
1243+ test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1242 opts.Cookie.Expire = time.Duration(24) * time.Hour
1244 opts.Cookie.Expire = time.Duration(24) * time.Hour
1243 })
1245 })
1244 if err != nil {
1246 if err != nil {
@@ -1258,7 +1260,7 @@ func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {
1258}
1260}
1259
1261
1260func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
1262func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
1261− test, err := NewAuthOnlyEndpointTest()
1263+ test, err := NewAuthOnlyEndpointTest("")
1262 if err != nil {
1264 if err != nil {
1263 t.Fatal(err)
1265 t.Fatal(err)
1264 }
1266 }
@@ -1960,7 +1962,7 @@ func TestGetJwtSession(t *testing.T) {
1960 verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
1962 verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
1961 &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
1963 &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
1962
1964
1963− test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
1965+ test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
1964 opts.InjectRequestHeaders = []options.Header{
1966 opts.InjectRequestHeaders = []options.Header{
1965 {
1967 {
1966 Name: "Authorization",
1968 Name: "Authorization",
@@ -2028,7 +2030,6 @@ func TestGetJwtSession(t *testing.T) {
2028 },
2030 },
2029 },
2031 },
2030 }
2032 }
2031−
2032 opts.SkipJwtBearerTokens = true
2033 opts.SkipJwtBearerTokens = true
2033 opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), verifier))
2034 opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), verifier))
2034 })
2035 })
@@ -2692,32 +2693,106 @@ func TestProxyAllowedGroups(t *testing.T) {
2692}
2693}
2693
2694
2694func TestAuthOnlyAllowedGroups(t *testing.T) {
2695func TestAuthOnlyAllowedGroups(t *testing.T) {
2695− tests := []struct {
2696+ testCases := []struct {
2696 name string
2697 name string
2697 allowedGroups []string
2698 allowedGroups []string
2698 groups []string
2699 groups []string
2700+ querystring string
2699 expectUnauthorized bool
2701 expectUnauthorized bool
2700 }{
2702 }{
2703+ {
2701− {"NoAllowedGroups", []string{}, []string{}, false},
2704+ name: "NoAllowedGroups",
2705+ allowedGroups: []string{},
2706+ groups: []string{},
2707+ querystring: "",
2708+ expectUnauthorized: false,
2709+ },
2710+ {
2702− {"NoAllowedGroupsUserHasGroups", []string{}, []string{"a", "b"}, false},
2711+ name: "NoAllowedGroupsUserHasGroups",
2712+ allowedGroups: []string{},
2713+ groups: []string{"a", "b"},
2714+ querystring: "",
2715+ expectUnauthorized: false,
2716+ },
2717+ {
2703− {"UserInAllowedGroup", []string{"a"}, []string{"a", "b"}, false},
2718+ name: "UserInAllowedGroup",
2719+ allowedGroups: []string{"a"},
2720+ groups: []string{"a", "b"},
2721+ querystring: "",
2722+ expectUnauthorized: false,
2723+ },
2724+ {
2704− {"UserNotInAllowedGroup", []string{"a"}, []string{"c"}, true},
2725+ name: "UserNotInAllowedGroup",
2726+ allowedGroups: []string{"a"},
2727+ groups: []string{"c"},
2728+ querystring: "",
2729+ expectUnauthorized: true,
2730+ },
2731+ {
2732+ name: "UserInQuerystringGroup",
2733+ allowedGroups: []string{"a", "b"},
2734+ groups: []string{"a", "c"},
2735+ querystring: "?allowed_group=a",
2736+ expectUnauthorized: false,
2737+ },
2738+ {
2739+ name: "UserInOnlyQuerystringGroup",
2740+ allowedGroups: []string{},
2741+ groups: []string{"a", "c"},
2742+ querystring: "?allowed_groups=a,b",
2743+ expectUnauthorized: false,
2744+ },
2745+ {
2746+ name: "UserInMultiParamQuerystringGroup",
2747+ allowedGroups: []string{"a", "b"},
2748+ groups: []string{"b"},
2749+ querystring: "?allowed_group=a&allowed_group=b",
2750+ expectUnauthorized: false,
2751+ },
2752+ {
2753+ name: "UserInDelimitedQuerystringGroup",
2754+ allowedGroups: []string{"a", "b", "c"},
2755+ groups: []string{"c"},
2756+ querystring: "?allowed_groups=a,c",
2757+ expectUnauthorized: false,
2758+ },
2759+ {
2760+ name: "UserNotInQuerystringGroup",
2761+ allowedGroups: []string{},
2762+ groups: []string{"c"},
2763+ querystring: "?allowed_group=a&allowed_group=b",
2764+ expectUnauthorized: true,
2765+ },
2766+ {
2767+ name: "UserInConfigGroupNotInQuerystringGroup",
2768+ allowedGroups: []string{"a", "b", "c"},
2769+ groups: []string{"c"},
2770+ querystring: "?allowed_group=a&allowed_group=b",
2771+ expectUnauthorized: true,
2772+ },
2773+ {
2774+ name: "UserInQuerystringGroupNotInConfigGroup",
2775+ allowedGroups: []string{"a", "b"},
2776+ groups: []string{"c"},
2777+ querystring: "?allowed_groups=b,c",
2778+ expectUnauthorized: true,
2779+ },
2705 }
2780 }
2706
2781
2707− for _, tt := range tests {
2782+ for _, tc := range testCases {
2708− t.Run(tt.name, func(t *testing.T) {
2783+ t.Run(tc.name, func(t *testing.T) {
2709 emailAddress := "test"
2784 emailAddress := "test"
2710 created := time.Now()
2785 created := time.Now()
2711
2786
2712 session := &sessions.SessionState{
2787 session := &sessions.SessionState{
2713− Groups: tt.groups,
2788+ Groups: tc.groups,
2714 Email: emailAddress,
2789 Email: emailAddress,
2715 AccessToken: "oauth_token",
2790 AccessToken: "oauth_token",
2716 CreatedAt: &created,
2791 CreatedAt: &created,
2717 }
2792 }
2718
2793
2719− test, err := NewAuthOnlyEndpointTest(func(opts *options.Options) {
2794+ test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {
2720− opts.AllowedGroups = tt.allowedGroups
2795+ opts.AllowedGroups = tc.allowedGroups
2721 })
2796 })
2722 if err != nil {
2797 if err != nil {
2723 t.Fatal(err)
2798 t.Fatal(err)
@@ -2728,7 +2803,7 @@ func TestAuthOnlyAllowedGroups(t *testing.T) {
2728
2803
2729 test.proxy.ServeHTTP(test.rw, test.req)
2804 test.proxy.ServeHTTP(test.rw, test.req)
2730
2805
2731− if tt.expectUnauthorized {
2806+ if tc.expectUnauthorized {
2732 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
2807 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
2733 } else {
2808 } else {
2734 assert.Equal(t, http.StatusAccepted, test.rw.Code)
2809 assert.Equal(t, http.StatusAccepted, test.rw.Code)