forked from ory/kratos
-
Notifications
You must be signed in to change notification settings - Fork 0
/
http_secure_redirect.go
136 lines (115 loc) · 4.01 KB
/
http_secure_redirect.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package x
import (
"net/http"
"net/url"
"strings"
"github.com/golang/gddo/httputil"
"github.com/pkg/errors"
"github.com/ory/herodot"
"github.com/ory/x/stringsx"
"github.com/ory/x/urlx"
"github.com/ory/kratos/driver/config"
)
type secureRedirectOptions struct {
whitelist []url.URL
defaultReturnTo *url.URL
sourceURL string
}
type SecureRedirectOption func(*secureRedirectOptions)
// SecureRedirectAllowURLs whitelists the given URLs for redirects.
func SecureRedirectAllowURLs(urls []url.URL) SecureRedirectOption {
return func(o *secureRedirectOptions) {
o.whitelist = append(o.whitelist, urls...)
}
}
// SecureRedirectUseSourceURL uses the given source URL (checks the `?return_to` value)
// instead of r.URL.
func SecureRedirectUseSourceURL(source string) SecureRedirectOption {
return func(o *secureRedirectOptions) {
o.sourceURL = source
}
}
// SecureRedirectAllowSelfServiceURLs allows the caller to define `?return_to=` values
// which contain the server's URL and `/self-service` path prefix. Useful for redirecting
// to the login endpoint, for example.
func SecureRedirectAllowSelfServiceURLs(publicURL *url.URL) SecureRedirectOption {
return func(o *secureRedirectOptions) {
o.whitelist = append(o.whitelist, *urlx.AppendPaths(publicURL, "/self-service"))
}
}
// SecureRedirectOverrideDefaultReturnTo overrides the defaultReturnTo address specified
// as the second arg.
func SecureRedirectOverrideDefaultReturnTo(defaultReturnTo *url.URL) SecureRedirectOption {
return func(o *secureRedirectOptions) {
o.defaultReturnTo = defaultReturnTo
}
}
// SecureRedirectTo implements a HTTP redirector who mitigates open redirect vulnerabilities by
// working with whitelisting.
func SecureRedirectTo(r *http.Request, defaultReturnTo *url.URL, opts ...SecureRedirectOption) (returnTo *url.URL, err error) {
o := &secureRedirectOptions{defaultReturnTo: defaultReturnTo}
for _, opt := range opts {
opt(o)
}
if len(o.whitelist) == 0 {
return o.defaultReturnTo, nil
}
source := RequestURL(r)
if o.sourceURL != "" {
source, err = url.ParseRequestURI(o.sourceURL)
if err != nil {
return nil, herodot.ErrInternalServerError.WithWrap(err).WithReasonf("Unable to parse the original request URL: %s", err)
}
}
if len(source.Query().Get("return_to")) == 0 {
return o.defaultReturnTo, nil
} else if returnTo, err = url.ParseRequestURI(source.Query().Get("return_to")); err != nil {
return nil, herodot.ErrInternalServerError.WithWrap(err).WithReasonf("Unable to parse the return_to query parameter as an URL: %s", err)
}
returnTo.Host = stringsx.Coalesce(returnTo.Host, o.defaultReturnTo.Host)
returnTo.Scheme = stringsx.Coalesce(returnTo.Scheme, o.defaultReturnTo.Scheme)
var found bool
for _, allowed := range o.whitelist {
if strings.EqualFold(allowed.Scheme, returnTo.Scheme) &&
strings.EqualFold(allowed.Host, returnTo.Host) &&
strings.HasPrefix(
stringsx.Coalesce(returnTo.Path, "/"),
stringsx.Coalesce(allowed.Path, "/")) {
found = true
}
}
if !found {
return nil, errors.WithStack(herodot.ErrBadRequest.
WithReasonf("Requested return_to URL \"%s\" is not whitelisted.", returnTo).
WithDebugf("Whitelisted domains are: %v", o.whitelist))
}
return returnTo, nil
}
func SecureContentNegotiationRedirection(
w http.ResponseWriter, r *http.Request, out interface{},
requestURL string, writer herodot.Writer, c *config.Config,
opts ...SecureRedirectOption,
) error {
switch httputil.NegotiateContentType(r, []string{
"text/html",
"application/json",
}, "text/html") {
case "application/json":
writer.Write(w, r, out)
case "text/html":
fallthrough
default:
ret, err := SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(),
append([]SecureRedirectOption{
SecureRedirectUseSourceURL(requestURL),
SecureRedirectAllowURLs(c.SelfServiceBrowserWhitelistedReturnToDomains()),
SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r)),
}, opts...)...,
)
if err != nil {
return err
}
http.Redirect(w, r, ret.String(), http.StatusFound)
}
return nil
}