@@ -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) {
233234 require .Equal (t , database .AuditActionLogin , auditor .AuditLogs ()[numLogs - 1 ].Action )
234235 })
235236
237+ // "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+
236290 t .Run ("Suspended" , func (t * testing.T ) {
237291 t .Parallel ()
238292 auditor := audit .NewMock ()
0 commit comments