package idp
import (
"net"
"net/url"
"path"
"strings"
)
func redirectURL(raw string) (*url.URL, string) {
u, err := url.Parse(raw)
if err != nil || u.Hostname() == "" || u.Opaque != "" {
return nil, "invalid_url"
}
if u.User != nil {
return nil, "userinfo"
}
if u.RawQuery != "" || u.ForceQuery {
return nil, "query"
}
if u.Fragment != "" || strings.Contains(raw, "#") {
return nil, "fragment"
}
if u.Scheme != "https" {
ip := net.ParseIP(u.Hostname())
if u.Scheme != "http" || ip == nil || !ip.IsLoopback() {
return nil, "scheme"
}
}
// Reject alternate encodings and traversal rather than normalizing an allowlist match.
cleanPath := path.Clean("/" + u.Path)
if u.RawPath != "" {
return nil, "encoded_path"
}
if strings.Contains(u.Path, "\\") {
return nil, "backslash"
}
if u.Path != "" && u.Path != "/" && cleanPath != strings.TrimRight(u.Path, "/") {
return nil, "noncanonical_path"
}
return u, ""
}
func validRedirectURIs(uris []string, config string) bool {
return redirectURIRejection(uris, config) == ""
}
func redirectURIRejection(uris []string, config string) string {
if len(uris) == 0 || len(uris) > 8 {
return "redirect_count"
}
var prefixes []*url.URL
if config != "" {
for _, raw := range strings.Split(config, ",") {
u, issue := redirectURL(strings.TrimSpace(raw))
if issue != "" {
return "invalid_policy"
}
prefixes = append(prefixes, u)
}
}
for _, raw := range uris {
if len(raw) > 2048 {
return "redirect_too_long"
}
if config == "" {
continue
}
u, issue := redirectURL(raw)
if issue != "" {
return issue
}
matched := false
for _, prefix := range prefixes {
base := strings.TrimRight(prefix.Path, "/")
if u.Scheme == prefix.Scheme && u.Host == prefix.Host && (u.Path == base || strings.HasPrefix(u.Path, base+"/")) {
matched = true
break
}
}
if !matched {
return "unmatched_prefix"
}
}
return ""
}