mirror of https://github.com/ory/hydra
feat: stateless logout
This commit is contained in:
parent
adf8fb2086
commit
666b67ff85
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
RequestedAt time.Time `json:"requested_at"`
|
||||
Client *client.Client `json:"client"`
|
||||
|
||||
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:"-"`
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue