Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule-restricted wildcard match of single subdomain #488 #1783

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/hashicorp/golang-lru v0.5.4
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kylelemons/godebug v1.1.0
github.com/lib/pq v1.3.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
Expand Down
123 changes: 120 additions & 3 deletions server/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import (
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"

jose "gopkg.in/square/go-jose.v2"

lru "github.com/hashicorp/golang-lru"

"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/server/internal"
"github.com/dexidp/dex/storage"
Expand Down Expand Up @@ -440,7 +443,7 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
}
}

if !validateRedirectURI(client, redirectURI) {
if !validateRedirectURI(client, s.wildcardMatcherCache, redirectURI) {
description := fmt.Sprintf("Unregistered redirect_uri (%q).", redirectURI)
return nil, &authErr{"", "", errInvalidRequest, description}
}
Expand Down Expand Up @@ -587,12 +590,23 @@ func (s *Server) validateCrossClientTrust(clientID, peerID string) (trusted bool
return false, nil
}

func validateRedirectURI(client storage.Client, redirectURI string) bool {
func validateRedirectURI(client storage.Client, wildcardCache *lru.ARCCache, redirectURI string) bool {
if !client.Public {
for _, uri := range client.RedirectURIs {
if redirectURI == uri {
// exact match is valid
return true
}

if urlRequested, err := url.Parse(redirectURI); err == nil && urlRequested != nil {
// validly-formed url
clobberMatcher := getClobberHostMatcher(uri, wildcardCache)
validClobber := clobberMatcher.HostMatcher != nil && clobberMatcher.ClientRedirectURL != nil
if validClobber && matchesClobber(clobberMatcher, urlRequested) {
// clobber match success
return true
}
}
}
return false
}
Expand All @@ -606,7 +620,8 @@ func validateRedirectURI(client storage.Client, redirectURI string) bool {
if err != nil {
return false
}
if u.Scheme != "http" {
// public clients should use http or https (#1300)
if u.Scheme != "http" && u.Scheme != "https" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was discussed in open issue #1300 – it was a quick/logical quick so included here, but not relevant to main issue of wildcard support.

return false
}
if u.Host == "localhost" {
Expand All @@ -616,6 +631,108 @@ func validateRedirectURI(client storage.Client, redirectURI string) bool {
return err == nil && host == "localhost"
}

func matchesClobber(clobberMatcher ClientRedirectClobberMatcher, urlRequested *url.URL) bool {
if clobberMatcher.ClientRedirectURL.Path != urlRequested.Path {
// failed path match
return false
}
if clobberMatcher.ClientRedirectURL.Scheme != urlRequested.Scheme {
// failed scheme match
return false
}
if !clobberMatcher.HostMatcher.MatchString(urlRequested.Host) {
// failed host match
return false
}
return true
}

type HostSplit struct {
HostSuffix string // e.g. host:port of two closest root domains e.g. example.com:8888
SubDomainPrefix string // e.g. dom.abc (from dom.abc.example.com)
}

type ClientRedirectClobberMatcher struct {
ClientRedirectURL *url.URL
HostMatcher *regexp.Regexp
}

// Returns a clobber/wildcard subdomain matcher struct
func createClientRedirectClobberMatcher(clientRedirectURI string) ClientRedirectClobberMatcher {
redirectURL, _ := url.Parse(clientRedirectURI)
if redirectURL == nil {
return ClientRedirectClobberMatcher{}
}
splitResult := splitClobberHTTPRedirectURL(redirectURL)
if splitResult == nil {
// not wildcard or invalid
return ClientRedirectClobberMatcher{}
}

// first replace double-asterisk globber with .+
var subdomain = strings.ReplaceAll(regexp.QuoteMeta(splitResult.SubDomainPrefix), "\\*\\*", ".+")
subdomain = strings.ReplaceAll(subdomain, "\\*", "[^.]+")

hostNameRegExStr := "^" + subdomain + "\\." + regexp.QuoteMeta(splitResult.HostSuffix) + "$"

hostNameRegEx, err := regexp.Compile(hostNameRegExStr)
if err != nil {
// bad pattern - unexpected
return ClientRedirectClobberMatcher{}
}

return ClientRedirectClobberMatcher{
ClientRedirectURL: redirectURL,
HostMatcher: hostNameRegEx,
}
}

// extract the scheme, host and path for wildcard checks.
// If the scheme is not http or https, or does not specify an absolute path beginning with '/'
// the parser will return nil.
func splitClobberHTTPRedirectURL(clientRedirectURLSpec *url.URL) *HostSplit {
host := clientRedirectURLSpec.Host
if !strings.Contains(host, "*") {
// for efficiency, check wildcard before DomainComponents
return nil
}

if strings.Contains(host, "***") {
// a maximum of 2 '*' characters are permitted in sequence
return nil
}

// sub-domain portion is first portion (greedy)
hostSplit := regexp.MustCompile(`^(.+)\.([^.]*[A-Za-z][^.]*\.[^.]*[A-Za-z][^.]+)$`)
hostRes := hostSplit.FindStringSubmatch(host)
if hostRes == nil {
// does not conform to requirements of a subdomain spec followed by two higher order domains
return nil
}

return &HostSplit{
HostSuffix: hostRes[2],
SubDomainPrefix: hostRes[1],
}
}

func getClobberHostMatcher(clientRedirectURI string, wildcardCache *lru.ARCCache) ClientRedirectClobberMatcher {
if wildcardCache == nil {
// unexpected error condition where cache is unavailable
return ClientRedirectClobberMatcher{}
}

if redirectMatcherStruct, ok := wildcardCache.Get(clientRedirectURI); ok {
// cache hit
return redirectMatcherStruct.(ClientRedirectClobberMatcher)
}
// cache miss - calc and add to cache
calcResult := createClientRedirectClobberMatcher(clientRedirectURI)
wildcardCache.Add(clientRedirectURI, calcResult)
// return cached result
return calcResult
}

func validateConnectorID(connectors []storage.Connector, connectorID string) bool {
for _, c := range connectors {
if c.ID == connectorID {
Expand Down
Loading