Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
AnunnakiSelva opened this issue May 22, 2019 · 14 comments
Labels
Milestone

Comments

@AnunnakiSelva
Copy link

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
Copy link

@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
Copy link

espray commented May 27, 2019

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
Copy link

@espray My error without AddAuthentication is 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
Copy link

espray commented May 27, 2019

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
Copy link

@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 httpContextAccessor injection 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
Copy link

espray commented May 27, 2019

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
Copy link

@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
Copy link

espray commented May 27, 2019

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
Copy link

@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 under AddIdentityCore() 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 TwoFactorUserIdScheme scheme 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
Copy link
Author

@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
Copy link

@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
Copy link

espray commented Jun 1, 2019

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

@acbdataminds
Copy link

acbdataminds commented Oct 5, 2019

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
Copy link

espray commented Dec 17, 2019

@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()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants