Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
356 lines
15 KiB
Go
356 lines
15 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package hook_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ory/kratos/hydra"
|
|
"github.com/ory/kratos/pkg/testhelpers"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/kratos/pkg"
|
|
"github.com/ory/kratos/selfservice/flow"
|
|
"github.com/ory/kratos/selfservice/flow/recovery"
|
|
"github.com/ory/kratos/selfservice/flow/registration"
|
|
"github.com/ory/kratos/selfservice/flow/verification"
|
|
"github.com/ory/kratos/selfservice/hook"
|
|
"github.com/ory/kratos/session"
|
|
"github.com/ory/kratos/x"
|
|
"github.com/ory/x/randx"
|
|
)
|
|
|
|
func TestSessionIssuer(t *testing.T) {
|
|
ctx := context.Background()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
reg.SetHydra(hydra.NewFake())
|
|
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "http://localhost/")
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/stub.schema.json")
|
|
|
|
var r http.Request
|
|
h := hook.NewSessionIssuer(reg)
|
|
|
|
t.Run("method=sign-up", func(t *testing.T) {
|
|
t.Run("flow=browser", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
s := testhelpers.CreateSession(t, reg)
|
|
f := ®istration.Flow{Type: flow.TypeBrowser}
|
|
|
|
require.NoError(t, h.ExecutePostRegistrationPostPersistHook(w, &r,
|
|
f, &session.Session{ID: s.ID, Identity: s.Identity, Token: randx.MustString(12, randx.AlphaLowerNum)}))
|
|
|
|
require.Empty(t, f.ContinueWithItems)
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID, got.ID)
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.Contains(t, w.Header().Get("Set-Cookie"), config.DefaultSessionCookieName)
|
|
})
|
|
|
|
t.Run("flow=api", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
s := &session.Session{
|
|
ID: x.NewUUID(),
|
|
Identity: i,
|
|
Token: randx.MustString(12, randx.AlphaLowerNum),
|
|
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
|
|
AuthenticatedAt: time.Now().UTC(),
|
|
}
|
|
f := ®istration.Flow{Type: flow.TypeAPI}
|
|
|
|
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
|
|
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s))
|
|
|
|
err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s)
|
|
require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err)
|
|
require.Len(t, f.ContinueWithItems, 1)
|
|
|
|
st := f.ContinueWithItems[0]
|
|
require.IsType(t, &flow.ContinueWithSetOrySessionToken{}, st)
|
|
assert.NotEmpty(t, st.(*flow.ContinueWithSetOrySessionToken).OrySessionToken)
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID.String(), got.ID.String())
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.Empty(t, w.Header().Get("Set-Cookie"))
|
|
body := w.Body.Bytes()
|
|
assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String())
|
|
assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String())
|
|
assert.Equal(t, got.Token, gjson.GetBytes(body, "session_token").String())
|
|
})
|
|
|
|
t.Run("flow=spa", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
s := &session.Session{
|
|
ID: x.NewUUID(),
|
|
Identity: i,
|
|
Token: randx.MustString(12, randx.AlphaLowerNum),
|
|
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
|
|
AuthenticatedAt: time.Now().UTC(),
|
|
}
|
|
f := ®istration.Flow{Type: flow.TypeBrowser}
|
|
|
|
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
|
|
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s))
|
|
|
|
err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s)
|
|
require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err)
|
|
require.Empty(t, f.ContinueWithItems)
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID.String(), got.ID.String())
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.NotEmpty(t, w.Header().Get("Set-Cookie"))
|
|
body := w.Body.Bytes()
|
|
assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String())
|
|
assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String())
|
|
assert.Empty(t, gjson.GetBytes(body, "session_token").String())
|
|
})
|
|
|
|
t.Run("flow=api with oidc code exchange", func(t *testing.T) {
|
|
// A native API OIDC flow with a session token exchange code redirects
|
|
// via MaybeRedirectAPICodeFlow. Once the redirect is written, the hook
|
|
// must abort the flow (ErrHookAbortFlow) so the executor's tail block
|
|
// does not call MaybeRedirectAPICodeFlow a second time and issue a
|
|
// redundant write to the GLOBAL session_token_exchanges table.
|
|
w := httptest.NewRecorder()
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
s := &session.Session{
|
|
ID: x.NewUUID(),
|
|
Identity: i,
|
|
Token: randx.MustString(12, randx.AlphaLowerNum),
|
|
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
|
|
AuthenticatedAt: time.Now().UTC(),
|
|
}
|
|
s.CompletedLoginFor(identity.CredentialsTypeOIDC, identity.AuthenticatorAssuranceLevel1)
|
|
f := ®istration.Flow{ID: x.NewUUID(), Type: flow.TypeAPI}
|
|
|
|
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
|
|
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s))
|
|
|
|
ex, err := reg.SessionTokenExchangePersister().CreateSessionTokenExchanger(ctx, f.ID)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "https://localhost/self-service/methods/oidc/callback/test", nil).WithContext(ctx)
|
|
req.Header.Set("Accept", "application/json")
|
|
err = h.ExecutePostRegistrationPostPersistHook(w, req, f, s)
|
|
require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err)
|
|
|
|
// The redirect carries the return-to code, proving the OIDC handled
|
|
// branch ran before aborting.
|
|
assert.Equal(t, http.StatusSeeOther, w.Code)
|
|
assert.Contains(t, w.Header().Get("Location"), "code="+ex.ReturnToCode)
|
|
|
|
// The exchanger was bound to the session exactly once.
|
|
got, _, err := reg.SessionTokenExchangePersister().CodeForFlow(ctx, f.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ex.ReturnToCode, got.ReturnToCode)
|
|
exchanged, err := reg.SessionTokenExchangePersister().GetExchangerFromCode(ctx, ex.InitCode, ex.ReturnToCode)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID, exchanged.SessionID.UUID)
|
|
})
|
|
})
|
|
|
|
t.Run("method=sign-up with oauth2", func(t *testing.T) {
|
|
t.Run("flow=browser", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
s := testhelpers.CreateSession(t, reg)
|
|
f := ®istration.Flow{
|
|
Type: flow.TypeBrowser,
|
|
OAuth2LoginChallenge: hydra.FakeValidLoginChallenge,
|
|
}
|
|
|
|
require.NoError(t, h.ExecutePostRegistrationPostPersistHook(w, &r,
|
|
f, &session.Session{ID: s.ID, Identity: s.Identity, Token: randx.MustString(12, randx.AlphaLowerNum)}))
|
|
|
|
require.Empty(t, f.ContinueWithItems)
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID, got.ID)
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.Contains(t, w.Header().Get("Set-Cookie"), config.DefaultSessionCookieName)
|
|
})
|
|
|
|
t.Run("flow=api", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
s := &session.Session{
|
|
ID: x.NewUUID(),
|
|
Identity: i,
|
|
Token: randx.MustString(12, randx.AlphaLowerNum),
|
|
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
|
|
AuthenticatedAt: time.Now().UTC(),
|
|
}
|
|
f := ®istration.Flow{
|
|
Type: flow.TypeAPI,
|
|
OAuth2LoginChallenge: hydra.FakeValidLoginChallenge,
|
|
}
|
|
|
|
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
|
|
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s))
|
|
|
|
err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s)
|
|
require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err)
|
|
require.Len(t, f.ContinueWithItems, 1)
|
|
|
|
st := f.ContinueWithItems[0]
|
|
require.IsType(t, &flow.ContinueWithSetOrySessionToken{}, st)
|
|
assert.NotEmpty(t, st.(*flow.ContinueWithSetOrySessionToken).OrySessionToken)
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID.String(), got.ID.String())
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.Empty(t, w.Header().Get("Set-Cookie"))
|
|
body := w.Body.Bytes()
|
|
assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String())
|
|
assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String())
|
|
assert.Equal(t, got.Token, gjson.GetBytes(body, "session_token").String())
|
|
})
|
|
|
|
t.Run("flow=spa", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
s := &session.Session{
|
|
ID: x.NewUUID(),
|
|
Identity: i,
|
|
Token: randx.MustString(12, randx.AlphaLowerNum),
|
|
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
|
|
AuthenticatedAt: time.Now().UTC(),
|
|
}
|
|
f := ®istration.Flow{
|
|
Type: flow.TypeBrowser,
|
|
OAuth2LoginChallenge: hydra.FakeValidLoginChallenge,
|
|
ContinueWithItems: []flow.ContinueWith{flow.NewContinueWithRedirectBrowserTo("https://ory.sh")},
|
|
}
|
|
|
|
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
|
|
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s))
|
|
|
|
err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s)
|
|
require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err)
|
|
require.Len(t, f.ContinueWithItems, 1)
|
|
require.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, f.ContinueWithItems[0].GetAction())
|
|
require.EqualValues(t, hydra.FakePostLoginURL, f.ContinueWithItems[0].(*flow.ContinueWithRedirectBrowserTo).RedirectTo)
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID.String(), got.ID.String())
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.NotEmpty(t, w.Header().Get("Set-Cookie"))
|
|
body := w.Body.Bytes()
|
|
assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String())
|
|
assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String())
|
|
assert.Empty(t, gjson.GetBytes(body, "session_token").String())
|
|
})
|
|
})
|
|
|
|
t.Run("method=sign-up with oauth2 and verification enabled", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
s := &session.Session{
|
|
ID: x.NewUUID(),
|
|
Identity: i,
|
|
Token: randx.MustString(12, randx.AlphaLowerNum),
|
|
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
|
|
AuthenticatedAt: time.Now().UTC(),
|
|
}
|
|
vf := &verification.Flow{
|
|
ID: x.NewUUID(),
|
|
}
|
|
f := ®istration.Flow{
|
|
Type: flow.TypeBrowser,
|
|
OAuth2LoginChallenge: hydra.FakeValidLoginChallenge,
|
|
ContinueWithItems: []flow.ContinueWith{flow.NewContinueWithVerificationUI(vf.ID, "some@ory.sh", "")},
|
|
}
|
|
|
|
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
|
|
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s))
|
|
|
|
err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s)
|
|
require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err)
|
|
require.Len(t, f.ContinueWithItems, 1)
|
|
require.EqualValues(t, flow.ContinueWithActionShowVerificationUIString, f.ContinueWithItems[0].GetAction())
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID.String(), got.ID.String())
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.NotEmpty(t, w.Header().Get("Set-Cookie"))
|
|
body := w.Body.Bytes()
|
|
assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String())
|
|
assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String())
|
|
assert.Empty(t, gjson.GetBytes(body, "session_token").String())
|
|
})
|
|
|
|
t.Run("method=sign-up with oauth2 and some other continue with item", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
|
s := &session.Session{
|
|
ID: x.NewUUID(),
|
|
Identity: i,
|
|
Token: randx.MustString(12, randx.AlphaLowerNum),
|
|
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
|
|
AuthenticatedAt: time.Now().UTC(),
|
|
}
|
|
vf := &recovery.Flow{
|
|
ID: x.NewUUID(),
|
|
}
|
|
f := ®istration.Flow{
|
|
Type: flow.TypeBrowser,
|
|
OAuth2LoginChallenge: hydra.FakeValidLoginChallenge,
|
|
ContinueWithItems: []flow.ContinueWith{flow.NewContinueWithRecoveryUI(vf)},
|
|
}
|
|
|
|
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
|
|
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, s))
|
|
|
|
err := h.ExecutePostRegistrationPostPersistHook(w, &http.Request{Header: http.Header{"Accept": {"application/json"}}}, f, s)
|
|
require.ErrorIs(t, err, registration.ErrHookAbortFlow, "%+v", err)
|
|
require.Len(t, f.ContinueWithItems, 2)
|
|
require.EqualValues(t, flow.ContinueWithActionShowRecoveryUIString, f.ContinueWithItems[0].GetAction())
|
|
require.EqualValues(t, hydra.FakePostLoginURL, f.ContinueWithItems[1].(*flow.ContinueWithRedirectBrowserTo).RedirectTo)
|
|
|
|
got, err := reg.SessionPersister().GetSession(context.Background(), s.ID, session.ExpandNothing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s.ID.String(), got.ID.String())
|
|
assert.True(t, got.AuthenticatedAt.After(time.Now().Add(-time.Minute)))
|
|
|
|
assert.NotEmpty(t, w.Header().Get("Set-Cookie"))
|
|
body := w.Body.Bytes()
|
|
assert.Equal(t, i.ID.String(), gjson.GetBytes(body, "identity.id").String())
|
|
assert.Equal(t, s.ID.String(), gjson.GetBytes(body, "session.id").String())
|
|
assert.Empty(t, gjson.GetBytes(body, "session_token").String())
|
|
})
|
|
}
|