Skip to content

Commit

Permalink
AX-29019: Implement wildcard redirect URI support
Browse files Browse the repository at this point in the history
This change introduces restricted support for wildcard matching of client redirectURIs. Using wildcards for redirectURIs introduces a security risk in production environments and should be used with caution in non-production environments only. In development environments, the use of wildcards allows for simpler management of Dex and client application deployments.

Wildcard support has been limited to the following rules, which are taken from Okta's implementation of the same feature:

- Any configured redirect URIs may contain a single * character in the lowest-level domain (for example, https://redirect-*-domain.example.com/oidc/redirect) to act as a wildcard.
- The wildcard subdomain must have at least one subdomain between it and the top level domain.
- The wildcard can match any valid hostname characters, but can’t span more than one domain. For example, if https://redirect-*-domain.example.com/oidc/redirect is configured as a redirect URI, then https://redirect-1-domain.example.com/oidc/redirect and https://redirect-sub-domain.example.com/oidc/redirect match, but https://redirect-1.sub-domain.example.com/oidc/redirect doesn’t match.
- Only the https URI scheme can use wildcard redirect URIs.
  • Loading branch information
michaelliau committed Oct 9, 2023
1 parent d41ffef commit 4bf9479
Show file tree
Hide file tree
Showing 2 changed files with 534 additions and 1 deletion.
66 changes: 65 additions & 1 deletion server/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ func validateRedirectURI(client storage.Client, redirectURI string) bool {
// Allow named RedirectURIs for both public and non-public clients.
// This is required make PKCE-enabled web apps work, when configured as public clients.
for _, uri := range client.RedirectURIs {
if redirectURI == uri {
if redirectURI == uri || isWildcardRedirectURIMatch(uri, redirectURI) {
return true
}
}
Expand Down Expand Up @@ -654,6 +654,70 @@ func validateRedirectURI(client storage.Client, redirectURI string) bool {
return err == nil && host == "localhost"
}

func isWildcardRedirectURIMatch(wildcardURI, redirectURI string) bool {
parsedWildcardURI, err := url.Parse(wildcardURI)
if err != nil {
return false
}

// Wildcard URIs must be https
if parsedWildcardURI.Scheme != "https" {
return false
}

// Wildcard URIs only apply to URIs with subdomains
wildcardDomains := strings.Split(parsedWildcardURI.Hostname(), ".")
if len(wildcardDomains) < 3 {
return false
}

// Wildcard URIs may only contain a single '*' and it must be in the lowest level domain
if strings.Count(wildcardURI, "*") != 1 {
return false
}
if !strings.Contains(wildcardDomains[0], "*") {
return false
}

parsedRedirectURI, err := url.Parse(redirectURI)
if err != nil {
return false
}
redirectDomains := strings.Split(parsedRedirectURI.Hostname(), ".")

return parsedRedirectURI.Scheme == "https" &&
wildcardMatch(wildcardDomains[0], redirectDomains[0]) &&
strings.Join(wildcardDomains[1:], ".") == strings.Join(redirectDomains[1:], ".") &&
parsedWildcardURI.Port() == parsedRedirectURI.Port() &&
parsedWildcardURI.Path == parsedRedirectURI.Path
}

func wildcardMatch(pattern, str string) bool {
if pattern == "*" {
return true
}
// Pattern starts with "*" so str must end with the last part of the pattern
if strings.HasPrefix(pattern, "*") {
return strings.HasSuffix(str, pattern[1:])
}

// Pattern ends with "*" so str must start with the first part of the pattern
if strings.HasSuffix(pattern, "*") {
return strings.HasPrefix(str, pattern[:len(pattern)-1])
}

parts := strings.Split(pattern, "*")

// Pattern doesn't contain "*", just do a simple equality check
if len(parts) == 1 {
return str == pattern
}

// Pattern contains "*" in the middle, so str must start with the first part and end with the last part
return strings.HasSuffix(str, parts[1]) &&
strings.HasPrefix(str[:strings.LastIndex(str, parts[1])], parts[0])
}

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

0 comments on commit 4bf9479

Please sign in to comment.