// 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) }