mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2024-11-28 09:08:44 +02:00
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>
This commit is contained in:
parent
c5a98c6d03
commit
2b4c8a9846
@ -13,6 +13,7 @@
|
|||||||
- [#1509](https://github.com/oauth2-proxy/oauth2-proxy/pull/1509) Update LoginGovProvider ValidateSession to pass access_token in Header (@pksheldon4)
|
- [#1509](https://github.com/oauth2-proxy/oauth2-proxy/pull/1509) Update LoginGovProvider ValidateSession to pass access_token in Header (@pksheldon4)
|
||||||
- [#1474](https://github.com/oauth2-proxy/oauth2-proxy/pull/1474) Support configuration of minimal acceptable TLS version (@polarctos)
|
- [#1474](https://github.com/oauth2-proxy/oauth2-proxy/pull/1474) Support configuration of minimal acceptable TLS version (@polarctos)
|
||||||
- [#1545](https://github.com/oauth2-proxy/oauth2-proxy/pull/1545) Fix issue with query string allowed group panic on skip methods (@andytson)
|
- [#1545](https://github.com/oauth2-proxy/oauth2-proxy/pull/1545) Fix issue with query string allowed group panic on skip methods (@andytson)
|
||||||
|
- [#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)
|
||||||
|
|
||||||
# V7.2.1
|
# V7.2.1
|
||||||
|
|
||||||
|
@ -197,12 +197,12 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
|
|||||||
| `--allowed-role` | string \| list | restrict logins to users with this role (may be given multiple times). Only works with the keycloak-oidc provider. | |
|
| `--allowed-role` | string \| list | restrict logins to users with this role (may be given multiple times). Only works with the keycloak-oidc provider. | |
|
||||||
| `--validate-url` | string | Access token validation endpoint | |
|
| `--validate-url` | string | Access token validation endpoint | |
|
||||||
| `--version` | n/a | print version string | |
|
| `--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`) \[[2](#footnote2)\] | |
|
| `--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)\] | |
|
||||||
| `--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. | |
|
| `--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. | |
|
||||||
|
|
||||||
\[<a name="footnote1">1</a>\]: Only these providers support `--cookie-refresh`: GitLab, Google and OIDC
|
\[<a name="footnote1">1</a>\]: Only these providers support `--cookie-refresh`: GitLab, Google and OIDC
|
||||||
|
|
||||||
\[<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:*`.
|
\[<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:*`.
|
||||||
|
|
||||||
See below for provider specific options
|
See below for provider specific options
|
||||||
|
|
||||||
|
@ -34,3 +34,11 @@ X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page
|
|||||||
(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.)
|
(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.)
|
||||||
|
|
||||||
BEWARE 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.
|
BEWARE 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.
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
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
|
@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
|
||||||
proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
|
proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
|
||||||
|
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||||
@ -971,28 +972,72 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
|
|||||||
|
|
||||||
// authOnlyAuthorize handles special authorization logic that is only done
|
// authOnlyAuthorize handles special authorization logic that is only done
|
||||||
// on the AuthOnly endpoint for use with Nginx subrequest architectures.
|
// on the AuthOnly endpoint for use with Nginx subrequest architectures.
|
||||||
//
|
|
||||||
// TODO (@NickMeves): This method is a placeholder to be extended but currently
|
|
||||||
// fails the linter. Remove the nolint when functionality expands.
|
|
||||||
//
|
|
||||||
//nolint:gosimple
|
|
||||||
func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
|
func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool {
|
||||||
// Allow requests previously allowed to be bypassed
|
// Allow requests previously allowed to be bypassed
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow secondary group restrictions based on the `allowed_groups`
|
constraints := []func(*http.Request, *sessionsapi.SessionState) bool{
|
||||||
// querystring parameter
|
checkAllowedGroups,
|
||||||
if !checkAllowedGroups(req, s) {
|
checkAllowedEmailDomains,
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
for _, constraint := range constraints {
|
||||||
|
if !constraint(req, s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractAllowedEntities aims to extract and split allowed entities linked by a key,
|
||||||
|
// from an HTTP request query. Output is a map[string]struct{} where keys are valuable,
|
||||||
|
// the goal is to avoid time complexity O(N^2) while finding matches during membership checks.
|
||||||
|
func extractAllowedEntities(req *http.Request, key string) map[string]struct{} {
|
||||||
|
entities := map[string]struct{}{}
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
for _, allowedEntities := range query[key] {
|
||||||
|
for _, entity := range strings.Split(allowedEntities, ",") {
|
||||||
|
if entity != "" {
|
||||||
|
entities[entity] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAllowedEmailDomains allow email domain restrictions based on the `allowed_email_domains`
|
||||||
|
// querystring parameter
|
||||||
|
func checkAllowedEmailDomains(req *http.Request, s *sessionsapi.SessionState) bool {
|
||||||
|
allowedEmailDomains := extractAllowedEntities(req, "allowed_email_domains")
|
||||||
|
if len(allowedEmailDomains) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
splitEmail := strings.Split(s.Email, "@")
|
||||||
|
if len(splitEmail) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, _ := url.Parse("")
|
||||||
|
endpoint.Host = splitEmail[1]
|
||||||
|
|
||||||
|
allowedEmailDomainsList := []string{}
|
||||||
|
for ed := range allowedEmailDomains {
|
||||||
|
allowedEmailDomainsList = append(allowedEmailDomainsList, ed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.IsEndpointAllowed(endpoint, allowedEmailDomainsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAllowedGroups allow secondary group restrictions based on the `allowed_groups`
|
||||||
|
// querystring parameter
|
||||||
func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
|
func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
|
||||||
allowedGroups := extractAllowedGroups(req)
|
allowedGroups := extractAllowedEntities(req, "allowed_groups")
|
||||||
if len(allowedGroups) == 0 {
|
if len(allowedGroups) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -1006,21 +1051,6 @@ func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractAllowedGroups(req *http.Request) map[string]struct{} {
|
|
||||||
groups := map[string]struct{}{}
|
|
||||||
|
|
||||||
query := req.URL.Query()
|
|
||||||
for _, allowedGroups := range query["allowed_groups"] {
|
|
||||||
for _, group := range strings.Split(allowedGroups, ",") {
|
|
||||||
if group != "" {
|
|
||||||
groups[group] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodedState builds the OAuth state param out of our nonce and
|
// encodedState builds the OAuth state param out of our nonce and
|
||||||
// original application redirect
|
// original application redirect
|
||||||
func encodeState(nonce string, redirect string) string {
|
func encodeState(nonce string, redirect string) string {
|
||||||
|
@ -2683,3 +2683,94 @@ func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
email string
|
||||||
|
querystring string
|
||||||
|
expectedStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "NotEmailRestriction",
|
||||||
|
email: "toto@example.com",
|
||||||
|
querystring: "",
|
||||||
|
expectedStatusCode: http.StatusAccepted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserInAllowedEmailDomain",
|
||||||
|
email: "toto@example.com",
|
||||||
|
querystring: "?allowed_email_domains=example.com",
|
||||||
|
expectedStatusCode: http.StatusAccepted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserNotInAllowedEmailDomain",
|
||||||
|
email: "toto@example.com",
|
||||||
|
querystring: "?allowed_email_domains=a.example.com",
|
||||||
|
expectedStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserInAllowedEmailDomains",
|
||||||
|
email: "toto@example.com",
|
||||||
|
querystring: "?allowed_email_domains=a.example.com,b.example.com",
|
||||||
|
expectedStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserInAllowedEmailDomains",
|
||||||
|
email: "toto@example.com",
|
||||||
|
querystring: "?allowed_email_domains=a.example.com,example.com",
|
||||||
|
expectedStatusCode: http.StatusAccepted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserInAllowedEmailDomainWildcard",
|
||||||
|
email: "toto@foo.example.com",
|
||||||
|
querystring: "?allowed_email_domains=*.example.com",
|
||||||
|
expectedStatusCode: http.StatusAccepted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserNotInAllowedEmailDomainWildcard",
|
||||||
|
email: "toto@example.com",
|
||||||
|
querystring: "?allowed_email_domains=*.a.example.com",
|
||||||
|
expectedStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserInAllowedEmailDomainsWildcard",
|
||||||
|
email: "toto@example.com",
|
||||||
|
querystring: "?allowed_email_domains=*.a.example.com,*.b.example.com",
|
||||||
|
expectedStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UserInAllowedEmailDomainsWildcard",
|
||||||
|
email: "toto@c.example.com",
|
||||||
|
querystring: "?allowed_email_domains=a.b.c.example.com,*.c.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -126,7 +126,7 @@ func NewFlagSet() *pflag.FlagSet {
|
|||||||
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)")
|
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)")
|
||||||
|
|
||||||
flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
||||||
flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
|
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)")
|
||||||
flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
|
flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
|
||||||
flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
|
flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption")
|
||||||
flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)")
|
flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)")
|
||||||
|
@ -6,6 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||||
|
|
||||||
|
util "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -50,28 +52,9 @@ func (v *validator) IsValidRedirect(redirect string) bool {
|
|||||||
logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
|
logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
redirectHostname := redirectURL.Hostname()
|
|
||||||
|
|
||||||
for _, allowedDomain := range v.allowedDomains {
|
if util.IsEndpointAllowed(redirectURL, v.allowedDomains) {
|
||||||
allowedHost, allowedPort := splitHostPort(allowedDomain)
|
return true
|
||||||
if allowedHost == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if redirectHostname == strings.TrimPrefix(allowedHost, ".") ||
|
|
||||||
(strings.HasPrefix(allowedHost, ".") &&
|
|
||||||
strings.HasSuffix(redirectHostname, allowedHost)) {
|
|
||||||
// the domain names match, now validate the ports
|
|
||||||
// if the whitelisted domain's port is '*', allow all ports
|
|
||||||
// if the whitelisted domain contains a specific port, only allow that port
|
|
||||||
// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
|
|
||||||
redirectPort := redirectURL.Port()
|
|
||||||
if allowedPort == "*" ||
|
|
||||||
allowedPort == redirectPort ||
|
|
||||||
(allowedPort == "" && redirectPort == "") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
|
logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
|
||||||
@ -81,40 +64,3 @@ func (v *validator) IsValidRedirect(redirect string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitHostPort separates host and port. If the port is not valid, it returns
|
|
||||||
// the entire input as host, and it doesn't check the validity of the host.
|
|
||||||
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
|
|
||||||
// *** taken from net/url, modified validOptionalPort() to accept ":*"
|
|
||||||
func splitHostPort(hostport string) (host, port string) {
|
|
||||||
host = hostport
|
|
||||||
|
|
||||||
colon := strings.LastIndexByte(host, ':')
|
|
||||||
if colon != -1 && validOptionalPort(host[colon:]) {
|
|
||||||
host, port = host[:colon], host[colon+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
|
||||||
host = host[1 : len(host)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// validOptionalPort reports whether port is either an empty string
|
|
||||||
// or matches /^:\d*$/
|
|
||||||
// *** taken from net/url, modified to accept ":*"
|
|
||||||
func validOptionalPort(port string) bool {
|
|
||||||
if port == "" || port == ":*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if port[0] != ':' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, b := range port[1:] {
|
|
||||||
if b < '0' || b > '9' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/ginkgo/extensions/table"
|
. "github.com/onsi/ginkgo/extensions/table"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
@ -22,6 +23,10 @@ var _ = Describe("Validator suite", func() {
|
|||||||
"anyport.bar:*",
|
"anyport.bar:*",
|
||||||
".sub.anyport.bar:*",
|
".sub.anyport.bar:*",
|
||||||
"www.whitelisteddomain.tld",
|
"www.whitelisteddomain.tld",
|
||||||
|
"*.wildcard.sub.port.bar:8080",
|
||||||
|
"*.wildcard.sub.anyport.bar:*",
|
||||||
|
"*.wildcard.bar",
|
||||||
|
"*.wildcard.proxy.foo.bar",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -96,7 +101,20 @@ var _ = Describe("Validator suite", func() {
|
|||||||
Entry("Quad Tab 2", "/\t\t\\\t\t/evil.com", false),
|
Entry("Quad Tab 2", "/\t\t\\\t\t/evil.com", false),
|
||||||
Entry("Relative Path", "/./\\evil.com", false),
|
Entry("Relative Path", "/./\\evil.com", false),
|
||||||
Entry("Relative Subpath", "/./../../\\evil.com", false),
|
Entry("Relative Subpath", "/./../../\\evil.com", false),
|
||||||
Entry("Partial Subdomain", "evilbar.foo", false),
|
Entry("Valid HTTP Wildcard Subdomain", "http://foo.wildcard.bar/redirect", true),
|
||||||
|
Entry("Valid HTTPS Wildcard Subdomain", "https://foo.wildcard.bar/redirect", true),
|
||||||
|
Entry("Valid HTTP Wildcard Subdomain Root", "http://wildcard.bar/redirect", true),
|
||||||
|
Entry("Valid HTTPS Wildcard Subdomain Root", "https://wildcard.bar/redirect", true),
|
||||||
|
Entry("Valid HTTP Wildcard Subdomain anyport", "http://foo.wildcard.sub.anyport.bar:4242/redirect", true),
|
||||||
|
Entry("Valid HTTPS Wildcard Subdomain anyport", "https://foo.wildcard.sub.anyport.bar:4242/redirect", true),
|
||||||
|
Entry("Valid HTTP Wildcard Subdomain Anyport Root", "http://wildcard.sub.anyport.bar:4242/redirect", true),
|
||||||
|
Entry("Valid HTTPS Wildcard Subdomain Anyport Root", "https://wildcard.sub.anyport.bar:4242/redirect", true),
|
||||||
|
Entry("Valid HTTP Wildcard Subdomain Defined Port", "http://foo.wildcard.sub.port.bar:8080/redirect", true),
|
||||||
|
Entry("Valid HTTPS Wildcard Subdomain Defined Port", "https://foo.wildcard.sub.port.bar:8080/redirect", true),
|
||||||
|
Entry("Valid HTTP Wildcard Subdomain Defined Port Root", "http://wildcard.sub.port.bar:8080/redirect", true),
|
||||||
|
Entry("Valid HTTPS Wildcard Subdomain Defined Port Root", "https://wildcard.sub.port.bar:8080/redirect", true),
|
||||||
|
Entry("Missing Protocol Root Domain", "foo.bar/redirect", false),
|
||||||
|
Entry("Missing Protocol Wildcard Subdomain", "proxy.wildcard.bar/redirect", false),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -109,7 +127,7 @@ var _ = Describe("Validator suite", func() {
|
|||||||
|
|
||||||
DescribeTable("Should split the host and port",
|
DescribeTable("Should split the host and port",
|
||||||
func(in splitHostPortTableInput) {
|
func(in splitHostPortTableInput) {
|
||||||
host, port := splitHostPort(in.hostport)
|
host, port := util.SplitHostPort(in.hostport)
|
||||||
Expect(host).To(Equal(in.expectedHost))
|
Expect(host).To(Equal(in.expectedHost))
|
||||||
Expect(port).To(Equal(in.expectedPort))
|
Expect(port).To(Equal(in.expectedPort))
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,3 +68,84 @@ func GenerateCert() ([]byte, []byte, error) {
|
|||||||
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
return certBytes, keyBytes, err
|
return certBytes, keyBytes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SplitHostPort separates host and port. If the port is not valid, it returns
|
||||||
|
// the entire input as host, and it doesn't check the validity of the host.
|
||||||
|
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
|
||||||
|
// *** taken from net/url, modified validOptionalPort() to accept ":*"
|
||||||
|
func SplitHostPort(hostport string) (host, port string) {
|
||||||
|
host = hostport
|
||||||
|
|
||||||
|
colon := strings.LastIndexByte(host, ':')
|
||||||
|
if colon != -1 && validOptionalPort(host[colon:]) {
|
||||||
|
host, port = host[:colon], host[colon+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||||
|
host = host[1 : len(host)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validOptionalPort reports whether port is either an empty string
|
||||||
|
// or matches /^:\d*$/
|
||||||
|
// *** taken from net/url, modified to accept ":*"
|
||||||
|
func validOptionalPort(port string) bool {
|
||||||
|
if port == "" || port == ":*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if port[0] != ':' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, b := range port[1:] {
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEndpointAllowed checks whether the endpoint URL is allowed based
|
||||||
|
// on an allowed domains list.
|
||||||
|
func IsEndpointAllowed(endpoint *url.URL, allowedDomains []string) bool {
|
||||||
|
hostname := endpoint.Hostname()
|
||||||
|
|
||||||
|
for _, allowedDomain := range allowedDomains {
|
||||||
|
allowedHost, allowedPort := SplitHostPort(allowedDomain)
|
||||||
|
if allowedHost == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHostnameAllowed(hostname, allowedHost) {
|
||||||
|
// the domain names match, now validate the ports
|
||||||
|
// if the allowed domain's port is '*', allow all ports
|
||||||
|
// if the allowed domain contains a specific port, only allow that port
|
||||||
|
// if the allowed domain doesn't contain a port at all, only allow empty redirect ports ie http and https
|
||||||
|
redirectPort := endpoint.Port()
|
||||||
|
if allowedPort == "*" ||
|
||||||
|
allowedPort == redirectPort ||
|
||||||
|
(allowedPort == "" && redirectPort == "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHostnameAllowed(hostname, allowedHost string) bool {
|
||||||
|
// check if we have a perfect match between hostname and allowedHost
|
||||||
|
if hostname == strings.TrimPrefix(allowedHost, ".") ||
|
||||||
|
hostname == strings.TrimPrefix(allowedHost, "*.") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if hostname is a sub domain of the allowedHost
|
||||||
|
if (strings.HasPrefix(allowedHost, ".") && strings.HasSuffix(hostname, allowedHost)) ||
|
||||||
|
(strings.HasPrefix(allowedHost, "*.") && strings.HasSuffix(hostname, allowedHost[1:])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -115,12 +115,15 @@ func isEmailValidWithDomains(email string, allowedDomains []string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow if the domain is prefixed with . and
|
// allow if the domain is prefixed with . or *. and
|
||||||
// the last element (split on @) has the suffix as the domain
|
// the last element (split on @) has the suffix as the domain
|
||||||
atoms := strings.Split(email, "@")
|
atoms := strings.Split(email, "@")
|
||||||
if strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain) {
|
|
||||||
|
if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
|
||||||
|
(strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -153,6 +153,13 @@ func TestValidatorCases(t *testing.T) {
|
|||||||
email: "foo.bar@example0.com",
|
email: "foo.bar@example0.com",
|
||||||
expectedAuthZ: false,
|
expectedAuthZ: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "EmailNotInCorrect1stSubDomainsNotInEmailsWildcard",
|
||||||
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
|
allowedDomains: []string{"*.example0.com", "*.example1.com"},
|
||||||
|
email: "foo.bar@example0.com",
|
||||||
|
expectedAuthZ: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "EmailInFirstDomain",
|
name: "EmailInFirstDomain",
|
||||||
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
@ -160,6 +167,13 @@ func TestValidatorCases(t *testing.T) {
|
|||||||
email: "foo@bar.example0.com",
|
email: "foo@bar.example0.com",
|
||||||
expectedAuthZ: true,
|
expectedAuthZ: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "EmailInFirstDomainWildcard",
|
||||||
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
|
allowedDomains: []string{"*.example0.com", "*.example1.com"},
|
||||||
|
email: "foo@bar.example0.com",
|
||||||
|
expectedAuthZ: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "EmailNotInCorrect2ndSubDomainsNotInEmails",
|
name: "EmailNotInCorrect2ndSubDomainsNotInEmails",
|
||||||
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
@ -174,6 +188,13 @@ func TestValidatorCases(t *testing.T) {
|
|||||||
email: "baz@quux.example1.com",
|
email: "baz@quux.example1.com",
|
||||||
expectedAuthZ: true,
|
expectedAuthZ: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "EmailInSecondDomainWildcard",
|
||||||
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
|
allowedDomains: []string{"*.example0.com", "*.example1.com"},
|
||||||
|
email: "baz@quux.example1.com",
|
||||||
|
expectedAuthZ: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "EmailInFirstEmailList",
|
name: "EmailInFirstEmailList",
|
||||||
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
@ -181,6 +202,13 @@ func TestValidatorCases(t *testing.T) {
|
|||||||
email: "xyzzy@example.com",
|
email: "xyzzy@example.com",
|
||||||
expectedAuthZ: true,
|
expectedAuthZ: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "EmailInFirstEmailListWildcard",
|
||||||
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
|
allowedDomains: []string{"*.example0.com", "*.example1.com"},
|
||||||
|
email: "xyzzy@example.com",
|
||||||
|
expectedAuthZ: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "EmailNotInDomainsNotInEmails",
|
name: "EmailNotInDomainsNotInEmails",
|
||||||
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
allowedEmails: []string{"xyzzy@example.com", "plugh@example.com"},
|
||||||
@ -370,6 +398,13 @@ func TestValidatorCases(t *testing.T) {
|
|||||||
allowedDomains: []string{"company.com"},
|
allowedDomains: []string{"company.com"},
|
||||||
expectedAuthZ: false,
|
expectedAuthZ: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "CheckForEqualityNotSuffixWildcard",
|
||||||
|
email: "foo@evilcompany.com",
|
||||||
|
allowedEmails: []string(nil),
|
||||||
|
allowedDomains: []string{"*.company.com"},
|
||||||
|
expectedAuthZ: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
Loading…
Reference in New Issue
Block a user