Search code examples
c#asp.netvb.netasp.net-identity

ASP.NET Identity executes database query on every page request when logged in


I have noticed that my ASP.NET Web Forms application based on Identity 2.2.1 fetches logged in user data from the database on every page request. Is this normal and designed behaviour? For performance reasons, user data might be cached in Session. I have double checked if I have not added extra code in master page which might cause this behaviour. But no, I am using standard Web Form template with no code added to the master page.

SQL statement executed on every page request:

SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Email] AS [Email], 
[Extent1].[EmailConfirmed] AS [EmailConfirmed], 
[Extent1].[PasswordHash] AS [PasswordHash], 
[Extent1].[SecurityStamp] AS [SecurityStamp], 
[Extent1].[PhoneNumber] AS [PhoneNumber], 
[Extent1].[PhoneNumberConfirmed] AS [PhoneNumberConfirmed], 
[Extent1].[TwoFactorEnabled] AS [TwoFactorEnabled], 
[Extent1].[LockoutEndDateUtc] AS [LockoutEndDateUtc], 
[Extent1].[LockoutEnabled] AS [LockoutEnabled], 
[Extent1].[AccessFailedCount] AS [AccessFailedCount], 
[Extent1].[UserName] AS [UserName]
FROM [dbo].[AspNetUsers] AS [Extent1]
WHERE [Extent1].[Id] = @p0

UPDATE 1

As application works in Azure using App Service and Azure SQL, proof for database query behind each page request was Application Insights, as per attached screenshot.

enter image description here

I have started to investigate further and have moved database to local environment. SQL Server Profiler shows there is in fact 10 queries to database on each page request. Those are SELECT to AspNetUsers, AspNetUserClaims, AspNetUserLogins, etc. Some of them are executed twice. This does not depend on master page. Pages not based on master page trigger same 10 queries as those based on one.

I have done few modifications to default Visual Studio template, as per below source code. I have double checked that new project based on same template does not trigger any database queries once user is logged in.

Modifications done:

  • additional fields in ApplicationUser class, added to database table via migrations
  • few configuration parameters
  • email service configuration

Source code:

Global_asax

Public Class Global_asax
Inherits HttpApplication

Sub Application_Start(sender As Object, e As EventArgs)
    ' Fires when the application is started
    RouteConfig.RegisterRoutes(RouteTable.Routes)
    BundleConfig.RegisterBundles(BundleTable.Bundles)
End Sub
End Class

Startup

Partial Public Class Startup

' For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301883
Public Sub ConfigureAuth(app As IAppBuilder)
    'Configure the db context, user manager and signin manager to use a single instance per request
    app.CreatePerOwinContext(AddressOf ApplicationDbContext.Create)
    app.CreatePerOwinContext(Of ApplicationUserManager)(AddressOf ApplicationUserManager.Create)
    app.CreatePerOwinContext(Of ApplicationSignInManager)(AddressOf ApplicationSignInManager.Create)

    ' Enable the application to use a cookie to store information for the signed in user
    app.UseCookieAuthentication(New CookieAuthenticationOptions() With {
        .AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        .Provider = New CookieAuthenticationProvider() With {
            .OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(Of ApplicationUserManager, ApplicationUser)(
                validateInterval:=TimeSpan.FromMinutes(0),
                regenerateIdentity:=Function(manager, user) user.GenerateUserIdentityAsync(manager))},
        .LoginPath = New PathString("/Account/Login"),
        .ExpireTimeSpan = TimeSpan.FromMinutes(20),
        .SlidingExpiration = True})

    ' Use a cookie to temporarily store information about a user logging in with a third party login provider
    'app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie)

    ' Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
    'app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5))

    ' Enables the application to remember the second login verification factor such as phone or email.
    ' Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
    ' This is similar to the RememberMe option when you log in.
    'app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie)

    ' Uncomment the following lines to enable logging in with third party login providers
    'app.UseMicrosoftAccountAuthentication(
    '    clientId:= "",
    '    clientSecret:= "")

    'app.UseTwitterAuthentication(
    '   consumerKey:= "",
    '   consumerSecret:= "")

    'app.UseFacebookAuthentication(
    '   appId:= "",
    '   appSecret:= "")

    'app.UseGoogleAuthentication(New GoogleOAuth2AuthenticationOptions() With {
    '   .ClientId = "",
    '   .ClientSecret = ""})
End Sub
End Class

IdentityConfig.vb

Public Class EmailService
Implements IIdentityMessageService
Public Function SendAsync(message As IdentityMessage) As Task Implements IIdentityMessageService.SendAsync
    ' Plug in your email service here to send an email.
    'Return Task.FromResult(0)
    Dim client As New Net.Mail.SmtpClient(DTAppSettings.SendGrid_SMTPServer, 587)
    Dim credentials As New Net.NetworkCredential(DTAppSettings.SendGrid_Username, DTAppSettings.SendGrid_Password)
    client.Credentials = credentials
    client.EnableSsl = True
    Dim mailmessage As New Net.Mail.MailMessage With {
        .From = New Net.Mail.MailAddress(DTAppSettings.SendGrid_FromAddress, DTAppSettings.SendGrid_FromName),
        .Subject = message.Subject,
        .Body = message.Body,
        .IsBodyHtml = True
    }
    mailmessage.To.Add(message.Destination)
    Return client.SendMailAsync(mailmessage)
End Function
End Class

Public Class SmsService
Implements IIdentityMessageService
Public Function SendAsync(message As IdentityMessage) As Task Implements IIdentityMessageService.SendAsync
    ' Plug in your SMS service here to send a text message.
    Return Task.FromResult(0)
End Function
End Class

' Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application.
Public Class ApplicationUserManager
Inherits UserManager(Of ApplicationUser)
Public Sub New(store As IUserStore(Of ApplicationUser))
    MyBase.New(store)
End Sub

Public Shared Function Create(options As IdentityFactoryOptions(Of ApplicationUserManager), context As IOwinContext) As ApplicationUserManager
    Dim manager = New ApplicationUserManager(New UserStore(Of ApplicationUser)(context.[Get](Of ApplicationDbContext)()))
    ' Configure validation logic for usernames
    manager.UserValidator = New UserValidator(Of ApplicationUser)(manager) With {
      .AllowOnlyAlphanumericUserNames = False,
      .RequireUniqueEmail = True
    }

    ' Configure validation logic for passwords
    manager.PasswordValidator = New PasswordValidator() With {
      .RequiredLength = 6,
      .RequireNonLetterOrDigit = True,
      .RequireDigit = True,
      .RequireLowercase = True,
      .RequireUppercase = True
    }
    ' Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user. 
    ' You can write your own provider and plug in here.
    'manager.RegisterTwoFactorProvider("Phone Code", New PhoneNumberTokenProvider(Of ApplicationUser)() With {
    '  .MessageFormat = "Your security code is {0}"
    '})
    'manager.RegisterTwoFactorProvider("Email Code", New EmailTokenProvider(Of ApplicationUser)() With {
    '  .Subject = "Security Code",
    '  .BodyFormat = "Your security code is {0}"
    '})

    ' Configure user lockout defaults
    manager.UserLockoutEnabledByDefault = True
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5)
    manager.MaxFailedAccessAttemptsBeforeLockout = 5

    manager.EmailService = New EmailService()
    manager.SmsService = New SmsService()
    Dim dataProtectionProvider = options.DataProtectionProvider
    If dataProtectionProvider IsNot Nothing Then
        manager.UserTokenProvider = New DataProtectorTokenProvider(Of ApplicationUser)(dataProtectionProvider.Create("ASP.NET Identity")) With {
            .TokenLifespan = TimeSpan.FromHours(1)
            }
    End If
    Return manager
End Function
End Class

Public Class ApplicationSignInManager
Inherits SignInManager(Of ApplicationUser, String)
Public Sub New(userManager As ApplicationUserManager, authenticationManager As IAuthenticationManager)
    MyBase.New(userManager, authenticationManager)
End Sub

Public Overrides Function CreateUserIdentityAsync(user As ApplicationUser) As Task(Of ClaimsIdentity)
    Return user.GenerateUserIdentityAsync(DirectCast(UserManager, ApplicationUserManager))
End Function

Public Shared Function Create(options As IdentityFactoryOptions(Of ApplicationSignInManager), context As IOwinContext) As ApplicationSignInManager
    Return New ApplicationSignInManager(context.GetUserManager(Of ApplicationUserManager)(), context.Authentication)
End Function
End Class

Solution

  • The problem is related to this bit of code:

    .OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(Of ApplicationUserManager, 
        ApplicationUser)(validateInterval:=TimeSpan.FromMinutes(0),
    

    Try a larger value, such as .FromMinutes(15)

    Since the validateInterval is 0, it's basically re-validating the Identity information on every page load.