Azure / azure-functions-host

The host/runtime that powers Azure Functions
https://functions.azure.com
MIT License
1.94k stars 441 forks source link

non static V2 functions throws error when using AddAuthentication() in functionstartup #4485

Open AnunnakiSelva opened 5 years ago

AnunnakiSelva commented 5 years ago

Is your question related to a specific version? If so, please specify:

Azure function v2 . .net core 2.2

What language does your question apply to? (e.g. C#, JavaScript, Java, All)

C#

Question

Am trying to implement custom token authentication with auth server . So i have used builder.Services.AddAuthenticatio in the FunctionStartUp, Am able to compile it . But while running in the local with the emulator.

When i call Http trigger functions am getting following error .

An unhandled host error has occurred. Microsoft.AspNetCore.Authentication.Core: No authentication handler is registered for the scheme 'WebJobsAuthLevel'. The registered schemes are: BearerIdentityServerAuthenticationJwt, BearerIdentityServerAuthenticationIntrospection, Bearer. Did you forget to call AddAuthentication().AddSomeAuthHandler?.

Can some one help me on this please ???

Does azure functions support middlewares with the latest release ?

hjpsievert commented 5 years ago

@AnunnakiSelva I jumped over to this thread from #4006. I have the same issue and am hoping that we get some resolution here. I was able to get ASP Idenity UserManager injected and working. SignInManager, even though it gets injected, is missing the Context and cannot be used to sign a user in or out. @espray has this issue as well.

espray commented 5 years ago
Azure Functions Core Tools (2.7.1158 Commit hash: f2d2a2816e038165826c7409c6d10c0527e8955b)
Function Runtime Version: 2.0.12438.0

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.2.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.2.3" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.28" />
  </ItemGroup>

SignInManager injection is fine.

This line results in the below error var signInResult = await _signInManager.PasswordSignInAsync(user, "password123456", false, false);

image

Calling builder.Services.AddAuthentication(); results in the below error image

hjpsievert commented 5 years ago

@espray My error without AddAuthenticationis a bit different, see screenshot, but the second error is identical.

SignIn Error I use the same invocation: var result = await _signInManager.PasswordSignInAsync(email, passWord, false, false);

My components are also very similar to yours, looks like the same versions at the package level:

      "frameworks": {
        "netcoreapp2.1": {
          "dependencies": {
            "Microsoft.AspNetCore.Identity": {
              "target": "Package",
              "version": "[2.2.0, )"
            },
            "Microsoft.AspNetCore.Identity.EntityFrameworkCore": {
              "target": "Package",
              "version": "[2.2.0, )"
            },
            "Microsoft.Azure.Functions.Extensions": {
              "target": "Package",
              "version": "[1.0.0, )"
            },
            "Microsoft.EntityFrameworkCore": {
              "target": "Package",
              "version": "[2.2.3, )"
            },
            "Microsoft.EntityFrameworkCore.SqlServer": {
              "target": "Package",
              "version": "[2.2.3, )"
            },
            "Microsoft.NET.Sdk.Functions": {
              "target": "Package",
              "version": "[1.0.28, )"
            },
            "Microsoft.NETCore.App": {
              "suppressParent": "All",
              "target": "Package",
              "version": "[2.1.0, )",
              "autoReferenced": true
            }
          },
          "imports": [
            "net461"
          ],
          "assetTargetFallback": true,
          "warn": true
        }
      }

I did run across this reference which points to a new set of functions for sign-in tied to the HTTPContext. The problem would still be that there is no Authentication Handler available that would actually enable the ASP Identity signin (I was using the TwoFactorCookie version).

espray commented 5 years ago

For the HttpContext null exception Take a dependency on IHttpContextAccessor then set it in your function _httpContextAccessor.HttpContext = req.HttpContext; Then you should get the No authentication handler is registered exception.

using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;

[assembly: FunctionsStartup(typeof(FunctionAppWithIdentity.Startup))]
namespace FunctionAppWithIdentity
{
    public class Startup
        : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddDbContext<ApplicationDbContext>(opt => {
                opt.UseInMemoryDatabase(Guid.NewGuid().ToString());
            });

            builder.Services.AddIdentityCore<ApplicationUser>(opt => 
                {
                    opt.Password.RequireDigit = false;
                    opt.Password.RequireLowercase = false;
                    opt.Password.RequireNonAlphanumeric = false;
                    opt.Password.RequireUppercase = false;
                })
              .AddSignInManager()
              .AddEntityFrameworkStores<ApplicationDbContext>()
              .AddDefaultTokenProviders();
        }
    }
}
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Identity;

namespace FunctionAppWithIdentity
{
    public class Function1
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly SignInManager<TenantApplicationUser> _signInManagerTenant;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public Function1(
            SignInManager<ApplicationUser> signInManager,
            ApplicationDbContext applicationDbContext,
            IHttpContextAccessor httpContextAccessor)
        {
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));

            var identityResult = _signInManager.UserManager.CreateAsync(
                new ApplicationUser()
                {
                    Id = Guid.Parse("eb43edfe-1fc0-4697-98ce-b1c5e8c99328"),
                    UserName = "FooBar",
                    EmailConfirmed = true,
                }, "password123456").GetAwaiter().GetResult();

            applicationDbContext.SaveChanges();
        }

        [FunctionName("Function1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            _httpContextAccessor.HttpContext = req.HttpContext;

            log.LogInformation("C# HTTP trigger function processed a request.");

            var user = await _signInManager.UserManager.FindByNameAsync("FooBar");

            if (user != null)
            {
                var canSignIn = await _signInManager.CanSignInAsync(user);
                var signInResult = await _signInManager.PasswordSignInAsync(user, "password123456", false, true);
            }

            return new OkResult();
        }
    }
}
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;

namespace FunctionAppWithIdentity
{
    public class ApplicationDbContext
        : IdentityUserContext<ApplicationUser, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            // Customize the ASP.NET Identity model and override the defaults if needed.
            // For example, you can rename the ASP.NET Identity table names and more.
            // Add your customizations after calling base.OnModelCreating(builder);
        }
    }
}
using Microsoft.AspNetCore.Identity;
using System;

namespace FunctionAppWithIdentity
{
    public class ApplicationUser
        : IdentityUser<Guid>
    { }
}
hjpsievert commented 5 years ago

@espray I have tried something very much like that, except I did not inject the IHttpContextAccessor, but instead simply set _signInManager.Context = req.HttpContext; which gave me the exact same error as your first one, I have an existing SQL Server database with some test users, so I do not have to create one.

Here my Startup Class (you can see the lines for AddAuthentication commented out; if I uncomment them I get the exact same error as your second one)

using FunEZPDBeta.Data;
using FunEZPDBeta.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace FunEZPDBeta
{
  public class Startup : FunctionsStartup
  {
    public override void Configure(IFunctionsHostBuilder builder)
    {
      builder.Services.AddDbContext<ApplicationDbContext>(options =>
      {
        options.UseSqlServer(System.Environment.GetEnvironmentVariable("identity_connection"));
      });
      builder.Services.AddHttpContextAccessor();
      builder.Services.AddIdentityCore<ApplicationUser>()
        .AddSignInManager()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        ;
      //builder.Services.AddAuthentication(IdentityConstants.TwoFactorUserIdScheme)
      //  .AddCookie(IdentityConstants.TwoFactorUserIdScheme);
      builder.Services.Configure<IdentityOptions>(options =>
      {
        options.SignIn.RequireConfirmedEmail = true;
        options.Password.RequiredLength = 8;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
        options.Lockout.MaxFailedAccessAttempts = 10;
      });
    }
  }
}

And here my Function Class (you can see I tried your httpContextAccessorinjection as well, now commented out, either way I get the same error as your first one now)

using FunEZPDBeta.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading.Tasks;
using static FunEZPDBeta.Services.Utils;

[assembly: FunctionsStartup(typeof(FunEZPDBeta.Startup))]
namespace FunEZPDBeta
{
  public class LoginUser
  {
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    //private readonly IHttpContextAccessor _httpContextAccessor;
    public LoginUser(
      UserManager<ApplicationUser> userManager, 
      SignInManager<ApplicationUser> signInManager, 
      IHttpContextAccessor httpContextAccessor)
    {
      _userManager = userManager;
      _signInManager = signInManager;
      //_httpContextAccessor = httpContextAccessor;
    }

    [FunctionName("LoginUser")]
    public async Task<APIReturn> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ClaimsPrincipal principal,
    ILogger log)
    {
      log.LogInformation("C# HTTP trigger function processed a request.");

      string email = req.Query["email"];
      string password = req.Query["password"];

      //_httpContextAccessor.HttpContext = req.HttpContext;
      _signInManager.Context = req.HttpContext;
      APIReturn myReturn = await InternalLoginUser(email, password);
      return myReturn;
    }

    private async Task<APIReturn> InternalLoginUser(string email, string passWord)
    {
      APIReturn ret;
      ret.success = false;
      ret.payLoad = "User Login";
      ret.code = 0;
      ret.err = null;

      if (email == null)
      {
        ret.err = "Missing email";
        return ret;
      }

      var user = await _userManager.FindByNameAsync(email);
      if (user != null)
      {
        if (!await _userManager.IsEmailConfirmedAsync(user))
        {
          ret.err = "User must have valid email to login";
          return ret;
        }
      }

      var result = await _signInManager.PasswordSignInAsync(email, passWord, false, false);
      if (result.Succeeded)
      {
        ret.success = true;
        ret.payLoad = "Login succeeded";
        return ret;
      }
      return ret;
    }
  }
}
espray commented 5 years ago

I think I got it. I used the Dynamic Schemes sample for adding Auth Schema at runtime. Then added the missing schema AddIdentity() would have added. I would assume to get 2FA working, adding the 2FA Auth schema and registering 2FA services, might do the trick. Now that I understand this more, I wonder if I could get IdentityServer or OpenIddict working...

https://github.com/aspnet/AuthSamples/blob/master/samples/DynamicSchemes/Controllers/AuthController.cs

https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs

https://github.com/aspnet/AspNetCore/blob/555b506a97a188583df2872913cc40b59e563da6/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs

https://github.com/aspnet/AspNetCore/blob/42b3fada3144fed68e5b20821d99c3684f1c3543/src/Security/Authentication/Cookies/src/CookieExtensions.cs

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System;

[assembly: FunctionsStartup(typeof(FunctionAppWithIdentity.Startup))]
namespace FunctionAppWithIdentity
{
    public class Startup
        : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddDbContext<ApplicationDbContext>(options => {
                options.UseInMemoryDatabase(Guid.NewGuid().ToString());
            });

            builder.Services.AddIdentityCore<ApplicationUser>(opt => 
                {
                    opt.Password.RequireDigit = false;
                    opt.Password.RequireLowercase = false;
                    opt.Password.RequireNonAlphanumeric = false;
                    opt.Password.RequireUppercase = false;
                })
              .AddSignInManager()
              .AddEntityFrameworkStores<ApplicationDbContext>()
              .AddDefaultTokenProviders();

            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
            builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme).Validate(o => o.Cookie.Expiration == null, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");
        }
    }
}
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace FunctionAppWithIdentity
{
    public class Function1
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

        public Function1(
            SignInManager<ApplicationUser> signInManager,
            ApplicationDbContext applicationDbContext,
            IHttpContextAccessor httpContextAccessor,
            IAuthenticationSchemeProvider authenticationSchemeProvider)
        {
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
            _authenticationSchemeProvider = authenticationSchemeProvider ?? throw new ArgumentNullException(nameof(authenticationSchemeProvider));

            if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.ApplicationScheme).GetAwaiter().GetResult() == null)
            {
                _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.ApplicationScheme, IdentityConstants.ApplicationScheme, typeof(CookieAuthenticationHandler)));
            }

            var identityResult = _signInManager.UserManager.CreateAsync(
                new ApplicationUser()
                {
                    Id = Guid.Parse("eb43edfe-1fc0-4697-98ce-b1c5e8c99328"),
                    UserName = "FooBar",
                    EmailConfirmed = true,
                }, "password123456").GetAwaiter().GetResult();

            applicationDbContext.SaveChanges();
        }

        [FunctionName("Function1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            _httpContextAccessor.HttpContext = req.HttpContext;

            log.LogInformation("C# HTTP trigger function processed a request.");

            var user = await _signInManager.UserManager.FindByNameAsync("FooBar");

            if (user != null)
            {
                var canSignIn = await _signInManager.CanSignInAsync(user);
                var signInResult = await _signInManager.PasswordSignInAsync(user, "password123456", false, true);
            }

            return new OkResult();
        }
    }
}
hjpsievert commented 5 years ago

@espray Way to go! I tried your approach and at first it failed with this error:

Authentication Error 2FA Enabled

Then I remembered that my Identity Database has 2FA enabled for this user. I share this database with the MVC Web API application I mentioned earlier. After I turned off 2FA at the user level, the login succeeded.

I did try to change all references to TwoFactorUserIdScheme and turned 2FA back on. This resulted in the following error: Error with 2fa UserIdScheme It correctly identifies TwoFactorUserIdScheme as a registered scheme, but then indicates that there is no handler registered for TwoFactorRememberMeScheme. If I switch to that scheme, I get the opposite error where it correctly shows TwoFactorRememberMeScheme as registered and complains about a missing handler for TwoFactorUserIdScheme.

Close, but not quite there...

espray commented 5 years ago

I dont have 2FA project to test with. You will need to diff the AddIdentity() and AddIdentityCore(), then add the missing services, configuring the Options & adding Auth Schema. Below are the Options & Auth Schema.

            builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
                .Validate(o => 
                {
                    o.LoginPath = new PathString("/Account/Login");
                    o.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
                    };

                    return o.Cookie.Expiration == null;
                } , "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");

            builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ExternalScheme)
                .Validate(o =>
                {
                    o.Cookie.Name = IdentityConstants.ExternalScheme;
                    o.ExpireTimeSpan = TimeSpan.FromMinutes(5);

                    return o.Cookie.Expiration == null;
                }, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");

            builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.TwoFactorRememberMeScheme)
                .Validate(o =>
                {
                    o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
                    o.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
                    };

                    return o.Cookie.Expiration == null;
                }, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");

            builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.TwoFactorUserIdScheme)
                .Validate(o =>
                {
                    o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
                    o.ExpireTimeSpan = TimeSpan.FromMinutes(5);

                    return o.Cookie.Expiration == null;
                }, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");
            if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.ApplicationScheme).GetAwaiter().GetResult() == null)
            {
                _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.ApplicationScheme, IdentityConstants.ApplicationScheme, typeof(CookieAuthenticationHandler)));
            }

            if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.ExternalScheme).GetAwaiter().GetResult() == null)
            {
                _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.ExternalScheme, IdentityConstants.ExternalScheme, typeof(CookieAuthenticationHandler)));
            }

            if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.TwoFactorRememberMeScheme).GetAwaiter().GetResult() == null)
            {
                _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.TwoFactorRememberMeScheme, IdentityConstants.TwoFactorRememberMeScheme, typeof(CookieAuthenticationHandler)));
            }

            if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.TwoFactorUserIdScheme).GetAwaiter().GetResult() == null)
            {
                _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.TwoFactorUserIdScheme, IdentityConstants.TwoFactorUserIdScheme, typeof(CookieAuthenticationHandler)));
            }
hjpsievert commented 5 years ago

@espray Thank you for trying to help me getting this figured out. I think I followed all of your recommendations, but somehow I do not get past the scheme being registered properly, but the authentication handler still missing:

Error with 2fa with diff services

My modified Startup with the additional services from AddIdentity(), and the cookie configuration. I tried both, adding the SignInManager underAddIdentityCore() where I get an error if I specify the <ApplicationUser> type and adding it as a separate service with the type, but it does not make any difference.

using FunEZPDBeta.Data;
using FunEZPDBeta.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System;

namespace FunEZPDBeta
{
  public class Startup : FunctionsStartup
  {
    public override void Configure(IFunctionsHostBuilder builder)
    {
      builder.Services.AddDbContext<ApplicationDbContext>(options =>
      {
        options.UseSqlServer(System.Environment.GetEnvironmentVariable("identity_connection"));
      });
      builder.Services.AddHttpContextAccessor();
      builder.Services.AddIdentityCore<ApplicationUser>(options =>
     {
       options.Password.RequireDigit = false;
       options.Password.RequireLowercase = false;
       options.Password.RequireNonAlphanumeric = false;
       options.Password.RequireUppercase = false;
     })
        .AddSignInManager()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
      builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.TwoFactorUserIdScheme)
      .Validate(options =>
        {
          options.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
          options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

          return options.Cookie.Expiration == null;
        }, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");

      builder.Services.TryAddScoped<IRoleValidator<IdentityRole>, RoleValidator<IdentityRole>>();
      builder.Services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<ApplicationUser>>();
      builder.Services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<ApplicationUser>>();
      //builder.Services.TryAddScoped<SignInManager<ApplicationUser>>();
      builder.Services.TryAddScoped<RoleManager<IdentityRole>>();

      builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
      builder.Services.Configure<IdentityOptions>(options =>
      {
        options.SignIn.RequireConfirmedEmail = true;
        options.Password.RequiredLength = 8;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
        options.Lockout.MaxFailedAccessAttempts = 10;
      });
    }
  }
}

And here the Login Function with the TwoFactorUserIdSchemescheme addition

using FunEZPDBeta.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading.Tasks;
using static FunEZPDBeta.Services.Utils;

[assembly: FunctionsStartup(typeof(FunEZPDBeta.Startup))]
namespace FunEZPDBeta
{
  public class LoginUser
  {
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public LoginUser(
      UserManager<ApplicationUser> userManager,
      SignInManager<ApplicationUser> signInManager,
      IHttpContextAccessor httpContextAccessor,
      IAuthenticationSchemeProvider authenticationSchemeProvider
      )
    {
      _userManager = userManager;
      _signInManager = signInManager;
      _httpContextAccessor = httpContextAccessor;
      _authenticationSchemeProvider = authenticationSchemeProvider;

      if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.TwoFactorUserIdScheme).GetAwaiter().GetResult() == null)
      {
        _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.TwoFactorUserIdScheme, IdentityConstants.TwoFactorUserIdScheme, typeof(CookieAuthenticationHandler)));
      }
    }

    [FunctionName("LoginUser")]
    public async Task<APIReturn> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ClaimsPrincipal principal,
    ILogger log)
    {
      log.LogInformation("C# HTTP trigger function processed a request.");

      string email = req.Query["email"];
      string password = req.Query["password"];

      _httpContextAccessor.HttpContext = req.HttpContext;
      _signInManager.Context = req.HttpContext;
      APIReturn myReturn = await InternalLoginUser(email, password);
      return myReturn;
    }

    private async Task<APIReturn> InternalLoginUser(string email, string passWord)
    {
      APIReturn ret;
      ret.success = false;
      ret.payLoad = "User Login";
      ret.code = 0;
      ret.err = null;

      if (email == null)
      {
        ret.err = "Missing email";
        return ret;
      }
      var user = await _userManager.FindByNameAsync(email);
      if (user != null)
      {
        if (!await _userManager.IsEmailConfirmedAsync(user))
        {
          ret.err = "User must have valid email to login";
          return ret;
        }
      }
      var result = await _signInManager.PasswordSignInAsync(email, passWord, false, false);
      if (result.Succeeded)
      {
        ret.success = true;
        ret.payLoad = "Login succeeded";
        return ret;
      }
      return ret;
    }
  }
}

I may let this sit for a bit, my head is spinning at this point from perusing too many source code snippets. I need to work on some of the data analysis aspects of my app, maybe someone from the Azure Functions team can shed some light on this. Again, many thanks for your help and suggestions!

AnunnakiSelva commented 5 years ago

@hjpsievert @espray I came across this -->

builder.Services.AddSingleton(new ClientCredentialsTokenRequest { Address = $"connect/token", ClientId = "ClientId", ClientSecret = "ClientSecret", Scope = "Scopes" });

        builder.Services.AddHttpClient<IIdentityServerClient, IdentityServerClient>(client =>
        {
            client.BaseAddress = new Uri("BaseAddress");
        });

        builder.Services.AddTransient<BearerTokenHandler>();

        builder.Services.AddHttpClient(Constants.PPDClient)
            .ConfigureHttpClient((s, c) => ConfigureApiClient("BaseAddress", c))
            .AddHttpMessageHandler<BearerTokenHandler>();
hjpsievert commented 5 years ago

@espray @AnunnakiSelva Finally got it to work. It turns out that all cookie authentication entries were needed. When I looked into SignInManager source code, it became clear that depending on the path through the sign-in process, any of the cookie schemes might be invoked. So here is my code for Startup and LoginUser.

Ignore the SMS/Twilio stuff, I am still working on that. It will send a text with the code, but I need to allow for email as well and the binding approach only allows for one return, right now the Twilio message.

Startup

using FunEZPDBeta.Data;
using FunEZPDBeta.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System;

namespace FunEZPDBeta
{
  public class Startup : FunctionsStartup
  {
    public override void Configure(IFunctionsHostBuilder builder)
    {
      builder.Services.AddDbContext<ApplicationDbContext>(options =>
      {
        options.UseSqlServer(System.Environment.GetEnvironmentVariable("identity_connection"));
      });
      builder.Services.AddIdentityCore<ApplicationUser>(options =>
     {
       options.Password.RequireDigit = false;
       options.Password.RequireLowercase = false;
       options.Password.RequireNonAlphanumeric = false;
       options.Password.RequireUppercase = false;
       options.SignIn.RequireConfirmedEmail = true;
       options.Password.RequiredLength = 8;
       options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
       options.Lockout.MaxFailedAccessAttempts = 10;
       options.User.RequireUniqueEmail = true;
     })
        .AddSignInManager()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

      builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
    .Validate(options =>
    {
      options.LoginPath = new PathString("/Account/Login");
      options.Events = new CookieAuthenticationEvents
      {
        OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
      };

      return options.Cookie.Expiration == null;
    }, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");

      builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.TwoFactorRememberMeScheme)
          .Validate(options =>
          {
            options.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
            options.Events = new CookieAuthenticationEvents
            {
              OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
            };

            return options.Cookie.Expiration == null;
          }, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");

      builder.Services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.TwoFactorUserIdScheme)
          .Validate(options =>
          {
            options.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
            options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

            return options.Cookie.Expiration == null;
          }, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");

      builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
    }
  }
}

LoginUser

using FunEZPDBeta.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using static FunEZPDBeta.Services.Utils;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
using Twilio;

[assembly: FunctionsStartup(typeof(FunEZPDBeta.Startup))]
namespace FunEZPDBeta
{
  public class LoginUser
  {
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public LoginUser(
      UserManager<ApplicationUser> userManager,
      SignInManager<ApplicationUser> signInManager,
      IAuthenticationSchemeProvider authenticationSchemeProvider
      )
    {
      _userManager = userManager;
      _signInManager = signInManager;
      _authenticationSchemeProvider = authenticationSchemeProvider;

      if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.ApplicationScheme).GetAwaiter().GetResult() == null)
      {
        _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.ApplicationScheme, IdentityConstants.ApplicationScheme, typeof(CookieAuthenticationHandler)));
      }
      if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.TwoFactorUserIdScheme).GetAwaiter().GetResult() == null)
      {
        _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.TwoFactorUserIdScheme, IdentityConstants.TwoFactorUserIdScheme, typeof(CookieAuthenticationHandler)));
      }
      if (_authenticationSchemeProvider.GetSchemeAsync(IdentityConstants.TwoFactorRememberMeScheme).GetAwaiter().GetResult() == null)
      {
        _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(IdentityConstants.TwoFactorRememberMeScheme, IdentityConstants.TwoFactorRememberMeScheme, typeof(CookieAuthenticationHandler)));
      }
    }

    [FunctionName("LoginUser")]
    [return: TwilioSms(AccountSidSetting = "Twilio_SID", AuthTokenSetting = "Twilio_AuthToken", From = "Twilio_FromNumber")]
    public async Task<CreateMessageOptions> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
    {
      log.LogInformation("C# HTTP trigger function processed a request.");

      string email = req.Query["email"];
      string password = req.Query["password"];
      string provider = req.Query["provider"];
      string phoneNumber = req.Query["phoneNumber"];

      _signInManager.Context = req.HttpContext;
      APIReturn myReturn = await InternalLoginUser(email, password, provider);

      TwilioClient.Init(Environment.GetEnvironmentVariable("Twilio_SID"), Environment.GetEnvironmentVariable("Twilio_AuthToken"));
      var fromNumber = new PhoneNumber(Environment.GetEnvironmentVariable("Twilio_FromNumber"));
      var message = new CreateMessageOptions(new PhoneNumber(phoneNumber))
      {
        Body = myReturn.payLoad.ToString(),
        PathAccountSid = Environment.GetEnvironmentVariable("Twilio_SID"),
        From = fromNumber
      };

      return message;
    }

    private async Task<APIReturn> InternalLoginUser(string email, string passWord, string provider)
    {

      APIReturn ret;
      ret.success = false;
      ret.payLoad = "User Login";
      ret.code = 0;
      ret.err = null;

      try
      {
        if (email == null)
        {
          ret.err = "Missing email";
          return ret;
        }

        var user = await _userManager.FindByNameAsync(email);
        if (user != null)
        {
          if (!await _userManager.IsEmailConfirmedAsync(user))
          {
            ret.err = "User must have valid email to login";
            return ret;
          }
        }
        var result = await _signInManager.PasswordSignInAsync(email, passWord, false, false);
        if (result.Succeeded)
        {
          ret.success = true;
          ret.payLoad = "Login succeeded";
          return ret;
        }

        if (result.RequiresTwoFactor)
        {
          if (provider == null)
          {
            var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user);
            if (userFactors.Contains("Phone"))
            {
              provider = "Phone";
            }
            else
            {
              provider = "Email";
            }
          }
          var code = await _userManager.GenerateTwoFactorTokenAsync(user, provider);
          if (string.IsNullOrWhiteSpace(code))
          {
            ret.err = "Error generating validation code";
            return ret;
          }
          var message = "EZPartD: Your " + provider + " Login Validation Code is " + code;
          ret.success = true;
          ret.payLoad = message;
          ret.code = 1;
          return ret;
        }
      }
      catch (Exception e)
      {
        ret.err = e;
        ret.code = -1;
        return ret;
      }
      return ret;
    }
  }
}
espray commented 5 years ago

@jeffhollan not sure if you have been watching this issue. Maybe an AddAuthenticationScheme() could be surfaced through 'Microsoft.Azure.Functions.Extensions.DependencyInjection.IFunctionsHostBuilder'?

acbdataminds commented 5 years ago

Through UserManager you can do the following

var usr = await _userManager.FindByEmailAsync("my@email.com"); var result = await _userManager.CheckPasswordAsync(usr, "xxxPasswordxxx");

Return token or something if result is true

espray commented 4 years ago

@jeffhollan @fabiocav Is it possible to surface a AddAuthenticationScheme() through Microsoft.Azure.Functions.Extensions.DependencyInjection.IFunctionsHostBuilder so AuthenticationScheme can be added? OR could AddAuthentication() be called before FunctionsStartup.Configure()