diff --git a/consent/handler.go b/consent/handler.go index 8178cf0cc6d..6f26913f097 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -932,9 +932,7 @@ func (h *Handler) rejectOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ // Accept OAuth 2.0 Logout Request // // swagger:parameters acceptOAuth2LogoutRequest -// -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type acceptOAuth2LogoutRequest struct { +type _ struct { // OAuth 2.0 Logout Request Challenge // // in: query @@ -964,23 +962,21 @@ func (h *Handler) acceptOAuth2LogoutRequest(w http.ResponseWriter, r *http.Reque r.URL.Query().Get("challenge"), ) - c, err := h.r.ConsentManager().AcceptLogoutRequest(r.Context(), challenge) + verifier, err := h.r.ConsentManager().AcceptLogoutRequest(r.Context(), challenge) if err != nil { h.r.Writer().WriteError(w, r, err) return } h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{ - RedirectTo: urlx.SetQuery(urlx.AppendPaths(h.c.PublicURL(r.Context()), "/oauth2/sessions/logout"), url.Values{"logout_verifier": {c.Verifier}}).String(), + RedirectTo: urlx.SetQuery(urlx.AppendPaths(h.c.PublicURL(r.Context()), "/oauth2/sessions/logout"), url.Values{"logout_verifier": {verifier}}).String(), }) } // Reject OAuth 2.0 Logout Request // // swagger:parameters rejectOAuth2LogoutRequest -// -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type rejectOAuth2LogoutRequest struct { +type _ struct { // in: query // required: true Challenge string `json:"logout_challenge"` @@ -1020,9 +1016,7 @@ func (h *Handler) rejectOAuth2LogoutRequest(w http.ResponseWriter, r *http.Reque // Get OAuth 2.0 Logout Request // // swagger:parameters getOAuth2LogoutRequest -// -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type getOAuth2LogoutRequest struct { +type _ struct { // in: query // required: true Challenge string `json:"logout_challenge"` @@ -1060,13 +1054,6 @@ func (h *Handler) getOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request, request.Client.Secret = "" } - if request.WasHandled { - h.r.Writer().WriteCode(w, r, http.StatusGone, &flow.OAuth2RedirectTo{ - RedirectTo: request.RequestURL, - }) - return - } - h.r.Writer().Write(w, r, request) } diff --git a/consent/handler_test.go b/consent/handler_test.go index f8638c07454..8db446a2100 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -31,61 +31,6 @@ import ( "github.com/ory/x/sqlxx" ) -func TestGetLogoutRequest(t *testing.T) { - for k, tc := range []struct { - exists bool - handled bool - status int - }{ - {false, false, http.StatusNotFound}, - {true, false, http.StatusOK}, - {true, true, http.StatusGone}, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - ctx := context.Background() - key := fmt.Sprint(k) - challenge := "challenge" + key - requestURL := "http://192.0.2.1" - - conf := testhelpers.NewConfigurationWithDefaults() - reg := testhelpers.NewRegistryMemory(t, conf, &contextx.Default{}) - - if tc.exists { - cl := &client.Client{ID: "client" + key} - require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) - require.NoError(t, reg.ConsentManager().CreateLogoutRequest(context.TODO(), &flow.LogoutRequest{ - Client: cl, - ID: challenge, - WasHandled: tc.handled, - RequestURL: requestURL, - })) - } - - h := NewHandler(reg, conf) - r := x.NewRouterAdmin(conf.AdminURL) - h.SetRoutes(r) - ts := httptest.NewServer(r) - defer ts.Close() - - c := &http.Client{} - resp, err := c.Get(ts.URL + "/admin" + LogoutPath + "?challenge=" + challenge) - require.NoError(t, err) - require.EqualValues(t, tc.status, resp.StatusCode) - - if tc.handled { - var result flow.OAuth2RedirectTo - require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) - require.Equal(t, requestURL, result.RedirectTo) - } else if tc.exists { - var result flow.LogoutRequest - require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) - require.Equal(t, challenge, result.ID) - require.Equal(t, requestURL, result.RequestURL) - } - }) - } -} - func TestGetLoginRequest(t *testing.T) { for k, tc := range []struct { exists bool diff --git a/consent/manager.go b/consent/manager.go index b5b2aba5691..fe9af59a306 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -57,9 +57,9 @@ type ( ListUserAuthenticatedClientsWithFrontChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) ListUserAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) - CreateLogoutRequest(ctx context.Context, request *flow.LogoutRequest) error + CreateLogoutChallenge(ctx context.Context, request *flow.LogoutRequest) (challenge string, err error) GetLogoutRequest(ctx context.Context, challenge string) (*flow.LogoutRequest, error) - AcceptLogoutRequest(ctx context.Context, challenge string) (*flow.LogoutRequest, error) + AcceptLogoutRequest(ctx context.Context, challenge string) (verifier string, err error) RejectLogoutRequest(ctx context.Context, challenge string) error VerifyAndInvalidateLogoutRequest(ctx context.Context, verifier string) (*flow.LogoutRequest, error) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 6244ea0a269..27f8558e544 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -21,14 +21,13 @@ import ( "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" - "github.com/ory/hydra/v2/flow" - "github.com/ory/hydra/v2/oauth2/flowctx" - "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/token/jwt" "github.com/ory/hydra/v2/client" "github.com/ory/hydra/v2/driver/config" + "github.com/ory/hydra/v2/flow" + "github.com/ory/hydra/v2/oauth2/flowctx" "github.com/ory/hydra/v2/x" "github.com/ory/x/errorsx" "github.com/ory/x/mapx" @@ -883,21 +882,18 @@ func (s *DefaultStrategy) issueLogoutVerifier(ctx context.Context, w http.Respon return nil, err } - challenge := uuid.New() - if err := s.r.ConsentManager().CreateLogoutRequest(r.Context(), &flow.LogoutRequest{ - RequestURL: r.URL.String(), - ID: challenge, - Subject: session.Subject, - SessionID: session.ID, - Verifier: uuid.New(), - RequestedAt: sqlxx.NullTime(time.Now().UTC().Round(time.Second)), - ExpiresAt: sqlxx.NullTime(time.Now().UTC().Round(time.Second).Add(s.c.ConsentRequestMaxAge(ctx))), - RPInitiated: false, - - // PostLogoutRedirectURI is set to the value from config.Provider().LogoutRedirectURL() + now := time.Now().UTC().Round(time.Second) + challenge, err := s.r.ConsentManager().CreateLogoutChallenge(ctx, &flow.LogoutRequest{ + RequestURL: r.URL.String(), + Subject: session.Subject, + SessionID: session.ID, + RequestedAt: now, + ExpiresAt: now.Add(s.c.ConsentRequestMaxAge(ctx)), + RPInitiated: false, PostLogoutRedirectURI: redir, - }); err != nil { - return nil, err + }) + if err != nil { + return nil, errors.WithStack(err) } s.r.AuditLogger(). @@ -923,13 +919,13 @@ func (s *DefaultStrategy) issueLogoutVerifier(ctx context.Context, w http.Respon ) } - now := time.Now().UTC().Unix() - if !claims.VerifyIssuedAt(now, true) { + now := time.Now().UTC().Round(time.Second) + if !claims.VerifyIssuedAt(now.Unix(), true) { return nil, errorsx.WithStack(fosite.ErrInvalidRequest. WithHintf( `Logout failed because iat claim value '%.0f' from query parameter id_token_hint is before now ('%d').`, mapx.GetFloat64Default(mksi, "iat", float64(0)), - now, + now.Unix(), ), ) } @@ -967,6 +963,7 @@ func (s *DefaultStrategy) issueLogoutVerifier(ctx context.Context, w http.Respon return nil, errorsx.WithStack(fosite.ErrInvalidRequest. WithHint("Logout failed because none of the listed audiences is a registered OAuth 2.0 Client.")) } + cl.Secret = "" // We don't want to expose the client secret. if len(requestedRedir) > 0 { var f *url.URL @@ -1007,20 +1004,19 @@ func (s *DefaultStrategy) issueLogoutVerifier(ctx context.Context, w http.Respon return nil, err } - challenge := uuid.New() - if err := s.r.ConsentManager().CreateLogoutRequest(r.Context(), &flow.LogoutRequest{ - RequestURL: r.URL.String(), - ID: challenge, - SessionID: hintSid, - Subject: session.Subject, - Verifier: uuid.New(), - Client: cl, - RPInitiated: true, - - // PostLogoutRedirectURI is set to the value from config.Provider().LogoutRedirectURL() + now = time.Now().UTC().Round(time.Second) + challenge, err := s.r.ConsentManager().CreateLogoutChallenge(ctx, &flow.LogoutRequest{ + RequestURL: r.URL.String(), + Subject: session.Subject, + SessionID: hintSid, + RequestedAt: now, + ExpiresAt: now.Add(s.c.ConsentRequestMaxAge(ctx)), + RPInitiated: true, PostLogoutRedirectURI: redir, - }); err != nil { - return nil, err + Client: cl, + }) + if err != nil { + return nil, errors.WithStack(err) } http.Redirect(w, r, urlx.SetQuery(s.c.LogoutURL(ctx), url.Values{"logout_challenge": {challenge}}).String(), http.StatusFound) diff --git a/consent/strategy_logout_test.go b/consent/strategy_logout_test.go index 80e633e7bf6..89c8d4c9218 100644 --- a/consent/strategy_logout_test.go +++ b/consent/strategy_logout_test.go @@ -16,21 +16,19 @@ import ( "testing" "time" - "github.com/ory/hydra/v2/internal/kratos" - "github.com/ory/x/pointerx" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" jwtgo "github.com/ory/fosite/token/jwt" - hydra "github.com/ory/hydra-client-go/v2" "github.com/ory/hydra/v2/client" "github.com/ory/hydra/v2/driver/config" + "github.com/ory/hydra/v2/internal/kratos" "github.com/ory/hydra/v2/internal/testhelpers" "github.com/ory/x/contextx" "github.com/ory/x/ioutilx" + "github.com/ory/x/pointerx" ) func TestLogoutFlows(t *testing.T) { @@ -163,14 +161,17 @@ func TestLogoutFlows(t *testing.T) { defer wg.Done() } - res, _, err := adminApi.OAuth2API.GetOAuth2LogoutRequest(ctx).LogoutChallenge(r.URL.Query().Get("logout_challenge")).Execute() + challenge := r.URL.Query().Get("logout_challenge") + res, _, err := adminApi.OAuth2API.GetOAuth2LogoutRequest(ctx).LogoutChallenge(challenge).Execute() if cb != nil { cb(t, res, err) } else { require.NoError(t, err) } + require.NotNil(t, res) + require.NotNil(t, res.Challenge) - v, _, err := adminApi.OAuth2API.AcceptOAuth2LogoutRequest(ctx).LogoutChallenge(r.URL.Query().Get("logout_challenge")).Execute() + v, _, err := adminApi.OAuth2API.AcceptOAuth2LogoutRequest(ctx).LogoutChallenge(*res.Challenge).Execute() require.NoError(t, err) require.NotEmpty(t, v.RedirectTo) http.Redirect(w, r, v.RedirectTo, http.StatusFound) @@ -277,20 +278,20 @@ func TestLogoutFlows(t *testing.T) { acceptLoginAs(t, subject) browser := createBrowserWithSession(t, createSampleClient(t)) - var logoutReq *hydra.OAuth2LogoutRequest + var logoutChallenge string setupCheckAndAcceptLogoutHandler(t, nil, func(t *testing.T, req *hydra.OAuth2LogoutRequest, err error) { require.NoError(t, err) - logoutReq = req + require.NotNil(t, req.Challenge) + logoutChallenge = *req.Challenge }) // run once to log out logoutAndExpectPostLogoutPage(t, browser, http.MethodGet, url.Values{}, defaultRedirectedMessage) - // run again to ensure that the logout challenge is invalid - _, _, err := adminApi.OAuth2API.GetOAuth2LogoutRequest(ctx).LogoutChallenge(logoutReq.GetChallenge()).Execute() - assert.Error(t, err) + require.NotZero(t, logoutChallenge) - v, _, err := adminApi.OAuth2API.AcceptOAuth2LogoutRequest(ctx).LogoutChallenge(logoutReq.GetChallenge()).Execute() + // double-submit: still works + v, _, err := adminApi.OAuth2API.AcceptOAuth2LogoutRequest(ctx).LogoutChallenge(logoutChallenge).Execute() require.NoError(t, err) require.NotEmpty(t, v.RedirectTo) @@ -485,7 +486,7 @@ func TestLogoutFlows(t *testing.T) { c := createSampleClient(t) acceptLoginAs(t, subject) - setupCheckAndAcceptLogoutHandler(t, nil, func(t *testing.T, res *hydra.OAuth2LogoutRequest, err error) { + setupCheckAndAcceptLogoutHandler(t, nil, func(t *testing.T, _ *hydra.OAuth2LogoutRequest, _ error) { t.Fatalf("Logout should not have been called") }) browser := createBrowserWithSession(t, c) diff --git a/consent/strategy_oauth_test.go b/consent/strategy_oauth_test.go index 319e350dc2e..9773cd1bb3b 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -53,7 +53,7 @@ func TestStrategyLoginConsentNext(t *testing.T) { adminClient := hydra.NewAPIClient(hydra.NewConfiguration()) adminClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: adminTS.URL}} - oauth2Config := func(t *testing.T, c *client.Client) *oauth2.Config { + oauth2Config := func(_ *testing.T, c *client.Client) *oauth2.Config { return &oauth2.Config{ ClientID: c.GetID(), ClientSecret: c.Secret, diff --git a/consent/test/manager_test_helpers.go b/consent/test/manager_test_helpers.go index 2f2b280c0e5..c9225144d89 100644 --- a/consent/test/manager_test_helpers.go +++ b/consent/test/manager_test_helpers.go @@ -118,15 +118,13 @@ func MockLogoutRequest(key string, withClient bool, network string) (c *flow.Log } return &flow.LogoutRequest{ Subject: "subject" + key, - ID: makeID("challenge", network, key), - Verifier: makeID("verifier", network, key), SessionID: makeID("session", network, key), RPInitiated: true, RequestURL: "http://request-me/", PostLogoutRedirectURI: "http://redirect-me/", - WasHandled: false, - Accepted: false, Client: cl, + RequestedAt: time.Now().UTC().Add(-time.Minute), + ExpiresAt: time.Now().UTC().Add(time.Hour), } } @@ -1086,66 +1084,6 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo }) } }) - - t.Run("case=LogoutRequest", func(t *testing.T) { - for k, tc := range []struct { - key string - authAt bool - withClient bool - }{ - {"LogoutRequest-1", true, true}, - {"LogoutRequest-2", true, true}, - {"LogoutRequest-3", true, true}, - {"LogoutRequest-4", true, true}, - {"LogoutRequest-5", true, false}, - {"LogoutRequest-6", false, false}, - } { - t.Run("key="+tc.key, func(t *testing.T) { - challenge := makeID("challenge", network, tc.key) - verifier := makeID("verifier", network, tc.key) - c := MockLogoutRequest(tc.key, tc.withClient, network) - if tc.withClient { - require.NoError(t, clientManager.CreateClient(ctx, c.Client)) // Ignore errors that are caused by duplication - } - - _, err := m.GetLogoutRequest(ctx, challenge) - require.Error(t, err) - - require.NoError(t, m.CreateLogoutRequest(ctx, c)) - - got2, err := m.GetLogoutRequest(ctx, challenge) - require.NoError(t, err) - assert.False(t, got2.WasHandled) - assert.False(t, got2.Accepted) - compareLogoutRequest(t, c, got2) - - if k%2 == 0 { - got2, err = m.AcceptLogoutRequest(ctx, challenge) - require.NoError(t, err) - assert.True(t, got2.Accepted) - compareLogoutRequest(t, c, got2) - - got3, err := m.VerifyAndInvalidateLogoutRequest(ctx, verifier) - require.NoError(t, err) - assert.True(t, got3.Accepted) - assert.True(t, got3.WasHandled) - compareLogoutRequest(t, c, got3) - - _, err = m.VerifyAndInvalidateLogoutRequest(ctx, verifier) - require.NoError(t, err) - - got2, err = m.GetLogoutRequest(ctx, challenge) - require.NoError(t, err) - compareLogoutRequest(t, got3, got2) - assert.True(t, got2.WasHandled) - } else { - require.NoError(t, m.RejectLogoutRequest(ctx, challenge)) - _, err = m.GetLogoutRequest(ctx, challenge) - require.Error(t, err) - } - }) - } - }) }) t.Run("case=foreign key regression", func(t *testing.T) { @@ -1213,9 +1151,7 @@ func compareLogoutRequest(t *testing.T, a, b *flow.LogoutRequest) { assert.EqualValues(t, a.Client.GetID(), b.Client.GetID()) } - assert.EqualValues(t, a.ID, b.ID) assert.EqualValues(t, a.Subject, b.Subject) - assert.EqualValues(t, a.Verifier, b.Verifier) assert.EqualValues(t, a.RequestURL, b.RequestURL) assert.EqualValues(t, a.PostLogoutRedirectURI, b.PostLogoutRedirectURI) assert.EqualValues(t, a.RPInitiated, b.RPInitiated) diff --git a/flow/.snapshots/TestLogoutRequest_MarshalJSON.json b/flow/.snapshots/TestLogoutRequest_MarshalJSON.json index 4132efb0269..8320f69bf27 100644 --- a/flow/.snapshots/TestLogoutRequest_MarshalJSON.json +++ b/flow/.snapshots/TestLogoutRequest_MarshalJSON.json @@ -1 +1 @@ -"{\"challenge\":\"\",\"subject\":\"\",\"request_url\":\"\",\"rp_initiated\":false,\"expires_at\":null,\"requested_at\":null,\"client\":null}" +"{\"challenge\":\"\",\"subject\":\"\",\"request_url\":\"\",\"rp_initiated\":false,\"expires_at\":\"0001-01-01T00:00:00Z\",\"requested_at\":\"0001-01-01T00:00:00Z\",\"client\":null}" diff --git a/flow/consent_types.go b/flow/consent_types.go index 64f9434629c..5b4eac51dd2 100644 --- a/flow/consent_types.go +++ b/flow/consent_types.go @@ -4,21 +4,18 @@ package flow import ( - "database/sql" "database/sql/driver" "encoding/json" "fmt" "net/http" "time" - "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/ory/x/errorsx" "github.com/ory/fosite" "github.com/ory/hydra/v2/client" - "github.com/ory/x/sqlcon" "github.com/ory/x/sqlxx" ) @@ -478,58 +475,27 @@ func (n *OAuth2ConsentRequestOpenIDConnectContext) Value() (driver.Value, error) // // swagger:model oAuth2LogoutRequest type LogoutRequest struct { - // Challenge is the identifier of the logout authentication request. - ID string `json:"challenge" db:"challenge"` - NID uuid.UUID `json:"-" db:"nid"` + // Challenge is used to retrieve/accept/deny the logout request. + Challenge string `json:"challenge" db:"challenge"` - // Subject is the user for whom the logout was request. - Subject string `json:"subject" db:"subject"` + // Subject is the user for whom the logout was requested. + Subject string `json:"subject"` // SessionID is the login session ID that was requested to log out. - SessionID string `json:"sid,omitempty" db:"sid"` + SessionID string `json:"sid,omitempty"` // RequestURL is the original Logout URL requested. - RequestURL string `json:"request_url" db:"request_url"` + RequestURL string `json:"request_url"` // RPInitiated is set to true if the request was initiated by a Relying Party (RP), also known as an OAuth 2.0 Client. - RPInitiated bool `json:"rp_initiated" db:"rp_initiated"` + RPInitiated bool `json:"rp_initiated"` - // If set to true means that the request was already handled. This - // can happen on form double-submit or other errors. If this is set - // we recommend redirecting the user to `request_url` to re-initiate - // the flow. - WasHandled bool `json:"-" db:"was_used"` - - Verifier string `json:"-" db:"verifier"` - PostLogoutRedirectURI string `json:"-" db:"redir_url"` - Accepted bool `json:"-" db:"accepted"` - Rejected bool `db:"rejected" json:"-"` - ClientID sql.NullString `json:"-" db:"client_id"` - ExpiresAt sqlxx.NullTime `json:"expires_at" db:"expires_at"` - RequestedAt sqlxx.NullTime `json:"requested_at" db:"requested_at"` - Client *client.Client `json:"client" db:"-"` -} + ExpiresAt time.Time `json:"expires_at"` + RequestedAt time.Time `json:"requested_at"` + Client *client.Client `json:"client"` -func (LogoutRequest) TableName() string { - return "hydra_oauth2_logout_request" -} - -func (r *LogoutRequest) BeforeSave(_ *pop.Connection) error { - if r.Client != nil { - r.ClientID = sql.NullString{ - Valid: true, - String: r.Client.GetID(), - } - } - return nil -} - -func (r *LogoutRequest) AfterFind(c *pop.Connection) error { - if r.ClientID.Valid { - r.Client = &client.Client{} - return sqlcon.HandleError(c.Where("id = ?", r.ClientID.String).First(r.Client)) - } - return nil + // swagger:ignore + PostLogoutRedirectURI string `json:"redir_url,omitempty"` } // Returned when the log out request was used. diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml index 36def440519..a82934e6168 100644 --- a/internal/httpclient/api/openapi.yaml +++ b/internal/httpclient/api/openapi.yaml @@ -3867,20 +3867,18 @@ components: sid: sid properties: challenge: - description: Challenge is the identifier of the logout authentication request. + description: Challenge is used to retrieve/accept/deny the logout request. type: string client: $ref: '#/components/schemas/oAuth2Client' expires_at: format: date-time - title: NullTime implements sql.NullTime functionality. type: string request_url: description: RequestURL is the original Logout URL requested. type: string requested_at: format: date-time - title: NullTime implements sql.NullTime functionality. type: string rp_initiated: description: "RPInitiated is set to true if the request was initiated by\ @@ -3891,7 +3889,7 @@ components: out. type: string subject: - description: Subject is the user for whom the logout was request. + description: Subject is the user for whom the logout was requested. type: string title: Contains information about an ongoing logout request. type: object diff --git a/internal/httpclient/docs/OAuth2LogoutRequest.md b/internal/httpclient/docs/OAuth2LogoutRequest.md index 81da891ef54..8676a0b3da4 100644 --- a/internal/httpclient/docs/OAuth2LogoutRequest.md +++ b/internal/httpclient/docs/OAuth2LogoutRequest.md @@ -4,14 +4,14 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**Challenge** | Pointer to **string** | Challenge is the identifier of the logout authentication request. | [optional] +**Challenge** | Pointer to **string** | Challenge is used to retrieve/accept/deny the logout request. | [optional] **Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [optional] **ExpiresAt** | Pointer to **time.Time** | | [optional] **RequestUrl** | Pointer to **string** | RequestURL is the original Logout URL requested. | [optional] **RequestedAt** | Pointer to **time.Time** | | [optional] **RpInitiated** | Pointer to **bool** | RPInitiated is set to true if the request was initiated by a Relying Party (RP), also known as an OAuth 2.0 Client. | [optional] **Sid** | Pointer to **string** | SessionID is the login session ID that was requested to log out. | [optional] -**Subject** | Pointer to **string** | Subject is the user for whom the logout was request. | [optional] +**Subject** | Pointer to **string** | Subject is the user for whom the logout was requested. | [optional] ## Methods diff --git a/internal/httpclient/model_o_auth2_logout_request.go b/internal/httpclient/model_o_auth2_logout_request.go index 8a792ab43ec..c4ba39b5611 100644 --- a/internal/httpclient/model_o_auth2_logout_request.go +++ b/internal/httpclient/model_o_auth2_logout_request.go @@ -21,7 +21,7 @@ var _ MappedNullable = &OAuth2LogoutRequest{} // OAuth2LogoutRequest struct for OAuth2LogoutRequest type OAuth2LogoutRequest struct { - // Challenge is the identifier of the logout authentication request. + // Challenge is used to retrieve/accept/deny the logout request. Challenge *string `json:"challenge,omitempty"` Client *OAuth2Client `json:"client,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` @@ -32,7 +32,7 @@ type OAuth2LogoutRequest struct { RpInitiated *bool `json:"rp_initiated,omitempty"` // SessionID is the login session ID that was requested to log out. Sid *string `json:"sid,omitempty"` - // Subject is the user for whom the logout was request. + // Subject is the user for whom the logout was requested. Subject *string `json:"subject,omitempty"` } diff --git a/oauth2/flowctx/encoding.go b/oauth2/flowctx/encoding.go index 8c659ad724e..ee8023d7dbf 100644 --- a/oauth2/flowctx/encoding.go +++ b/oauth2/flowctx/encoding.go @@ -29,6 +29,8 @@ const ( deviceVerifier consentChallenge consentVerifier + logoutChallenge + logoutVerifier ) func withPurpose(purpose purpose) CodecOption { return func(ad *data) { ad.Purpose = purpose } } @@ -40,6 +42,8 @@ var ( AsDeviceVerifier = withPurpose(deviceVerifier) AsConsentChallenge = withPurpose(consentChallenge) AsConsentVerifier = withPurpose(consentVerifier) + AsLogoutChallenge = withPurpose(logoutChallenge) + AsLogoutVerifier = withPurpose(logoutVerifier) ) func additionalDataFromOpts(opts ...CodecOption) []byte { diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0009.json b/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0009.json deleted file mode 100644 index 7681dc70e21..00000000000 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0009.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "challenge": "challenge-0009", - "subject": "subject-0009", - "sid": "session_id-0009", - "request_url": "http://request/0009", - "rp_initiated": true, - "expires_at": null, - "requested_at": null, - "client": null -} diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0010.json b/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0010.json deleted file mode 100644 index d1cb5f6aa61..00000000000 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0010.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "challenge": "challenge-0010", - "subject": "subject-0010", - "sid": "session_id-0010", - "request_url": "http://request/0010", - "rp_initiated": true, - "expires_at": null, - "requested_at": null, - "client": null -} diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0011.json b/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0011.json deleted file mode 100644 index 3c81d38cb47..00000000000 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0011.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "challenge": "challenge-0011", - "subject": "subject-0011", - "sid": "session_id-0011", - "request_url": "http://request/0011", - "rp_initiated": true, - "expires_at": null, - "requested_at": null, - "client": null -} diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0012.json b/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0012.json deleted file mode 100644 index 67c839b88a0..00000000000 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0012.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "challenge": "challenge-0012", - "subject": "subject-0012", - "sid": "session_id-0012", - "request_url": "http://request/0012", - "rp_initiated": true, - "expires_at": null, - "requested_at": null, - "client": null -} diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0013.json b/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0013.json deleted file mode 100644 index f8b84db4b56..00000000000 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0013.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "challenge": "challenge-0013", - "subject": "subject-0013", - "sid": "session_id-0013", - "request_url": "http://request/0013", - "rp_initiated": true, - "expires_at": null, - "requested_at": null, - "client": null -} diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0014.json b/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0014.json deleted file mode 100644 index c5194805b6a..00000000000 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-0014.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "challenge": "challenge-0014", - "subject": "subject-0014", - "sid": "session_id-0014", - "request_url": "http://request/0014", - "rp_initiated": true, - "expires_at": null, - "requested_at": null, - "client": null -} diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-20240916105610000001.json b/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-20240916105610000001.json deleted file mode 100644 index 8b1841c9be6..00000000000 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_logout_request/challenge-20240916105610000001.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "challenge": "challenge-20240916105610000001", - "subject": "subject-0014", - "sid": "session_id-0014", - "request_url": "http://request/0014", - "rp_initiated": true, - "expires_at": "2022-02-15T22:20:20Z", - "requested_at": "2022-02-15T22:20:20Z", - "client": null -} diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 71435d95687..0833e4182d3 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -85,7 +85,7 @@ func TestMigrations(t *testing.T) { }) } - var test = func(db string, c *pop.Connection) func(t *testing.T) { + var test = func(_ string, c *pop.Connection) func(t *testing.T) { return func(t *testing.T) { ctx := context.Background() x.CleanSQLPop(t, c) @@ -168,19 +168,6 @@ func TestMigrations(t *testing.T) { } }) - t.Run("case=hydra_oauth2_logout_request", func(t *testing.T) { - lrs := []flow.LogoutRequest{} - require.NoError(t, c.All(&lrs)) - require.Equal(t, 7, len(lrs)) - - for _, s := range lrs { - testhelpersuuid.AssertUUID(t, s.NID) - s.NID = uuid.Nil - s.Client = nil - CompareWithFixture(t, s, "hydra_oauth2_logout_request", s.ID) - } - }) - t.Run("case=hydra_oauth2_jti_blacklist", func(t *testing.T) { bjtis := []oauth2.BlacklistedJTI{} require.NoError(t, c.All(&bjtis)) diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 76ef0c9096c..43f72fb9dbe 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -756,79 +756,73 @@ WHERE return cs, nil } -func (p *Persister) CreateLogoutRequest(ctx context.Context, request *flow.LogoutRequest) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLogoutRequest") +func (p *Persister) CreateLogoutChallenge(ctx context.Context, request *flow.LogoutRequest) (challenge string, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLogoutChallenge") defer otelx.End(span, &err) - return errorsx.WithStack(p.CreateWithNetwork(ctx, request)) + request.Challenge = "" + challenge, err = flowctx.Encode(ctx, p.r.FlowCipher(), request, flowctx.AsLogoutChallenge) + if err != nil { + return "", errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithHintf("Failed to encrypt the logout challenge.")) + } + return challenge, nil } -func (p *Persister) AcceptLogoutRequest(ctx context.Context, challenge string) (_ *flow.LogoutRequest, err error) { +func (p *Persister) AcceptLogoutRequest(ctx context.Context, challenge string) (verifier string, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.AcceptLogoutRequest") defer otelx.End(span, &err) - if err := p.Connection(ctx).RawQuery("UPDATE hydra_oauth2_logout_request SET accepted=true, rejected=false WHERE challenge=? AND nid = ?", challenge, p.NetworkID(ctx)).Exec(); err != nil { - return nil, sqlcon.HandleError(err) + req, err := flowctx.Decode[flow.LogoutRequest](ctx, p.r.FlowCipher(), challenge, flowctx.AsLogoutChallenge) + if err != nil { + return "", errorsx.WithStack(x.ErrNotFound.WithWrap(err).WithHintf("Failed to decrypt the logout challenge.")) + } + req.Challenge = "" + + verifier, err = flowctx.Encode(ctx, p.r.FlowCipher(), req, flowctx.AsLogoutVerifier) + if err != nil { + return "", errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithHintf("Failed to encrypty the logout verifier.")) } - return p.GetLogoutRequest(ctx, challenge) + return verifier, nil } func (p *Persister) RejectLogoutRequest(ctx context.Context, challenge string) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RejectLogoutRequest") defer otelx.End(span, &err) - count, err := p.Connection(ctx). - RawQuery("UPDATE hydra_oauth2_logout_request SET rejected=true, accepted=false WHERE challenge=? AND nid = ?", challenge, p.NetworkID(ctx)). - ExecWithCount() - if count == 0 { - return errorsx.WithStack(x.ErrNotFound) - } else { - return errorsx.WithStack(err) + _, err = flowctx.Decode[flow.LogoutRequest](ctx, p.r.FlowCipher(), challenge, flowctx.AsLogoutChallenge) + if err != nil { + return errorsx.WithStack(x.ErrNotFound.WithWrap(err).WithHintf("Failed to decrypt the logout challenge.")) } + return nil } func (p *Persister) GetLogoutRequest(ctx context.Context, challenge string) (_ *flow.LogoutRequest, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetLogoutRequest") defer otelx.End(span, &err) - var lr flow.LogoutRequest - return &lr, sqlcon.HandleError(p.QueryWithNetwork(ctx).Where("challenge = ? AND rejected = FALSE", challenge).First(&lr)) + request, err := flowctx.Decode[flow.LogoutRequest](ctx, p.r.FlowCipher(), challenge, flowctx.AsLogoutChallenge) + if err != nil { + return nil, errorsx.WithStack(x.ErrNotFound.WithWrap(err).WithHintf("Failed to decrypt the logout challenge.")) + } + request.Challenge = challenge + return request, nil } func (p *Persister) VerifyAndInvalidateLogoutRequest(ctx context.Context, verifier string) (_ *flow.LogoutRequest, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.VerifyAndInvalidateLogoutRequest") defer otelx.End(span, &err) - var lr flow.LogoutRequest - if count, err := p.Connection(ctx).RawQuery(` -UPDATE hydra_oauth2_logout_request - SET was_used = TRUE -WHERE nid = ? - AND verifier = ? - AND accepted = TRUE - AND rejected = FALSE`, - p.NetworkID(ctx), - verifier, - ).ExecWithCount(); count == 0 && err == nil { - return nil, errorsx.WithStack(x.ErrNotFound) - } else if err != nil { - return nil, sqlcon.HandleError(err) - } - - err = sqlcon.HandleError(p.QueryWithNetwork(ctx).Where("verifier = ?", verifier).First(&lr)) + lr, err := flowctx.Decode[flow.LogoutRequest](ctx, p.r.FlowCipher(), verifier, flowctx.AsLogoutVerifier) if err != nil { - return nil, err + return nil, errorsx.WithStack(x.ErrNotFound.WithWrap(err).WithHintf("Failed to decrypt the logout verifier.")) } - if expiry := time.Time(lr.ExpiresAt); - // If the expiry is unset, we are in a legacy use case (allow logout). - // TODO: Remove this in the future. - !expiry.IsZero() && expiry.Before(time.Now().UTC()) { + if lr.ExpiresAt.Before(time.Now()) { return nil, errorsx.WithStack(flow.ErrorLogoutFlowExpired) } - return &lr, nil + return lr, nil } func (p *Persister) FlushInactiveLoginConsentRequests(ctx context.Context, notAfter time.Time, limit int, batchSize int) (err error) { diff --git a/persistence/sql/persister_nid_test.go b/persistence/sql/persister_nid_test.go index 108ae42e7cc..8a182911b15 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -83,29 +83,6 @@ func (s *PersisterTestSuite) TearDownTest() { } } -func (s *PersisterTestSuite) TestAcceptLogoutRequest() { - t := s.T() - lr := newLogoutRequest() - - for k, r := range s.registries { - t.Run("dialect="+k, func(*testing.T) { - require.NoError(t, r.ConsentManager().CreateLogoutRequest(s.t1, lr)) - - expected, err := r.ConsentManager().GetLogoutRequest(s.t1, lr.ID) - require.NoError(t, err) - require.Equal(t, false, expected.Accepted) - - lrAccepted, err := r.ConsentManager().AcceptLogoutRequest(s.t2, lr.ID) - require.Error(t, err) - require.Equal(t, &flow.LogoutRequest{}, lrAccepted) - - actual, err := r.ConsentManager().GetLogoutRequest(s.t1, lr.ID) - require.NoError(t, err) - require.Equal(t, expected, actual) - }) - } -} - func (s *PersisterTestSuite) TestAddKeyGetKeyDeleteKey() { t := s.T() key := newKey("test-ks", "test") @@ -456,27 +433,6 @@ func (s *PersisterTestSuite) TestCreateLoginSession() { }) } } - -func (s *PersisterTestSuite) TestCreateLogoutRequest() { - t := s.T() - for k, r := range s.registries { - t.Run(k, func(t *testing.T) { - client := &client.Client{ID: "client-id"} - lr := flow.LogoutRequest{ - // TODO there is not FK for SessionID so we don't need it here; TODO make sure the missing FK is intentional - ID: uuid.Must(uuid.NewV4()).String(), - ClientID: sql.NullString{Valid: true, String: client.ID}, - } - - require.NoError(t, r.Persister().CreateClient(s.t1, client)) - require.NoError(t, r.Persister().CreateLogoutRequest(s.t1, &lr)) - actual, err := r.Persister().GetLogoutRequest(s.t1, lr.ID) - require.NoError(t, err) - require.Equal(t, s.t1NID, actual.NID) - }) - } -} - func (s *PersisterTestSuite) TestCreateOpenIDConnectSession() { t := s.T() for k, r := range s.registries { @@ -1236,30 +1192,6 @@ func (s *PersisterTestSuite) TestGetLoginRequest() { } } -func (s *PersisterTestSuite) TestGetLogoutRequest() { - t := s.T() - for k, r := range s.registries { - t.Run(k, func(t *testing.T) { - client := &client.Client{ID: "client-id"} - lr := flow.LogoutRequest{ - ID: uuid.Must(uuid.NewV4()).String(), - ClientID: sql.NullString{Valid: true, String: client.ID}, - } - - require.NoError(t, r.Persister().CreateClient(s.t1, client)) - require.NoError(t, r.Persister().CreateLogoutRequest(s.t1, &lr)) - - actual, err := r.Persister().GetLogoutRequest(s.t2, lr.ID) - require.Error(t, err) - require.Equal(t, &flow.LogoutRequest{}, actual) - - actual, err = r.Persister().GetLogoutRequest(s.t1, lr.ID) - require.NoError(t, err) - require.NotEqual(t, &flow.LogoutRequest{}, actual) - }) - } -} - func (s *PersisterTestSuite) TestGetOpenIDConnectSession() { t := s.T() for k, r := range s.registries { @@ -1723,27 +1655,6 @@ func (s *PersisterTestSuite) TestQueryWithNetwork() { }) } } - -func (s *PersisterTestSuite) TestRejectLogoutRequest() { - t := s.T() - for k, r := range s.registries { - t.Run(k, func(t *testing.T) { - lr := newLogoutRequest() - require.NoError(t, r.ConsentManager().CreateLogoutRequest(s.t1, lr)) - - require.Error(t, r.ConsentManager().RejectLogoutRequest(s.t2, lr.ID)) - actual, err := r.ConsentManager().GetLogoutRequest(s.t1, lr.ID) - require.NoError(t, err) - require.Equal(t, lr, actual) - - require.NoError(t, r.ConsentManager().RejectLogoutRequest(s.t1, lr.ID)) - actual, err = r.ConsentManager().GetLogoutRequest(s.t1, lr.ID) - require.Error(t, err) - require.Equal(t, &flow.LogoutRequest{}, actual) - }) - } -} - func (s *PersisterTestSuite) TestRevokeAccessToken() { t := s.T() for k, r := range s.registries { @@ -2136,62 +2047,6 @@ func (s *PersisterTestSuite) TestVerifyAndInvalidateLoginRequest() { } } -func (s *PersisterTestSuite) TestVerifyAndInvalidateLogoutRequest() { - t := s.T() - for k, r := range s.registries { - t.Run(k, func(t *testing.T) { - run := func(t *testing.T, lr *flow.LogoutRequest) { - lr.Verifier = uuid.Must(uuid.NewV4()).String() - lr.Accepted = true - lr.Rejected = false - require.NoError(t, r.ConsentManager().CreateLogoutRequest(s.t1, lr)) - - expected, err := r.ConsentManager().GetLogoutRequest(s.t1, lr.ID) - require.NoError(t, err) - - lrInvalidated, err := r.ConsentManager().VerifyAndInvalidateLogoutRequest(s.t2, lr.Verifier) - require.Error(t, err) - require.Nil(t, lrInvalidated) - actual := &flow.LogoutRequest{} - require.NoError(t, r.Persister().Connection(context.Background()).Find(actual, lr.ID)) - require.Equal(t, expected, actual) - - lrInvalidated, err = r.ConsentManager().VerifyAndInvalidateLogoutRequest(s.t1, lr.Verifier) - require.NoError(t, err) - require.NoError(t, r.Persister().Connection(context.Background()).Find(actual, lr.ID)) - require.Equal(t, lrInvalidated, actual) - require.Equal(t, true, actual.WasHandled) - } - - t.Run("case=legacy logout request without expiry", func(t *testing.T) { - lr := newLogoutRequest() - run(t, lr) - }) - - t.Run("case=logout request with expiry", func(t *testing.T) { - lr := newLogoutRequest() - lr.ExpiresAt = sqlxx.NullTime(time.Now().Add(time.Hour)) - run(t, lr) - }) - - t.Run("case=logout request that expired returns error", func(t *testing.T) { - lr := newLogoutRequest() - lr.ExpiresAt = sqlxx.NullTime(time.Now().UTC().Add(-time.Hour)) - lr.Verifier = uuid.Must(uuid.NewV4()).String() - lr.Accepted = true - lr.Rejected = false - require.NoError(t, r.ConsentManager().CreateLogoutRequest(s.t1, lr)) - - _, err := r.ConsentManager().VerifyAndInvalidateLogoutRequest(s.t2, lr.Verifier) - require.ErrorIs(t, err, x.ErrNotFound) - - _, err = r.ConsentManager().VerifyAndInvalidateLogoutRequest(s.t1, lr.Verifier) - require.ErrorIs(t, err, flow.ErrorLogoutFlowExpired) - }) - }) - } -} - func (s *PersisterTestSuite) TestWithFallbackNetworkID() { t := s.T() for k, r := range s.registries { @@ -2254,12 +2109,6 @@ func newGrant(keySet string, keyID string) trust.Grant { } } -func newLogoutRequest() *flow.LogoutRequest { - return &flow.LogoutRequest{ - ID: uuid.Must(uuid.NewV4()).String(), - } -} - func newKey(ksID string, use string) jose.JSONWebKey { ks, err := jwk.GenerateJWK(context.Background(), jose.RS256, ksID, use) if err != nil { diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go index b4c88ef01c3..95a02c472b5 100644 --- a/persistence/sql/persister_test.go +++ b/persistence/sql/persister_test.go @@ -38,7 +38,7 @@ func init() { func testRegistry(t *testing.T, ctx context.Context, k string, t1 driver.Registry, t2 driver.Registry) { t.Run("package=client/manager="+k, func(t *testing.T) { - t.Run("case=create-get-update-delete", client.TestHelperCreateGetUpdateDeleteClient(k, t1.Persister().Connection(context.Background()), t1.ClientManager(), t2.ClientManager())) + t.Run("case=create-get-update-delete", client.TestHelperCreateGetUpdateDeleteClient(k, t1.Persister().Connection(ctx), t1.ClientManager(), t2.ClientManager())) t.Run("case=autogenerate-key", client.TestHelperClientAutoGenerateKey(k, t1.ClientManager())) diff --git a/spec/api.json b/spec/api.json index 772573fdb61..6724976062e 100644 --- a/spec/api.json +++ b/spec/api.json @@ -1138,21 +1138,23 @@ "oAuth2LogoutRequest": { "properties": { "challenge": { - "description": "Challenge is the identifier of the logout authentication request.", + "description": "Challenge is used to retrieve/accept/deny the logout request.", "type": "string" }, "client": { "$ref": "#/components/schemas/oAuth2Client" }, "expires_at": { - "$ref": "#/components/schemas/nullTime" + "format": "date-time", + "type": "string" }, "request_url": { "description": "RequestURL is the original Logout URL requested.", "type": "string" }, "requested_at": { - "$ref": "#/components/schemas/nullTime" + "format": "date-time", + "type": "string" }, "rp_initiated": { "description": "RPInitiated is set to true if the request was initiated by a Relying Party (RP), also known as an OAuth 2.0 Client.", @@ -1163,7 +1165,7 @@ "type": "string" }, "subject": { - "description": "Subject is the user for whom the logout was request.", + "description": "Subject is the user for whom the logout was requested.", "type": "string" } }, diff --git a/spec/swagger.json b/spec/swagger.json index 0db4dac74e7..617c0aaef21 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3246,21 +3246,23 @@ "title": "Contains information about an ongoing logout request.", "properties": { "challenge": { - "description": "Challenge is the identifier of the logout authentication request.", + "description": "Challenge is used to retrieve/accept/deny the logout request.", "type": "string" }, "client": { "$ref": "#/definitions/oAuth2Client" }, "expires_at": { - "$ref": "#/definitions/nullTime" + "type": "string", + "format": "date-time" }, "request_url": { "description": "RequestURL is the original Logout URL requested.", "type": "string" }, "requested_at": { - "$ref": "#/definitions/nullTime" + "type": "string", + "format": "date-time" }, "rp_initiated": { "description": "RPInitiated is set to true if the request was initiated by a Relying Party (RP), also known as an OAuth 2.0 Client.", @@ -3271,7 +3273,7 @@ "type": "string" }, "subject": { - "description": "Subject is the user for whom the logout was request.", + "description": "Subject is the user for whom the logout was requested.", "type": "string" } }