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
225 lines
7.0 KiB
Go
225 lines
7.0 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package password_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/phayes/freeport"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/api/types/network"
|
|
|
|
dockertest "github.com/ory/dockertest/v4"
|
|
hydraclientgo "github.com/ory/hydra-client-go/v2"
|
|
"github.com/ory/kratos/pkg/testhelpers"
|
|
"github.com/ory/x/urlx"
|
|
)
|
|
|
|
type clientAppConfig struct {
|
|
client *oauth2.Config
|
|
expectToken bool
|
|
state *clientAppState
|
|
}
|
|
|
|
type clientAppState struct {
|
|
visits int64
|
|
tokens int64
|
|
}
|
|
|
|
type callTrace string
|
|
|
|
const (
|
|
RegistrationUI callTrace = "registration-ui"
|
|
RegistrationWithOAuth2LoginChallenge callTrace = "registration-with-oauth2-login-challenge"
|
|
RegistrationWithFlowID callTrace = "registration-with-flow-id"
|
|
LoginUI callTrace = "login-ui"
|
|
LoginWithOAuth2LoginChallenge callTrace = "login-with-oauth2-login-challenge"
|
|
LoginWithFlowID callTrace = "login-with-flow-id"
|
|
Consent callTrace = "consent"
|
|
ConsentWithChallenge callTrace = "consent-with-challenge"
|
|
ConsentAccept callTrace = "consent-accept"
|
|
ConsentSkip callTrace = "consent-skip"
|
|
ConsentClientSkip callTrace = "consent-client-skip"
|
|
CodeExchange callTrace = "code-exchange"
|
|
CodeExchangeWithToken callTrace = "code-exchange-with-token"
|
|
)
|
|
|
|
type testContextKey string
|
|
|
|
const (
|
|
TestUIConfig testContextKey = "test-ui-config"
|
|
TestOAuthClientState testContextKey = "test-oauth-client-state"
|
|
)
|
|
|
|
type testConfig struct {
|
|
identifier string
|
|
password string
|
|
browserClient *http.Client
|
|
kratosPublicTS *httptest.Server
|
|
clientAppTS *httptest.Server
|
|
hydraAdminClient hydraclientgo.OAuth2API
|
|
consentRemember bool
|
|
requestedScope []string
|
|
callTrace *[]callTrace
|
|
}
|
|
|
|
func createHydraOAuth2ApiClient(url string) hydraclientgo.OAuth2API {
|
|
configuration := hydraclientgo.NewConfiguration()
|
|
configuration.Host = urlx.ParseOrPanic(url).Host
|
|
configuration.Servers = hydraclientgo.ServerConfigurations{{URL: url}}
|
|
|
|
return hydraclientgo.NewAPIClient(configuration).OAuth2API
|
|
}
|
|
|
|
func createOAuth2Client(t *testing.T, ctx context.Context, hydraAdmin hydraclientgo.OAuth2API, redirectURIs []string, scope string, skipConsent bool) string {
|
|
t.Helper()
|
|
|
|
clientName := "kratos-hydra-integration-test-client-1"
|
|
tokenEndpointAuthMethod := "client_secret_post"
|
|
clientSecret := "client-secret"
|
|
|
|
c, r, err := hydraAdmin.CreateOAuth2Client(ctx).OAuth2Client(
|
|
hydraclientgo.OAuth2Client{
|
|
ClientName: &clientName,
|
|
RedirectUris: redirectURIs,
|
|
Scope: &scope,
|
|
TokenEndpointAuthMethod: &tokenEndpointAuthMethod,
|
|
ClientSecret: &clientSecret,
|
|
SkipConsent: &skipConsent,
|
|
},
|
|
).Execute()
|
|
require.NoError(t, err)
|
|
require.Equal(t, r.StatusCode, http.StatusCreated)
|
|
return *c.ClientId
|
|
}
|
|
|
|
func makeAuthCodeURL(t *testing.T, c *oauth2.Config, requestedClaims string, isForced bool) string {
|
|
t.Helper()
|
|
|
|
var options []oauth2.AuthCodeOption
|
|
|
|
if isForced {
|
|
options = append(options, oauth2.SetAuthURLParam("prompt", "login"))
|
|
}
|
|
if requestedClaims != "" {
|
|
options = append(options, oauth2.SetAuthURLParam("claims", requestedClaims))
|
|
}
|
|
|
|
state := fmt.Sprintf("%x", uuid.Must(uuid.NewV4()))
|
|
return c.AuthCodeURL(state, options...)
|
|
}
|
|
|
|
func newHydra(t *testing.T, loginUI string, consentUI string) (hydraAdmin string, hydraPublic string) {
|
|
pool := dockertest.NewPoolT(t, "")
|
|
|
|
// The public host port must be known before the container starts because it
|
|
// is part of URLS_SELF_ISSUER. freeport releases the port before Docker binds
|
|
// it, so another process can grab it in between — retry on that race.
|
|
var hydraResource dockertest.ClosableResource
|
|
ctx := t.Context()
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
publicPort, err := freeport.GetFreePort()
|
|
require.NoError(t, err)
|
|
|
|
hydraResource, err = pool.Run(ctx, "oryd/hydra",
|
|
// Keep tag in sync with the version in ci.yaml
|
|
dockertest.WithTag("v2.2.0"),
|
|
dockertest.WithoutReuse(),
|
|
dockertest.WithEnv([]string{
|
|
"DSN=memory",
|
|
fmt.Sprintf("URLS_SELF_ISSUER=http://127.0.0.1:%d/", publicPort),
|
|
"URLS_LOGIN=" + loginUI,
|
|
"URLS_CONSENT=" + consentUI,
|
|
"LOG_LEAK_SENSITIVE_VALUES=true",
|
|
"SECRETS_SYSTEM=someverylongsecretthatis32byteslong",
|
|
}),
|
|
dockertest.WithCmd([]string{"serve", "all", "--dev"}),
|
|
dockertest.WithContainerConfig(func(cc *container.Config) {
|
|
cc.ExposedPorts = network.PortSet{
|
|
network.MustParsePort("4444/tcp"): struct{}{},
|
|
network.MustParsePort("4445/tcp"): struct{}{},
|
|
}
|
|
}),
|
|
dockertest.WithHostConfig(func(hc *container.HostConfig) {
|
|
hc.PortBindings = network.PortMap{
|
|
network.MustParsePort("4444/tcp"): {{HostPort: strconv.Itoa(publicPort)}},
|
|
network.MustParsePort("4445/tcp"): {{HostPort: ""}},
|
|
}
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
}, 2*time.Minute, 100*time.Millisecond)
|
|
hydraResource.Cleanup(t)
|
|
require.NotEmpty(t, hydraResource.GetPort("4444/tcp"), "%+v", hydraResource.Container().NetworkSettings.Ports)
|
|
require.NotEmpty(t, hydraResource.GetPort("4445/tcp"), "%+v", hydraResource.Container)
|
|
|
|
hydraPublic = "http://127.0.0.1:" + hydraResource.GetPort("4444/tcp")
|
|
hydraAdmin = "http://127.0.0.1:" + hydraResource.GetPort("4445/tcp")
|
|
|
|
go func() {
|
|
stdout, stderr, err := hydraResource.Logs(context.Background())
|
|
if err == nil {
|
|
if stdout != "" {
|
|
t.Logf("[hydra stdout]:\n%s\n", stdout)
|
|
}
|
|
if stderr != "" {
|
|
t.Logf("[hydra stderr]:\n%s\n", stderr)
|
|
}
|
|
}
|
|
}()
|
|
client := testhelpers.NewTestClient(t)
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
res, err := client.Get(hydraPublic + "/health/ready")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 200, res.StatusCode)
|
|
|
|
res, err = client.Get(hydraAdmin + "/health/ready")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 200, res.StatusCode)
|
|
}, 30*time.Second, time.Second)
|
|
|
|
t.Logf("Ory Hydra running at: %s %s", hydraPublic, hydraAdmin)
|
|
|
|
return hydraAdmin, hydraPublic
|
|
}
|
|
|
|
type TestLogWriter struct {
|
|
streamName string
|
|
*testing.T
|
|
}
|
|
|
|
func (t TestLogWriter) Write(p []byte) (int, error) {
|
|
t.Logf("[%d bytes @ %s]:\n\n%s\n", len(p), t.streamName, string(p))
|
|
return len(p), nil
|
|
}
|
|
|
|
func doOAuthFlow(t *testing.T, ctx context.Context, oauthClient *oauth2.Config, browserClient *http.Client) {
|
|
t.Helper()
|
|
|
|
authCodeURL := makeAuthCodeURL(t, oauthClient, "", false)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authCodeURL, nil)
|
|
require.NoError(t, err)
|
|
res, err := browserClient.Do(req)
|
|
require.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, res.Body.Close())
|
|
require.Equal(t, "", string(body))
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
|
}
|