mirror of https://github.com/ory/hydra
284 lines
12 KiB
Go
284 lines
12 KiB
Go
// Copyright © 2022 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package client_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mohae/deepcopy"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
goauth2 "golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/clientcredentials"
|
|
|
|
hydra "github.com/ory/hydra-client-go/v2"
|
|
"github.com/ory/hydra/v2/client"
|
|
"github.com/ory/hydra/v2/driver"
|
|
"github.com/ory/hydra/v2/driver/config"
|
|
"github.com/ory/hydra/v2/internal/testhelpers"
|
|
"github.com/ory/hydra/v2/oauth2"
|
|
"github.com/ory/hydra/v2/x"
|
|
"github.com/ory/x/assertx"
|
|
"github.com/ory/x/configx"
|
|
"github.com/ory/x/ioutilx"
|
|
"github.com/ory/x/pointerx"
|
|
"github.com/ory/x/prometheusx"
|
|
"github.com/ory/x/uuidx"
|
|
)
|
|
|
|
func createTestClient(prefix string) hydra.OAuth2Client {
|
|
return hydra.OAuth2Client{
|
|
ClientName: pointerx.Ptr(prefix + "name"),
|
|
ClientSecret: pointerx.Ptr(prefix + "secret"),
|
|
ClientUri: pointerx.Ptr(prefix + "uri"),
|
|
Contacts: []string{prefix + "peter", prefix + "pan"},
|
|
GrantTypes: []string{prefix + "client_credentials", prefix + "authorize_code"},
|
|
LogoUri: pointerx.Ptr(prefix + "logo"),
|
|
Owner: pointerx.Ptr(prefix + "an-owner"),
|
|
PolicyUri: pointerx.Ptr(prefix + "policy-uri"),
|
|
Scope: pointerx.Ptr(prefix + "foo bar baz"),
|
|
TosUri: pointerx.Ptr("https://example.org/" + prefix + "tos"),
|
|
ResponseTypes: []string{prefix + "id_token", prefix + "code"},
|
|
RedirectUris: []string{"https://" + prefix + "redirect-url", "https://" + prefix + "redirect-uri"},
|
|
ClientSecretExpiresAt: pointerx.Ptr[int64](0),
|
|
TokenEndpointAuthMethod: pointerx.Ptr("client_secret_basic"),
|
|
UserinfoSignedResponseAlg: pointerx.Ptr("none"),
|
|
SubjectType: pointerx.Ptr("public"),
|
|
Metadata: map[string]interface{}{"foo": "bar"},
|
|
// because these values are not nullable in the SQL schema, we have to set them not nil
|
|
AllowedCorsOrigins: []string{},
|
|
Audience: []string{},
|
|
Jwks: &hydra.JsonWebKeySet{},
|
|
SkipConsent: pointerx.Ptr(false),
|
|
}
|
|
}
|
|
|
|
var defaultIgnoreFields = []string{"client_id", "registration_access_token", "registration_client_uri", "created_at", "updated_at"}
|
|
|
|
func TestClientSDK(t *testing.T) {
|
|
ctx := context.Background()
|
|
r := testhelpers.NewRegistryMemory(t, driver.WithConfigOptions(configx.WithValues(map[string]any{
|
|
config.KeySubjectTypesSupported: []string{"public"},
|
|
config.KeyDefaultClientScope: []string{"foo", "bar"},
|
|
config.KeyPublicAllowDynamicRegistration: true,
|
|
})))
|
|
|
|
metrics := prometheusx.NewMetricsManagerWithPrefix("hydra", prometheusx.HTTPMetrics, config.Version, config.Commit, config.Date)
|
|
routerAdmin := x.NewRouterAdmin(metrics)
|
|
routerPublic := x.NewRouterPublic(metrics)
|
|
clHandler := client.NewHandler(r)
|
|
clHandler.SetPublicRoutes(routerPublic)
|
|
clHandler.SetAdminRoutes(routerAdmin)
|
|
o2Handler := oauth2.NewHandler(r)
|
|
o2Handler.SetPublicRoutes(routerPublic, func(h http.Handler) http.Handler { return h })
|
|
o2Handler.SetAdminRoutes(routerAdmin)
|
|
|
|
server := httptest.NewServer(routerAdmin)
|
|
t.Cleanup(server.Close)
|
|
publicServer := httptest.NewServer(routerPublic)
|
|
t.Cleanup(publicServer.Close)
|
|
r.Config().MustSet(ctx, config.KeyAdminURL, server.URL)
|
|
r.Config().MustSet(ctx, config.KeyOAuth2TokenURL, publicServer.URL+"/oauth2/token")
|
|
|
|
c := hydra.NewAPIClient(hydra.NewConfiguration())
|
|
c.GetConfig().Servers = hydra.ServerConfigurations{{URL: server.URL}}
|
|
|
|
t.Run("case=client default scopes are set", func(t *testing.T) {
|
|
result, _, err := c.OAuth2API.CreateOAuth2Client(ctx).OAuth2Client(hydra.OAuth2Client{}).Execute()
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, r.Config().DefaultClientScope(ctx), strings.Split(*result.Scope, " "))
|
|
|
|
_, err = c.OAuth2API.DeleteOAuth2Client(ctx, *result.ClientId).Execute()
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("case=client is created and updated", func(t *testing.T) {
|
|
createClient := createTestClient("")
|
|
compareClient := createClient
|
|
// This is not yet supported:
|
|
// createClient.SecretExpiresAt = 10
|
|
|
|
// returned client is correct on Create
|
|
result, res, err := c.OAuth2API.CreateOAuth2Client(ctx).OAuth2Client(createClient).Execute()
|
|
if !assert.NoError(t, err) {
|
|
t.Fatalf("error: %s", ioutilx.MustReadAll(res.Body))
|
|
}
|
|
assert.NotEmpty(t, result.UpdatedAt)
|
|
assert.NotEmpty(t, result.CreatedAt)
|
|
assert.NotEmpty(t, result.RegistrationAccessToken)
|
|
assert.NotEmpty(t, result.RegistrationClientUri)
|
|
assert.NotEmpty(t, *result.TosUri)
|
|
assert.NotEmpty(t, result.ClientId)
|
|
createClient.ClientId = result.ClientId
|
|
|
|
assertx.EqualAsJSONExcept(t, compareClient, result, defaultIgnoreFields)
|
|
assert.EqualValues(t, "bar", result.Metadata.(map[string]interface{})["foo"])
|
|
|
|
// secret is not returned on GetOAuth2Client
|
|
compareClient.ClientSecret = pointerx.Ptr("")
|
|
gresult, _, err := c.OAuth2API.GetOAuth2Client(context.Background(), *createClient.ClientId).Execute()
|
|
require.NoError(t, err)
|
|
assertx.EqualAsJSONExcept(t, compareClient, gresult, append(defaultIgnoreFields, "client_secret"))
|
|
|
|
// get client will return The request could not be authorized
|
|
gresult, _, err = c.OAuth2API.GetOAuth2Client(context.Background(), "unknown").Execute()
|
|
require.Error(t, err)
|
|
assert.Empty(t, gresult)
|
|
assert.True(t, strings.Contains(err.Error(), "404"), err.Error())
|
|
|
|
// listing clients returns the only added one
|
|
results, _, err := c.OAuth2API.ListOAuth2Clients(context.Background()).PageSize(100).Execute()
|
|
require.NoError(t, err)
|
|
assert.Len(t, results, 1)
|
|
assertx.EqualAsJSONExcept(t, compareClient, results[0], append(defaultIgnoreFields, "client_secret"))
|
|
|
|
// SecretExpiresAt gets overwritten with 0 on Update
|
|
compareClient.ClientSecret = createClient.ClientSecret
|
|
uresult, _, err := c.OAuth2API.SetOAuth2Client(context.Background(), *createClient.ClientId).OAuth2Client(createClient).Execute()
|
|
require.NoError(t, err)
|
|
assertx.EqualAsJSONExcept(t, compareClient, uresult, append(defaultIgnoreFields, "client_secret"))
|
|
|
|
// create another client
|
|
updateClient := createTestClient("foo")
|
|
uresult, _, err = c.OAuth2API.SetOAuth2Client(context.Background(), *createClient.ClientId).OAuth2Client(updateClient).Execute()
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, updateClient.ClientId, uresult.ClientId)
|
|
updateClient.ClientId = uresult.ClientId
|
|
assertx.EqualAsJSONExcept(t, updateClient, uresult, append(defaultIgnoreFields, "client_secret"))
|
|
|
|
// again, test if secret is not returned on Get
|
|
compareClient = updateClient
|
|
compareClient.ClientSecret = pointerx.Ptr("")
|
|
gresult, _, err = c.OAuth2API.GetOAuth2Client(context.Background(), *updateClient.ClientId).Execute()
|
|
require.NoError(t, err)
|
|
assertx.EqualAsJSONExcept(t, compareClient, gresult, append(defaultIgnoreFields, "client_secret"))
|
|
|
|
// client can not be found after being deleted
|
|
_, err = c.OAuth2API.DeleteOAuth2Client(context.Background(), *updateClient.ClientId).Execute()
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = c.OAuth2API.GetOAuth2Client(context.Background(), *updateClient.ClientId).Execute()
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("case=public client is transmitted without secret", func(t *testing.T) {
|
|
result, _, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(hydra.OAuth2Client{
|
|
TokenEndpointAuthMethod: pointerx.Ptr("none"),
|
|
}).Execute()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "", pointerx.Deref(result.ClientSecret))
|
|
|
|
result, _, err = c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(createTestClient("")).Execute()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "secret", pointerx.Deref(result.ClientSecret))
|
|
})
|
|
|
|
t.Run("case=id can be set", func(t *testing.T) {
|
|
id := uuidx.NewV4().String()
|
|
result, _, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(hydra.OAuth2Client{ClientId: pointerx.Ptr(id)}).Execute()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, id, pointerx.Deref(result.ClientId))
|
|
})
|
|
|
|
t.Run("case=patch client legally", func(t *testing.T) {
|
|
op := "add"
|
|
path := "/redirect_uris/-"
|
|
value := "http://foo.bar"
|
|
|
|
cl := createTestClient("")
|
|
created, _, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(cl).Execute()
|
|
require.NoError(t, err)
|
|
cl.ClientId = created.ClientId
|
|
|
|
expected := deepcopy.Copy(cl).(hydra.OAuth2Client)
|
|
expected.RedirectUris = append(expected.RedirectUris, value)
|
|
|
|
result, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *cl.ClientId).JsonPatch([]hydra.JsonPatch{{Op: op, Path: path, Value: value}}).Execute()
|
|
require.NoError(t, err)
|
|
expected.CreatedAt = result.CreatedAt
|
|
expected.UpdatedAt = result.UpdatedAt
|
|
expected.ClientSecret = result.ClientSecret
|
|
expected.ClientSecretExpiresAt = result.ClientSecretExpiresAt
|
|
assertx.EqualAsJSONExcept(t, expected, result, nil)
|
|
})
|
|
|
|
t.Run("case=patch client illegally", func(t *testing.T) {
|
|
op := "replace"
|
|
path := "/id"
|
|
value := "foo"
|
|
|
|
cl := createTestClient("")
|
|
created, res, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(cl).Execute()
|
|
require.NoError(t, err, "%s", ioutilx.MustReadAll(res.Body))
|
|
cl.ClientId = created.ClientId
|
|
|
|
_, _, err = c.OAuth2API.PatchOAuth2Client(context.Background(), *cl.ClientId).JsonPatch([]hydra.JsonPatch{{Op: op, Path: path, Value: value}}).Execute()
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("case=patch should not alter secret if not requested", func(t *testing.T) {
|
|
created, _, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(createTestClient("")).Execute()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "secret", *created.ClientSecret)
|
|
|
|
cc := clientcredentials.Config{
|
|
ClientID: *created.ClientId,
|
|
ClientSecret: "secret",
|
|
TokenURL: r.Config().OAuth2TokenURL(t.Context()).String(),
|
|
AuthStyle: goauth2.AuthStyleInHeader,
|
|
}
|
|
token, err := cc.Token(t.Context())
|
|
require.NoError(t, err)
|
|
require.NotZero(t, token.AccessToken)
|
|
|
|
ignoreFields := []string{"registration_access_token", "registration_client_uri", "updated_at"}
|
|
|
|
patchedURI, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *created.ClientId).JsonPatch([]hydra.JsonPatch{{Op: "replace", Path: "/client_uri", Value: "http://foo.bar"}}).Execute()
|
|
require.NoError(t, err)
|
|
require.Nil(t, patchedURI.ClientSecret, "client secret should not be returned in the response if it wasn't changed")
|
|
assertx.EqualAsJSONExcept(t, created, patchedURI, append(ignoreFields, "client_uri", "client_secret"), "client unchanged except client_uri; client_secret should not be returned")
|
|
|
|
token2, err := cc.Token(t.Context())
|
|
require.NoError(t, err)
|
|
require.NotZero(t, token2.AccessToken)
|
|
require.NotEqual(t, token.AccessToken, token2.AccessToken, "Got a new token after patching, with unchanged secret")
|
|
|
|
patchedSecret, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *created.ClientId).JsonPatch([]hydra.JsonPatch{{Op: "replace", Path: "/client_secret", Value: "newsecret"}}).Execute()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "newsecret", *patchedSecret.ClientSecret, "client secret should be returned if it was changed")
|
|
assertx.EqualAsJSONExcept(t, patchedURI, patchedSecret, append(ignoreFields, "client_secret"), "client unchanged except secret")
|
|
|
|
_, err = cc.Token(t.Context())
|
|
require.ErrorContains(t, err, "Client authentication failed", "should not be able to get a token with the old secret")
|
|
|
|
cc.ClientSecret = "newsecret"
|
|
token3, err := cc.Token(t.Context())
|
|
require.NoError(t, err, "should be able to get a token with the new secret")
|
|
require.NotZero(t, token3.AccessToken, "Got a new token after patching with new secret")
|
|
})
|
|
|
|
t.Run("case=patch client that has JSONWebKeysURI", func(t *testing.T) {
|
|
op := "replace"
|
|
path := "/client_name"
|
|
value := "test"
|
|
|
|
cl := createTestClient("")
|
|
cl.SetJwksUri("https://example.org/.well-known/jwks.json")
|
|
created, _, err := c.OAuth2API.CreateOAuth2Client(context.Background()).OAuth2Client(cl).Execute()
|
|
require.NoError(t, err)
|
|
cl.ClientId = created.ClientId
|
|
|
|
result, _, err := c.OAuth2API.PatchOAuth2Client(context.Background(), *cl.ClientId).JsonPatch([]hydra.JsonPatch{{Op: op, Path: path, Value: value}}).Execute()
|
|
require.NoError(t, err)
|
|
require.Equal(t, value, pointerx.Deref(result.ClientName))
|
|
})
|
|
}
|