Require unique phone number in Asp.Net Core Identity
Asked Answered
D

4

8

In Asp.Net Core Identity framework, I can easily require a unique email address by setting RequireUniqueEmail = true.

Is there any way to do the same for the user's phone number? Note that I don't want to require a confirmed phone number to sign in. The user is not required to enter a phone number but if they do, it must be unique.

Dunite answered 4/1, 2018 at 19:31 Comment(4)
RequireConfirmed* is available for both email and phone, but RequireUnique* is only available for email. Is there a reason for this?Dunite
one reason could be to allow the user to use SMS for 2FAKaveri
Using SMS for 2FA is exactly why I want to require a unique phone number. I don't want to require a confimed phone number, since the user is allowed to use email for 2FA instead.Dunite
Sorry I think I misread your first comment. I would think that the reason that RequireUnique is only available for email is that many sites use the email as a part of the login credentials. At the time of Identity's creation, sites like Twitter may not have yet made the phone number a part of the login credential option. So the lack of demand probably resulted in not having RequireUnique on Phone.Kaveri
V
3

You can try this, basically enforce it at the Db level first, followed by implementing proper checks at the Manager level.

At DbContext, I declared the indexing and uniqueness of both the username and email properties.

taken from the link

    // ================== Customizing IdentityCore Tables ================== //

            builder.Entity<User>().ToTable("Users").Property(p => p.Id).HasColumnName("Id").ValueGeneratedOnAdd();
            builder.Entity<User>(entity =>
            {
                entity.HasIndex(u => u.UserName).IsUnique();
                entity.HasIndex(u => u.NormalizedUserName).IsUnique();
                entity.HasIndex(u => u.Email).IsUnique();
                entity.HasIndex(u => u.NormalizedEmail).IsUnique();

                entity.Property(u => u.Rating).HasDefaultValue(0).IsRequired();
                entity.HasMany(u => u.UserRoles).WithOne(ur => ur.User)
                    .HasForeignKey(ur => ur.UserId).OnDelete(DeleteBehavior.Restrict);
                entity.HasMany(u => u.UserClaims).WithOne(uc => uc.User)
                    .HasForeignKey(uc => uc.UserId).OnDelete(DeleteBehavior.Restrict);
            });

And for the Manager level code:

    /// <summary>
            /// Sets the <paramref name="email"/> address for a <paramref name="user"/>.
            /// </summary>
            /// <param name="user">The user whose email should be set.</param>
            /// <param name="email">The email to set.</param>
            /// <returns>
            /// The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/>
            /// of the operation.
            /// </returns>
            public override async Task<IdentityResult> SetEmailAsync(User user, string email)
            {
                var dupeUser = await FindByEmailAsync(email);
    
                if (dupeUser != null)
                {
                    return IdentityResult.Failed(new IdentityError() {
                        Code = "DuplicateEmailException", // Wrong practice, lets set some beautiful code values in the future
                        Description = "An existing user with the new email already exists."
                    });
                }
    
                // Perform dupe checks
    
                // Code that runs in SetEmailAsync
                // Adapted from: aspnet/Identity/blob/dev/src/Core/UserManager.cs
                //
                // ThrowIfDisposed();
                // var store = GetEmailStore();
                // if (user == null)
                // {
                //     throw new ArgumentNullException(nameof(user));
                // }
    
                // await store.SetEmailAsync(user, email, CancellationToken);
                // await store.SetEmailConfirmedAsync(user, false, CancellationToken);
                // await UpdateSecurityStampInternal(user);
    
                //return await UpdateUserAsync(user);
    
                return await base.SetEmailAsync(user, email);
            }

This way, we retain .NET Core Identity Code's Integrity while enforcing the uniqueness of the property/properties that we want.

Note that the samples above are for email as of now. Simply do the same and then instead of modifying SetEmailAsync, work on SetPhoneNumberAsync at UserManager.cs.

Veterinary answered 13/6, 2018 at 6:9 Comment(0)
W
8

The easiest way might be to simply search for the phone number in your controller...

bool IsPhoneAlreadyRegistered = _userManager.Users.Any(item => item.PhoneNumber == model.PhoneNumber);
Westernmost answered 21/2, 2019 at 13:54 Comment(2)
This is a quick and easy solution to implement. But will it impact performance if there are more than a million users in the database?Magical
This is not working is same phone number hit with few ms different I'm facing same issues with 1000 users and got two entriesLaoighis
V
3

You can try this, basically enforce it at the Db level first, followed by implementing proper checks at the Manager level.

At DbContext, I declared the indexing and uniqueness of both the username and email properties.

taken from the link

    // ================== Customizing IdentityCore Tables ================== //

            builder.Entity<User>().ToTable("Users").Property(p => p.Id).HasColumnName("Id").ValueGeneratedOnAdd();
            builder.Entity<User>(entity =>
            {
                entity.HasIndex(u => u.UserName).IsUnique();
                entity.HasIndex(u => u.NormalizedUserName).IsUnique();
                entity.HasIndex(u => u.Email).IsUnique();
                entity.HasIndex(u => u.NormalizedEmail).IsUnique();

                entity.Property(u => u.Rating).HasDefaultValue(0).IsRequired();
                entity.HasMany(u => u.UserRoles).WithOne(ur => ur.User)
                    .HasForeignKey(ur => ur.UserId).OnDelete(DeleteBehavior.Restrict);
                entity.HasMany(u => u.UserClaims).WithOne(uc => uc.User)
                    .HasForeignKey(uc => uc.UserId).OnDelete(DeleteBehavior.Restrict);
            });

And for the Manager level code:

    /// <summary>
            /// Sets the <paramref name="email"/> address for a <paramref name="user"/>.
            /// </summary>
            /// <param name="user">The user whose email should be set.</param>
            /// <param name="email">The email to set.</param>
            /// <returns>
            /// The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/>
            /// of the operation.
            /// </returns>
            public override async Task<IdentityResult> SetEmailAsync(User user, string email)
            {
                var dupeUser = await FindByEmailAsync(email);
    
                if (dupeUser != null)
                {
                    return IdentityResult.Failed(new IdentityError() {
                        Code = "DuplicateEmailException", // Wrong practice, lets set some beautiful code values in the future
                        Description = "An existing user with the new email already exists."
                    });
                }
    
                // Perform dupe checks
    
                // Code that runs in SetEmailAsync
                // Adapted from: aspnet/Identity/blob/dev/src/Core/UserManager.cs
                //
                // ThrowIfDisposed();
                // var store = GetEmailStore();
                // if (user == null)
                // {
                //     throw new ArgumentNullException(nameof(user));
                // }
    
                // await store.SetEmailAsync(user, email, CancellationToken);
                // await store.SetEmailConfirmedAsync(user, false, CancellationToken);
                // await UpdateSecurityStampInternal(user);
    
                //return await UpdateUserAsync(user);
    
                return await base.SetEmailAsync(user, email);
            }

This way, we retain .NET Core Identity Code's Integrity while enforcing the uniqueness of the property/properties that we want.

Note that the samples above are for email as of now. Simply do the same and then instead of modifying SetEmailAsync, work on SetPhoneNumberAsync at UserManager.cs.

Veterinary answered 13/6, 2018 at 6:9 Comment(0)
B
0

If you are using .NET 5+ and EF Core 5.0+ take a look at this:

using Microsoft.EntityFrameworkCore;

[Index(nameof(PhoneNumber), IsUnique = true)]
public class ApplicationUser : IdentityUser
{
    //...
}

public class MyDbContext : IdentityDbContext<ApplicationUser>
{
    //...
}

Reference: Entity Framework Core / Create a model / Indexes and constraints

Bigmouth answered 22/2, 2022 at 18:41 Comment(2)
Apparently it's ok, but I tried it and it didn't work!??Catchings
@مهدی I used this approach and it worked. Explore generated migration's codes to find something like this: migrationBuilder.CreateIndex(name: "IX_AspNetUsers_PhoneNumber", schema: "identity", table: "AspNetUsers", column: "PhoneNumber", unique: true, filter: "[PhoneNumber] IS NOT NULL");Bigmouth
H
0
public DbSet<mdlApplicationUser> mdlApplicationUser { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<mdlApplicationUser>()
        .HasIndex(b => b.PhoneNumber)
        .IsUnique();
    base.OnModelCreating(modelBuilder);
}

in Manage/index.cshtml.cs:

if (Input.PhoneNumber != phoneNumber)
    {
        try
        {

            var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);

            if (!setPhoneResult.Succeeded)
            {
                StatusMessage = "Unexpected error when trying to set phone number.";
                return RedirectToPage();
            }
        
        }
        catch (Exception ex)
        {

                string err=ex.InnerException.ToString();
            
                if (err.IndexOf("duplicate key row in object 'dbunk.AspNetUsers' with unique index 'IX_AspNetUsers_PhoneNumber'.") > -1)//Checking part of the error that sql returns
            {
                j = 1;
            
            }
            
        }
    }
    await _signInManager.RefreshSignInAsync(user);
    if(j==1)
    {
        StatusMessage = "Number " + Input.PhoneNumber + " Number 0 has been used by someone else";
    }
    else
    {
    StatusMessage = "Your profile has been updated";
    }
Hummer answered 10/1, 2023 at 6:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.