public async Task <UserLoginResult> CreateAccountAndLoginAsync(IActivityMonitor monitor, IWebFrontAuthAutoCreateAccountContext context) { var userTable = _stObjMap.StObjs.Obtain <CustomUserTable>(); var actorEmail = _stObjMap.StObjs.Obtain <ActorEMailTable>(); var oidcTable = _stObjMap.StObjs.Obtain <UserOidcTable>(); ICustomUserOidcInfos infos = (ICustomUserOidcInfos)context.Payload; ISqlCallContext ctx = context.HttpContext.RequestServices.GetService <ISqlCallContext>(); SqlDefaultDatabase db = _stObjMap.StObjs.Obtain <SqlDefaultDatabase>(); using (var sqlCtx = new SqlStandardCallContext()) { UserQueries userQueries = new UserQueries(sqlCtx, db); int exists = await userQueries.CheckEmail(infos.Email); if (exists == 0) { int userId = await userTable.CreateUserAsync(ctx, 1, Guid.NewGuid().ToString(), infos.FirstName, infos.LastName); await actorEmail.AddEMailAsync(ctx, 1, userId, infos.Email, true, false); UCLResult result = await oidcTable.CreateOrUpdateOidcUserAsync( ctx, 1, userId, infos, UCLMode.CreateOrUpdate | UCLMode.WithActualLogin); if (result.OperationResult != UCResult.Created) { return(null); } return(await _dbAuth.CreateUserLoginResultFromDatabase(ctx, _typeSystem, result.LoginResult)); } else { UCLResult result = await oidcTable.CreateOrUpdateOidcUserAsync( ctx, 1, exists, infos, UCLMode.CreateOrUpdate | UCLMode.WithActualLogin); return(await _dbAuth.CreateUserLoginResultFromDatabase(ctx, _typeSystem, result.LoginResult)); } } }
async Task <LoginResult> DoVerifyAsync(ISqlCallContext ctx, SqlCommand hashReader, string password, object objectKey, bool actualLogin, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(password)) { return(new LoginResult(KnownLoginFailureCode.InvalidCredentials)); } // 1 - Get the PwdHash, UserId and FailedAttemptCount. // hash is null if the user is not a UserPassword: we'll try to migrate it. // hash can be empty iif a previous attempt to migrate it has failed. var read = await ctx[Database].ExecuteSingleRowAsync(hashReader, row => row == null ? (0, null, -1) : (UserId: row.GetInt32(1), PwdHash: row.IsDBNull(0) ? null : row.GetBytes(0), FailedAttemptCount: row.IsDBNull(2) ? -1 : row.GetByte(2))); if (read.UserId == 0) { return(new LoginResult(KnownLoginFailureCode.InvalidUserKey)); } PasswordVerificationResult result = PasswordVerificationResult.Failed; PasswordHasher p = null; IUserPasswordMigrator migrator = null; // 2 - Handle external password migration or check the hash. if (read.PwdHash == null || read.PwdHash.Length == 0) { migrator = UserPasswordPackage.PasswordMigrator; if (migrator != null && migrator.VerifyPassword(ctx, read.UserId, password)) { result = PasswordVerificationResult.SuccessRehashNeeded; p = new PasswordHasher(HashIterationCount); } } else { p = new PasswordHasher(HashIterationCount); result = p.VerifyHashedPassword(read.PwdHash, password); } // 3 - Handle result. var mode = actualLogin ? UCLMode.WithActualLogin : UCLMode.WithCheckLogin; if (result == PasswordVerificationResult.SuccessRehashNeeded) { // 3.1 - If migration occurred, create the user with its hashed password. // Otherwise, rehash the password and update the database. mode |= UCLMode.CreateOrUpdate; UCLResult r = await PasswordUserUCLAsync(ctx, 1, read.UserId, p.HashPassword(password), mode, null, cancellationToken).ConfigureAwait(false); if (r.OperationResult != UCResult.None && migrator != null) { migrator.MigrationDone(ctx, read.UserId); } return(r.LoginResult); } if (result == PasswordVerificationResult.Failed && migrator != null) { // 3.2 - Migration failed, create the user with an empty hash. // so that FailedAttemptCount (or other) can be handled. mode |= UCLMode.CreateOnly; mode &= ~UCLMode.UpdateOnly; // KnownLoginFailureCode is UnregisteredUser: the fact that we have a password migrator // (that failed to migrate) results in the user actually not registered in the UserPassword provider. UCLResult r = await PasswordUserUCLAsync(ctx, 1, read.UserId, Array.Empty <byte>(), mode, (int)KnownLoginFailureCode.UnregisteredUser, cancellationToken); return(r.LoginResult); } // 4 - Challenges the database login checks. int?failureCode = null; if (result == PasswordVerificationResult.Failed) { failureCode = (int)KnownLoginFailureCode.InvalidCredentials; } return((await PasswordUserUCLAsync(ctx, 1, read.UserId, null, mode, failureCode, cancellationToken) .ConfigureAwait(false)).LoginResult); }
static public async Task StandardTestForGenericAuthenticationProviderAsync( Package auth, string schemeOrProviderName, Func <int, string, object> payloadForCreateOrUpdate, Func <int, string, object> payloadForLogin, Func <int, string, object> payloadForLoginFail ) { var user = TestHelper.StObjMap.Default.Obtain <Actor.UserTable>(); IGenericAuthenticationProvider g = auth.FindProvider(schemeOrProviderName); using (var ctx = new SqlStandardCallContext()) { string userName = Guid.NewGuid().ToString(); int userId = await user.CreateUserAsync(ctx, 1, userName); using (TestHelper.Monitor.OpenInfo($"StandardTestAsync for generic {schemeOrProviderName} with userId:{userId} and userName:{userName}.")) { IUserAuthInfo info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.UserId.Should().Be(userId); info.UserName.Should().Be(userName); info.Schemes.Should().BeEmpty(); using (TestHelper.Monitor.OpenInfo("CreateOrUpdateUser without login.")) { (await g.CreateOrUpdateUserAsync(ctx, 1, userId, payloadForCreateOrUpdate(userId, userName))).OperationResult.Should().Be(UCResult.Created); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Should().BeEmpty("Still no scheme since we did not use WithLogin."); (await g.LoginUserAsync(ctx, payloadForLogin(userId, userName), actualLogin: false)).UserId.Should().Be(userId); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Should().BeEmpty("Still no scheme since we challenge login but not use WithLogin."); (await g.LoginUserAsync(ctx, payloadForLogin(userId, userName))).UserId.Should().Be(userId); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Should().HaveCount(1); info.Schemes[0].Name.Should().StartWith(g.ProviderName); info.Schemes[0].Name.Should().BeEquivalentTo(schemeOrProviderName); info.Schemes[0].LastUsed.Should().BeCloseTo(DateTime.UtcNow, 1000); await g.DestroyUserAsync(ctx, 1, userId); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Should().BeEmpty(); } using (TestHelper.Monitor.OpenInfo("CreateOrUpdateUser WithActualLogin.")) { info.UserId.Should().Be(userId); info.UserName.Should().Be(userName); info.Schemes.Should().BeEmpty(); var result = await g.CreateOrUpdateUserAsync(ctx, 1, userId, payloadForCreateOrUpdate( userId, userName ), UCLMode.CreateOnly | UCLMode.WithActualLogin); result.OperationResult.Should().Be(UCResult.Created); result.LoginResult.UserId.Should().Be(userId); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Count.Should().Be(1); info.Schemes[0].Name.Should().StartWith(g.ProviderName); info.Schemes[0].Name.Should().BeEquivalentTo(schemeOrProviderName); info.Schemes[0].LastUsed.Should().BeCloseTo(DateTime.UtcNow, 1000); (await g.LoginUserAsync(ctx, payloadForLoginFail(userId, userName))).UserId.Should().Be(0); await g.DestroyUserAsync(ctx, 1, userId); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Should().BeEmpty(); } using (TestHelper.Monitor.OpenInfo("Login for an unregistered user.")) { info.UserId.Should().Be(userId); info.UserName.Should().Be(userName); info.Schemes.Should().BeEmpty(); var result = await g.LoginUserAsync(ctx, payloadForLogin( userId, userName )); result.IsSuccess.Should().BeFalse(); result.UserId.Should().Be(0); result.FailureCode.Should().Be((int)KnownLoginFailureCode.UnregisteredUser); result.FailureReason.Should().Be("Unregistered user."); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Should().BeEmpty(); await g.DestroyUserAsync(ctx, 1, userId); info = await auth.ReadUserAuthInfoAsync(ctx, 1, userId); info.Schemes.Should().BeEmpty(); } using (TestHelper.Monitor.OpenInfo("Invalid payload MUST throw an ArgumentException.")) { g.Awaiting(sut => sut.CreateOrUpdateUserAsync(ctx, 1, userId, DBNull.Value)).Should().Throw <ArgumentException>(); g.Awaiting(sut => sut.LoginUserAsync(ctx, DBNull.Value)).Should().Throw <ArgumentException>(); } using (TestHelper.Monitor.OpenInfo("Injecting disabled user in sAuthUserOnLogin.")) using (auth.Database.TemporaryTransform(@" create transformer on CK.sAuthUserOnLogin as begin inject ""set @FailureCode = 6; -- GloballyDisabledUser"" into ""CheckLoginFailure""; end ")) { UCLResult result = await g.CreateOrUpdateUserAsync(ctx, 1, userId, payloadForCreateOrUpdate( userId, userName ), UCLMode.CreateOnly | UCLMode.WithActualLogin); result.OperationResult.Should().Be(UCResult.Created); result.LoginResult.UserId.Should().Be(0); result.LoginResult.IsSuccess.Should().BeFalse(); result.LoginResult.FailureCode.Should().Be((int)KnownLoginFailureCode.GloballyDisabledUser); LoginResult login = await g.LoginUserAsync(ctx, payloadForLogin( userId, userName )); login.IsSuccess.Should().BeFalse(); login.UserId.Should().Be(0); login.FailureCode.Should().Be((int)KnownLoginFailureCode.GloballyDisabledUser); login = await g.LoginUserAsync(ctx, payloadForLogin( userId, userName ), actualLogin : false); login.IsSuccess.Should().BeFalse(); login.UserId.Should().Be(0); login.FailureCode.Should().Be((int)KnownLoginFailureCode.GloballyDisabledUser); } } await user.DestroyUserAsync(ctx, 1, userId); } }
async Task <LoginResult> DoVerifyAsync(ISqlCallContext ctx, SqlCommand hashReader, string password, object objectKey, bool actualLogin, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(password)) { return(new LoginResult(KnownLoginFailureCode.InvalidCredentials)); } // 1 - Get the PwdHash and UserId. // hash is null if the user is not a UserPassword: we'll try to migrate it. var read = await ctx[Database].ExecuteSingleRowAsync(hashReader, row => row == null ? (0, null) : (UserId: row.GetInt32(1), PwdHash: row.IsDBNull(0) ? null : row.GetBytes(0))); if (read.UserId == 0) { return(new LoginResult(KnownLoginFailureCode.InvalidUserKey)); } PasswordVerificationResult result = PasswordVerificationResult.Failed; PasswordHasher p = null; IUserPasswordMigrator migrator = null; // 2 - Handle external password migration or check the hash. if (read.PwdHash == null) { migrator = _package.PasswordMigrator; if (migrator != null && migrator.VerifyPassword(ctx, read.UserId, password)) { result = PasswordVerificationResult.SuccessRehashNeeded; p = new PasswordHasher(HashIterationCount); } } else { p = new PasswordHasher(HashIterationCount); result = p.VerifyHashedPassword(read.PwdHash, password); } // 3 - Handle result. var mode = actualLogin ? UCLMode.WithActualLogin : UCLMode.WithCheckLogin; if (result == PasswordVerificationResult.SuccessRehashNeeded) { // 3.1 - If migration occurred, create the user with its hashed password. // Otherwise, rehash the password and update the database. mode |= UCLMode.CreateOrUpdate; UCLResult r = await PasswordUserUCLAsync(ctx, 1, read.UserId, p.HashPassword(password), mode, null, cancellationToken).ConfigureAwait(false); if (r.OperationResult != UCResult.None && migrator != null) { migrator.MigrationDone(ctx, read.UserId); } return(r.LoginResult); } // 4 - Challenges the database login checks. int?failureCode = null; if (result == PasswordVerificationResult.Failed) { failureCode = (int)KnownLoginFailureCode.InvalidCredentials; } return((await PasswordUserUCLAsync(ctx, 1, read.UserId, null, mode, failureCode, cancellationToken) .ConfigureAwait(false)).LoginResult); }