mirror of https://github.com/ory/hydra
677 lines
26 KiB
Go
677 lines
26 KiB
Go
// Copyright © 2022 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package client_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"github.com/urfave/negroni"
|
|
|
|
"github.com/ory/x/httprouterx"
|
|
"github.com/ory/x/prometheusx"
|
|
"github.com/ory/x/sqlxx"
|
|
"github.com/ory/x/urlx"
|
|
|
|
"github.com/tidwall/sjson"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/tidwall/gjson"
|
|
|
|
"github.com/ory/hydra/v2/internal/testhelpers"
|
|
"github.com/ory/hydra/v2/x"
|
|
|
|
"github.com/ory/hydra/v2/driver/config"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/hydra/v2/client"
|
|
"github.com/ory/x/snapshotx"
|
|
)
|
|
|
|
type responseSnapshot struct {
|
|
Body json.RawMessage `json:"body"`
|
|
Status int `json:"status"`
|
|
}
|
|
|
|
func newResponseSnapshot(body string, res *http.Response) *responseSnapshot {
|
|
return &responseSnapshot{
|
|
Body: json.RawMessage(body),
|
|
Status: res.StatusCode,
|
|
}
|
|
}
|
|
|
|
func getClientID(body string) string {
|
|
return gjson.Get(body, "client_id").String()
|
|
}
|
|
|
|
func TestHandler(t *testing.T) {
|
|
ctx := context.Background()
|
|
reg := testhelpers.NewRegistryMemory(t)
|
|
h := client.NewHandler(reg)
|
|
|
|
t.Run("create client registration tokens", func(t *testing.T) {
|
|
for k, tc := range []struct {
|
|
c *client.Client
|
|
dynamic bool
|
|
}{
|
|
{dynamic: true, c: new(client.Client)},
|
|
{c: new(client.Client)},
|
|
{c: &client.Client{Secret: "01bbf13a-ae3e-44d5-b4b4-dd78137041be"}},
|
|
} {
|
|
t.Run(fmt.Sprintf("case=%d/dynamic=%v", k, tc.dynamic), func(t *testing.T) {
|
|
var b bytes.Buffer
|
|
require.NoError(t, json.NewEncoder(&b).Encode(tc.c))
|
|
r, err := http.NewRequest("POST", "/openid/registration", &b)
|
|
require.NoError(t, err)
|
|
|
|
hadSecret := len(tc.c.Secret) > 0
|
|
c, err := h.CreateClient(r, func(ctx context.Context, c *client.Client) error {
|
|
return nil
|
|
}, tc.dynamic)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, c.NID, uuid.Nil)
|
|
|
|
except := []string{"client_id", "registration_access_token", "updated_at", "created_at", "registration_client_uri"}
|
|
require.NotEmpty(t, c.RegistrationAccessToken)
|
|
require.NotEqual(t, c.RegistrationAccessTokenSignature, c.RegistrationAccessToken)
|
|
if !hadSecret {
|
|
require.NotEmpty(t, c.Secret)
|
|
except = append(except, "client_secret")
|
|
}
|
|
|
|
if tc.dynamic {
|
|
require.NotEmpty(t, c.GetID())
|
|
assert.Equal(t, reg.Config().PublicURL(ctx).String()+"oauth2/register/"+c.GetID(), c.RegistrationClientURI)
|
|
except = append(except, "client_id", "client_secret", "registration_client_uri")
|
|
}
|
|
|
|
snapshotx.SnapshotT(t, c, snapshotx.ExceptPaths(except...))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("dynamic client registration protocol authentication", func(t *testing.T) {
|
|
r, err := http.NewRequest("POST", "/openid/registration", bytes.NewBufferString("{}"))
|
|
require.NoError(t, err)
|
|
expected, err := h.CreateClient(r, func(ctx context.Context, c *client.Client) error {
|
|
return nil
|
|
}, true)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("valid auth", func(t *testing.T) {
|
|
actual, err := h.ValidDynamicAuth(&http.Request{Header: http.Header{"Authorization": {"Bearer " + expected.RegistrationAccessToken}}}, expected.ID)
|
|
require.NoError(t, err, "authentication with registration access token works")
|
|
assert.EqualValues(t, expected.GetID(), actual.GetID())
|
|
})
|
|
|
|
t.Run("missing auth", func(t *testing.T) {
|
|
_, err = h.ValidDynamicAuth(&http.Request{}, expected.ID)
|
|
require.Error(t, err, "authentication without registration access token fails")
|
|
})
|
|
|
|
t.Run("incorrect auth", func(t *testing.T) {
|
|
_, err = h.ValidDynamicAuth(&http.Request{Header: http.Header{"Authorization": {"Bearer invalid"}}}, expected.ID)
|
|
require.Error(t, err, "authentication with invalid registration access token fails")
|
|
})
|
|
})
|
|
|
|
newServer := func(t *testing.T, dynamicEnabled bool) (adminTs *httptest.Server, publicTs *httptest.Server) {
|
|
require.NoError(t, reg.Config().Set(ctx, config.KeyPublicAllowDynamicRegistration, dynamicEnabled))
|
|
|
|
metrics := prometheusx.NewMetricsManagerWithPrefix("hydra", prometheusx.HTTPMetrics, config.Version, config.Commit, config.Date)
|
|
{
|
|
n := negroni.New()
|
|
n.UseFunc(httprouterx.TrimTrailingSlashNegroni)
|
|
n.UseFunc(httprouterx.NoCacheNegroni)
|
|
n.UseFunc(httprouterx.AddAdminPrefixIfNotPresentNegroni)
|
|
n.Use(metrics)
|
|
|
|
router := x.NewRouterAdmin(metrics)
|
|
h.SetAdminRoutes(router)
|
|
router.Handler("GET", prometheusx.MetricsPrometheusPath, promhttp.Handler())
|
|
n.UseHandler(router)
|
|
|
|
adminTs = httptest.NewServer(n)
|
|
t.Cleanup(adminTs.Close)
|
|
reg.Config().MustSet(ctx, config.KeyAdminURL, adminTs.URL)
|
|
}
|
|
{
|
|
n := negroni.New()
|
|
n.UseFunc(httprouterx.TrimTrailingSlashNegroni)
|
|
n.UseFunc(httprouterx.NoCacheNegroni)
|
|
|
|
router := x.NewRouterPublic(metrics)
|
|
h.SetPublicRoutes(router)
|
|
n.UseHandler(router)
|
|
|
|
publicTs = httptest.NewServer(n)
|
|
t.Cleanup(publicTs.Close)
|
|
reg.Config().MustSet(ctx, config.KeyAdminURL, publicTs.URL)
|
|
}
|
|
return
|
|
}
|
|
|
|
fetch := func(t *testing.T, url string) (string, *http.Response) {
|
|
res, err := http.Get(url)
|
|
require.NoError(t, err)
|
|
defer res.Body.Close() //nolint:errcheck
|
|
body, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
return string(body), res
|
|
}
|
|
|
|
fetchWithBearerAuth := func(t *testing.T, method, url, token string, body io.Reader) (string, *http.Response) {
|
|
r, err := http.NewRequest(method, url, body)
|
|
require.NoError(t, err)
|
|
r.Header.Set("Authorization", "Bearer "+token)
|
|
r.Header.Set("Accept", "application/json")
|
|
res, err := http.DefaultClient.Do(r)
|
|
require.NoError(t, err)
|
|
defer res.Body.Close() //nolint:errcheck
|
|
out, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
return string(out), res
|
|
}
|
|
|
|
makeJSON := func(t *testing.T, ts *httptest.Server, method string, path string, body interface{}) (string, *http.Response) {
|
|
var b bytes.Buffer
|
|
require.NoError(t, json.NewEncoder(&b).Encode(body))
|
|
r, err := http.NewRequest(method, ts.URL+path, &b)
|
|
require.NoError(t, err)
|
|
r.Header.Set("Content-Type", "application/json")
|
|
res, err := ts.Client().Do(r)
|
|
require.NoError(t, err)
|
|
defer res.Body.Close() //nolint:errcheck
|
|
rb, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
return string(rb), res
|
|
}
|
|
|
|
createClient := func(t *testing.T, c *client.Client, ts *httptest.Server, path string) string {
|
|
body, res := makeJSON(t, ts, "POST", path, c)
|
|
require.Equal(t, http.StatusCreated, res.StatusCode, body)
|
|
return body
|
|
}
|
|
|
|
t.Run("selfservice disabled", func(t *testing.T) {
|
|
adminTs, publicTs := newServer(t, false)
|
|
|
|
trap := &client.Client{}
|
|
actual := createClient(t, trap, adminTs, client.ClientsHandlerPath)
|
|
actualID := getClientID(actual)
|
|
|
|
for _, tc := range []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{method: "GET", path: urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(actualID))},
|
|
{method: "POST", path: urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath)},
|
|
{method: "PUT", path: urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(actualID))},
|
|
{method: "DELETE", path: urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(actualID))},
|
|
} {
|
|
t.Run("method="+tc.method, func(t *testing.T) {
|
|
req, err := http.NewRequest(tc.method, tc.path, nil)
|
|
require.NoError(t, err)
|
|
|
|
res, err := publicTs.Client().Do(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=selfservice with incorrect or missing auth", func(t *testing.T) {
|
|
adminTs, publicTs := newServer(t, true)
|
|
expectedFirst := createClient(t, &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
expectedFirstID := getClientID(expectedFirst)
|
|
|
|
// Create the second client
|
|
expectedSecond := createClient(t, &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
expectedSecondID := getClientID(expectedSecond)
|
|
|
|
t.Run("endpoint=selfservice", func(t *testing.T) {
|
|
for _, method := range []string{"GET", "DELETE", "PUT"} {
|
|
t.Run("method="+method, func(t *testing.T) {
|
|
t.Run("without auth", func(t *testing.T) {
|
|
req, err := http.NewRequest(method, urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedFirstID)), nil)
|
|
require.NoError(t, err)
|
|
|
|
res, err := publicTs.Client().Do(req)
|
|
require.NoError(t, err)
|
|
defer res.Body.Close() //nolint:errcheck
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(string(body), res))
|
|
})
|
|
|
|
t.Run("without incorrect auth", func(t *testing.T) {
|
|
body, res := fetchWithBearerAuth(t, method, urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedFirstID)), "incorrect", nil)
|
|
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
|
|
t.Run("with a different client auth", func(t *testing.T) {
|
|
body, res := fetchWithBearerAuth(t, method, urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedFirstID)), expectedSecondID, nil)
|
|
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
t.Run("common", func(t *testing.T) {
|
|
adminTs, publicTs := newServer(t, true)
|
|
expected := createClient(t, &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
|
|
t.Run("case=create clients", func(t *testing.T) {
|
|
for k, tc := range []struct {
|
|
d string
|
|
payload *client.Client
|
|
path string
|
|
statusCode int
|
|
}{
|
|
{
|
|
d: "basic dynamic client registration",
|
|
payload: &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
},
|
|
path: client.DynClientsHandlerPath,
|
|
statusCode: http.StatusCreated,
|
|
},
|
|
{
|
|
d: "basic admin registration",
|
|
payload: &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
Metadata: []byte(`{"foo":"bar"}`),
|
|
},
|
|
path: client.ClientsHandlerPath,
|
|
statusCode: http.StatusCreated,
|
|
},
|
|
{
|
|
d: "metadata fails for dynamic client registration",
|
|
payload: &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
Metadata: []byte(`{"foo":"bar"}`),
|
|
},
|
|
path: client.DynClientsHandlerPath,
|
|
statusCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
d: "short secret fails for admin",
|
|
payload: &client.Client{
|
|
Secret: "short",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
},
|
|
path: client.ClientsHandlerPath,
|
|
statusCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
d: "non-uuid works",
|
|
payload: &client.Client{
|
|
ID: "not-a-uuid",
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
},
|
|
path: client.ClientsHandlerPath,
|
|
statusCode: http.StatusCreated,
|
|
},
|
|
{
|
|
d: "setting client id as uuid works",
|
|
payload: &client.Client{
|
|
ID: "98941dac-f963-4468-8a23-9483b1e04e3c",
|
|
Secret: "not too short",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
},
|
|
path: client.ClientsHandlerPath,
|
|
statusCode: http.StatusCreated,
|
|
},
|
|
{
|
|
d: "setting access token strategy fails",
|
|
payload: &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
AccessTokenStrategy: "jwt",
|
|
},
|
|
path: client.DynClientsHandlerPath,
|
|
statusCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
d: "setting skip_consent fails for dynamic registration",
|
|
payload: &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
SkipConsent: true,
|
|
},
|
|
path: client.DynClientsHandlerPath,
|
|
statusCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
d: "setting skip_consent succeeds for admin registration",
|
|
payload: &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
Secret: "2SKZkBf2P5g4toAXXnCrr~_sDM",
|
|
SkipConsent: true,
|
|
},
|
|
path: client.ClientsHandlerPath,
|
|
statusCode: http.StatusCreated,
|
|
},
|
|
{
|
|
d: "setting skip_logout_consent fails for dynamic registration",
|
|
payload: &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
SkipLogoutConsent: sqlxx.NullBool{Bool: true, Valid: true},
|
|
},
|
|
path: client.DynClientsHandlerPath,
|
|
statusCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
d: "setting skip_logout_consent succeeds for admin registration",
|
|
payload: &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
SkipLogoutConsent: sqlxx.NullBool{Bool: true, Valid: true},
|
|
Secret: "2SKZkBf2P5g4toAXXnCrr~_sDM",
|
|
},
|
|
path: client.ClientsHandlerPath,
|
|
statusCode: http.StatusCreated,
|
|
},
|
|
{
|
|
d: "basic dynamic client registration",
|
|
payload: &client.Client{
|
|
ID: "ead800c5-a316-4d0c-bf00-d25666ba72cf",
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
},
|
|
path: client.DynClientsHandlerPath,
|
|
statusCode: http.StatusBadRequest,
|
|
},
|
|
{
|
|
d: "empty ID succeeds",
|
|
payload: &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
},
|
|
path: client.ClientsHandlerPath,
|
|
statusCode: http.StatusCreated,
|
|
},
|
|
} {
|
|
t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) {
|
|
var ts *httptest.Server
|
|
if strings.HasPrefix(tc.path, client.DynClientsHandlerPath) {
|
|
ts = publicTs
|
|
} else {
|
|
ts = adminTs
|
|
}
|
|
body, res := makeJSON(t, ts, "POST", tc.path, tc.payload)
|
|
require.Equal(t, tc.statusCode, res.StatusCode, body)
|
|
exclude := []string{"updated_at", "created_at", "registration_access_token"}
|
|
if tc.path == client.DynClientsHandlerPath {
|
|
exclude = append(exclude, "client_id", "client_secret", "registration_client_uri")
|
|
}
|
|
if tc.payload.ID == "" {
|
|
exclude = append(exclude, "client_id", "registration_client_uri")
|
|
assert.NotEqual(t, uuid.Nil.String(), gjson.Get(body, "client_id").String(), body)
|
|
}
|
|
if tc.statusCode == http.StatusOK {
|
|
for _, key := range exclude {
|
|
assert.NotEmpty(t, gjson.Get(body, key).String(), "%s in %s", key, body)
|
|
}
|
|
}
|
|
snapshotx.SnapshotT(t, json.RawMessage(body), snapshotx.ExceptPaths(exclude...))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=fetching non-existing client", func(t *testing.T) {
|
|
for _, path := range []string{
|
|
urlx.MustJoin(client.DynClientsHandlerPath, "foo"),
|
|
urlx.MustJoin(client.ClientsHandlerPath, "foo"),
|
|
} {
|
|
t.Run("path="+path, func(t *testing.T) {
|
|
var ts *httptest.Server
|
|
if strings.HasPrefix(path, client.DynClientsHandlerPath) {
|
|
ts = publicTs
|
|
} else {
|
|
ts = adminTs
|
|
}
|
|
body, res := fetchWithBearerAuth(t, "GET", ts.URL+path, gjson.Get(expected, "registration_access_token").String(), nil)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=updating non-existing client", func(t *testing.T) {
|
|
for _, path := range []string{
|
|
urlx.MustJoin(client.DynClientsHandlerPath, "foo"),
|
|
urlx.MustJoin(client.ClientsHandlerPath, "foo"),
|
|
} {
|
|
t.Run("path="+path, func(t *testing.T) {
|
|
var ts *httptest.Server
|
|
if strings.HasPrefix(path, client.DynClientsHandlerPath) {
|
|
ts = publicTs
|
|
} else {
|
|
ts = adminTs
|
|
}
|
|
body, res := fetchWithBearerAuth(t, "PUT", ts.URL+path, "invalid", bytes.NewBufferString("{}"))
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=delete non-existing client", func(t *testing.T) {
|
|
for _, path := range []string{
|
|
urlx.MustJoin(client.DynClientsHandlerPath, "foo"),
|
|
urlx.MustJoin(client.ClientsHandlerPath, "foo"),
|
|
} {
|
|
var ts *httptest.Server
|
|
if strings.HasPrefix(path, client.DynClientsHandlerPath) {
|
|
ts = publicTs
|
|
} else {
|
|
ts = adminTs
|
|
}
|
|
t.Run("path="+path, func(t *testing.T) {
|
|
body, res := fetchWithBearerAuth(t, "DELETE", ts.URL+path, "invalid", nil)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=patching non-existing client", func(t *testing.T) {
|
|
body, res := fetchWithBearerAuth(t, "PATCH", urlx.MustJoin(adminTs.URL, client.ClientsHandlerPath, "foo"), "", nil)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
|
|
t.Run("case=fetching existing client", func(t *testing.T) {
|
|
expected := createClient(t, &client.Client{
|
|
Secret: "rdetzfuzgihojuzgtfrdes",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
id := gjson.Get(expected, "client_id").String()
|
|
rat := gjson.Get(expected, "registration_access_token").String()
|
|
|
|
t.Run("endpoint=admin", func(t *testing.T) {
|
|
body, res := fetch(t, urlx.MustJoin(adminTs.URL, client.ClientsHandlerPath, url.PathEscape(id)))
|
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
|
assert.Equal(t, id, gjson.Get(body, "client_id").String())
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res), snapshotx.ExceptPaths("body.client_id", "body.created_at", "body.updated_at"))
|
|
})
|
|
|
|
t.Run("endpoint=selfservice", func(t *testing.T) {
|
|
body, res := fetchWithBearerAuth(t, "GET", urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(id)), rat, nil)
|
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
|
assert.Equal(t, id, gjson.Get(body, "client_id").String())
|
|
assert.False(t, gjson.Get(body, "metadata").Bool())
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res), snapshotx.ExceptPaths("body.client_id", "body.created_at", "body.updated_at"))
|
|
})
|
|
})
|
|
|
|
t.Run("case=updating existing client fails with metadata on self service", func(t *testing.T) {
|
|
expected := createClient(t, &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
id := gjson.Get(expected, "client_id").String()
|
|
|
|
// Possible to update the secret
|
|
payload, err := sjson.SetRaw(expected, "metadata", `{"foo":"bar"}`)
|
|
require.NoError(t, err)
|
|
|
|
payload, err = sjson.Set(payload, "client_secret", "")
|
|
require.NoError(t, err)
|
|
|
|
body, res := fetchWithBearerAuth(t, "PUT", urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(id)), gjson.Get(expected, "registration_access_token").String(), bytes.NewBufferString(payload))
|
|
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "%s\n%s", body, payload)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
|
|
t.Run("case=updating existing client", func(t *testing.T) {
|
|
t.Run("endpoint=admin", func(t *testing.T) {
|
|
expected := createClient(t, &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
expectedID := getClientID(expected)
|
|
|
|
payload, _ := sjson.Set(expected, "redirect_uris", []string{"http://localhost:3000/cb", "https://foobar.com"})
|
|
body, res := makeJSON(t, adminTs, "PUT", urlx.MustJoin(client.ClientsHandlerPath, url.PathEscape(expectedID)), json.RawMessage(payload))
|
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res), snapshotx.ExceptPaths("body.created_at", "body.updated_at", "body.client_id", "body.registration_client_uri", "body.registration_access_token"))
|
|
})
|
|
|
|
t.Run("endpoint=dynamic client registration", func(t *testing.T) {
|
|
expected := createClient(t, &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
expectedID := getClientID(expected)
|
|
|
|
// Possible to update the secret
|
|
payload, _ := sjson.Set(expected, "redirect_uris", []string{"http://localhost:3000/cb", "https://foobar.com"})
|
|
payload, _ = sjson.Delete(payload, "client_secret")
|
|
payload, _ = sjson.Delete(payload, "metadata")
|
|
|
|
originalRAT := gjson.Get(expected, "registration_access_token").String()
|
|
body, res := fetchWithBearerAuth(t, "PUT", urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedID)), originalRAT, bytes.NewBufferString(payload))
|
|
assert.Equal(t, http.StatusOK, res.StatusCode, "%s\n%s", body, payload)
|
|
newToken := gjson.Get(body, "registration_access_token").String()
|
|
assert.NotEmpty(t, newToken)
|
|
require.NotEqual(t, originalRAT, newToken, "the new token should be different from the old token")
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res), snapshotx.ExceptPaths("body.created_at", "body.updated_at", "body.registration_access_token", "body.client_id", "body.registration_client_uri"))
|
|
|
|
_, res = fetchWithBearerAuth(t, "GET", urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedID)), originalRAT, bytes.NewBufferString(payload))
|
|
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
|
|
body, res = fetchWithBearerAuth(t, "GET", urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedID)), newToken, bytes.NewBufferString(payload))
|
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
|
assert.Empty(t, gjson.Get(body, "registration_access_token").String())
|
|
})
|
|
|
|
t.Run("endpoint=dynamic client registration does not allow changing the secret", func(t *testing.T) {
|
|
expected := createClient(t, &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
expectedID := getClientID(expected)
|
|
|
|
// Possible to update the secret
|
|
payload, _ := sjson.Set(expected, "redirect_uris", []string{"http://localhost:3000/cb", "https://foobar.com"})
|
|
payload, _ = sjson.Set(payload, "secret", "")
|
|
|
|
originalRAT := gjson.Get(expected, "registration_access_token").String()
|
|
body, res := fetchWithBearerAuth(t, "PUT", urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedID)), originalRAT, bytes.NewBufferString(payload))
|
|
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
})
|
|
|
|
t.Run("case=creating a client dynamically does not allow setting the secret", func(t *testing.T) {
|
|
body, res := makeJSON(t, publicTs, "POST", client.DynClientsHandlerPath, &client.Client{
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
Secret: "foobarbaz",
|
|
})
|
|
require.Equal(t, http.StatusBadRequest, res.StatusCode, body)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res))
|
|
})
|
|
|
|
t.Run("case=update the lifespans of an OAuth2 client", func(t *testing.T) {
|
|
expected := &client.Client{
|
|
Name: "update-existing-client-lifespans",
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}
|
|
body, res := makeJSON(t, adminTs, "POST", client.ClientsHandlerPath, expected)
|
|
require.Equal(t, http.StatusCreated, res.StatusCode, body)
|
|
|
|
body, res = makeJSON(t, adminTs, "PUT", urlx.MustJoin(client.ClientsHandlerPath, url.PathEscape(gjson.Get(body, "client_id").String()), "lifespans"), testhelpers.TestLifespans)
|
|
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
|
snapshotx.SnapshotT(t, newResponseSnapshot(body, res), snapshotx.ExceptPaths("body.client_id", "body.created_at", "body.updated_at"))
|
|
// Check metrics.
|
|
{
|
|
req, _ := http.NewRequest("GET", adminTs.URL+"/admin"+prometheusx.MetricsPrometheusPath, nil)
|
|
res, err := adminTs.Client().Do(req)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, http.StatusOK, res.StatusCode)
|
|
|
|
respBody, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, respBody)
|
|
}
|
|
|
|
})
|
|
|
|
t.Run("case=delete existing client", func(t *testing.T) {
|
|
t.Run("endpoint=admin", func(t *testing.T) {
|
|
expected := createClient(t, &client.Client{
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
expectedID := getClientID(expected)
|
|
|
|
_, res := makeJSON(t, adminTs, "DELETE", urlx.MustJoin(client.ClientsHandlerPath, url.PathEscape(expectedID)), nil)
|
|
assert.Equal(t, http.StatusNoContent, res.StatusCode)
|
|
})
|
|
|
|
t.Run("endpoint=selfservice", func(t *testing.T) {
|
|
expected := createClient(t, &client.Client{
|
|
Secret: "averylongsecret",
|
|
RedirectURIs: []string{"http://localhost:3000/cb"},
|
|
TokenEndpointAuthMethod: "client_secret_basic",
|
|
}, adminTs, client.ClientsHandlerPath)
|
|
expectedID := getClientID(expected)
|
|
|
|
originalRAT := gjson.Get(expected, "registration_access_token").String()
|
|
_, res := fetchWithBearerAuth(t, "DELETE", urlx.MustJoin(publicTs.URL, client.DynClientsHandlerPath, url.PathEscape(expectedID)), originalRAT, nil)
|
|
assert.Equal(t, http.StatusNoContent, res.StatusCode)
|
|
|
|
})
|
|
})
|
|
})
|
|
}
|