snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

41
x/redir/port_redirect.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package redir
import (
"net/http"
"path"
"strings"
"github.com/ory/kratos/driver/config"
"github.com/ory/x/httprouterx"
)
func RedirectToAdminRoute(reg config.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
admin := reg.Config().SelfAdminURL(r.Context())
dest := *r.URL
dest.Host = admin.Host
dest.Scheme = admin.Scheme
dest.Path = strings.TrimPrefix(dest.Path, httprouterx.AdminPrefix)
dest.Path = path.Join(admin.Path, httprouterx.AdminPrefix, dest.Path)
http.Redirect(w, r, dest.String(), http.StatusTemporaryRedirect)
}
}
func RedirectToPublicRoute(reg config.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
public := reg.Config().SelfPublicURL(r.Context())
dest := *r.URL
dest.Host = public.Host
dest.Scheme = public.Scheme
dest.Path = strings.TrimPrefix(dest.Path, httprouterx.AdminPrefix)
dest.Path = path.Join(public.Path, dest.Path)
http.Redirect(w, r, dest.String(), http.StatusTemporaryRedirect)
}
}

View File

@@ -0,0 +1,76 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package redir_test
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/redir"
"github.com/ory/x/configx"
"github.com/ory/x/httprouterx"
)
func TestRedirectToPublicAdminRoute(t *testing.T) {
pub, adm := httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix()
adminTS := httptest.NewServer(adm)
pubTS := httptest.NewServer(pub)
t.Cleanup(pubTS.Close)
t.Cleanup(adminTS.Close)
_, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{
config.ViperKeyAdminBaseURL: adminTS.URL,
config.ViperKeyPublicBaseURL: pubTS.URL,
}))
pub.POST("/privileged", redir.RedirectToAdminRoute(reg))
pub.POST("/admin/privileged", redir.RedirectToAdminRoute(reg))
adm.POST("/privileged", func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(w, r.Body)
})
adm.POST("/read", redir.RedirectToPublicRoute(reg))
pub.POST("/read", func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(w, r.Body)
})
for k, tc := range []struct {
source string
dest string
}{
{
source: pubTS.URL + "/privileged?foo=bar",
dest: adminTS.URL + "/admin/privileged?foo=bar",
},
{
source: pubTS.URL + "/admin/privileged?foo=bar",
dest: adminTS.URL + "/admin/privileged?foo=bar",
},
{
source: adminTS.URL + "/admin/read?foo=bar",
dest: pubTS.URL + "/read?foo=bar",
},
} {
t.Run(fmt.Sprintf("%d", k), func(t *testing.T) {
id := x.NewUUID().String()
res, err := adminTS.Client().Post(tc.source, "", strings.NewReader(id))
require.NoError(t, err)
assert.EqualValues(t, http.StatusOK, res.StatusCode)
assert.Equal(t, tc.dest, res.Request.URL.String())
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, id, string(body))
})
}
}

204
x/redir/secure_redirect.go Normal file
View File

@@ -0,0 +1,204 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package redir
import (
"cmp"
"net/http"
"net/url"
"strings"
"github.com/ory/kratos/x"
"github.com/ory/kratos/text"
"github.com/golang/gddo/httputil"
"github.com/pkg/errors"
"github.com/ory/herodot"
"github.com/ory/x/urlx"
"github.com/ory/kratos/driver/config"
)
type secureRedirectOptions struct {
allowlist []url.URL
defaultReturnTo *url.URL
returnTo string
sourceURL string
}
type SecureRedirectOption func(*secureRedirectOptions)
// SecureRedirectAllowURLs allows the given URLs for redirects.
func SecureRedirectAllowURLs(urls []url.URL) SecureRedirectOption {
return func(o *secureRedirectOptions) {
o.allowlist = append(o.allowlist, 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
}
}
// SecureRedirectReturnTo uses the provided URL to redirect the user to it.
func SecureRedirectReturnTo(returnTo string) SecureRedirectOption {
return func(o *secureRedirectOptions) {
o.returnTo = returnTo
}
}
// 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.allowlist = append(o.allowlist, *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
}
}
// SecureRedirectToIsAllowedHost validates if the redirect_to param is allowed for a given wildcard.
//
// The DNS-label boundary on wildcard hosts is enforced upstream, where the
// allow-list is loaded: Config.SelfServiceBrowserAllowedReturnToDomains drops
// unsafe wildcards (via corsx.ClassifyOrigin) unless the project is
// grandfathered. This function trusts that and only matches; do not re-add a
// boundary check here, or grandfathered projects would be enforced
// inconsistently.
func SecureRedirectToIsAllowedHost(returnTo *url.URL, allowed url.URL) bool {
if allowed.Host != "" && allowed.Host[:1] == "*" {
return strings.HasSuffix(strings.ToLower(returnTo.Host), strings.ToLower(allowed.Host)[1:])
}
return strings.EqualFold(allowed.Host, returnTo.Host)
}
// TakeOverReturnToParameter carries over the return_to parameter to a new URL
// If `from` does not contain the `return_to` query parameter, the first non-empty value from `fallback` is used instead.
func TakeOverReturnToParameter(from string, to string, fallback ...string) (string, error) {
fromURL, err := url.Parse(from)
if err != nil {
return "", err
}
returnTo := cmp.Or(append([]string{fromURL.Query().Get("return_to")}, fallback...)...)
// Empty return_to parameter, return early
if returnTo == "" {
return to, nil
}
toURL, err := url.Parse(to)
if err != nil {
return "", err
}
toQuery := toURL.Query()
toQuery.Set("return_to", returnTo)
toURL.RawQuery = toQuery.Encode()
return toURL.String(), nil
}
// SecureRedirectTo implements a HTTP redirector who mitigates open redirect vulnerabilities by
// working with allow lists.
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.allowlist) == 0 {
return o.defaultReturnTo, nil
}
source := x.RequestURL(r)
if o.sourceURL != "" {
source, err = url.ParseRequestURI(o.sourceURL)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().WithWrap(err).WithReasonf("Unable to parse the original request URL: %s", err))
}
}
rawReturnTo := cmp.Or(o.returnTo, source.Query().Get("return_to"))
if rawReturnTo == "" {
return o.defaultReturnTo, nil
}
returnTo, err = url.Parse(rawReturnTo)
if err != nil {
return nil, errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Unable to parse the return_to query parameter as an URL: %s", err))
}
returnTo.Host = cmp.Or(returnTo.Host, o.defaultReturnTo.Host)
returnTo.Scheme = cmp.Or(returnTo.Scheme, o.defaultReturnTo.Scheme)
for _, allowed := range o.allowlist {
if strings.EqualFold(allowed.Scheme, returnTo.Scheme) &&
SecureRedirectToIsAllowedHost(returnTo, allowed) &&
strings.HasPrefix(
cmp.Or(returnTo.Path, "/"),
cmp.Or(allowed.Path, "/")) {
return returnTo, nil
}
}
return nil, errors.WithStack(herodot.ErrBadRequest().
WithID(text.ErrIDRedirectURLNotAllowed).
WithReasonf("Requested return_to URL %q is not allowed.", returnTo),
)
}
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(r.Context()),
append([]SecureRedirectOption{
SecureRedirectUseSourceURL(requestURL),
SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(r.Context())),
SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())),
}, opts...)...,
)
if err != nil {
return err
}
http.Redirect(w, r, ret.String(), http.StatusSeeOther)
}
return nil
}
func ContentNegotiationRedirection(
w http.ResponseWriter, r *http.Request, out interface{}, writer herodot.Writer, returnTo string,
) {
switch httputil.NegotiateContentType(r, []string{
"text/html",
"application/json",
}, "text/html") {
case "application/json":
writer.Write(w, r, out)
case "text/html":
fallthrough
default:
http.Redirect(w, r, returnTo, http.StatusSeeOther)
}
}

View File

@@ -0,0 +1,322 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package redir_test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/ory/kratos/x/redir"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/herodot"
"github.com/ory/x/urlx"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/x"
)
func TestSecureContentNegotiationRedirection(t *testing.T) {
conf, _ := pkg.NewFastRegistryWithMocks(t)
var jsonActual = json.RawMessage(`{"foo":"bar"}` + "\n")
writer := herodot.NewJSONWriter(nil)
router := http.NewServeMux()
router.HandleFunc("GET /redir", func(w http.ResponseWriter, r *http.Request) {
require.NoError(t, redir.SecureContentNegotiationRedirection(w, r, jsonActual, x.RequestURL(r).String(), writer, conf))
})
router.HandleFunc("GET /default-return-to", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
router.HandleFunc("GET /return-to", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
ts := httptest.NewServer(router)
defer ts.Close()
ctx := context.Background()
defaultReturnTo := ts.URL + "/default-return-to"
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, defaultReturnTo)
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, ts.URL)
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{ts.URL})
run := func(t *testing.T, href string, contentType string) (*http.Response, string) {
req, err := http.NewRequest("GET", href, nil)
require.NoError(t, err)
req.Header.Add("Accept", contentType)
res, err := ts.Client().Do(req)
require.NoError(t, err)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, res.Body.Close())
return res, string(body)
}
t.Run("case=html browser causes redirect", func(t *testing.T) {
res, _ := run(t, ts.URL+"/redir", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
assert.EqualValues(t, res.Request.URL.String(), defaultReturnTo)
})
t.Run("case=html browser causes redirect with redirect_to", func(t *testing.T) {
res, _ := run(t, ts.URL+"/redir?return_to=/redirect-to", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
assert.EqualValues(t, res.Request.URL.String(), ts.URL+"/redirect-to")
})
t.Run("case=html browser causes redirect with redirect_to", func(t *testing.T) {
res, body := run(t, ts.URL+"/redir?return_to=/redirect-to", "application/json")
assert.EqualValues(t, res.Request.URL.String(), ts.URL+"/redir?return_to=/redirect-to")
assert.EqualValues(t, body, jsonActual)
})
}
func TestSecureRedirectToIsAllowedHost(t *testing.T) {
type testCase struct {
allowedURL string
redirectURL string
valid bool
}
tests := map[string]testCase{
"case=Domain is allowed": {allowedURL: "https://foo.bar", redirectURL: "https://foo.bar/redir", valid: true},
"case=Domain prefix is allowed": {allowedURL: "https://*.bar", redirectURL: "https://foo.bar/redir", valid: true},
"case=Subdomain prefix is allowed": {allowedURL: "https://*.foo.bar", redirectURL: "https://auth.foo.bar/redir", valid: true},
"case=Domain is not allowed": {allowedURL: "https://foo.baz", redirectURL: "https://foo.bar/redir", valid: false},
"case=Domain wildcard is not allowed": {allowedURL: "https://*.foo.baz", redirectURL: "https://foo.bar/redir", valid: false},
"case=Subdomain is not allowed": {allowedURL: "https://*.foo.baz", redirectURL: "https://auth.foo.bar/redir", valid: false},
// A labelled wildcard's leading-dot suffix keeps the label boundary:
// subdomains match, suffix-only hosts do not. (Unbound wildcards are
// dropped upstream by SelfServiceBrowserAllowedReturnToDomains, so the
// matcher never sees them outside grandfathered projects.)
"case=Labelled wildcard matches subdomain": {allowedURL: "https://*.foo.com", redirectURL: "https://sub.foo.com/redir", valid: true},
"case=Labelled wildcard does not match suffix host": {allowedURL: "https://*.foo.com", redirectURL: "https://evilfoo.com/redir", valid: false},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
allowedURL, err := url.Parse(tc.allowedURL)
require.NoError(t, err)
redirectURL, err := url.Parse(tc.redirectURL)
require.NoError(t, err)
assert.Equal(t, redir.SecureRedirectToIsAllowedHost(redirectURL, *allowedURL), tc.valid)
})
}
}
func TestTakeOverReturnToParameter(t *testing.T) {
type testCase struct {
fromUrl string
toURL string
expectedOutputUrl string
}
tests := map[string]testCase{
"case=return_to is taken over": {fromUrl: "https://original.bar?return_to=https://allowed.domain", toURL: "https://output.bar", expectedOutputUrl: "https://output.bar?return_to=https%3A%2F%2Fallowed.domain"},
"case=only return_to is taken over when multiple query parameters are set": {fromUrl: "https://original.bar?return_to=https://allowed.domain&flow=12312", toURL: "https://output.bar", expectedOutputUrl: "https://output.bar?return_to=https%3A%2F%2Fallowed.domain"},
"case=output query parameters are preserved": {fromUrl: "https://original.bar?return_to=https://allowed.domain", toURL: "https://output.bar?flow=123321", expectedOutputUrl: "https://output.bar?flow=123321&return_to=https%3A%2F%2Fallowed.domain"},
"case=when original return_to is empty do nothing": {fromUrl: "https://original.bar?return_to=", toURL: "https://output.bar?flow=123123", expectedOutputUrl: "https://output.bar?flow=123123"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
output, err := redir.TakeOverReturnToParameter(tc.fromUrl, tc.toURL)
require.NoError(t, err)
assert.Equal(t, output, tc.expectedOutputUrl)
})
}
}
func TestSecureRedirectTo(t *testing.T) {
newServer := func(t *testing.T, isTLS bool, isRelative bool, expectErr bool, opts func(ts *httptest.Server) []redir.SecureRedirectOption) *httptest.Server {
var ts *httptest.Server
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if opts == nil {
opts = func(ts *httptest.Server) []redir.SecureRedirectOption {
return nil
}
}
defaultReturnTo := "/default-return-to"
if !isRelative {
defaultReturnTo = ts.URL + defaultReturnTo
}
returnTo, err := redir.SecureRedirectTo(r, urlx.ParseOrPanic(defaultReturnTo), opts(ts)...)
if expectErr {
require.Error(t, err)
_, _ = w.Write([]byte("error"))
return
}
require.NoError(t, err)
_, _ = w.Write([]byte(returnTo.String()))
})
if isTLS {
ts = httptest.NewTLSServer(handler)
} else {
ts = httptest.NewServer(handler)
}
t.Cleanup(ts.Close)
return ts
}
makeRequest := func(t *testing.T, ts *httptest.Server, path string) (*http.Response, string) {
res, err := ts.Client().Get(ts.URL + "/" + path)
require.NoError(t, err)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, res.Body.Close())
return res, string(body)
}
t.Run("case=return to a relative path with anchor works", func(t *testing.T) {
returnTo, err := redir.SecureRedirectTo(
httptest.NewRequest("GET", "/?return_to=/foo/kratos%23abcd", nil),
urlx.ParseOrPanic("/default-return-to"),
redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("/foo")}),
)
require.NoError(t, err)
assert.Equal(t, returnTo.String(), "/foo/kratos#abcd")
})
t.Run("case=return to default URL if nothing is allowed", func(t *testing.T) {
returnTo, err := redir.SecureRedirectTo(
httptest.NewRequest("GET", "/?return_to=/foo", nil),
urlx.ParseOrPanic("https://www.ory.sh/default-return-to"),
)
require.NoError(t, err)
assert.Equal(t, returnTo.String(), "https://www.ory.sh/default-return-to")
})
t.Run("case=return to foo with server baseURL if allowed", func(t *testing.T) {
returnTo, err := redir.SecureRedirectTo(
httptest.NewRequest("GET", "/?return_to=/foo", nil),
urlx.ParseOrPanic("https://www.ory.sh/default-return-to"),
redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh")}),
)
require.NoError(t, err)
assert.Equal(t, returnTo.String(), "https://www.ory.sh/foo")
})
t.Run("case=return to a relative path works", func(t *testing.T) {
returnTo, err := redir.SecureRedirectTo(
httptest.NewRequest("GET", "/?return_to=/foo/kratos", nil),
urlx.ParseOrPanic("/default-return-to"),
redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("/foo")}),
)
require.NoError(t, err)
assert.Equal(t, returnTo.String(), "/foo/kratos")
})
t.Run("case=return to a fully qualified domain is forbidden if allowlist is relative", func(t *testing.T) {
_, err := redir.SecureRedirectTo(
httptest.NewRequest("GET", "/?return_to=https://www.ory.sh/foo/kratos", nil),
urlx.ParseOrPanic("/default-return-to"),
redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("/foo")}),
)
require.Error(t, err)
})
t.Run("case=return to another domain works", func(t *testing.T) {
returnTo, err := redir.SecureRedirectTo(
httptest.NewRequest("GET", "https://example.com/?return_to=https://www.ory.sh/foo/kratos", nil),
urlx.ParseOrPanic("https://www.ory.sh/default-return-to"),
redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh/foo")}),
)
require.NoError(t, err)
assert.Equal(t, returnTo.String(), "https://www.ory.sh/foo/kratos")
})
t.Run("case=return to another domain fails if host mismatches", func(t *testing.T) {
s := newServer(t, false, false, true, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.not-ory.sh/")})}
})
_, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos")
assert.Equal(t, body, "error")
})
t.Run("case=return to another domain fails if path mismatches", func(t *testing.T) {
s := newServer(t, false, false, true, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh/not-kratos")})}
})
_, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos")
assert.Equal(t, body, "error")
})
t.Run("case=return to another domain fails if scheme mismatches", func(t *testing.T) {
s := newServer(t, false, false, true, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("http://www.ory.sh/")})}
})
_, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos")
assert.Equal(t, body, "error")
})
t.Run("case=should work with self-service modifier", func(t *testing.T) {
s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{redir.SecureRedirectAllowSelfServiceURLs(urlx.ParseOrPanic(ts.URL))}
})
_, body := makeRequest(t, s, "?return_to=/self-service/foo")
assert.Equal(t, body, s.URL+"/self-service/foo")
})
t.Run("case=should work with default return to", func(t *testing.T) {
s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{redir.SecureRedirectOverrideDefaultReturnTo(urlx.ParseOrPanic(ts.URL + "/another-default"))}
})
_, body := makeRequest(t, s, "")
assert.Equal(t, body, s.URL+"/another-default")
})
t.Run("case=should override return_to", func(t *testing.T) {
s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{
redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic(ts.URL)}),
redir.SecureRedirectUseSourceURL("https://foo/bar?return_to=/override"),
}
})
_, body := makeRequest(t, s, "?return_to=/original")
assert.Equal(t, body, s.URL+"/override")
})
t.Run("case=should work with subdomain wildcard", func(t *testing.T) {
s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://*.ory.sh/")})}
})
_, body := makeRequest(t, s, "?return_to=https://www.ory.sh/kratos")
assert.Equal(t, body, "https://www.ory.sh/kratos")
_, body = makeRequest(t, s, "?return_to=https://even.deeper.nested.ory.sh/kratos")
assert.Equal(t, body, "https://even.deeper.nested.ory.sh/kratos")
})
t.Run("case=should fallback to default return_to scheme", func(t *testing.T) {
s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{
redir.SecureRedirectAllowURLs([]url.URL{*urlx.ParseOrPanic("https://www.ory.sh")}),
redir.SecureRedirectOverrideDefaultReturnTo(urlx.ParseOrPanic("https://www.ory.sh/docs")),
}
})
_, body := makeRequest(t, s, "?return_to=//www.ory.sh/kratos")
assert.Equal(t, body, "https://www.ory.sh/kratos")
})
t.Run("case=should fallback to default return_to host", func(t *testing.T) {
s := newServer(t, false, false, false, func(ts *httptest.Server) []redir.SecureRedirectOption {
return []redir.SecureRedirectOption{
redir.SecureRedirectAllowURLs([]url.URL{
*urlx.ParseOrPanic("https://www.ory.sh"),
*urlx.ParseOrPanic("http://www.ory.sh"),
}),
redir.SecureRedirectOverrideDefaultReturnTo(urlx.ParseOrPanic("https://www.ory.sh/docs")),
}
})
_, body := makeRequest(t, s, "?return_to=http:///kratos")
assert.Equal(t, body, "http://www.ory.sh/kratos")
})
}