diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..ba59834 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,3 @@ +reviews: + path_filters: + - "!pkg/client/**" \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 696d5cf..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: 2 -updates: - - package-ecosystem: gomod - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 40 - labels: [ "dependencies" ] - # Groups are updated together in one pull request - groups: - otel: - patterns: - - "go.opentelemetry.io/otel*" - otel-collector: - patterns: - - "go.opentelemetry.io/collector*" - - "github.com/open-telemetry/o*-collector-contrib/*" - otel-instrumentation: - patterns: - - "go.opentelemetry.io/contrib/instrumentation/*" - go-openapi: - patterns: - - "github.com/go-openapi/*" - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - labels: [ "dependencies" ] diff --git a/cmd/serve.go b/cmd/serve.go index 0581f3f..de2b2cb 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,6 +5,9 @@ import ( "crypto/x509" "encoding/pem" "fmt" + authlib "github.com/formancehq/go-libs/v3/auth" + oidclib "github.com/formancehq/go-libs/v3/oidc" + "github.com/go-jose/go-jose/v4" "net/http" "os" @@ -77,20 +80,24 @@ EL/wy5C80pa3jahniqVgO5L6zz0ZLtRIRE7aCtCIu826gctJ1+ShIso= ) func otlpHttpClientModule(debug bool) fx.Option { - return fx.Provide(func() *http.Client { - return &http.Client{ - Transport: otlp.NewRoundTripper(http.DefaultTransport, debug, otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { - str := fmt.Sprintf("%s %s", r.Method, r.URL.Path) - if len(r.URL.Query()) == 0 { - return str - } - - return fmt.Sprintf("%s?%s", str, r.URL.Query().Encode()) - })), - } + return fx.Decorate(func() *http.Client { + return otlpHttpClient(debug) }) } +func otlpHttpClient(debug bool) *http.Client { + return &http.Client{ + Transport: otlp.NewRoundTripper(http.DefaultTransport, debug, otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { + str := fmt.Sprintf("%s %s", r.Method, r.URL.Path) + if len(r.URL.Query()) == 0 { + return str + } + + return fmt.Sprintf("%s?%s", str, r.URL.Query().Encode()) + })), + } +} + func newServeCommand() *cobra.Command { cmd := &cobra.Command{ Use: "serve", @@ -104,6 +111,7 @@ func newServeCommand() *cobra.Command { cmd.Flags().String(SigningKeyFlag, defaultSigningKey, "Signing key") cmd.Flags().String(ListenFlag, ":8080", "Listening address") cmd.Flags().String(ConfigFlag, "", "Config file name without extension") + cmd.Flags().Bool(authlib.AuthCheckScopesFlag, false, "Enable scope checking") service.AddFlags(cmd.Flags()) licence.AddFlags(cmd.Flags()) @@ -169,15 +177,35 @@ func runServe(cmd *cobra.Command, _ []string) error { } listen, _ := cmd.Flags().GetString(ListenFlag) + checkScopes, _ := cmd.Flags().GetBool(authlib.AuthCheckScopesFlag) options := []fx.Option{ otlpHttpClientModule(service.IsDebug(cmd)), fx.Supply(fx.Annotate(cmd.Context(), fx.As(new(context.Context)))), sqlstorage.Module(*connectionOptions, key, service.IsDebug(cmd), o.Clients...), oidc.Module(key, baseUrl, o.Clients...), - api.Module(listen, baseUrl, sharedapi.ServiceInfo{ - Version: Version, - Debug: service.IsDebug(cmd), - }, service.IsDebug(cmd)), + api.Module( + listen, + baseUrl, + sharedapi.ServiceInfo{ + Version: Version, + Debug: service.IsDebug(cmd), + }, + service.IsDebug(cmd), + ), + authlib.Module(authlib.ModuleConfig{ + Enabled: true, + Issuer: baseUrl, + CheckScopes: checkScopes, + Service: ServiceName, + }), + fx.Decorate(func() oidclib.KeySet { + return oidclib.NewStaticKeySet(jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: oidc.KeyID, + Algorithm: string(jose.RS256), + Use: oidclib.KeyUseSignature, + }) + }), } delegatedIssuer, _ := cmd.Flags().GetString(DelegatedIssuerFlag) diff --git a/go.mod b/go.mod index 18beb4b..ad0669b 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,9 @@ replace github.com/formancehq/auth/pkg/client => ./pkg/client require ( github.com/formancehq/auth/pkg/client v0.0.0-00010101000000-000000000000 - github.com/formancehq/go-libs/v3 v3.3.0 + github.com/formancehq/go-libs/v3 v3.4.0 github.com/go-chi/chi/v5 v5.2.3 + github.com/go-jose/go-jose/v4 v4.1.3 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 @@ -92,7 +93,9 @@ require ( github.com/gorilla/schema v1.4.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect @@ -114,12 +117,13 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/user v0.4.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opencontainers/runc v1.1.14 // indirect + github.com/opencontainers/runc v1.2.8 // indirect github.com/ory/dockertest/v3 v3.11.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect @@ -139,6 +143,8 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/dburl v0.23.8 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zitadel/oidc/v3 v3.45.0 // indirect + github.com/zitadel/schema v1.3.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/host v0.62.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 // indirect diff --git a/go.sum b/go.sum index 1ac8e36..f65b5f8 100644 --- a/go.sum +++ b/go.sum @@ -74,10 +74,12 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v3 v3.3.0 h1:Qs6oPRNHJbR4xu3Lzl2zGAiQD94XLpabAFYPq3YBqK0= -github.com/formancehq/go-libs/v3 v3.3.0/go.mod h1:kr5pgap99LPdSVcYWgI+mCN51wr5D+YxEnt9A6JUiFU= +github.com/formancehq/go-libs/v3 v3.4.0 h1:/hk51xOL++qRItlrZEhYlPQX+o9ePz/eBc3KiLK03Dc= +github.com/formancehq/go-libs/v3 v3.4.0/go.mod h1:Lr0qE3ioCTFlm+BuXSwB7qpGF12/IfKYOpFvszTFRJk= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -124,8 +126,12 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= @@ -142,8 +148,8 @@ github.com/jackc/pgxlisten v0.0.0-20250802141604-12b92425684c h1:on93qgQJqBwjHYj github.com/jackc/pgxlisten v0.0.0-20250802141604-12b92425684c/go.mod h1:ygR1JwoRvIb4hhLukHQxSB3u/sRQT4Laylx0rDtNEhE= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= -github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -183,6 +189,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= @@ -201,8 +209,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= -github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= +github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q= +github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -283,6 +291,10 @@ github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= +github.com/zitadel/oidc/v3 v3.45.0 h1:SaVJ2kdcJi/zdEWWlAns+81VxmfdYX4E+2mWFVIH7Ec= +github.com/zitadel/oidc/v3 v3.45.0/go.mod h1:UeK0iVOoqfMuDVgSfv56BqTz8YQC2M+tGRIXZ7Ii3VY= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/host v0.62.0 h1:hAVkLihKCrIkiX/cUvY0qn6yi0uMdr1/zWpb7lEjdYY= diff --git a/openapi.yaml b/openapi.yaml index b5085e9..0f80a3b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -253,7 +253,8 @@ components: Metadata: type: object nullable: true - additionalProperties: {} + additionalProperties: + type: string ClientOptions: type: object properties: diff --git a/pkg/api/authorization/accesstoken.go b/pkg/api/authorization/accesstoken.go deleted file mode 100644 index 9bbfca4..0000000 --- a/pkg/api/authorization/accesstoken.go +++ /dev/null @@ -1,35 +0,0 @@ -package authorization - -import ( - "net/http" - "strings" - - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" -) - -func verifyAccessToken(r *http.Request, o op.OpenIDProvider) error { - if !strings.HasPrefix(r.URL.String(), "/clients") && - !strings.HasPrefix(r.URL.String(), "/users") { - return nil - } - - authHeader := r.Header.Get("authorization") - if authHeader == "" { - return ErrMissingAuthHeader - } - - if !strings.HasPrefix(authHeader, strings.ToLower(oidc.PrefixBearer)) && - !strings.HasPrefix(authHeader, oidc.PrefixBearer) { - return ErrMalformedAuthHeader - } - - token := strings.TrimPrefix(authHeader, strings.ToLower(oidc.PrefixBearer)) - token = strings.TrimPrefix(token, oidc.PrefixBearer) - - if _, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](r.Context(), token, o.AccessTokenVerifier(r.Context())); err != nil { - return ErrVerifyAuthToken - } - - return nil -} diff --git a/pkg/api/authorization/accesstoken_test.go b/pkg/api/authorization/accesstoken_test.go deleted file mode 100644 index 569a5fa..0000000 --- a/pkg/api/authorization/accesstoken_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package authorization - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "fmt" - "net" - "net/http" - "testing" - - "github.com/formancehq/go-libs/v3/bun/bundebug" - "github.com/uptrace/bun" - - "github.com/formancehq/go-libs/v3/logging" - - "github.com/formancehq/go-libs/v3/bun/bunconnect" - - auth "github.com/formancehq/auth/pkg" - "github.com/formancehq/auth/pkg/delegatedauth" - authoidc "github.com/formancehq/auth/pkg/oidc" - "github.com/formancehq/auth/pkg/storage/sqlstorage" - "github.com/oauth2-proxy/mockoidc" - "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" -) - -func TestVerifyAccessToken(t *testing.T) { - t.Parallel() - - mockOIDC, err := mockoidc.Run() - require.NoError(t, err) - defer func() { - require.NoError(t, mockOIDC.Shutdown()) - }() - - // Prepare a tcp connection, listening on :0 to select a random port - l, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - - // Compute server url, it will be the "issuer" of our oidc provider - serverURL := fmt.Sprintf("http://%s", l.Addr().String()) - - hooks := make([]bun.QueryHook, 0) - if testing.Verbose() { - hooks = append(hooks, bundebug.NewQueryHook()) - } - - // Construct our storage - postgresDB := srv.NewDatabase(t) - db, err := bunconnect.OpenSQLDB(logging.TestingContext(), bunconnect.ConnectionOptions{ - DatabaseSourceName: postgresDB.ConnString(), - }, hooks...) - require.NoError(t, err) - defer db.Close() - - require.NoError(t, sqlstorage.Migrate(context.Background(), db)) - - storage := sqlstorage.New(db) - - serverRelyingParty, err := rp.NewRelyingPartyOIDC(mockOIDC.Issuer(), mockOIDC.ClientID, mockOIDC.ClientSecret, - fmt.Sprintf("%s/authorize/callback", serverURL), []string{"openid", "email"}) - require.NoError(t, err) - - key, _ := rsa.GenerateKey(rand.Reader, 2048) - - staticClients := []auth.StaticClient{{ - ClientOptions: auth.ClientOptions{ - Id: "test", - Public: true, - RedirectURIs: []string{"http://localhost:3000/auth-callback"}, - Name: "test", - PostLogoutRedirectUris: []string{"http://localhost:3000/"}, - Trusted: true, - }, - }} - storageFacade := authoidc.NewStorageFacade(storage, serverRelyingParty, key, staticClients...) - - keySet, err := authoidc.ReadKeySet(http.DefaultClient, context.Background(), delegatedauth.Config{ - Issuer: mockOIDC.Issuer(), - ClientID: mockOIDC.ClientID, - ClientSecret: mockOIDC.ClientSecret, - }) - require.NoError(t, err) - - provider, err := authoidc.NewOpenIDProvider(storageFacade, serverURL, mockOIDC.Issuer(), keySet) - require.NoError(t, err) - - ar := &oidc.AuthRequest{ - ClientID: staticClients[0].Id, - } - authReq, err := provider.Storage().CreateAuthRequest(context.Background(), ar, "") - require.NoError(t, err) - - client, err := provider.Storage().GetClientByClientID(context.Background(), authReq.GetClientID()) - require.NoError(t, err) - - tokenResponse, err := op.CreateTokenResponse(context.Background(), authReq, client, provider, true, "", "") - require.NoError(t, err) - - t.Run("unprotected route", func(t *testing.T) { - t.Parallel() - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/any", nil) - require.NoError(t, err) - require.NoError(t, verifyAccessToken(req, provider)) - }) - - t.Run("protected routes", func(t *testing.T) { - t.Parallel() - - protectedRoutes := []string{"/clients", "/users"} - for _, route := range protectedRoutes { - - t.Run("no token", func(t *testing.T) { - t.Parallel() - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, route, nil) - require.NoError(t, err) - - err = verifyAccessToken(req, provider) - require.Error(t, err) - require.EqualError(t, err, ErrMissingAuthHeader.Error()) - }) - - t.Run("malformed token", func(t *testing.T) { - t.Parallel() - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, route, nil) - require.NoError(t, err) - - req.Header.Set("Authorization", "malformed") - err = verifyAccessToken(req, provider) - require.Error(t, err) - require.EqualError(t, err, ErrMalformedAuthHeader.Error()) - }) - - t.Run("unverified token", func(t *testing.T) { - t.Parallel() - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, route, nil) - require.NoError(t, err) - - req.Header.Set("Authorization", oidc.PrefixBearer+"unverified") - err = verifyAccessToken(req, provider) - require.Error(t, err) - require.EqualError(t, err, ErrVerifyAuthToken.Error()) - }) - - t.Run("verified token", func(t *testing.T) { - t.Parallel() - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, route, nil) - require.NoError(t, err) - - req.Header.Set("Authorization", oidc.PrefixBearer+tokenResponse.AccessToken) - require.NoError(t, verifyAccessToken(req, provider)) - }) - } - }) -} diff --git a/pkg/api/authorization/main_test.go b/pkg/api/authorization/main_test.go deleted file mode 100644 index d1a5564..0000000 --- a/pkg/api/authorization/main_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package authorization - -import ( - "testing" - - "github.com/formancehq/go-libs/v3/logging" - "github.com/formancehq/go-libs/v3/testing/docker" - "github.com/formancehq/go-libs/v3/testing/utils" - - "github.com/formancehq/go-libs/v3/testing/platform/pgtesting" -) - -var srv *pgtesting.PostgresServer - -func TestMain(m *testing.M) { - utils.WithTestMain(func(t *utils.TestingTForMain) int { - srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing())) - - return m.Run() - }) -} diff --git a/pkg/api/authorization/middleware.go b/pkg/api/authorization/middleware.go deleted file mode 100644 index 25e4966..0000000 --- a/pkg/api/authorization/middleware.go +++ /dev/null @@ -1,28 +0,0 @@ -package authorization - -import ( - "errors" - "net/http" - - "github.com/zitadel/oidc/v2/pkg/op" -) - -var ( - ErrMissingAuthHeader = errors.New("missing authorization header") - ErrMalformedAuthHeader = errors.New("malformed authorization header") - ErrVerifyAuthToken = errors.New("could not verify access token") -) - -func Middleware(o op.OpenIDProvider) func(h http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - return http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - if err := verifyAccessToken(r, o); err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - - h.ServeHTTP(w, r) - }) - } -} diff --git a/pkg/api/clients.go b/pkg/api/clients.go index 16b496b..ae01af2 100644 --- a/pkg/api/clients.go +++ b/pkg/api/clients.go @@ -1,6 +1,7 @@ package api import ( + authlib "github.com/formancehq/go-libs/v3/auth" "net/http" "github.com/go-chi/chi/v5" @@ -11,8 +12,8 @@ import ( _ "github.com/formancehq/go-libs/v3/api" ) -func addClientRoutes(db *bun.DB, r chi.Router) { - r.Route("/clients", func(r chi.Router) { +func addClientRoutes(db *bun.DB, r chi.Router, authenticator authlib.Authenticator) { + r.With(authlib.Middleware(authenticator)).Route("/clients", func(r chi.Router) { r.Post("/", createClient(db)) r.Get("/", listClients(db)) r.Route("/{clientId}", func(r chi.Router) { @@ -48,6 +49,7 @@ func mapBusinessClient(c auth.Client) clientView { PostLogoutRedirectUris: c.PostLogoutRedirectUris, Metadata: c.Metadata, Scopes: c.Scopes, + Trusted: c.Trusted, }, ID: c.Id, Secrets: mapList(c.Secrets, func(i auth.ClientSecret) clientSecretView { diff --git a/pkg/api/clients_test.go b/pkg/api/clients_test.go index bfdee70..1096204 100644 --- a/pkg/api/clients_test.go +++ b/pkg/api/clients_test.go @@ -2,6 +2,7 @@ package api import ( "context" + authlib "github.com/formancehq/go-libs/v3/auth" "net/http" "net/http/httptest" "testing" @@ -40,7 +41,7 @@ func withDbAndClientRouter(t *testing.T, callback func(router chi.Router, db *bu require.NoError(t, sqlstorage.Migrate(context.Background(), db)) router := chi.NewRouter() - addClientRoutes(db, router) + addClientRoutes(db, router, authlib.NewNoAuth()) callback(router, db) } diff --git a/pkg/api/module.go b/pkg/api/module.go index 2b4d2de..2b6d5d7 100644 --- a/pkg/api/module.go +++ b/pkg/api/module.go @@ -7,8 +7,6 @@ import ( "github.com/formancehq/go-libs/v3/service" - "github.com/formancehq/auth/pkg/api/authorization" - "github.com/formancehq/go-libs/v3/api" "github.com/formancehq/go-libs/v3/health" "github.com/formancehq/go-libs/v3/httpserver" @@ -17,7 +15,11 @@ import ( "go.uber.org/fx" ) -func CreateRootRouter(o op.OpenIDProvider, logger logging.Logger, issuer string, debug bool) chi.Router { +func CreateRootRouter( + logger logging.Logger, + issuer string, + debug bool, +) chi.Router { rootRouter := chi.NewRouter() rootRouter.Use(service.OTLPMiddleware("auth", debug)) rootRouter.Use(httpserver.LoggerMiddleware(logger)) @@ -34,7 +36,6 @@ func CreateRootRouter(o op.OpenIDProvider, logger logging.Logger, issuer string, )) }) }) - rootRouter.Use(authorization.Middleware(o)) return rootRouter } @@ -46,8 +47,8 @@ func Module(addr, issuer string, serviceInfo api.ServiceInfo, debug bool) fx.Opt return fx.Options( health.Module(), fx.Supply(serviceInfo), - fx.Provide(func(o op.OpenIDProvider, logger logging.Logger) chi.Router { - return CreateRootRouter(o, logger, issuer, debug) + fx.Provide(func(logger logging.Logger) chi.Router { + return CreateRootRouter(logger, issuer, debug) }), fx.Invoke( addInfoRoute, diff --git a/pkg/api/users.go b/pkg/api/users.go index a2b5e12..59a2c7f 100644 --- a/pkg/api/users.go +++ b/pkg/api/users.go @@ -1,6 +1,7 @@ package api import ( + authlib "github.com/formancehq/go-libs/v3/auth" "net/http" "github.com/go-chi/chi/v5" @@ -10,8 +11,8 @@ import ( auth "github.com/formancehq/auth/pkg" ) -func addUserRoutes(db *bun.DB, r chi.Router) { - r.Route("/users", func(r chi.Router) { +func addUserRoutes(db *bun.DB, r chi.Router, authenticator authlib.Authenticator) { + r.With(authlib.Middleware(authenticator)).Route("/users", func(r chi.Router) { r.Get("/", listUsers(db)) r.Route("/{userId}", func(r chi.Router) { r.Get("/", readUser(db)) diff --git a/pkg/api/users_test.go b/pkg/api/users_test.go index 87181e1..18a87b1 100644 --- a/pkg/api/users_test.go +++ b/pkg/api/users_test.go @@ -2,6 +2,7 @@ package api import ( "context" + authlib "github.com/formancehq/go-libs/v3/auth" "net/http" "net/http/httptest" "testing" @@ -86,7 +87,7 @@ func withDbAndUserRouter(t *testing.T, callback func(router chi.Router, db *bun. require.NoError(t, sqlstorage.Migrate(context.Background(), db)) router := chi.NewRouter() - addUserRoutes(db, router) + addUserRoutes(db, router, authlib.NewNoAuth()) callback(router, db) } diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 73b36b7..8b9b811 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: 4eb12f96-f428-4bb6-b939-70582497ed17 management: - docChecksum: a6bac58f2bf21d8303073b5c8c97a5f9 + docChecksum: da3570db8f24f2bb3368851fc229da5e docVersion: 0.1.0 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.7.1 - configChecksum: 4a252e7b5466e4cc16e74f744bc0939c + releaseVersion: 0.7.2 + configChecksum: b42bc89823921ddb04450c7033462573 features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index 47456f6..b15d618 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -12,7 +12,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.7.1 + version: 0.7.2 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/components/client.md b/pkg/client/docs/models/components/client.md index bdb6ab0..2ab1946 100644 --- a/pkg/client/docs/models/components/client.md +++ b/pkg/client/docs/models/components/client.md @@ -11,7 +11,7 @@ | `Name` | *string* | :heavy_check_mark: | N/A | | `Trusted` | **bool* | :heavy_minus_sign: | N/A | | `PostLogoutRedirectUris` | []*string* | :heavy_minus_sign: | N/A | -| `Metadata` | map[string]*any* | :heavy_minus_sign: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | | `Scopes` | []*string* | :heavy_minus_sign: | N/A | | `ID` | *string* | :heavy_check_mark: | N/A | | `Secrets` | [][components.ClientSecret](../../models/components/clientsecret.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/clientsecret.md b/pkg/client/docs/models/components/clientsecret.md index b0b9e1a..da3b801 100644 --- a/pkg/client/docs/models/components/clientsecret.md +++ b/pkg/client/docs/models/components/clientsecret.md @@ -3,9 +3,9 @@ ## Fields -| Field | Type | Required | Description | -| ------------------ | ------------------ | ------------------ | ------------------ | -| `LastDigits` | *string* | :heavy_check_mark: | N/A | -| `Name` | *string* | :heavy_check_mark: | N/A | -| `ID` | *string* | :heavy_check_mark: | N/A | -| `Metadata` | map[string]*any* | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| ------------------- | ------------------- | ------------------- | ------------------- | +| `LastDigits` | *string* | :heavy_check_mark: | N/A | +| `Name` | *string* | :heavy_check_mark: | N/A | +| `ID` | *string* | :heavy_check_mark: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/createclientrequest.md b/pkg/client/docs/models/components/createclientrequest.md index 481c693..a5363f2 100644 --- a/pkg/client/docs/models/components/createclientrequest.md +++ b/pkg/client/docs/models/components/createclientrequest.md @@ -11,5 +11,5 @@ | `Name` | *string* | :heavy_check_mark: | N/A | | `Trusted` | **bool* | :heavy_minus_sign: | N/A | | `PostLogoutRedirectUris` | []*string* | :heavy_minus_sign: | N/A | -| `Metadata` | map[string]*any* | :heavy_minus_sign: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | | `Scopes` | []*string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/createsecretrequest.md b/pkg/client/docs/models/components/createsecretrequest.md index e621693..755c67e 100644 --- a/pkg/client/docs/models/components/createsecretrequest.md +++ b/pkg/client/docs/models/components/createsecretrequest.md @@ -3,7 +3,7 @@ ## Fields -| Field | Type | Required | Description | -| ------------------ | ------------------ | ------------------ | ------------------ | -| `Name` | *string* | :heavy_check_mark: | N/A | -| `Metadata` | map[string]*any* | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| ------------------- | ------------------- | ------------------- | ------------------- | +| `Name` | *string* | :heavy_check_mark: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/secret.md b/pkg/client/docs/models/components/secret.md index 43e1d19..2bcf366 100644 --- a/pkg/client/docs/models/components/secret.md +++ b/pkg/client/docs/models/components/secret.md @@ -3,10 +3,10 @@ ## Fields -| Field | Type | Required | Description | -| ------------------ | ------------------ | ------------------ | ------------------ | -| `Name` | *string* | :heavy_check_mark: | N/A | -| `Metadata` | map[string]*any* | :heavy_minus_sign: | N/A | -| `ID` | *string* | :heavy_check_mark: | N/A | -| `LastDigits` | *string* | :heavy_check_mark: | N/A | -| `Clear` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| ------------------- | ------------------- | ------------------- | ------------------- | +| `Name` | *string* | :heavy_check_mark: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | +| `ID` | *string* | :heavy_check_mark: | N/A | +| `LastDigits` | *string* | :heavy_check_mark: | N/A | +| `Clear` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/updateclientrequest.md b/pkg/client/docs/models/components/updateclientrequest.md index 3084d52..822c955 100644 --- a/pkg/client/docs/models/components/updateclientrequest.md +++ b/pkg/client/docs/models/components/updateclientrequest.md @@ -11,5 +11,5 @@ | `Name` | *string* | :heavy_check_mark: | N/A | | `Trusted` | **bool* | :heavy_minus_sign: | N/A | | `PostLogoutRedirectUris` | []*string* | :heavy_minus_sign: | N/A | -| `Metadata` | map[string]*any* | :heavy_minus_sign: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | | `Scopes` | []*string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/formance.go b/pkg/client/formance.go index e7c64e6..4e84f5b 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "0.1.0", - SDKVersion: "0.7.1", + SDKVersion: "0.7.2", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.7.1 2.384.1 0.1.0 github.com/formancehq/auth/pkg/client", + UserAgent: "speakeasy-sdk/go 0.7.2 2.384.1 0.1.0 github.com/formancehq/auth/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/components/client.go b/pkg/client/models/components/client.go index efd6dee..736822c 100644 --- a/pkg/client/models/components/client.go +++ b/pkg/client/models/components/client.go @@ -3,16 +3,16 @@ package components type Client struct { - Public *bool `json:"public,omitempty"` - RedirectUris []string `json:"redirectUris,omitempty"` - Description *string `json:"description,omitempty"` - Name string `json:"name"` - Trusted *bool `json:"trusted,omitempty"` - PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"` - Metadata map[string]any `json:"metadata,omitempty"` - Scopes []string `json:"scopes,omitempty"` - ID string `json:"id"` - Secrets []ClientSecret `json:"secrets,omitempty"` + Public *bool `json:"public,omitempty"` + RedirectUris []string `json:"redirectUris,omitempty"` + Description *string `json:"description,omitempty"` + Name string `json:"name"` + Trusted *bool `json:"trusted,omitempty"` + PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Scopes []string `json:"scopes,omitempty"` + ID string `json:"id"` + Secrets []ClientSecret `json:"secrets,omitempty"` } func (o *Client) GetPublic() *bool { @@ -57,7 +57,7 @@ func (o *Client) GetPostLogoutRedirectUris() []string { return o.PostLogoutRedirectUris } -func (o *Client) GetMetadata() map[string]any { +func (o *Client) GetMetadata() map[string]string { if o == nil { return nil } diff --git a/pkg/client/models/components/clientsecret.go b/pkg/client/models/components/clientsecret.go index f47e4db..db0e6b4 100644 --- a/pkg/client/models/components/clientsecret.go +++ b/pkg/client/models/components/clientsecret.go @@ -3,10 +3,10 @@ package components type ClientSecret struct { - LastDigits string `json:"lastDigits"` - Name string `json:"name"` - ID string `json:"id"` - Metadata map[string]any `json:"metadata,omitempty"` + LastDigits string `json:"lastDigits"` + Name string `json:"name"` + ID string `json:"id"` + Metadata map[string]string `json:"metadata,omitempty"` } func (o *ClientSecret) GetLastDigits() string { @@ -30,7 +30,7 @@ func (o *ClientSecret) GetID() string { return o.ID } -func (o *ClientSecret) GetMetadata() map[string]any { +func (o *ClientSecret) GetMetadata() map[string]string { if o == nil { return nil } diff --git a/pkg/client/models/components/createclientrequest.go b/pkg/client/models/components/createclientrequest.go index 97283f1..b83d90e 100644 --- a/pkg/client/models/components/createclientrequest.go +++ b/pkg/client/models/components/createclientrequest.go @@ -3,14 +3,14 @@ package components type CreateClientRequest struct { - Public *bool `json:"public,omitempty"` - RedirectUris []string `json:"redirectUris,omitempty"` - Description *string `json:"description,omitempty"` - Name string `json:"name"` - Trusted *bool `json:"trusted,omitempty"` - PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"` - Metadata map[string]any `json:"metadata,omitempty"` - Scopes []string `json:"scopes,omitempty"` + Public *bool `json:"public,omitempty"` + RedirectUris []string `json:"redirectUris,omitempty"` + Description *string `json:"description,omitempty"` + Name string `json:"name"` + Trusted *bool `json:"trusted,omitempty"` + PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Scopes []string `json:"scopes,omitempty"` } func (o *CreateClientRequest) GetPublic() *bool { @@ -55,7 +55,7 @@ func (o *CreateClientRequest) GetPostLogoutRedirectUris() []string { return o.PostLogoutRedirectUris } -func (o *CreateClientRequest) GetMetadata() map[string]any { +func (o *CreateClientRequest) GetMetadata() map[string]string { if o == nil { return nil } diff --git a/pkg/client/models/components/createsecretrequest.go b/pkg/client/models/components/createsecretrequest.go index b435f94..7f352c5 100644 --- a/pkg/client/models/components/createsecretrequest.go +++ b/pkg/client/models/components/createsecretrequest.go @@ -3,8 +3,8 @@ package components type CreateSecretRequest struct { - Name string `json:"name"` - Metadata map[string]any `json:"metadata,omitempty"` + Name string `json:"name"` + Metadata map[string]string `json:"metadata,omitempty"` } func (o *CreateSecretRequest) GetName() string { @@ -14,7 +14,7 @@ func (o *CreateSecretRequest) GetName() string { return o.Name } -func (o *CreateSecretRequest) GetMetadata() map[string]any { +func (o *CreateSecretRequest) GetMetadata() map[string]string { if o == nil { return nil } diff --git a/pkg/client/models/components/secret.go b/pkg/client/models/components/secret.go index 8575699..17c6159 100644 --- a/pkg/client/models/components/secret.go +++ b/pkg/client/models/components/secret.go @@ -3,11 +3,11 @@ package components type Secret struct { - Name string `json:"name"` - Metadata map[string]any `json:"metadata,omitempty"` - ID string `json:"id"` - LastDigits string `json:"lastDigits"` - Clear string `json:"clear"` + Name string `json:"name"` + Metadata map[string]string `json:"metadata,omitempty"` + ID string `json:"id"` + LastDigits string `json:"lastDigits"` + Clear string `json:"clear"` } func (o *Secret) GetName() string { @@ -17,7 +17,7 @@ func (o *Secret) GetName() string { return o.Name } -func (o *Secret) GetMetadata() map[string]any { +func (o *Secret) GetMetadata() map[string]string { if o == nil { return nil } diff --git a/pkg/client/models/components/updateclientrequest.go b/pkg/client/models/components/updateclientrequest.go index 9fd29da..94753b6 100644 --- a/pkg/client/models/components/updateclientrequest.go +++ b/pkg/client/models/components/updateclientrequest.go @@ -3,14 +3,14 @@ package components type UpdateClientRequest struct { - Public *bool `json:"public,omitempty"` - RedirectUris []string `json:"redirectUris,omitempty"` - Description *string `json:"description,omitempty"` - Name string `json:"name"` - Trusted *bool `json:"trusted,omitempty"` - PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"` - Metadata map[string]any `json:"metadata,omitempty"` - Scopes []string `json:"scopes,omitempty"` + Public *bool `json:"public,omitempty"` + RedirectUris []string `json:"redirectUris,omitempty"` + Description *string `json:"description,omitempty"` + Name string `json:"name"` + Trusted *bool `json:"trusted,omitempty"` + PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Scopes []string `json:"scopes,omitempty"` } func (o *UpdateClientRequest) GetPublic() *bool { @@ -55,7 +55,7 @@ func (o *UpdateClientRequest) GetPostLogoutRedirectUris() []string { return o.PostLogoutRedirectUris } -func (o *UpdateClientRequest) GetMetadata() map[string]any { +func (o *UpdateClientRequest) GetMetadata() map[string]string { if o == nil { return nil } diff --git a/pkg/oidc/storage.go b/pkg/oidc/storage.go index 6d1f9bd..8160d31 100644 --- a/pkg/oidc/storage.go +++ b/pkg/oidc/storage.go @@ -539,7 +539,7 @@ func NewStorageFacade(storage Storage, rp rp.RelyingParty, privateKey *rsa.Priva return &storageFacade{ Storage: storage, signingKey: signingKey{ - id: "id", + id: KeyID, algorithm: "RS256", key: privateKey, }, @@ -547,3 +547,5 @@ func NewStorageFacade(storage Storage, rp rp.RelyingParty, privateKey *rsa.Priva staticClients: staticClients, } } + +const KeyID = "id" \ No newline at end of file diff --git a/pkg/testserver/server.go b/pkg/testserver/server.go index b2cfadc..ac4108e 100644 --- a/pkg/testserver/server.go +++ b/pkg/testserver/server.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + authlib "github.com/formancehq/go-libs/v3/auth" "io" "net/http" "os" @@ -58,6 +59,7 @@ type Configuration struct { OTLPConfig *OTLPConfig BaseURL string Clients []auth.StaticClient + CheckScopes bool } type Logger interface { @@ -185,6 +187,9 @@ func (s *Server) Start() error { if s.configuration.Debug { args = append(args, "--"+service.DebugFlag) } + if s.configuration.CheckScopes { + args = append(args, "--"+authlib.AuthCheckScopesFlag) + } s.logger.Logf("Starting application with flags: %s", strings.Join(args, " ")) rootCmd.SetArgs(args) diff --git a/test/e2e/client_credentials_test.go b/test/e2e/client_credentials_test.go index 1cbf540..b77d795 100644 --- a/test/e2e/client_credentials_test.go +++ b/test/e2e/client_credentials_test.go @@ -43,6 +43,7 @@ var _ = Context("Auth - Client credentials", func() { Name: "global", Id: "global", Trusted: true, + Scopes: []string{"auth:write"}, }, Secrets: []string{"global"}, }}, @@ -56,6 +57,7 @@ var _ = Context("Auth - Client credentials", func() { config := clientcredentials.Config{ ClientID: "global", ClientSecret: "global", + Scopes: []string{"auth:write"}, TokenURL: fmt.Sprintf("%s/oauth/token", srv.ServerURL()), } httpClient = config.Client(ctx) diff --git a/test/e2e/oauth2_clients_test.go b/test/e2e/oauth2_clients_test.go new file mode 100644 index 0000000..d93716f --- /dev/null +++ b/test/e2e/oauth2_clients_test.go @@ -0,0 +1,742 @@ +//go:build it + +package suite_test + +import ( + "fmt" + "net/http" + + "github.com/formancehq/go-libs/v3/pointer" + + auth "github.com/formancehq/auth/pkg" + "github.com/formancehq/auth/pkg/client/models/components" + "github.com/formancehq/auth/pkg/client/models/operations" + "github.com/formancehq/auth/pkg/testserver" + "github.com/formancehq/go-libs/v3/logging" + "github.com/formancehq/go-libs/v3/testing/platform/pgtesting" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "golang.org/x/oauth2/clientcredentials" +) + +var _ = Context("Auth - OAuth2 Clients Management", func() { + + var ( + db = pgtesting.UsePostgresDatabase(pgServer) + srv *testserver.Server + ctx = logging.TestingContext() + ) + + BeforeEach(func() { + srv = testserver.New(GinkgoT(), testserver.Configuration{ + PostgresConfiguration: db.GetValue().ConnectionOptions(), + DelegatedConfiguration: testserver.DelegatedConfiguration{ + ClientID: mockOIDC.ClientID, + ClientSecret: mockOIDC.ClientSecret, + Issuer: mockOIDC.Issuer(), + }, + Output: GinkgoWriter, + Debug: debug, + BaseURL: "http://localhost:8080", + Clients: []auth.StaticClient{{ + ClientOptions: auth.ClientOptions{ + Name: "global", + Id: "global", + Trusted: true, + Scopes: []string{"auth:write"}, + }, + Secrets: []string{"global"}, + }}, + CheckScopes: true, + }) + }) + + Context("with the default static client", func() { + var ( + httpClient *http.Client + ) + + BeforeEach(func() { + config := clientcredentials.Config{ + ClientID: "global", + ClientSecret: "global", + Scopes: []string{"auth:write"}, + TokenURL: fmt.Sprintf("%s/oauth/token", srv.ServerURL()), + } + httpClient = config.Client(ctx) + }) + + Describe("Creating clients", func() { + When("creating a client with minimal information", func() { + var ( + response *operations.CreateClientResponse + err error + ) + + BeforeEach(func() { + response, err = srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "test-client", + }) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + Expect(response.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusCreated)) + }) + + It("should return the created client with an ID", func() { + Expect(response.CreateClientResponse).NotTo(BeNil()) + Expect(response.CreateClientResponse.Data.ID).NotTo(BeEmpty()) + Expect(response.CreateClientResponse.Data.Name).To(Equal("test-client")) + }) + }) + + When("creating a client with all fields", func() { + var ( + response *operations.CreateClientResponse + err error + ) + + BeforeEach(func() { + public := true + trusted := false + response, err = srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "full-client", + Description: pointer.For("A full featured client"), + Public: &public, + Trusted: &trusted, + RedirectUris: []string{"https://example.com/callback"}, + PostLogoutRedirectUris: []string{"https://example.com/logout"}, + Scopes: []string{"scope1", "scope2"}, + Metadata: map[string]string{ + "key1": "value1", + "key2": "42", + }, + }) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + Expect(response.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusCreated)) + }) + + It("should return the client with all fields set", func() { + client := response.CreateClientResponse.Data + Expect(client.Name).To(Equal("full-client")) + Expect(client.Description).NotTo(BeNil()) + Expect(*client.Description).To(Equal("A full featured client")) + Expect(client.Public).NotTo(BeNil()) + Expect(*client.Public).To(BeTrue()) + Expect(client.Trusted).NotTo(BeNil()) + Expect(*client.Trusted).To(BeFalse()) + Expect(client.RedirectUris).To(ConsistOf("https://example.com/callback")) + Expect(client.PostLogoutRedirectUris).To(ConsistOf("https://example.com/logout")) + Expect(client.Scopes).To(ConsistOf("scope1", "scope2")) + Expect(client.Metadata).To(HaveKeyWithValue("key1", "value1")) + Expect(client.Metadata).To(HaveKeyWithValue("key2", "42")) + Expect(client.Secrets).To(BeEmpty()) + }) + }) + }) + + Describe("Reading clients", func() { + var ( + createdClient *operations.CreateClientResponse + ) + + BeforeEach(func() { + var err error + createdClient, err = srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "readable-client", + Description: pointer.For("Client for reading tests"), + Scopes: []string{"read:scope"}, + }) + Expect(err).To(Succeed()) + Expect(createdClient.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusCreated)) + }) + + When("reading an existing client", func() { + var ( + response *operations.ReadClientResponse + err error + ) + + BeforeEach(func() { + response, err = srv.Client(httpClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + }) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + Expect(response.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusOK)) + }) + + It("should return the correct client", func() { + client := response.ReadClientResponse.Data + Expect(client.ID).To(Equal(createdClient.CreateClientResponse.Data.ID)) + Expect(client.Name).To(Equal("readable-client")) + Expect(client.Description).NotTo(BeNil()) + Expect(*client.Description).To(Equal("Client for reading tests")) + Expect(client.Scopes).To(ConsistOf("read:scope")) + }) + }) + + When("reading a non-existent client", func() { + var ( + err error + ) + + BeforeEach(func() { + _, err = srv.Client(httpClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: "non-existent-id", + }) + }) + + It("should return an error", func() { + Expect(err).NotTo(BeNil()) + }) + }) + }) + + Describe("Listing clients", func() { + var ( + client1ID string + client2ID string + ) + + BeforeEach(func() { + // Create first client + response1, err := srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "client-1", + Scopes: []string{"scope1"}, + }) + Expect(err).To(Succeed()) + client1ID = response1.CreateClientResponse.Data.ID + + // Create second client + response2, err := srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "client-2", + Scopes: []string{"scope2"}, + }) + Expect(err).To(Succeed()) + client2ID = response2.CreateClientResponse.Data.ID + }) + + When("listing all clients", func() { + var ( + response *operations.ListClientsResponse + err error + ) + + BeforeEach(func() { + response, err = srv.Client(httpClient).Auth.V1.ListClients(ctx) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + Expect(response.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusOK)) + }) + + It("should return all created clients", func() { + clients := response.ListClientsResponse.Data + Expect(len(clients)).To(BeNumerically(">=", 2)) + + clientIDs := make([]string, 0, len(clients)) + for _, client := range clients { + clientIDs = append(clientIDs, client.ID) + } + Expect(clientIDs).To(ContainElements(client1ID, client2ID)) + }) + + It("should include client details", func() { + clients := response.ListClientsResponse.Data + var client1 *components.Client + var client2 *components.Client + + for i := range clients { + if clients[i].ID == client1ID { + client1 = &clients[i] + } + if clients[i].ID == client2ID { + client2 = &clients[i] + } + } + + Expect(client1).NotTo(BeNil()) + Expect(client1.Name).To(Equal("client-1")) + Expect(client1.Scopes).To(ConsistOf("scope1")) + + Expect(client2).NotTo(BeNil()) + Expect(client2.Name).To(Equal("client-2")) + Expect(client2.Scopes).To(ConsistOf("scope2")) + }) + }) + }) + + Describe("Updating clients", func() { + var ( + createdClient *operations.CreateClientResponse + ) + + BeforeEach(func() { + var err error + createdClient, err = srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "original-name", + Description: pointer.For("Original description"), + Scopes: []string{"original-scope"}, + }) + Expect(err).To(Succeed()) + Expect(createdClient.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusCreated)) + }) + + When("updating a client with new values", func() { + var ( + response *operations.UpdateClientResponse + err error + ) + + BeforeEach(func() { + public := true + trusted := true + response, err = srv.Client(httpClient).Auth.V1.UpdateClient(ctx, operations.UpdateClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + UpdateClientRequest: &components.UpdateClientRequest{ + Name: "updated-name", + Description: pointer.For("Updated description"), + Public: &public, + Trusted: &trusted, + RedirectUris: []string{"https://example.com/new-callback"}, + PostLogoutRedirectUris: []string{"https://example.com/new-logout"}, + Scopes: []string{"updated-scope1", "updated-scope2"}, + Metadata: map[string]string{ + "updated": "value", + }, + }, + }) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + Expect(response.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusOK)) + }) + + It("should return the updated client", func() { + client := response.UpdateClientResponse.Data + Expect(client.ID).To(Equal(createdClient.CreateClientResponse.Data.ID)) + Expect(client.Name).To(Equal("updated-name")) + Expect(client.Description).NotTo(BeNil()) + Expect(*client.Description).To(Equal("Updated description")) + Expect(client.Public).NotTo(BeNil()) + Expect(*client.Public).To(BeTrue()) + Expect(client.Trusted).NotTo(BeNil()) + Expect(*client.Trusted).To(BeTrue()) + Expect(client.RedirectUris).To(ConsistOf("https://example.com/new-callback")) + Expect(client.PostLogoutRedirectUris).To(ConsistOf("https://example.com/new-logout")) + Expect(client.Scopes).To(ConsistOf("updated-scope1", "updated-scope2")) + Expect(client.Metadata).To(HaveKeyWithValue("updated", "value")) + }) + + It("should persist the changes", func() { + readResponse, err := srv.Client(httpClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + }) + Expect(err).To(Succeed()) + + client := readResponse.ReadClientResponse.Data + Expect(client.Name).To(Equal("updated-name")) + Expect(client.Description).NotTo(BeNil()) + Expect(*client.Description).To(Equal("Updated description")) + Expect(client.Scopes).To(ConsistOf("updated-scope1", "updated-scope2")) + }) + }) + + When("updating a non-existent client", func() { + var err error + BeforeEach(func() { + _, err = srv.Client(httpClient).Auth.V1.UpdateClient(ctx, operations.UpdateClientRequest{ + ClientID: "non-existent-id", + UpdateClientRequest: &components.UpdateClientRequest{ + Name: "updated-name", + }, + }) + }) + + It("should return an error", func() { + Expect(err).NotTo(BeNil()) + }) + }) + }) + + Describe("Deleting clients", func() { + var ( + createdClient *operations.CreateClientResponse + ) + + BeforeEach(func() { + var err error + createdClient, err = srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "deletable-client", + }) + Expect(err).To(Succeed()) + Expect(createdClient.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusCreated)) + }) + + When("deleting an existing client", func() { + var ( + response *operations.DeleteClientResponse + err error + ) + + BeforeEach(func() { + response, err = srv.Client(httpClient).Auth.V1.DeleteClient(ctx, operations.DeleteClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + }) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + Expect(response.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusNoContent)) + }) + + It("should remove the client from the database", func() { + _, err := srv.Client(httpClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + }) + Expect(err).NotTo(BeNil()) + }) + }) + + When("deleting a non-existent client", func() { + var ( + response *operations.DeleteClientResponse + err error + ) + + BeforeEach(func() { + response, err = srv.Client(httpClient).Auth.V1.DeleteClient(ctx, operations.DeleteClientRequest{ + ClientID: "non-existent-id", + }) + }) + + It("should return no error (idempotent)", func() { + // Delete is typically idempotent, so it might return 204 even if the client doesn't exist + // The exact behavior depends on the implementation + Expect(err).To(BeNil()) + Expect(response.GetHTTPMeta().Response.StatusCode).To(BeElementOf(http.StatusNoContent, http.StatusNotFound)) + }) + }) + }) + + Describe("Managing client secrets", func() { + var ( + createdClient *operations.CreateClientResponse + ) + + BeforeEach(func() { + var err error + createdClient, err = srv.Client(httpClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "client-with-secrets", + }) + Expect(err).To(Succeed()) + Expect(createdClient.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusCreated)) + }) + + When("creating a secret for a client", func() { + var ( + createSecretResponse *operations.CreateSecretResponse + err error + ) + + BeforeEach(func() { + createSecretResponse, err = srv.Client(httpClient).Auth.V1.CreateSecret(ctx, operations.CreateSecretRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + CreateSecretRequest: &components.CreateSecretRequest{ + Name: "secret-1", + Metadata: map[string]string{ + "purpose": "testing", + }, + }, + }) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + }) + + It("should return the secret with clear text", func() { + secret := createSecretResponse.CreateSecretResponse.Data + Expect(secret.ID).NotTo(BeEmpty()) + Expect(secret.Name).To(Equal("secret-1")) + Expect(secret.Clear).NotTo(BeEmpty()) + Expect(secret.LastDigits).NotTo(BeEmpty()) + Expect(len(secret.LastDigits)).To(Equal(4)) + }) + + It("should add the secret to the client", func() { + readResponse, err := srv.Client(httpClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + }) + Expect(err).To(Succeed()) + + client := readResponse.ReadClientResponse.Data + Expect(client.Secrets).To(HaveLen(1)) + Expect(client.Secrets[0].ID).To(Equal(createSecretResponse.CreateSecretResponse.Data.ID)) + Expect(client.Secrets[0].Name).To(Equal("secret-1")) + Expect(client.Secrets[0].LastDigits).NotTo(BeEmpty()) + }) + + When("creating multiple secrets", func() { + BeforeEach(func() { + _, err := srv.Client(httpClient).Auth.V1.CreateSecret(ctx, operations.CreateSecretRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + CreateSecretRequest: &components.CreateSecretRequest{ + Name: "secret-2", + }, + }) + Expect(err).To(Succeed()) + }) + + It("should have both secrets in the client", func() { + readResponse, err := srv.Client(httpClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + }) + Expect(err).To(Succeed()) + + client := readResponse.ReadClientResponse.Data + Expect(client.Secrets).To(HaveLen(2)) + + secretNames := make([]string, 0, len(client.Secrets)) + for _, secret := range client.Secrets { + secretNames = append(secretNames, secret.Name) + } + Expect(secretNames).To(ContainElements("secret-1", "secret-2")) + }) + }) + + When("deleting a secret", func() { + var err error + BeforeEach(func() { + _, err = srv.Client(httpClient).Auth.V1.DeleteSecret(ctx, operations.DeleteSecretRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + SecretID: createSecretResponse.CreateSecretResponse.Data.ID, + }) + }) + + It("should succeed", func() { + Expect(err).To(Succeed()) + }) + + It("should remove the secret from the client", func() { + readResponse, err := srv.Client(httpClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + }) + Expect(err).To(Succeed()) + + client := readResponse.ReadClientResponse.Data + Expect(client.Secrets).To(BeEmpty()) + }) + }) + + When("deleting a non-existent secret", func() { + var err error + + BeforeEach(func() { + _, err = srv.Client(httpClient).Auth.V1.DeleteSecret(ctx, operations.DeleteSecretRequest{ + ClientID: createdClient.CreateClientResponse.Data.ID, + SecretID: "non-existent-secret-id", + }) + }) + + It("should return an error", func() { + Expect(err).NotTo(BeNil()) + }) + }) + }) + + When("creating a secret for a non-existent client", func() { + var err error + BeforeEach(func() { + _, err = srv.Client(httpClient).Auth.V1.CreateSecret(ctx, operations.CreateSecretRequest{ + ClientID: "non-existent-id", + CreateSecretRequest: &components.CreateSecretRequest{ + Name: "secret-name", + }, + }) + }) + + It("should return an error", func() { + Expect(err).NotTo(BeNil()) + }) + }) + }) + }) + + Context("with an unauthorized client", func() { + var ( + unauthorizedClientID string + unauthorizedClientSecret string + unauthorizedHTTPClient *http.Client + ) + + BeforeEach(func() { + // Create a client with the global client (which has auth:write) + globalConfig := clientcredentials.Config{ + ClientID: "global", + ClientSecret: "global", + Scopes: []string{"auth:write"}, + TokenURL: fmt.Sprintf("%s/oauth/token", srv.ServerURL()), + } + globalHTTPClient := globalConfig.Client(ctx) + + // Create a new client without auth:write scope + createResponse, err := srv.Client(globalHTTPClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "unauthorized-client", + Scopes: []string{"other:scope"}, + }) + Expect(err).To(Succeed()) + Expect(createResponse.GetHTTPMeta().Response.StatusCode).To(Equal(http.StatusCreated)) + unauthorizedClientID = createResponse.CreateClientResponse.Data.ID + + // Create a secret for this client + createSecretResponse, err := srv.Client(globalHTTPClient).Auth.V1.CreateSecret(ctx, operations.CreateSecretRequest{ + ClientID: unauthorizedClientID, + CreateSecretRequest: &components.CreateSecretRequest{ + Name: "unauthorized-secret", + }, + }) + Expect(err).To(Succeed()) + unauthorizedClientSecret = createSecretResponse.CreateSecretResponse.Data.Clear + + // Create HTTP client with unauthorized client credentials + unauthorizedConfig := clientcredentials.Config{ + ClientID: unauthorizedClientID, + ClientSecret: unauthorizedClientSecret, + Scopes: []string{}, + TokenURL: fmt.Sprintf("%s/oauth/token", srv.ServerURL()), + } + unauthorizedHTTPClient = unauthorizedConfig.Client(ctx) + }) + + Describe("Accessing clients API without proper authorization", func() { + When("trying to create a client", func() { + var ( + err error + ) + + BeforeEach(func() { + _, err = srv.Client(unauthorizedHTTPClient).Auth.V1.CreateClient(ctx, &components.CreateClientRequest{ + Name: "should-fail", + }) + }) + + It("should be refused", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + When("trying to list clients", func() { + var ( + err error + ) + + BeforeEach(func() { + _, err = srv.Client(unauthorizedHTTPClient).Auth.V1.ListClients(ctx) + }) + + It("should be refused", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + When("trying to read a client", func() { + var ( + err error + ) + + BeforeEach(func() { + _, err = srv.Client(unauthorizedHTTPClient).Auth.V1.ReadClient(ctx, operations.ReadClientRequest{ + ClientID: unauthorizedClientID, + }) + }) + + It("should be refused", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + When("trying to update a client", func() { + var ( + err error + ) + + BeforeEach(func() { + _, err = srv.Client(unauthorizedHTTPClient).Auth.V1.UpdateClient(ctx, operations.UpdateClientRequest{ + ClientID: unauthorizedClientID, + UpdateClientRequest: &components.UpdateClientRequest{ + Name: "updated-name", + }, + }) + }) + + It("should be refused", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + When("trying to delete a client", func() { + var ( + err error + ) + + BeforeEach(func() { + _, err = srv.Client(unauthorizedHTTPClient).Auth.V1.DeleteClient(ctx, operations.DeleteClientRequest{ + ClientID: unauthorizedClientID, + }) + }) + + It("should be refused", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + When("trying to create a secret", func() { + var ( + err error + ) + + BeforeEach(func() { + _, err = srv.Client(unauthorizedHTTPClient).Auth.V1.CreateSecret(ctx, operations.CreateSecretRequest{ + ClientID: unauthorizedClientID, + CreateSecretRequest: &components.CreateSecretRequest{ + Name: "new-secret", + }, + }) + }) + + It("should be refused", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + When("trying to delete a secret", func() { + var ( + err error + ) + + BeforeEach(func() { + // First, we need to get the secret ID from the client + // But we can't read the client, so we'll use a dummy ID + _, err = srv.Client(unauthorizedHTTPClient).Auth.V1.DeleteSecret(ctx, operations.DeleteSecretRequest{ + ClientID: unauthorizedClientID, + SecretID: "dummy-secret-id", + }) + }) + + It("should be refused", func() { + Expect(err).NotTo(BeNil()) + }) + }) + }) + }) +})