diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cffe7e928b3..78902e5f5dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -317,6 +317,9 @@ flows. ([39fa31f](https://github.com/ory/kratos/commit/39fa31f85deb3f015aa0f1b30b4a17e4b51d461b)) - Identity.CopyWithoutCredentials ([989c99d](https://github.com/ory/kratos/commit/989c99d6a32e02759a8a7a07606a90832afec460)) +- Implement offline scope in the way google expects + ([#3088](https://github.com/ory/kratos/issues/3088)) + ([39043d4](https://github.com/ory/kratos/commit/39043d451e154af44123ba031381f0e3c10fbb00)) - Issuer missing from netid claims ([#3080](https://github.com/ory/kratos/issues/3080)) ([dec7cbc](https://github.com/ory/kratos/commit/dec7cbc4286cbbe2d787b1f8998ee57054d7c95b)): @@ -378,6 +381,8 @@ flows. closes [#2422](https://github.com/ory/kratos/issues/2422) - Don't pre-generate UUIDs for transient objects ([e17f307](https://github.com/ory/kratos/commit/e17f307732f8ced34727d5f3a70929866a0595e0)) +- Identity by identifier ([#3077](https://github.com/ory/kratos/issues/3077)) + ([c288d4d](https://github.com/ory/kratos/commit/c288d4d136bca1a9ed3931b4827967eb44e80ede)) - Improve tracing span naming in hooks ([bf828d3](https://github.com/ory/kratos/commit/bf828d3f5d56a963529e98958f4039f0dc569979)) - Let DB generate ID for session devices diff --git a/Makefile b/Makefile index 67f5ff24d3f8..33a95356b0f9 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ GO_DEPENDENCIES = github.com/ory/go-acc \ define make-go-dependency # go install is responsible for not re-building when the code hasn't changed - .bin/$(notdir $1): go.mod go.sum Makefile + .bin/$(notdir $1): go.mod go.sum GOBIN=$(PWD)/.bin/ go install $1 endef $(foreach dep, $(GO_DEPENDENCIES), $(eval $(call make-go-dependency, $(dep)))) @@ -54,7 +54,7 @@ docs/swagger: .bin/ory: Makefile curl https://raw.githubusercontent.com/ory/meta/master/install.sh | bash -s -- -b .bin ory v0.2.2 - touch .bin/ory + touch -a -m .bin/ory .PHONY: lint lint: .bin/golangci-lint diff --git a/identity/handler.go b/identity/handler.go index 741474ccaa67..20222b528df0 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -126,6 +126,12 @@ type listIdentitiesResponse struct { //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type listIdentitiesParameters struct { migrationpagination.RequestParameters + + // CredentialsIdentifier is the identifier (username, email) of the credentials to look up. + // + // required: false + // in: query + CredentialsIdentifier string `json:"credentials_identifier"` } // swagger:route GET /admin/identities identity listIdentities @@ -147,22 +153,31 @@ type listIdentitiesParameters struct { // default: errorGeneric func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { page, itemsPerPage := x.ParsePagination(r) - is, err := h.r.IdentityPool().ListIdentities(r.Context(), ExpandDefault, page, itemsPerPage) - if err != nil { - h.r.Writer().WriteError(w, r, err) - return + + params := ListIdentityParameters{Expand: ExpandDefault, Page: page, PerPage: itemsPerPage, CredentialsIdentifier: r.URL.Query().Get("credentials_identifier")} + if params.CredentialsIdentifier != "" { + params.Expand = ExpandEverything } - total, err := h.r.IdentityPool().CountIdentities(r.Context()) + is, err := h.r.IdentityPool().ListIdentities(r.Context(), params) if err != nil { h.r.Writer().WriteError(w, r, err) return } + total := int64(len(is)) + if params.CredentialsIdentifier == "" { + total, err = h.r.IdentityPool().CountIdentities(r.Context()) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + // Identities using the marshaler for including metadata_admin - isam := make([]WithAdminMetadataInJSON, len(is)) + isam := make([]WithCredentialsMetadataAndAdminMetadataInJSON, len(is)) for i, identity := range is { - isam[i] = WithAdminMetadataInJSON(identity) + isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(identity) } migrationpagination.PaginationHeader(w, urlx.AppendPaths(h.r.Config().SelfAdminURL(r.Context()), RouteCollection), total, page, itemsPerPage) diff --git a/identity/handler_test.go b/identity/handler_test.go index 8b47082d6774..99f63368ddbf 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -375,6 +375,36 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=should return an empty array on a failed lookup with identifier", func(t *testing.T) { + res := get(t, adminTS, "/identities?credentials_identifier=find.by.non.existing.identifier@bar.com", http.StatusOK) + assert.EqualValues(t, int64(0), res.Get("#").Int(), "%s", res.Raw) + }) + + t.Run("case=should be able to lookup the identity using identifier", func(t *testing.T) { + i1 := &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{"find.by.identifier@bar.com"}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar + }}, + State: identity.StateActive, + Traits: identity.Traits(`{"username":"find.by.identifier@bar.com"}`), + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i1)) + + res := get(t, adminTS, "/identities?credentials_identifier=find.by.identifier@bar.com", http.StatusOK) + assert.EqualValues(t, i1.ID.String(), res.Get("0.id").String(), "%s", res.Raw) + assert.EqualValues(t, "find.by.identifier@bar.com", res.Get("0.traits.username").String(), "%s", res.Raw) + assert.EqualValues(t, defaultSchemaExternalURL, res.Get("0.schema_url").String(), "%s", res.Raw) + assert.EqualValues(t, config.DefaultIdentityTraitsSchemaID, res.Get("0.schema_id").String(), "%s", res.Raw) + assert.EqualValues(t, identity.StateActive, res.Get("0.state").String(), "%s", res.Raw) + assert.EqualValues(t, "password", res.Get("0.credentials.password.type").String(), res.Raw) + assert.EqualValues(t, "1", res.Get("0.credentials.password.identifiers.#").String(), res.Raw) + assert.EqualValues(t, "find.by.identifier@bar.com", res.Get("0.credentials.password.identifiers.0").String(), res.Raw) + }) + t.Run("case=should get oidc credential", func(t *testing.T) { id := createOidcIdentity(t, "foo.oidc@bar.com", "access_token", "refresh_token", "id_token", true) for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { diff --git a/identity/pool.go b/identity/pool.go index e4830535b7fd..2109890fc481 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -12,9 +12,16 @@ import ( ) type ( + ListIdentityParameters struct { + Expand Expandables + CredentialsIdentifier string + Page int + PerPage int + } + Pool interface { // ListIdentities lists all identities in the store given the page and itemsPerPage. - ListIdentities(ctx context.Context, expandables sqlxx.Expandables, page, itemsPerPage int) ([]Identity, error) + ListIdentities(ctx context.Context, params ListIdentityParameters) ([]Identity, error) // CountIdentities counts the number of identities in the store. CountIdentities(ctx context.Context) (int64, error) diff --git a/identity/test/pool.go b/identity/test/pool.go index 5f5a9d793ddb..58fda09e3610 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -113,7 +113,7 @@ func TestPool(ctx context.Context, conf *config.Config, p interface { }) t.Run("list", func(t *testing.T) { - actual, err := p.ListIdentities(ctx, expand, 0, 10) + actual, err := p.ListIdentities(ctx, identity.ListIdentityParameters{Expand: expand, Page: 0, PerPage: 10}) require.NoError(t, err) require.Len(t, actual, 1) assertion(t, &actual[0]) @@ -569,7 +569,7 @@ func TestPool(ctx context.Context, conf *config.Config, p interface { }) t.Run("case=list", func(t *testing.T) { - is, err := p.ListIdentities(ctx, identity.ExpandDefault, 0, 25) + is, err := p.ListIdentities(ctx, identity.ListIdentityParameters{Expand: identity.ExpandDefault, Page: 0, PerPage: 25}) require.NoError(t, err) assert.Len(t, is, len(createdIDs)) for _, id := range createdIDs { @@ -587,13 +587,81 @@ func TestPool(ctx context.Context, conf *config.Config, p interface { t.Run("no results on other network", func(t *testing.T) { _, p := testhelpers.NewNetwork(t, ctx, p) - is, err := p.ListIdentities(ctx, identity.ExpandDefault, 0, 25) + is, err := p.ListIdentities(ctx, identity.ListIdentityParameters{Expand: identity.ExpandDefault, Page: 0, PerPage: 25}) require.NoError(t, err) assert.Len(t, is, 0) }) }) t.Run("case=find identity by its credentials identifier", func(t *testing.T) { + var expectedIdentifiers []string + var expectedIdentities []*identity.Identity + + for _, c := range []identity.CredentialsType{ + identity.CredentialsTypePassword, + identity.CredentialsTypeWebAuthn, + identity.CredentialsTypeOIDC, + } { + identityIdentifier := fmt.Sprintf("find-identity-by-identifier-%s@ory.sh", c) + expected := identity.NewIdentity("") + expected.SetCredentials(c, identity.Credentials{Type: c, Identifiers: []string{identityIdentifier}, Config: sqlxx.JSONRawMessage(`{}`)}) + + require.NoError(t, p.CreateIdentity(ctx, expected)) + createdIDs = append(createdIDs, expected.ID) + expectedIdentifiers = append(expectedIdentifiers, identityIdentifier) + expectedIdentities = append(expectedIdentities, expected) + } + + actual, err := p.ListIdentities(ctx, identity.ListIdentityParameters{ + Expand: identity.ExpandEverything, + }) + require.NoError(t, err) + require.True(t, len(actual) > 0) + + for c, ct := range []identity.CredentialsType{ + identity.CredentialsTypePassword, + identity.CredentialsTypeWebAuthn, + } { + t.Run(ct.String(), func(t *testing.T) { + actual, err := p.ListIdentities(ctx, identity.ListIdentityParameters{ + // Match is normalized + CredentialsIdentifier: expectedIdentifiers[c], + }) + require.NoError(t, err) + + expected := expectedIdentities[c] + require.Len(t, actual, 1) + assertx.EqualAsJSONExcept(t, expected, actual[0], []string{"credentials.config", "created_at", "updated_at", "state_changed_at"}) + }) + } + + t.Run("only webauthn and password", func(t *testing.T) { + actual, err := p.ListIdentities(ctx, identity.ListIdentityParameters{ + CredentialsIdentifier: "find-identity-by-identifier-oidc@ory.sh", + }) + require.NoError(t, err) + assert.Len(t, actual, 0) + }) + + t.Run("non existing identifier", func(t *testing.T) { + actual, err := p.ListIdentities(ctx, identity.ListIdentityParameters{ + CredentialsIdentifier: "find-identity-by-identifier-non-existing@ory.sh", + }) + require.NoError(t, err) + assert.Len(t, actual, 0) + }) + + t.Run("not if on another network", func(t *testing.T) { + _, on := testhelpers.NewNetwork(t, ctx, p) + actual, err := on.ListIdentities(ctx, identity.ListIdentityParameters{ + CredentialsIdentifier: expectedIdentifiers[0], + }) + require.NoError(t, err) + assert.Len(t, actual, 0) + }) + }) + + t.Run("case=find identity by its credentials type and identifier", func(t *testing.T) { expected := passwordIdentity("", "find-credentials-identifier@ory.sh") expected.Traits = identity.Traits(`{}`) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index 300f0f008f6c..bf72874ed716 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -1884,10 +1884,11 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest } type IdentityApiApiListIdentitiesRequest struct { - ctx context.Context - ApiService IdentityApi - perPage *int64 - page *int64 + ctx context.Context + ApiService IdentityApi + perPage *int64 + page *int64 + credentialsIdentifier *string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -1898,6 +1899,10 @@ func (r IdentityApiApiListIdentitiesRequest) Page(page int64) IdentityApiApiList r.page = &page return r } +func (r IdentityApiApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityApiApiListIdentitiesRequest { + r.credentialsIdentifier = &credentialsIdentifier + return r +} func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -1947,6 +1952,9 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.page != nil { localVarQueryParams.Add("page", parameterToString(*r.page, "")) } + if r.credentialsIdentifier != nil { + localVarQueryParams.Add("credentials_identifier", parameterToString(*r.credentialsIdentifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index 300f0f008f6c..bf72874ed716 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -1884,10 +1884,11 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest } type IdentityApiApiListIdentitiesRequest struct { - ctx context.Context - ApiService IdentityApi - perPage *int64 - page *int64 + ctx context.Context + ApiService IdentityApi + perPage *int64 + page *int64 + credentialsIdentifier *string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -1898,6 +1899,10 @@ func (r IdentityApiApiListIdentitiesRequest) Page(page int64) IdentityApiApiList r.page = &page return r } +func (r IdentityApiApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityApiApiListIdentitiesRequest { + r.credentialsIdentifier = &credentialsIdentifier + return r +} func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -1947,6 +1952,9 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.page != nil { localVarQueryParams.Add("page", parameterToString(*r.page, "")) } + if r.credentialsIdentifier != nil { + localVarQueryParams.Add("credentials_identifier", parameterToString(*r.credentialsIdentifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 22dbe9ead3bb..abd67f2f6bc2 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -157,7 +157,7 @@ func TestMigrations(t *testing.T) { defer wg.Done() t.Parallel() - ids, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) require.NoError(t, err) require.NotEmpty(t, ids) @@ -185,7 +185,7 @@ func TestMigrations(t *testing.T) { defer wg.Done() t.Parallel() - ids, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandNothing, 0, 1000) + ids, err := d.PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandNothing, Page: 0, PerPage: 1000}) require.NoError(t, err) require.NotEmpty(t, ids) diff --git a/persistence/sql/persister_identity.go b/persistence/sql/persister_identity.go index f839b429b5d5..914c33863e0e 100644 --- a/persistence/sql/persister_identity.go +++ b/persistence/sql/persister_identity.go @@ -377,26 +377,38 @@ func (p *Persister) HydrateIdentityAssociations(ctx context.Context, i *identity return p.injectTraitsSchemaURL(ctx, i) } -func (p *Persister) ListIdentities(ctx context.Context, expand identity.Expandables, page, perPage int) (res []identity.Identity, err error) { +func (p *Persister) ListIdentities(ctx context.Context, params identity.ListIdentityParameters) (res []identity.Identity, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListIdentities") defer otelx.End(span, &err) span.SetAttributes( - attribute.Int("page", page), - attribute.Int("per_page", perPage), - attribute.StringSlice("expand", expand.ToEager()), + attribute.Int("page", params.Page), + attribute.Int("per_page", params.PerPage), + attribute.StringSlice("expand", params.Expand.ToEager()), + attribute.Bool("use:credential_identifier_filter", params.CredentialsIdentifier != ""), attribute.String("network.id", p.NetworkID(ctx).String()), ) is := make([]identity.Identity, 0) con := p.GetConnection(ctx) - query := con. - Where("nid = ?", p.NetworkID(ctx)). - Paginate(page, perPage). - Order("id DESC") - - if len(expand) > 0 { - query = query.EagerPreload(expand.ToEager()...) + nid := p.NetworkID(ctx) + query := con.Where("identities.nid = ?", nid).Paginate(params.Page, params.PerPage). + Order("identities.id DESC") + + if len(params.Expand) > 0 { + query = query.EagerPreload(params.Expand.ToEager()...) + } + + if match := params.CredentialsIdentifier; len(match) > 0 { + // When filtering by credentials identifier, we most likely are looking for a username or email. It is therefore + // important to normalize the identifier before querying the database. + match = p.normalizeIdentifier(identity.CredentialsTypePassword, match) + query = query. + InnerJoin("identity_credentials ic", "ic.identity_id = identities.id"). + InnerJoin("identity_credential_types ict", "ict.id = ic.identity_credential_type_id"). + InnerJoin("identity_credential_identifiers ici", "ici.identity_credential_id = ic.id"). + Where("(ic.nid = ? AND ici.nid = ? AND ici.identifier = ?)", nid, nid, match). + Where("ict.name IN (?)", identity.CredentialsTypeWebAuthn, identity.CredentialsTypePassword) } /* #nosec G201 TableName is static */ diff --git a/selfservice/strategy/oidc/provider_google.go b/selfservice/strategy/oidc/provider_google.go index 117471b9379b..cb98696484ca 100644 --- a/selfservice/strategy/oidc/provider_google.go +++ b/selfservice/strategy/oidc/provider_google.go @@ -3,6 +3,15 @@ package oidc +import ( + "context" + + gooidc "github.com/coreos/go-oidc" + "golang.org/x/oauth2" + + "github.com/ory/x/stringslice" +) + type ProviderGoogle struct { *ProviderGenericOIDC } @@ -19,3 +28,41 @@ func NewProviderGoogle( }, } } + +func (g *ProviderGoogle) oauth2ConfigFromEndpoint(ctx context.Context, endpoint oauth2.Endpoint) *oauth2.Config { + scope := g.config.Scope + if !stringslice.Has(scope, gooidc.ScopeOpenID) { + scope = append(scope, gooidc.ScopeOpenID) + } + + scope = stringslice.Filter(scope, func(s string) bool { return s == gooidc.ScopeOfflineAccess }) + + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: endpoint, + Scopes: scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderGoogle) OAuth2(ctx context.Context) (*oauth2.Config, error) { + p, err := g.provider(ctx) + if err != nil { + return nil, err + } + + endpoint := p.Endpoint() + return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil +} + +func (g *ProviderGoogle) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + scope := g.config.Scope + options := g.ProviderGenericOIDC.AuthCodeURLOptions(r) + + if stringslice.Has(scope, gooidc.ScopeOfflineAccess) { + options = append(options, oauth2.AccessTypeOffline) + } + + return options +} diff --git a/selfservice/strategy/oidc/provider_google_test.go b/selfservice/strategy/oidc/provider_google_test.go new file mode 100644 index 000000000000..f7cd21d77819 --- /dev/null +++ b/selfservice/strategy/oidc/provider_google_test.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestProviderGoogle_Scope(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + + p := oidc.NewProviderGoogle(&oidc.Configuration{ + Provider: "google", + ID: "valid", + ClientID: "client", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{"email", "profile", "offline_access"}, + }, reg) + + c, _ := p.OAuth2(context.Background()) + assert.NotContains(t, c.Scopes, "offline_access") +} + +func TestProviderGoogle_AccessType(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + + p := oidc.NewProviderGoogle(&oidc.Configuration{ + Provider: "google", + ID: "valid", + ClientID: "client", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{"email", "profile", "offline_access"}, + }, reg) + + r := &login.Flow{ + ID: x.NewUUID(), + } + + options := p.AuthCodeURLOptions(r) + assert.Contains(t, options, oauth2.AccessTypeOffline) +} diff --git a/spec/api.json b/spec/api.json index 2c70eae6c68f..5f217d6c0b87 100755 --- a/spec/api.json +++ b/spec/api.json @@ -3237,6 +3237,14 @@ "minimum": 1, "type": "integer" } + }, + { + "description": "CredentialsIdentifier is the identifier (username, email) of the credentials to look up.", + "in": "query", + "name": "credentials_identifier", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index 4c93f75ef5ae..9717a5b9be54 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -203,6 +203,12 @@ "description": "Pagination Page\n\nThis value is currently an integer, but it is not sequential. The value is not the page number, but a\nreference. The next page can be any number and some numbers might return an empty list.\n\nFor example, page 2 might not follow after page 1. And even if page 3 and 5 exist, but page 4 might not exist.", "name": "page", "in": "query" + }, + { + "type": "string", + "description": "CredentialsIdentifier is the identifier (username, email) of the credentials to look up.", + "name": "credentials_identifier", + "in": "query" } ], "responses": {