mirror of https://github.com/ory/hydra
364 lines
12 KiB
Go
364 lines
12 KiB
Go
// Copyright © 2022 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package client_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-jose/go-jose/v3"
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
. "github.com/ory/hydra/v2/client"
|
|
"github.com/ory/hydra/v2/driver"
|
|
"github.com/ory/hydra/v2/driver/config"
|
|
"github.com/ory/hydra/v2/fosite"
|
|
"github.com/ory/hydra/v2/internal/testhelpers"
|
|
"github.com/ory/hydra/v2/x"
|
|
"github.com/ory/x/configx"
|
|
"github.com/ory/x/httpx"
|
|
)
|
|
|
|
func TestValidate(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := testhelpers.NewRegistryMemory(t, driver.WithConfigOptions(configx.WithValues(map[string]any{
|
|
config.KeySubjectTypesSupported: []string{"pairwise", "public"},
|
|
config.KeyDefaultClientScope: []string{"openid"},
|
|
})))
|
|
v := NewValidator(reg)
|
|
|
|
dec := json.NewDecoder(strings.NewReader(validJWKS))
|
|
dec.DisallowUnknownFields()
|
|
var goodJWKS jose.JSONWebKeySet
|
|
require.NoError(t, dec.Decode(&goodJWKS))
|
|
|
|
for k, tc := range []struct {
|
|
in *Client
|
|
check func(*testing.T, *Client)
|
|
assertErr func(t assert.TestingT, err error, msg ...interface{}) bool
|
|
v func(*testing.T) *Validator
|
|
}{
|
|
{
|
|
in: new(Client),
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.Zero(t, c.GetID())
|
|
assert.EqualValues(t, c.GetID(), c.ID)
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo"},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.EqualValues(t, c.GetID(), c.ID)
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo"},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.EqualValues(t, c.GetID(), c.ID)
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", UserinfoSignedResponseAlg: "foo"},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", TokenEndpointAuthMethod: "private_key_jwt"},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", JSONWebKeys: &x.JoseJSONWebKeySet{JSONWebKeySet: new(jose.JSONWebKeySet)}, JSONWebKeysURI: "asdf", TokenEndpointAuthMethod: "private_key_jwt"},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", JSONWebKeys: &x.JoseJSONWebKeySet{JSONWebKeySet: new(jose.JSONWebKeySet)}, TokenEndpointAuthMethod: "private_key_jwt", TokenEndpointAuthSigningAlgorithm: "HS256"},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", JSONWebKeys: &x.JoseJSONWebKeySet{JSONWebKeySet: new(jose.JSONWebKeySet)}, JSONWebKeysURI: "https://example.org/jwks.json"},
|
|
assertErr: func(t assert.TestingT, err error, msg ...interface{}) bool {
|
|
e := new(fosite.RFC6749Error)
|
|
assert.ErrorAs(t, err, &e)
|
|
assert.Contains(t, e.HintField, "jwks and jwks_uri can not both be set")
|
|
return true
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", JSONWebKeys: &x.JoseJSONWebKeySet{JSONWebKeySet: &goodJWKS}},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.Len(t, c.JSONWebKeys.Keys, 2)
|
|
assert.Equal(t, c.JSONWebKeys.Keys[0].KeyID, "1")
|
|
assert.Equal(t, c.JSONWebKeys.Keys[1].KeyID, "2011-04-29")
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", JSONWebKeys: &x.JoseJSONWebKeySet{JSONWebKeySet: &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{}}}}},
|
|
assertErr: func(t assert.TestingT, err error, msg ...interface{}) bool {
|
|
e := new(fosite.RFC6749Error)
|
|
assert.ErrorAs(t, err, &e)
|
|
assert.Contains(t, e.HintField, "Invalid JSON web key in set")
|
|
return true
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", JSONWebKeys: new(x.JoseJSONWebKeySet), JSONWebKeysURI: "https://example.org/jwks.json"},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.Nil(t, c.GetJSONWebKeys())
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", PostLogoutRedirectURIs: []string{"https://bar/"}, RedirectURIs: []string{"https://foo/"}},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", PostLogoutRedirectURIs: []string{"http://foo/"}, RedirectURIs: []string{"https://foo/"}},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", PostLogoutRedirectURIs: []string{"https://foo:1234/"}, RedirectURIs: []string{"https://foo/"}},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", PostLogoutRedirectURIs: []string{"https://foo/"}, RedirectURIs: []string{"https://foo/"}},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.Equal(t, []string{"https://foo/"}, []string(c.PostLogoutRedirectURIs))
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", TermsOfServiceURI: "https://example.org"},
|
|
assertErr: assert.NoError,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", TermsOfServiceURI: "javascript:alert('XSS')"},
|
|
assertErr: assert.Error,
|
|
},
|
|
{
|
|
in: &Client{ID: "foo"},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.Equal(t, "public", c.SubjectType)
|
|
},
|
|
},
|
|
{
|
|
v: func(t *testing.T) *Validator {
|
|
reg.Config().MustSet(ctx, config.KeySubjectTypesSupported, []string{"pairwise"})
|
|
return NewValidator(reg)
|
|
},
|
|
in: &Client{ID: "foo"},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.Equal(t, "pairwise", c.SubjectType)
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", SubjectType: "pairwise"},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.Equal(t, "pairwise", c.SubjectType)
|
|
},
|
|
},
|
|
{
|
|
in: &Client{ID: "foo", SubjectType: "foo"},
|
|
assertErr: assert.Error,
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
|
if tc.v == nil {
|
|
tc.v = func(t *testing.T) *Validator {
|
|
return v
|
|
}
|
|
}
|
|
err := tc.v(t).Validate(ctx, tc.in)
|
|
if tc.assertErr != nil {
|
|
tc.assertErr(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
tc.check(t, tc.in)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type fakeHTTP struct {
|
|
*driver.RegistrySQL
|
|
c *http.Client
|
|
}
|
|
|
|
func (f *fakeHTTP) HTTPClient(_ context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client {
|
|
c := httpx.NewResilientClient(opts...)
|
|
c.HTTPClient = f.c
|
|
return c
|
|
}
|
|
|
|
func TestValidateSectorIdentifierURL(t *testing.T) {
|
|
reg := testhelpers.NewRegistryMemory(t)
|
|
var payload string
|
|
|
|
var h http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(payload))
|
|
}
|
|
ts := httptest.NewTLSServer(h)
|
|
defer ts.Close()
|
|
|
|
v := NewValidator(&fakeHTTP{RegistrySQL: reg, c: ts.Client()})
|
|
for k, tc := range []struct {
|
|
p string
|
|
r []string
|
|
u string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
u: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
u: "http://foo/bar",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
u: ts.URL,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
p: `["http://foo"]`,
|
|
u: ts.URL,
|
|
expectErr: false,
|
|
r: []string{"http://foo"},
|
|
},
|
|
{
|
|
p: `["http://foo"]`,
|
|
u: ts.URL,
|
|
expectErr: true,
|
|
r: []string{"http://foo", "http://not-foo"},
|
|
},
|
|
} {
|
|
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
|
payload = tc.p
|
|
err := v.ValidateSectorIdentifierURL(context.Background(), tc.u, tc.r)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// from https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.2
|
|
const validJWKS = `
|
|
{"keys":
|
|
[
|
|
{"kty":"EC",
|
|
"crv":"P-256",
|
|
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
|
|
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
|
|
"d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",
|
|
"use":"enc",
|
|
"kid":"1"},
|
|
|
|
{"kty":"RSA",
|
|
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
|
|
"e":"AQAB",
|
|
"d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q",
|
|
"p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs",
|
|
"q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk",
|
|
"dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0",
|
|
"dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk",
|
|
"qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU",
|
|
"alg":"RS256",
|
|
"kid":"2011-04-29"}
|
|
]
|
|
}
|
|
`
|
|
|
|
func TestValidateIPRanges(t *testing.T) {
|
|
ctx := t.Context()
|
|
reg := testhelpers.NewRegistryMemory(t)
|
|
|
|
v := NewValidator(reg)
|
|
reg.Config().MustSet(t.Context(), config.KeyClientHTTPNoPrivateIPRanges, true)
|
|
require.NoError(t, v.ValidateDynamicRegistration(ctx, &Client{}))
|
|
require.ErrorContains(t, v.ValidateDynamicRegistration(ctx, &Client{JSONWebKeysURI: "https://localhost:1234"}), "invalid_client_metadata")
|
|
require.ErrorContains(t, v.ValidateDynamicRegistration(ctx, &Client{BackChannelLogoutURI: "https://localhost:1234"}), "invalid_client_metadata")
|
|
require.ErrorContains(t, v.ValidateDynamicRegistration(ctx, &Client{RequestURIs: []string{"https://google", "https://localhost:1234"}}), "invalid_client_metadata")
|
|
|
|
reg.Config().MustSet(t.Context(), config.KeyClientHTTPNoPrivateIPRanges, false)
|
|
require.NoError(t, v.ValidateDynamicRegistration(ctx, &Client{}))
|
|
require.NoError(t, v.ValidateDynamicRegistration(ctx, &Client{JSONWebKeysURI: "https://localhost:1234"}))
|
|
require.NoError(t, v.ValidateDynamicRegistration(ctx, &Client{BackChannelLogoutURI: "https://localhost:1234"}))
|
|
require.NoError(t, v.ValidateDynamicRegistration(ctx, &Client{RequestURIs: []string{"https://google", "https://localhost:1234"}}))
|
|
}
|
|
|
|
func TestValidateDynamicRegistration(t *testing.T) {
|
|
reg := testhelpers.NewRegistryMemory(t, driver.WithConfigOptions(configx.WithValues(map[string]any{
|
|
config.KeySubjectTypesSupported: []string{"pairwise", "public"},
|
|
config.KeyDefaultClientScope: []string{"openid"},
|
|
})))
|
|
|
|
v := NewValidator(reg)
|
|
for k, tc := range []struct {
|
|
in *Client
|
|
check func(t *testing.T, c *Client)
|
|
expectErr bool
|
|
v func(t *testing.T) *Validator
|
|
}{
|
|
{
|
|
in: &Client{
|
|
ID: "foo",
|
|
PostLogoutRedirectURIs: []string{"https://foo/"},
|
|
RedirectURIs: []string{"https://foo/"},
|
|
Metadata: []byte(`{"access_token_ttl":10}`),
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
in: &Client{
|
|
ID: "foo",
|
|
PostLogoutRedirectURIs: []string{"https://foo/"},
|
|
RedirectURIs: []string{"https://foo/"},
|
|
Metadata: []byte(`{"id_token_ttl":10}`),
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
in: &Client{
|
|
ID: "foo",
|
|
PostLogoutRedirectURIs: []string{"https://foo/"},
|
|
RedirectURIs: []string{"https://foo/"},
|
|
Metadata: []byte(`{"anything":10}`),
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
in: &Client{
|
|
ID: "foo",
|
|
PostLogoutRedirectURIs: []string{"https://foo/"},
|
|
RedirectURIs: []string{"https://foo/"},
|
|
},
|
|
check: func(t *testing.T, c *Client) {
|
|
assert.EqualValues(t, "foo", c.ID)
|
|
},
|
|
},
|
|
} {
|
|
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
|
if tc.v == nil {
|
|
tc.v = func(t *testing.T) *Validator {
|
|
return v
|
|
}
|
|
}
|
|
err := tc.v(t).ValidateDynamicRegistration(t.Context(), tc.in)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
tc.check(t, tc.in)
|
|
}
|
|
})
|
|
}
|
|
}
|