git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #1286 from instadeepai/allowed_email_domains-on-auth_request-endpoint
263a5dfJoel Speed
CHANGELOG.md+1
@@ -18,6 +18,7 @@
18- [#1509](https://github.com/oauth2-proxy/oauth2-proxy/pull/1509) Update LoginGovProvider ValidateSession to pass access_token in Header (@pksheldon4)
18- [#1509](https://github.com/oauth2-proxy/oauth2-proxy/pull/1509) Update LoginGovProvider ValidateSession to pass access_token in Header (@pksheldon4)
19- [#1474](https://github.com/oauth2-proxy/oauth2-proxy/pull/1474) Support configuration of minimal acceptable TLS version (@polarctos)
19- [#1474](https://github.com/oauth2-proxy/oauth2-proxy/pull/1474) Support configuration of minimal acceptable TLS version (@polarctos)
20- [#1545](https://github.com/oauth2-proxy/oauth2-proxy/pull/1545) Fix issue with query string allowed group panic on skip methods (@andytson)
20- [#1545](https://github.com/oauth2-proxy/oauth2-proxy/pull/1545) Fix issue with query string allowed group panic on skip methods (@andytson)
21+- [#1286](https://github.com/oauth2-proxy/oauth2-proxy/pull/1286) Add the `allowed_email_domains` and the `allowed_groups` on the `auth_request` + support standard wildcard char for validation with sub-domain and email-domain. (@w3st3ry @armandpicard)
21
22
22# V7.2.1
23# V7.2.1
23
24
docs/docs/configuration/overview.md+2−2
@@ -199,12 +199,12 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
199| `--allowed-role` | string \| list | restrict logins to users with this role (may be given multiple times). Only works with the keycloak-oidc provider. | |
199| `--allowed-role` | string \| list | restrict logins to users with this role (may be given multiple times). Only works with the keycloak-oidc provider. | |
200| `--validate-url` | string | Access token validation endpoint | |
200| `--validate-url` | string | Access token validation endpoint | |
201| `--version` | n/a | print version string | |
201| `--version` | n/a | print version string | |
202−| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (e.g. `.example.com`) \[[2](#footnote2)\] | |
202+| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` or a `*.` to allow subdomains (e.g. `.example.com`, `*.example.com`) \[[2](#footnote2)\] | |
203| `--trusted-ip` | string \| list | list of IPs or CIDR ranges to allow to bypass authentication (may be given multiple times). When combined with `--reverse-proxy` and optionally `--real-client-ip-header` this will evaluate the trust of the IP stored in an HTTP header by a reverse proxy rather than the layer-3/4 remote address. WARNING: trusting IPs has inherent security flaws, especially when obtaining the IP address from an HTTP header (reverse-proxy mode). Use this option only if you understand the risks and how to manage them. | |
203| `--trusted-ip` | string \| list | list of IPs or CIDR ranges to allow to bypass authentication (may be given multiple times). When combined with `--reverse-proxy` and optionally `--real-client-ip-header` this will evaluate the trust of the IP stored in an HTTP header by a reverse proxy rather than the layer-3/4 remote address. WARNING: trusting IPs has inherent security flaws, especially when obtaining the IP address from an HTTP header (reverse-proxy mode). Use this option only if you understand the risks and how to manage them. | |
204
204
205\[<a name="footnote1">1</a>\]: Only these providers support `--cookie-refresh`: GitLab, Google and OIDC
205\[<a name="footnote1">1</a>\]: Only these providers support `--cookie-refresh`: GitLab, Google and OIDC
206
206
207−\[<a name="footnote2">2</a>\]: When using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`.
207+\[<a name="footnote2">2</a>\]: When using the `whitelist-domain` option, any domain prefixed with a `.` or a `*.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`.
208
208
209See below for provider specific options
209See below for provider specific options
210
210
docs/docs/features/endpoints.md+8
@@ -34,3 +34,11 @@ X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page
34(The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.)
34(The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.)
35
35
36BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored.
36BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored.
37+
38+### Auth
39+
40+This endpoint returns 202 Accepted response or a 401 Unauthorized response.
41+
42+It can be configured using the following query parameters query parameters:
43+- `allowed_groups`: comma separated list of allowed groups
44+- `allowed_email_domains`: comma separated list of allowed email domains
oauthproxy.go+55−25
@@ -26,6 +26,7 @@ import (
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
26 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
27 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
28 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
28 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
29+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
29
30
30 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
31 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
31 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
32 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -976,28 +977,72 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
976
977
977// authOnlyAuthorize handles special authorization logic that is only done
978// authOnlyAuthorize handles special authorization logic that is only done
978// on the AuthOnly endpoint for use with Nginx subrequest architectures.
979// on the AuthOnly endpoint for use with Nginx subrequest architectures.
979−//
980−// TODO (@NickMeves): This method is a placeholder to be extended but currently
981−// fails the linter. Remove the nolint when functionality expands.
982−//
983−//nolint:gosimple
984func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
980func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
985 // Allow requests previously allowed to be bypassed
981 // Allow requests previously allowed to be bypassed
986 if s == nil {
982 if s == nil {
987 return true
983 return true
988 }
984 }
989
985
990− // Allow secondary group restrictions based on the `allowed_groups`
986+ constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
991− // querystring parameter
987+ checkAllowedGroups,
992− if !checkAllowedGroups(req, s) {
988+ checkAllowedEmailDomains,
989+ }
990+
991+ for _, constraint := range constraints {
992+ if !constraint(req, s) {
993− return false
993+ return false
994+ }
994 }
995 }
995
996
996 return true
997 return true
997}
998}
998
999
1000+// extractAllowedEntities aims to extract and split allowed entities linked by a key,
1001+// from an HTTP request query. Output is a map[string]struct{} where keys are valuable,
1002+// the goal is to avoid time complexity O(N^2) while finding matches during membership checks.
1003+func extractAllowedEntities(req *http.Request, key string) map[string]struct{} {
1004+ entities := map[string]struct{}{}
1005+
1006+ query := req.URL.Query()
1007+ for _, allowedEntities := range query[key] {
1008+ for _, entity := range strings.Split(allowedEntities, ",") {
1009+ if entity != "" {
1010+ entities[entity] = struct{}{}
1011+ }
1012+ }
1013+ }
1014+
1015+ return entities
1016+}
1017+
1018+// checkAllowedEmailDomains allow email domain restrictions based on the `allowed_email_domains`
1019+// querystring parameter
1020+func checkAllowedEmailDomains(req *http.Request, s *sessionsapi.SessionState) bool {
1021+ allowedEmailDomains := extractAllowedEntities(req, "allowed_email_domains")
1022+ if len(allowedEmailDomains) == 0 {
1023+ return true
1024+ }
1025+
1026+ splitEmail := strings.Split(s.Email, "@")
1027+ if len(splitEmail) != 2 {
1028+ return false
1029+ }
1030+
1031+ endpoint, _ := url.Parse("")
1032+ endpoint.Host = splitEmail[1]
1033+
1034+ allowedEmailDomainsList := []string{}
1035+ for ed := range allowedEmailDomains {
1036+ allowedEmailDomainsList = append(allowedEmailDomainsList, ed)
1037+ }
1038+
1039+ return util.IsEndpointAllowed(endpoint, allowedEmailDomainsList)
1040+}
1041+
1042+// checkAllowedGroups allow secondary group restrictions based on the `allowed_groups`
1043+// querystring parameter
999func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1044func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1000− allowedGroups := extractAllowedGroups(req)
1045+ allowedGroups := extractAllowedEntities(req, "allowed_groups")
1001 if len(allowedGroups) == 0 {
1046 if len(allowedGroups) == 0 {
1002 return true
1047 return true
1003 }
1048 }
@@ -1011,21 +1056,6 @@ func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
1011 return false
1056 return false
1012}
1057}
1013
1058
1014−func extractAllowedGroups(req *http.Request) map[string]struct{} {
1015− groups := map[string]struct{}{}
1016−
1017− query := req.URL.Query()
1018− for _, allowedGroups := range query["allowed_groups"] {
1019− for _, group := range strings.Split(allowedGroups, ",") {
1020− if group != "" {
1021− groups[group] = struct{}{}
1022− }
1023− }
1024− }
1025−
1026− return groups
1027−}
1028−
1029// encodedState builds the OAuth state param out of our nonce and
1059// encodedState builds the OAuth state param out of our nonce and
1030// original application redirect
1060// original application redirect
1031func encodeState(nonce string, redirect string) string {
1061func encodeState(nonce string, redirect string) string {
oauthproxy_test.go+91
@@ -2698,3 +2698,94 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
2698 })
2698 })
2699 }
2699 }
2700}
2700}
2701+
2702+func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
2703+ testCases := []struct {
2704+ name string
2705+ email string
2706+ querystring string
2707+ expectedStatusCode int
2708+ }{
2709+ {
2710+ name: "NotEmailRestriction",
2711+ email: "toto@example.com",
2712+ querystring: "",
2713+ expectedStatusCode: http.StatusAccepted,
2714+ },
2715+ {
2716+ name: "UserInAllowedEmailDomain",
2717+ email: "toto@example.com",
2718+ querystring: "?allowed_email_domains=example.com",
2719+ expectedStatusCode: http.StatusAccepted,
2720+ },
2721+ {
2722+ name: "UserNotInAllowedEmailDomain",
2723+ email: "toto@example.com",
2724+ querystring: "?allowed_email_domains=a.example.com",
2725+ expectedStatusCode: http.StatusForbidden,
2726+ },
2727+ {
2728+ name: "UserInAllowedEmailDomains",
2729+ email: "toto@example.com",
2730+ querystring: "?allowed_email_domains=a.example.com,b.example.com",
2731+ expectedStatusCode: http.StatusForbidden,
2732+ },
2733+ {
2734+ name: "UserInAllowedEmailDomains",
2735+ email: "toto@example.com",
2736+ querystring: "?allowed_email_domains=a.example.com,example.com",
2737+ expectedStatusCode: http.StatusAccepted,
2738+ },
2739+ {
2740+ name: "UserInAllowedEmailDomainWildcard",
2741+ email: "toto@foo.example.com",
2742+ querystring: "?allowed_email_domains=*.example.com",
2743+ expectedStatusCode: http.StatusAccepted,
2744+ },
2745+ {
2746+ name: "UserNotInAllowedEmailDomainWildcard",
2747+ email: "toto@example.com",
2748+ querystring: "?allowed_email_domains=*.a.example.com",
2749+ expectedStatusCode: http.StatusForbidden,
2750+ },
2751+ {
2752+ name: "UserInAllowedEmailDomainsWildcard",
2753+ email: "toto@example.com",
2754+ querystring: "?allowed_email_domains=*.a.example.com,*.b.example.com",
2755+ expectedStatusCode: http.StatusForbidden,
2756+ },
2757+ {
2758+ name: "UserInAllowedEmailDomainsWildcard",
2759+ email: "toto@c.example.com",
2760+ querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
2761+ expectedStatusCode: http.StatusAccepted,
2762+ },
2763+ }
2764+
2765+ for _, tc := range testCases {
2766+ t.Run(tc.name, func(t *testing.T) {
2767+ groups := []string{}
2768+
2769+ created := time.Now()
2770+
2771+ session := &sessions.SessionState{
2772+ Groups: groups,
2773+ Email: tc.email,
2774+ AccessToken: "oauth_token",
2775+ CreatedAt: &created,
2776+ }
2777+
2778+ test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {})
2779+ if err != nil {
2780+ t.Fatal(err)
2781+ }
2782+
2783+ err = test.SaveSession(session)
2784+ assert.NoError(t, err)
2785+
2786+ test.proxy.ServeHTTP(test.rw, test.req)
2787+
2788+ assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
2789+ })
2790+ }
2791+}
pkg/apis/options/options.go+1−1
@@ -124,7 +124,7 @@ func NewFlagSet() *pflag.FlagSet {
124 flagSet.StringSlice("extra-jwt-issuers", []string{}, "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
124 flagSet.StringSlice("extra-jwt-issuers", []string{}, "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
125
125
126 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
126 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
127− flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
127+ flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . or a *. to allow subdomains (eg .example.com, *.example.com)")
128 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
128 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
129 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
129 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
130 flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)")
130 flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)")
pkg/app/redirect/validator.go+4−58
@@ -6,6 +6,8 @@ import (
6 "strings"
6 "strings"
7
7
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
8 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
9+
10+ util "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
9)
11)
10
12
11var (
13var (
@@ -50,28 +52,9 @@ func (v *validator) IsValidRedirect(redirect string) bool {
50 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
52 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
51 return false
53 return false
52 }
54 }
53− redirectHostname := redirectURL.Hostname()
54−
55− for _, allowedDomain := range v.allowedDomains {
56− allowedHost, allowedPort := splitHostPort(allowedDomain)
57− if allowedHost == "" {
58− continue
59− }
60
55
61− if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
56+ if util.IsEndpointAllowed(redirectURL, v.allowedDomains) {
62− (strings.HasPrefix(allowedHost, ".") &&
63− strings.HasSuffix(redirectHostname, allowedHost)) {
64− // the domain names match, now validate the ports
65− // if the whitelisted domain's port is '*', allow all ports
66− // if the whitelisted domain contains a specific port, only allow that port
67− // if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
68− redirectPort := redirectURL.Port()
69− if allowedPort == "*" ||
70− allowedPort == redirectPort ||
71− (allowedPort == "" && redirectPort == "") {
72− return true
57+ return true
73− }
74− }
75 }
58 }
76
59
77 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
60 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
@@ -81,40 +64,3 @@ func (v *validator) IsValidRedirect(redirect string) bool {
81 return false
64 return false
82 }
65 }
83}
66}
84−
85−// splitHostPort separates host and port. If the port is not valid, it returns
86−// the entire input as host, and it doesn't check the validity of the host.
87−// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
88−// *** taken from net/url, modified validOptionalPort() to accept ":*"
89−func splitHostPort(hostport string) (host, port string) {
90− host = hostport
91−
92− colon := strings.LastIndexByte(host, ':')
93− if colon != -1 && validOptionalPort(host[colon:]) {
94− host, port = host[:colon], host[colon+1:]
95− }
96−
97− if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
98− host = host[1 : len(host)-1]
99− }
100−
101− return
102−}
103−
104−// validOptionalPort reports whether port is either an empty string
105−// or matches /^:\d*$/
106−// *** taken from net/url, modified to accept ":*"
107−func validOptionalPort(port string) bool {
108− if port == "" || port == ":*" {
109− return true
110− }
111− if port[0] != ':' {
112− return false
113− }
114− for _, b := range port[1:] {
115− if b < '0' || b > '9' {
116− return false
117− }
118− }
119− return true
120−}
pkg/app/redirect/validator_test.go+20−2
@@ -5,6 +5,7 @@ import (
5 "net/url"
5 "net/url"
6 "os"
6 "os"
7
7
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
8 . "github.com/onsi/ginkgo"
9 . "github.com/onsi/ginkgo"
9 . "github.com/onsi/ginkgo/extensions/table"
10 . "github.com/onsi/ginkgo/extensions/table"
10 . "github.com/onsi/gomega"
11 . "github.com/onsi/gomega"
@@ -22,6 +23,10 @@ var _ = Describe("Validator suite", func() {
22 "anyport.bar:*",
23 "anyport.bar:*",
23 ".sub.anyport.bar:*",
24 ".sub.anyport.bar:*",
24 "www.whitelisteddomain.tld",
25 "www.whitelisteddomain.tld",
26+ "*.wildcard.sub.port.bar:8080",
27+ "*.wildcard.sub.anyport.bar:*",
28+ "*.wildcard.bar",
29+ "*.wildcard.proxy.foo.bar",
25 }
30 }
26 })
31 })
27
32
@@ -96,7 +101,20 @@ var _ = Describe("Validator suite", func() {
96 Entry("Quad Tab 2", "/\t\t\\\t\t/evil.com", false),
101 Entry("Quad Tab 2", "/\t\t\\\t\t/evil.com", false),
97 Entry("Relative Path", "/./\\evil.com", false),
102 Entry("Relative Path", "/./\\evil.com", false),
98 Entry("Relative Subpath", "/./../../\\evil.com", false),
103 Entry("Relative Subpath", "/./../../\\evil.com", false),
99− Entry("Partial Subdomain", "evilbar.foo", false),
104+ Entry("Valid HTTP Wildcard Subdomain", "http://foo.wildcard.bar/redirect", true),
105+ Entry("Valid HTTPS Wildcard Subdomain", "https://foo.wildcard.bar/redirect", true),
106+ Entry("Valid HTTP Wildcard Subdomain Root", "http://wildcard.bar/redirect", true),
107+ Entry("Valid HTTPS Wildcard Subdomain Root", "https://wildcard.bar/redirect", true),
108+ Entry("Valid HTTP Wildcard Subdomain anyport", "http://foo.wildcard.sub.anyport.bar:4242/redirect", true),
109+ Entry("Valid HTTPS Wildcard Subdomain anyport", "https://foo.wildcard.sub.anyport.bar:4242/redirect", true),
110+ Entry("Valid HTTP Wildcard Subdomain Anyport Root", "http://wildcard.sub.anyport.bar:4242/redirect", true),
111+ Entry("Valid HTTPS Wildcard Subdomain Anyport Root", "https://wildcard.sub.anyport.bar:4242/redirect", true),
112+ Entry("Valid HTTP Wildcard Subdomain Defined Port", "http://foo.wildcard.sub.port.bar:8080/redirect", true),
113+ Entry("Valid HTTPS Wildcard Subdomain Defined Port", "https://foo.wildcard.sub.port.bar:8080/redirect", true),
114+ Entry("Valid HTTP Wildcard Subdomain Defined Port Root", "http://wildcard.sub.port.bar:8080/redirect", true),
115+ Entry("Valid HTTPS Wildcard Subdomain Defined Port Root", "https://wildcard.sub.port.bar:8080/redirect", true),
116+ Entry("Missing Protocol Root Domain", "foo.bar/redirect", false),
117+ Entry("Missing Protocol Wildcard Subdomain", "proxy.wildcard.bar/redirect", false),
100 )
118 )
101 })
119 })
102
120
@@ -109,7 +127,7 @@ var _ = Describe("Validator suite", func() {
109
127
110 DescribeTable("Should split the host and port",
128 DescribeTable("Should split the host and port",
111 func(in splitHostPortTableInput) {
129 func(in splitHostPortTableInput) {
112− host, port := splitHostPort(in.hostport)
130+ host, port := util.SplitHostPort(in.hostport)
113 Expect(host).To(Equal(in.expectedHost))
131 Expect(host).To(Equal(in.expectedHost))
114 Expect(port).To(Equal(in.expectedPort))
132 Expect(port).To(Equal(in.expectedPort))
115 },
133 },
pkg/util/util.go+83
@@ -9,6 +9,8 @@ import (
9 "io/ioutil"
9 "io/ioutil"
10 "math/big"
10 "math/big"
11 "net"
11 "net"
12+ "net/url"
13+ "strings"
12 "time"
14 "time"
13)
15)
14
16
@@ -66,3 +68,84 @@ func GenerateCert() ([]byte, []byte, error) {
66 certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
68 certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
67 return certBytes, keyBytes, err
69 return certBytes, keyBytes, err
68}
70}
71+
72+// SplitHostPort separates host and port. If the port is not valid, it returns
73+// the entire input as host, and it doesn't check the validity of the host.
74+// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
75+// *** taken from net/url, modified validOptionalPort() to accept ":*"
76+func SplitHostPort(hostport string) (host, port string) {
77+ host = hostport
78+
79+ colon := strings.LastIndexByte(host, ':')
80+ if colon != -1 && validOptionalPort(host[colon:]) {
81+ host, port = host[:colon], host[colon+1:]
82+ }
83+
84+ if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
85+ host = host[1 : len(host)-1]
86+ }
87+
88+ return
89+}
90+
91+// validOptionalPort reports whether port is either an empty string
92+// or matches /^:\d*$/
93+// *** taken from net/url, modified to accept ":*"
94+func validOptionalPort(port string) bool {
95+ if port == "" || port == ":*" {
96+ return true
97+ }
98+ if port[0] != ':' {
99+ return false
100+ }
101+ for _, b := range port[1:] {
102+ if b < '0' || b > '9' {
103+ return false
104+ }
105+ }
106+ return true
107+}
108+
109+// IsEndpointAllowed checks whether the endpoint URL is allowed based
110+// on an allowed domains list.
111+func IsEndpointAllowed(endpoint *url.URL, allowedDomains []string) bool {
112+ hostname := endpoint.Hostname()
113+
114+ for _, allowedDomain := range allowedDomains {
115+ allowedHost, allowedPort := SplitHostPort(allowedDomain)
116+ if allowedHost == "" {
117+ continue
118+ }
119+
120+ if isHostnameAllowed(hostname, allowedHost) {
121+ // the domain names match, now validate the ports
122+ // if the allowed domain's port is '*', allow all ports
123+ // if the allowed domain contains a specific port, only allow that port
124+ // if the allowed domain doesn't contain a port at all, only allow empty redirect ports ie http and https
125+ redirectPort := endpoint.Port()
126+ if allowedPort == "*" ||
127+ allowedPort == redirectPort ||
128+ (allowedPort == "" && redirectPort == "") {
129+ return true
130+ }
131+ }
132+ }
133+
134+ return false
135+}
136+
137+func isHostnameAllowed(hostname, allowedHost string) bool {
138+ // check if we have a perfect match between hostname and allowedHost
139+ if hostname == strings.TrimPrefix(allowedHost, ".") ||
140+ hostname == strings.TrimPrefix(allowedHost, "*.") {
141+ return true
142+ }
143+
144+ // check if hostname is a sub domain of the allowedHost
145+ if (strings.HasPrefix(allowedHost, ".") && strings.HasSuffix(hostname, allowedHost)) ||
146+ (strings.HasPrefix(allowedHost, "*.") && strings.HasSuffix(hostname, allowedHost[1:])) {
147+ return true
148+ }
149+
150+ return false
151+}
validator.go+5−2
@@ -115,12 +115,15 @@ func isEmailValidWithDomains(email string, allowedDomains []string) bool {
115 return true
115 return true
116 }
116 }
117
117
118− // allow if the domain is prefixed with . and
118+ // allow if the domain is prefixed with . or *. and
119 // the last element (split on @) has the suffix as the domain
119 // the last element (split on @) has the suffix as the domain
120 atoms := strings.Split(email, "@")
120 atoms := strings.Split(email, "@")
121+
122+ if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
121− if strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain) {
123+ (strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
122 return true
124 return true
123 }
125 }
124 }
126 }
127+
125 return false
128 return false
126}
129}
validator_test.go+35
@@ -153,6 +153,13 @@ func TestValidatorCases(t *testing.T) {
153 email: "foo.bar@example0.com",
153 email: "foo.bar@example0.com",
154 expectedAuthZ: false,
154 expectedAuthZ: false,
155 },
155 },
156+ {
157+ name: "EmailNotInCorrect1stSubDomainsNotInEmailsWildcard",
158+ allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
159+ allowedDomains: []string{"*.example0.com", "*.example1.com"},
160+ email: "foo.bar@example0.com",
161+ expectedAuthZ: false,
162+ },
156 {
163 {
157 name: "EmailInFirstDomain",
164 name: "EmailInFirstDomain",
158 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
165 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -160,6 +167,13 @@ func TestValidatorCases(t *testing.T) {
160 email: "foo@bar.example0.com",
167 email: "foo@bar.example0.com",
161 expectedAuthZ: true,
168 expectedAuthZ: true,
162 },
169 },
170+ {
171+ name: "EmailInFirstDomainWildcard",
172+ allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
173+ allowedDomains: []string{"*.example0.com", "*.example1.com"},
174+ email: "foo@bar.example0.com",
175+ expectedAuthZ: true,
176+ },
163 {
177 {
164 name: "EmailNotInCorrect2ndSubDomainsNotInEmails",
178 name: "EmailNotInCorrect2ndSubDomainsNotInEmails",
165 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
179 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -174,6 +188,13 @@ func TestValidatorCases(t *testing.T) {
174 email: "baz@quux.example1.com",
188 email: "baz@quux.example1.com",
175 expectedAuthZ: true,
189 expectedAuthZ: true,
176 },
190 },
191+ {
192+ name: "EmailInSecondDomainWildcard",
193+ allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
194+ allowedDomains: []string{"*.example0.com", "*.example1.com"},
195+ email: "baz@quux.example1.com",
196+ expectedAuthZ: true,
197+ },
177 {
198 {
178 name: "EmailInFirstEmailList",
199 name: "EmailInFirstEmailList",
179 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
200 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -181,6 +202,13 @@ func TestValidatorCases(t *testing.T) {
181 email: "xyzzy@example.com",
202 email: "xyzzy@example.com",
182 expectedAuthZ: true,
203 expectedAuthZ: true,
183 },
204 },
205+ {
206+ name: "EmailInFirstEmailListWildcard",
207+ allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
208+ allowedDomains: []string{"*.example0.com", "*.example1.com"},
209+ email: "xyzzy@example.com",
210+ expectedAuthZ: true,
211+ },
184 {
212 {
185 name: "EmailNotInDomainsNotInEmails",
213 name: "EmailNotInDomainsNotInEmails",
186 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
214 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -370,6 +398,13 @@ func TestValidatorCases(t *testing.T) {
370 allowedDomains: []string{"company.com"},
398 allowedDomains: []string{"company.com"},
371 expectedAuthZ: false,
399 expectedAuthZ: false,
372 },
400 },
401+ {
402+ name: "CheckForEqualityNotSuffixWildcard",
403+ email: "foo@evilcompany.com",
404+ allowedEmails: []string(nil),
405+ allowedDomains: []string{"*.company.com"},
406+ expectedAuthZ: false,
407+ },
373 }
408 }
374
409
375 for _, tc := range testCases {
410 for _, tc := range testCases {