Open AnunnakiSelva opened 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.
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);
Calling builder.Services.AddAuthentication();
results in the below error
@espray My error without AddAuthentication
is a bit different, see screenshot, but the second error is identical.
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).
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>
{ }
}
@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;
}
}
}
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...
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();
}
}
}
@espray Way to go! I tried your approach and at first it failed with this error:
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:
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...
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)));
}
@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:
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 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!
@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>();
@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;
}
}
}
@jeffhollan not sure if you have been watching this issue. Maybe an AddAuthenticationScheme()
could be surfaced through 'Microsoft.Azure.Functions.Extensions.DependencyInjection.IFunctionsHostBuilder'?
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
@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()
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 ?