mirror of https://github.com/ory/hydra
347 lines
11 KiB
Go
347 lines
11 KiB
Go
// Copyright © 2025 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package fosite_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ory/hydra/v2/fosite"
|
|
"github.com/ory/hydra/v2/fosite/internal"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIsLocalhost(t *testing.T) {
|
|
for k, c := range []struct {
|
|
expect bool
|
|
rawurl string
|
|
}{
|
|
{expect: false, rawurl: "https://foo.bar"},
|
|
{expect: true, rawurl: "https://localhost"},
|
|
{expect: true, rawurl: "https://localhost:1234"},
|
|
{expect: true, rawurl: "https://127.0.0.1:1234"},
|
|
{expect: true, rawurl: "https://127.0.0.1"},
|
|
{expect: true, rawurl: "https://test.localhost:1234"},
|
|
{expect: true, rawurl: "https://test.localhost"},
|
|
} {
|
|
u, _ := url.Parse(c.rawurl)
|
|
assert.Equal(t, c.expect, fosite.IsLocalhost(u), "case %d", k)
|
|
}
|
|
}
|
|
|
|
// rfc6749 10.6.
|
|
// Authorization Code Redirection URI Manipulation
|
|
// The authorization server MUST require public clients and SHOULD require confidential clients
|
|
// to register their redirection URIs. If a redirection URI is provided
|
|
// in the request, the authorization server MUST validate it against the
|
|
// registered value.
|
|
//
|
|
// rfc6819 4.4.1.7.
|
|
// Threat: Authorization "code" Leakage through Counterfeit Client
|
|
// The authorization server may also enforce the usage and validation
|
|
// of pre-registered redirect URIs (see Section 5.2.3.5).
|
|
func TestDoesClientWhiteListRedirect(t *testing.T) {
|
|
for k, c := range []struct {
|
|
client fosite.Client
|
|
url string
|
|
isError bool
|
|
expected string
|
|
}{
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{""}},
|
|
url: "https://foo.com/cb",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"wta://auth"}},
|
|
url: "wta://auth",
|
|
expected: "wta://auth",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"wta:///auth"}},
|
|
url: "wta:///auth",
|
|
expected: "wta:///auth",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"wta://foo/auth"}},
|
|
url: "wta://foo/auth",
|
|
expected: "wta://foo/auth",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"https://bar.com/cb"}},
|
|
url: "https://foo.com/cb",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"https://bar.com/cb"}},
|
|
url: "",
|
|
isError: false,
|
|
expected: "https://bar.com/cb",
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{""}},
|
|
url: "",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"https://bar.com/cb"}},
|
|
url: "https://bar.com/cb",
|
|
isError: false,
|
|
expected: "https://bar.com/cb",
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"https://bar.com/cb"}},
|
|
url: "https://bar.com/cb123",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://[::1]"}},
|
|
url: "http://[::1]:1024",
|
|
expected: "http://[::1]:1024",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://[::1]"}},
|
|
url: "http://[::1]:1024/cb",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://[::1]/cb"}},
|
|
url: "http://[::1]:1024/cb",
|
|
expected: "http://[::1]:1024/cb",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://[::1]"}},
|
|
url: "http://foo.bar/bar",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
|
|
url: "http://127.0.0.1:1024",
|
|
expected: "http://127.0.0.1:1024",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1/cb"}},
|
|
url: "http://127.0.0.1:64000/cb",
|
|
expected: "http://127.0.0.1:64000/cb",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
|
|
url: "http://127.0.0.1:64000/cb",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
|
|
url: "http://127.0.0.1",
|
|
expected: "http://127.0.0.1",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1/Cb"}},
|
|
url: "http://127.0.0.1:8080/Cb",
|
|
expected: "http://127.0.0.1:8080/Cb",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
|
|
url: "http://foo.bar/bar",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1"}},
|
|
url: ":/invalid.uri)bar",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb"}},
|
|
url: "http://127.0.0.1:8080/Cb",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb"}},
|
|
url: "http://127.0.0.1:8080/cb?foo=bar",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb?foo=bar"}},
|
|
url: "http://127.0.0.1:8080/cb?foo=bar",
|
|
expected: "http://127.0.0.1:8080/cb?foo=bar",
|
|
isError: false,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb?foo=bar"}},
|
|
url: "http://127.0.0.1:8080/cb?baz=bar&foo=bar",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb?foo=bar&baz=bar"}},
|
|
url: "http://127.0.0.1:8080/cb?baz=bar&foo=bar",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"https://www.ory.sh/cb"}},
|
|
url: "http://127.0.0.1:8080/cb",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"http://127.0.0.1:8080/cb"}},
|
|
url: "https://www.ory.sh/cb",
|
|
isError: true,
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"web+application://callback"}},
|
|
url: "web+application://callback",
|
|
isError: false,
|
|
expected: "web+application://callback",
|
|
},
|
|
{
|
|
client: &fosite.DefaultClient{RedirectURIs: []string{"https://google.com/?foo=bar%20foo+baz"}},
|
|
url: "https://google.com/?foo=bar%20foo+baz",
|
|
isError: false,
|
|
expected: "https://google.com/?foo=bar%20foo+baz",
|
|
},
|
|
} {
|
|
redir, err := fosite.MatchRedirectURIWithClientRedirectURIs(c.url, c.client)
|
|
assert.Equal(t, c.isError, err != nil, "%d: %+v", k, c)
|
|
if err == nil {
|
|
require.NotNil(t, redir, "%d", k)
|
|
assert.Equal(t, c.expected, redir.String(), "%d", k)
|
|
}
|
|
t.Logf("Passed test case %d", k)
|
|
}
|
|
}
|
|
|
|
func TestIsRedirectURISecure(t *testing.T) {
|
|
for d, c := range []struct {
|
|
u string
|
|
err bool
|
|
}{
|
|
{u: "http://google.com", err: true},
|
|
{u: "https://google.com", err: false},
|
|
{u: "http://localhost", err: false},
|
|
{u: "http://test.localhost", err: false},
|
|
{u: "http://127.0.0.1/", err: false},
|
|
{u: "http://[::1]/", err: false},
|
|
{u: "http://127.0.0.1:8080/", err: false},
|
|
{u: "http://[::1]:8080/", err: false},
|
|
{u: "http://testlocalhost", err: true},
|
|
{u: "wta://auth", err: false},
|
|
} {
|
|
uu, err := url.Parse(c.u)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, !c.err, fosite.IsRedirectURISecure(context.Background(), uu), "case %d", d)
|
|
}
|
|
}
|
|
|
|
func TestWriteAuthorizeFormPostResponse(t *testing.T) {
|
|
for d, c := range []struct {
|
|
parameters url.Values
|
|
check func(code string, state string, customParams url.Values, d int)
|
|
}{
|
|
{
|
|
parameters: url.Values{"code": {"lshr755nsg39fgur"}, "state": {"924659540232"}},
|
|
check: func(code string, state string, customParams url.Values, d int) {
|
|
assert.Equal(t, "lshr755nsg39fgur", code, "case %d", d)
|
|
assert.Equal(t, "924659540232", state, "case %d", d)
|
|
},
|
|
},
|
|
{
|
|
parameters: url.Values{"code": {"lshr75*ns-39f+ur"}, "state": {"9a:* <&)"}},
|
|
check: func(code string, state string, customParams url.Values, d int) {
|
|
assert.Equal(t, "lshr75*ns-39f+ur", code, "case %d", d)
|
|
assert.Equal(t, "9a:* <&)", state, "case %d", d)
|
|
},
|
|
},
|
|
{
|
|
parameters: url.Values{"code": {"1234"}, "custom": {"test2", "test3"}},
|
|
check: func(code string, state string, customParams url.Values, d int) {
|
|
assert.Equal(t, "1234", code, "case %d", d)
|
|
assert.Equal(t, []string{"test2", "test3"}, customParams["custom"], "case %d", d)
|
|
},
|
|
},
|
|
{
|
|
parameters: url.Values{"code": {"1234"}, "custom": {"<b>Bold</b>"}},
|
|
check: func(code string, state string, customParams url.Values, d int) {
|
|
assert.Equal(t, "1234", code, "case %d", d)
|
|
assert.Equal(t, "<b>Bold</b>", customParams.Get("custom"), "case %d", d)
|
|
},
|
|
},
|
|
} {
|
|
var responseBuffer bytes.Buffer
|
|
redirectURL := "https://localhost:8080/cb"
|
|
|
|
fosite.WriteAuthorizeFormPostResponse(redirectURL, c.parameters, fosite.DefaultFormPostTemplate, &responseBuffer)
|
|
|
|
code, state, _, _, customParams, _ := internal.ParseFormPostResponse(t, redirectURL, bytes.NewReader(responseBuffer.Bytes()))
|
|
c.check(code, state, customParams, d)
|
|
}
|
|
}
|
|
|
|
func TestIsRedirectURISecureStrict(t *testing.T) {
|
|
for d, c := range []struct {
|
|
u string
|
|
err bool
|
|
}{
|
|
{u: "http://google.com", err: true},
|
|
{u: "https://google.com", err: false},
|
|
{u: "http://localhost", err: false},
|
|
{u: "http://test.localhost", err: false},
|
|
{u: "http://127.0.0.1/", err: false},
|
|
{u: "http://[::1]/", err: false},
|
|
{u: "http://127.0.0.1:8080/", err: false},
|
|
{u: "http://[::1]:8080/", err: false},
|
|
{u: "http://testlocalhost", err: true},
|
|
{u: "wta://auth", err: true},
|
|
} {
|
|
uu, err := url.Parse(c.u)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, !c.err, fosite.IsRedirectURISecureStrict(context.Background(), uu), "case %d", d)
|
|
}
|
|
}
|
|
|
|
func TestURLSetFragment(t *testing.T) {
|
|
for d, c := range []struct {
|
|
u string
|
|
a string
|
|
f url.Values
|
|
}{
|
|
{u: "http://google.com", a: "http://google.com#code=567060896", f: url.Values{"code": []string{"567060896"}}},
|
|
{u: "http://google.com", a: "http://google.com#code=567060896&scope=read", f: url.Values{"code": []string{"567060896"}, "scope": []string{"read"}}},
|
|
{u: "http://google.com", a: "http://google.com#code=567060896&scope=read%20mail", f: url.Values{"code": []string{"567060896j"}, "scope": []string{"read mail"}}},
|
|
{u: "http://google.com", a: "http://google.com#code=567060896&scope=read+write", f: url.Values{"code": []string{"567060896"}, "scope": []string{"read+write"}}},
|
|
{u: "http://google.com", a: "http://google.com#code=567060896&scope=api:*", f: url.Values{"code": []string{"567060896"}, "scope": []string{"api:*"}}},
|
|
{u: "https://google.com?foo=bar", a: "https://google.com?foo=bar#code=567060896", f: url.Values{"code": []string{"567060896"}}},
|
|
{u: "http://localhost?foo=bar&baz=foo", a: "http://localhost?foo=bar&baz=foo#code=567060896", f: url.Values{"code": []string{"567060896"}}},
|
|
} {
|
|
uu, err := url.Parse(c.u)
|
|
require.NoError(t, err)
|
|
fosite.URLSetFragment(uu, c.f)
|
|
tURL, err := url.Parse(uu.String())
|
|
require.NoError(t, err)
|
|
r := ParseURLFragment(tURL.Fragment)
|
|
assert.Equal(t, c.f.Get("code"), r.Get("code"), "case %d", d)
|
|
assert.Equal(t, c.f.Get("scope"), r.Get("scope"), "case %d", d)
|
|
}
|
|
}
|
|
func ParseURLFragment(fragment string) url.Values {
|
|
r := url.Values{}
|
|
kvs := strings.Split(fragment, "&")
|
|
for _, kv := range kvs {
|
|
kva := strings.Split(kv, "=")
|
|
r.Add(kva[0], kva[1])
|
|
}
|
|
return r
|
|
}
|