git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add the allowed_email_domains and the allowed_groups on the auth_request endpoint + support standard wildcard char for validation with sub-domain and email-domain.

Signed-off-by: Valentin Pichard <github@w3st.fr>

commit2b4c8a984669605c4b3b4db4874e832078b23e48 authorValentin Pichard <github@w3st.fr> dateWed 28 Jul 2021, 10:12 (5 years ago) parentc5a98c6
11 files changed+305−90
CHANGELOG.md+1 docs/docs/configuration/overview.md+2−2 docs/docs/features/endpoints.md+8 oauthproxy.go+55−25 oauthproxy_test.go+91 pkg/apis/options/options.go+1−1 pkg/app/redirect/validator.go+4−58 pkg/app/redirect/validator_test.go+20−2 pkg/util/util.go+83 validator.go+5−2 validator_test.go+35
CHANGELOG.md+1
@@ -13,6 +13,7 @@
1313- [#1509](https://github.com/oauth2-proxy/oauth2-proxy/pull/1509) Update LoginGovProvider ValidateSession to pass access_token in Header (@pksheldon4)
1414- [#1474](https://github.com/oauth2-proxy/oauth2-proxy/pull/1474) Support configuration of minimal acceptable TLS version (@polarctos)
1515- [#1545](https://github.com/oauth2-proxy/oauth2-proxy/pull/1545) Fix issue with query string allowed group panic on skip methods (@andytson)
16+- [#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)
1617
1718# V7.2.1
1819
docs/docs/configuration/overview.md+2−2
@@ -197,12 +197,12 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
197197| `--allowed-role` | string \| list | restrict logins to users with this role (may be given multiple times). Only works with the keycloak-oidc provider. | |
198198| `--validate-url` | string | Access token validation endpoint | |
199199| `--version` | n/a | print version string | |
200−| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (e.g. `.example.com`)&nbsp;\[[2](#footnote2)\] | |
200+| `--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`)&nbsp;\[[2](#footnote2)\] | |
201201| `--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. | |
202202
203203\[<a name="footnote1">1</a>\]: Only these providers support `--cookie-refresh`: GitLab, Google and OIDC
204204
205−\[<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:*`.
205+\[<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:*`.
206206
207207See below for provider specific options
208208
docs/docs/features/endpoints.md+8
@@ -34,3 +34,11 @@ X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page
3434(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.)
3535
3636BEWARE 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 (
2626 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
2727 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
2828 proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
29+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
2930
3031 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
3132 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -971,28 +972,72 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
971972
972973// authOnlyAuthorize handles special authorization logic that is only done
973974// on the AuthOnly endpoint for use with Nginx subrequest architectures.
974−//
975−// TODO (@NickMeves): This method is a placeholder to be extended but currently
976−// fails the linter. Remove the nolint when functionality expands.
977−//
978−//nolint:gosimple
979975func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
980976 // Allow requests previously allowed to be bypassed
981977 if s == nil {
982978 return true
983979 }
984980
985− // Allow secondary group restrictions based on the `allowed_groups`
986− // querystring parameter
987− if !checkAllowedGroups(req, s) {
988− return false
981+ constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
982+ checkAllowedGroups,
983+ checkAllowedEmailDomains,
984+ }
985+
986+ for _, constraint := range constraints {
987+ if !constraint(req, s) {
988+ return false
989+ }
989990 }
990991
991992 return true
992993}
993994
995+// extractAllowedEntities aims to extract and split allowed entities linked by a key,
996+// from an HTTP request query. Output is a map[string]struct{} where keys are valuable,
997+// the goal is to avoid time complexity O(N^2) while finding matches during membership checks.
998+func extractAllowedEntities(req *http.Request, key string) map[string]struct{} {
999+ entities := map[string]struct{}{}
1000+
1001+ query := req.URL.Query()
1002+ for _, allowedEntities := range query[key] {
1003+ for _, entity := range strings.Split(allowedEntities, ",") {
1004+ if entity != "" {
1005+ entities[entity] = struct{}{}
1006+ }
1007+ }
1008+ }
1009+
1010+ return entities
1011+}
1012+
1013+// checkAllowedEmailDomains allow email domain restrictions based on the `allowed_email_domains`
1014+// querystring parameter
1015+func checkAllowedEmailDomains(req *http.Request, s *sessionsapi.SessionState) bool {
1016+ allowedEmailDomains := extractAllowedEntities(req, "allowed_email_domains")
1017+ if len(allowedEmailDomains) == 0 {
1018+ return true
1019+ }
1020+
1021+ splitEmail := strings.Split(s.Email, "@")
1022+ if len(splitEmail) != 2 {
1023+ return false
1024+ }
1025+
1026+ endpoint, _ := url.Parse("")
1027+ endpoint.Host = splitEmail[1]
1028+
1029+ allowedEmailDomainsList := []string{}
1030+ for ed := range allowedEmailDomains {
1031+ allowedEmailDomainsList = append(allowedEmailDomainsList, ed)
1032+ }
1033+
1034+ return util.IsEndpointAllowed(endpoint, allowedEmailDomainsList)
1035+}
1036+
1037+// checkAllowedGroups allow secondary group restrictions based on the `allowed_groups`
1038+// querystring parameter
9941039func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
995− allowedGroups := extractAllowedGroups(req)
1040+ allowedGroups := extractAllowedEntities(req, "allowed_groups")
9961041 if len(allowedGroups) == 0 {
9971042 return true
9981043 }
@@ -1006,21 +1051,6 @@ func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
10061051 return false
10071052}
10081053
1009−func extractAllowedGroups(req *http.Request) map[string]struct{} {
1010− groups := map[string]struct{}{}
1011−
1012− query := req.URL.Query()
1013− for _, allowedGroups := range query["allowed_groups"] {
1014− for _, group := range strings.Split(allowedGroups, ",") {
1015− if group != "" {
1016− groups[group] = struct{}{}
1017− }
1018− }
1019− }
1020−
1021− return groups
1022−}
1023−
10241054// encodedState builds the OAuth state param out of our nonce and
10251055// original application redirect
10261056func encodeState(nonce string, redirect string) string {
oauthproxy_test.go+91
@@ -2683,3 +2683,94 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
26832683 })
26842684 }
26852685}
2686+
2687+func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
2688+ testCases := []struct {
2689+ name string
2690+ email string
2691+ querystring string
2692+ expectedStatusCode int
2693+ }{
2694+ {
2695+ name: "NotEmailRestriction",
2696+ email: "toto@example.com",
2697+ querystring: "",
2698+ expectedStatusCode: http.StatusAccepted,
2699+ },
2700+ {
2701+ name: "UserInAllowedEmailDomain",
2702+ email: "toto@example.com",
2703+ querystring: "?allowed_email_domains=example.com",
2704+ expectedStatusCode: http.StatusAccepted,
2705+ },
2706+ {
2707+ name: "UserNotInAllowedEmailDomain",
2708+ email: "toto@example.com",
2709+ querystring: "?allowed_email_domains=a.example.com",
2710+ expectedStatusCode: http.StatusForbidden,
2711+ },
2712+ {
2713+ name: "UserInAllowedEmailDomains",
2714+ email: "toto@example.com",
2715+ querystring: "?allowed_email_domains=a.example.com,b.example.com",
2716+ expectedStatusCode: http.StatusForbidden,
2717+ },
2718+ {
2719+ name: "UserInAllowedEmailDomains",
2720+ email: "toto@example.com",
2721+ querystring: "?allowed_email_domains=a.example.com,example.com",
2722+ expectedStatusCode: http.StatusAccepted,
2723+ },
2724+ {
2725+ name: "UserInAllowedEmailDomainWildcard",
2726+ email: "toto@foo.example.com",
2727+ querystring: "?allowed_email_domains=*.example.com",
2728+ expectedStatusCode: http.StatusAccepted,
2729+ },
2730+ {
2731+ name: "UserNotInAllowedEmailDomainWildcard",
2732+ email: "toto@example.com",
2733+ querystring: "?allowed_email_domains=*.a.example.com",
2734+ expectedStatusCode: http.StatusForbidden,
2735+ },
2736+ {
2737+ name: "UserInAllowedEmailDomainsWildcard",
2738+ email: "toto@example.com",
2739+ querystring: "?allowed_email_domains=*.a.example.com,*.b.example.com",
2740+ expectedStatusCode: http.StatusForbidden,
2741+ },
2742+ {
2743+ name: "UserInAllowedEmailDomainsWildcard",
2744+ email: "toto@c.example.com",
2745+ querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com",
2746+ expectedStatusCode: http.StatusAccepted,
2747+ },
2748+ }
2749+
2750+ for _, tc := range testCases {
2751+ t.Run(tc.name, func(t *testing.T) {
2752+ groups := []string{}
2753+
2754+ created := time.Now()
2755+
2756+ session := &sessions.SessionState{
2757+ Groups: groups,
2758+ Email: tc.email,
2759+ AccessToken: "oauth_token",
2760+ CreatedAt: &created,
2761+ }
2762+
2763+ test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {})
2764+ if err != nil {
2765+ t.Fatal(err)
2766+ }
2767+
2768+ err = test.SaveSession(session)
2769+ assert.NoError(t, err)
2770+
2771+ test.proxy.ServeHTTP(test.rw, test.req)
2772+
2773+ assert.Equal(t, tc.expectedStatusCode, test.rw.Code)
2774+ })
2775+ }
2776+}
pkg/apis/options/options.go+1−1
@@ -126,7 +126,7 @@ func NewFlagSet() *pflag.FlagSet {
126126 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)")
127127
128128 flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
129− flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
129+ 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)")
130130 flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
131131 flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
132132 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 (
66 "strings"
77
88 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
9+
10+ util "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
911)
1012
1113var (
@@ -50,28 +52,9 @@ func (v *validator) IsValidRedirect(redirect string) bool {
5052 logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
5153 return false
5254 }
53− redirectHostname := redirectURL.Hostname()
54−
55− for _, allowedDomain := range v.allowedDomains {
56− allowedHost, allowedPort := splitHostPort(allowedDomain)
57− if allowedHost == "" {
58− continue
59− }
6055
61− if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
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
73− }
74− }
56+ if util.IsEndpointAllowed(redirectURL, v.allowedDomains) {
57+ return true
7558 }
7659
7760 logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
@@ -81,40 +64,3 @@ func (v *validator) IsValidRedirect(redirect string) bool {
8164 return false
8265 }
8366}
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 (
55 "net/url"
66 "os"
77
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
89 . "github.com/onsi/ginkgo"
910 . "github.com/onsi/ginkgo/extensions/table"
1011 . "github.com/onsi/gomega"
@@ -22,6 +23,10 @@ var _ = Describe("Validator suite", func() {
2223 "anyport.bar:*",
2324 ".sub.anyport.bar:*",
2425 "www.whitelisteddomain.tld",
26+ "*.wildcard.sub.port.bar:8080",
27+ "*.wildcard.sub.anyport.bar:*",
28+ "*.wildcard.bar",
29+ "*.wildcard.proxy.foo.bar",
2530 }
2631 })
2732
@@ -96,7 +101,20 @@ var _ = Describe("Validator suite", func() {
96101 Entry("Quad Tab 2", "/\t\t\\\t\t/evil.com", false),
97102 Entry("Relative Path", "/./\\evil.com", false),
98103 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),
100118 )
101119 })
102120
@@ -109,7 +127,7 @@ var _ = Describe("Validator suite", func() {
109127
110128 DescribeTable("Should split the host and port",
111129 func(in splitHostPortTableInput) {
112− host, port := splitHostPort(in.hostport)
130+ host, port := util.SplitHostPort(in.hostport)
113131 Expect(host).To(Equal(in.expectedHost))
114132 Expect(port).To(Equal(in.expectedPort))
115133 },
pkg/util/util.go+83
@@ -9,6 +9,8 @@ import (
99 "io/ioutil"
1010 "math/big"
1111 "net"
12+ "net/url"
13+ "strings"
1214 "time"
1315)
1416
@@ -66,3 +68,84 @@ func GenerateCert() ([]byte, []byte, error) {
6668 certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
6769 return certBytes, keyBytes, err
6870}
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 {
115115 return true
116116 }
117117
118− // allow if the domain is prefixed with . and
118+ // allow if the domain is prefixed with . or *. and
119119 // the last element (split on @) has the suffix as the domain
120120 atoms := strings.Split(email, "@")
121− if strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain) {
121+
122+ if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
123+ (strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
122124 return true
123125 }
124126 }
127+
125128 return false
126129}
validator_test.go+35
@@ -153,6 +153,13 @@ func TestValidatorCases(t *testing.T) {
153153 email: "foo.bar@example0.com",
154154 expectedAuthZ: false,
155155 },
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+ },
156163 {
157164 name: "EmailInFirstDomain",
158165 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -160,6 +167,13 @@ func TestValidatorCases(t *testing.T) {
160167 email: "foo@bar.example0.com",
161168 expectedAuthZ: true,
162169 },
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+ },
163177 {
164178 name: "EmailNotInCorrect2ndSubDomainsNotInEmails",
165179 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -174,6 +188,13 @@ func TestValidatorCases(t *testing.T) {
174188 email: "baz@quux.example1.com",
175189 expectedAuthZ: true,
176190 },
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+ },
177198 {
178199 name: "EmailInFirstEmailList",
179200 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -181,6 +202,13 @@ func TestValidatorCases(t *testing.T) {
181202 email: "xyzzy@example.com",
182203 expectedAuthZ: true,
183204 },
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+ },
184212 {
185213 name: "EmailNotInDomainsNotInEmails",
186214 allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
@@ -370,6 +398,13 @@ func TestValidatorCases(t *testing.T) {
370398 allowedDomains: []string{"company.com"},
371399 expectedAuthZ: false,
372400 },
401+ {
402+ name: "CheckForEqualityNotSuffixWildcard",
403+ email: "foo@evilcompany.com",
404+ allowedEmails: []string(nil),
405+ allowedDomains: []string{"*.company.com"},
406+ expectedAuthZ: false,
407+ },
373408 }
374409
375410 for _, tc := range testCases {