Repository: oauth2-proxy/oauth2-proxy
Pull request #1595 (2022-03-18 11:00:12)

Add allowed_emails option to the auth endpoint query string
<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->

This change allows specifying users who are allowed to access a site in the query string of the `/auth` endpoint

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

It is already possible to specify email domains and user groups in the query string, at our company we would like to specify individual users as well.

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

I have created an nginx configuration which adds this query parameter to the auth endpoint. Users specified in the query string were able to access the site, users who weren't landed on the nginx's 403 error page.

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My change requires a change to the documentation or CHANGELOG.
- [x] I have updated the documentation/CHANGELOG accordingly.
- [x] I have created a feature (non-master) branch for my PR.


braunsonm: I like dis. Please add to the changelog and tests if possible
zv0n: @braunsonm I've added some tests and updated changelog

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 831cd190..698141f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ If you are using an architecture specific tag (ex: v7.2.1-arm64) you should move
 
 ## Changes since v7.2.1
 
+- [#1595](https://github.com/oauth2-proxy/oauth2-proxy/pull/1595) Add optional `allowed_emails` query parameter to the `auth_request`. (@zv0n)
 - [#1478](https://github.com/oauth2-proxy/oauth2-proxy/pull/1478) Parameterise the runtime image (@omBratteng)
 - [#1583](https://github.com/oauth2-proxy/oauth2-proxy/pull/1583) Add groups to session too when creating session from bearer token (@adriananeci)
 - [#1418](https://github.com/oauth2-proxy/oauth2-proxy/pull/1418) Support for passing arbitrary query parameters through from `/oauth2/start` to the identity provider's login URL. Configuration settings control which parameters are passed by default and precisely which values can be overridden per-request (@ianroberts)
diff --git a/docs/docs/features/endpoints.md b/docs/docs/features/endpoints.md
index 4832a5de..61e1107b 100644
--- a/docs/docs/features/endpoints.md
+++ b/docs/docs/features/endpoints.md
@@ -41,4 +41,5 @@ This endpoint returns 202 Accepted response or a 401 Unauthorized response.
 
 It can be configured using the following query parameters query parameters:
 - `allowed_groups`: comma separated list of allowed groups
-- `allowed_email_domains`: comma separated list of allowed email domains
\ No newline at end of file
+- `allowed_email_domains`: comma separated list of allowed email domains
+- `allowed_emails`: comma separated list of allowed emails
\ No newline at end of file
diff --git a/oauthproxy.go b/oauthproxy.go
index 308f806b..a571fcab 100644
--- a/oauthproxy.go
+++ b/oauthproxy.go
@@ -1021,6 +1021,7 @@ func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
 	constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
 		checkAllowedGroups,
 		checkAllowedEmailDomains,
+		checkAllowedEmails,
 	}
 
 	for _, constraint := range constraints {
@@ -1091,6 +1092,26 @@ func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
 	return false
 }
 
+// checkAllowedEmails allow email restrictions based on the `allowed_emails`
+// querystring parameter
+func checkAllowedEmails(req *http.Request, s *sessionsapi.SessionState) bool {
+	allowedEmails := extractAllowedEntities(req, "allowed_emails")
+	if len(allowedEmails) == 0 {
+		return true
+	}
+
+	allowed := false
+
+	for email := range allowedEmails {
+		if email == s.Email {
+			allowed = true
+			break
+		}
+	}
+
+	return allowed
+}
+
 // encodedState builds the OAuth state param out of our nonce and
 // original application redirect
 func encodeState(nonce string, redirect string) string {


Test patch:
diff --git a/oauthproxy_test.go b/oauthproxy_test.go
index 90b27d59..25d23b45 100644
--- a/oauthproxy_test.go
+++ b/oauthproxy_test.go
@@ -2725,7 +2725,7 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
 			expectedStatusCode: http.StatusForbidden,
 		},
 		{
-			name:               "UserInAllowedEmailDomains",
+			name:               "UserNotInAllowedEmailDomains",
 			email:              "toto@example.com",
 			querystring:        "?allowed_email_domains=a.example.com,b.example.com",
 			expectedStatusCode: http.StatusForbidden,
@@ -2789,3 +2789,70 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
 		})
 	}
 }
+
+func TestAuthOnlyAllowedEmails(t *testing.T) {
+	testCases := []struct {
+		name               string
+		email              string
+		querystring        string
+		expectedStatusCode int
+	}{
+		{
+			name:               "NotEmailRestriction",
+			email:              "toto@example.com",
+			querystring:        "",
+			expectedStatusCode: http.StatusAccepted,
+		},
+		{
+			name:               "UserInAllowedEmail",
+			email:              "toto@example.com",
+			querystring:        "?allowed_emails=toto@example.com",
+			expectedStatusCode: http.StatusAccepted,
+		},
+		{
+			name:               "UserNotInAllowedEmail",
+			email:              "toto@example.com",
+			querystring:        "?allowed_emails=tete@example.com",
+			expectedStatusCode: http.StatusForbidden,
+		},
+		{
+			name:               "UserNotInAllowedEmails",
+			email:              "toto@example.com",
+			querystring:        "?allowed_emails=tete@example.com,tutu@example.com",
+			expectedStatusCode: http.StatusForbidden,
+		},
+		{
+			name:               "UserInAllowedEmails",
+			email:              "toto@example.com",
+			querystring:        "?allowed_emails=tete@example.com,toto@example.com",
+			expectedStatusCode: http.StatusAccepted,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			groups := []string{}
+
+			created := time.Now()
+
+			session := &sessions.SessionState{
+				Groups:      groups,
+				Email:       tc.email,
+				AccessToken: "oauth_token",
+				CreatedAt:   &created,
+			}
+
+			test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			err = test.SaveSession(session)
+			assert.NoError(t, err)
+
+			test.proxy.ServeHTTP(test.rw, test.req)
+
+			assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
+		})
+	}
+}