fix: use a random value for a simulated hash for built-in users (#262… · coder/coder@027cf9a
@@ -21,6 +21,7 @@ import (
2121"github.com/coder/coder/v2/coderd/coderdtest"
2222"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
2323"github.com/coder/coder/v2/coderd/database"
24+"github.com/coder/coder/v2/coderd/database/dbauthz"
2425"github.com/coder/coder/v2/coderd/database/dbfake"
2526"github.com/coder/coder/v2/coderd/database/dbgen"
2627"github.com/coder/coder/v2/coderd/database/dbtime"
@@ -233,6 +234,59 @@ func TestPostLogin(t *testing.T) {
233234require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-1].Action)
234235 })
235236237+// "hunter2" was the input of the previous hardcoded simulated hash, which
238+// an empty stored hash wrongly matched; this is a regression test.
239+t.Run("NonexistentUser401", func(t *testing.T) {
240+t.Parallel()
241+client := coderdtest.New(t, nil)
242+ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
243+defer cancel()
244+245+_, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
246+Email: "does-not-exist@coder.com",
247+Password: "hunter2",
248+ })
249+var apiErr *codersdk.Error
250+require.ErrorAs(t, err, &apiErr)
251+require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
252+require.Equal(t, "Incorrect email or password.", apiErr.Message)
253+ })
254+255+// Attempting built-in login as an SSO user returns a 401 to avoid
256+// divulging login type.
257+t.Run("SSOReturns401", func(t *testing.T) {
258+t.Parallel()
259+client, db := coderdtest.NewWithDatabase(t, nil)
260+ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
261+defer cancel()
262+263+// An SSO user has no password hash stored. Create one directly in the
264+// database since the API requires OIDC to be configured. dbgen.User
265+// substitutes a random hash for an empty one, so clear it explicitly.
266+ssoUser := dbgen.User(t, db, database.User{
267+Email: "sso-user@coder.com",
268+LoginType: database.LoginTypeOIDC,
269+ })
270+//nolint:gocritic // Test setup requires a system context to clear the hash.
271+err := db.UpdateUserHashedPassword(dbauthz.AsSystemRestricted(ctx), database.UpdateUserHashedPasswordParams{
272+ID: ssoUser.ID,
273+HashedPassword: []byte{},
274+ })
275+require.NoError(t, err)
276+277+anonClient := codersdk.New(client.URL)
278+_, err = anonClient.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
279+Email: ssoUser.Email,
280+Password: "hunter2",
281+ })
282+var apiErr *codersdk.Error
283+require.ErrorAs(t, err, &apiErr)
284+require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
285+require.Equal(t, "Incorrect email or password.", apiErr.Message)
286+// The login type must not be leaked.
287+require.NotContains(t, apiErr.Message, string(codersdk.LoginTypeOIDC))
288+ })
289+236290t.Run("Suspended", func(t *testing.T) {
237291t.Parallel()
238292auditor := audit.NewMock()