I have been struggling on this issue for weeks now.
I have an app where i have configured owin backend with web api and autofac DI with background handfire jobs. I have alsmost looked at every question on Stackoveflow regarding this but nothing seems to work.
My app regarding OWIN/Hangfire/WebAPI all seems to work okay. Until it comes to SignalR push messages.
If i call any notification hub endpoint from js client push messages go okay and i can receive push messages on any other connected client. But when i wan to send message from my api controller or hangfire job it never reaches to any client.
Startup.cs
public void Configuration(IAppBuilder app)
{
//var signalRHelper = new SignalRHelper(GlobalHost.ConnectionManager.GetHubContext<NotificationHub>());
var constants = new Constants();
constants.Set(ConstantTypes.AllyHrNoReplyEmailAddress, Util.Constants.AllyHrNoReplyEmailAddress);
constants.Set(ConstantTypes.SendGridKey, Util.Constants.SendGridKey);
constants.Set(ConstantTypes.EncryptionKey, Util.Constants.EncryptionKey);
constants.Set(ConstantTypes.ApiUrl, Util.Constants.ApiUrl);
constants.Set(ConstantTypes.RootFolder, Util.Constants.RootFolder);
constants.Set(ConstantTypes.FrontEndUrl, Util.Constants.FrontEndUrl);
GlobalConfiguration.Configuration
.UseSqlServerStorage("AllyHrDb");
var config = System.Web.Http.GlobalConfiguration.Configuration;
var builder = new ContainerBuilder();
var jobBuilder = new ContainerBuilder();
var signalRBuilder = new ContainerBuilder();
var hubConfig = new HubConfiguration();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()).PropertiesAutowired();
builder.Register(x => constants);
builder.RegisterModule(new ServiceModule());
jobBuilder.Register(x => constants);
jobBuilder.RegisterModule(new HangfireServiceModule());
signalRBuilder.RegisterModule(new SignalRServiceModule());
signalRBuilder.Register(x => constants);
signalRBuilder.RegisterType<AutofacDependencyResolver>().As<IDependencyResolver>().SingleInstance();
signalRBuilder.RegisterType<ConnectionManager>().As<IConnectionManager>().ExternallyOwned().SingleInstance();
signalRBuilder.RegisterType<NotificationHub>().ExternallyOwned().SingleInstance();
signalRBuilder.RegisterType<SignalRHelper>().PropertiesAutowired().ExternallyOwned().SingleInstance();
signalRBuilder.Register(context => context.Resolve<IDependencyResolver>().Resolve<IConnectionManager>().GetHubContext<NotificationHub, INotificationHub>()).ExternallyOwned().SingleInstance();
var hubContainer = signalRBuilder.Build();
builder.RegisterInstance(hubContainer.Resolve<IConnectionManager>());
builder.RegisterInstance(hubContainer.Resolve<IHubContext<INotificationHub>>());
builder.RegisterInstance(hubContainer.Resolve<NotificationHub>());
builder.RegisterInstance(hubContainer.Resolve<SignalRHelper>());
jobBuilder.RegisterInstance(hubContainer.Resolve<IHubContext<INotificationHub>>());
jobBuilder.RegisterInstance(hubContainer.Resolve<NotificationHub>());
jobBuilder.RegisterInstance(hubContainer.Resolve<SignalRHelper>());
var container = builder.Build();
var jobContainer = jobBuilder.Build();
var idProvider = new SignalRCustomUserIdProvider();
hubConfig.Resolver = new AutofacDependencyResolver(hubContainer);
hubConfig.Resolver.Register(typeof(IUserIdProvider), () => idProvider);
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerProvider()
});
map.RunSignalR(hubConfig);
});
GlobalConfiguration.Configuration.UseAutofacActivator(jobContainer);
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseHangfireServer();
app.UseHangfireDashboard();
ConfigureAuth(app);
app.UseWebApi(config);
}
I had to use different container because i have db set to InstancePerRequest scope.
All my services are being resolved in notification hub class, no problems there. The only issues is when i try and send message from hangfire service or even from api controller using hub context it never reaches to any client.
NotificationHub.cs
public interface INotificationHub
{
/// <summary>
///
/// </summary>
void pushNotification(string message);
/// <summary>
///
/// </summary>
/// <param name="model"></param>
void getNotification(object model);
void getMessage(object model);
}
/// <summary>
/// Notification Hub
/// </summary>
[HubName("NotificationHub")]
[Authorize]
public class NotificationHub : Hub<INotificationHub>
{
/// <summary>
///
/// </summary>
public static IHubContext<INotificationHub> GlobalContext { get; private set; }
private readonly IChatMessagingService _chatMessagingService;
private readonly IUserService _userService;
private Guid LoggedInUserId
{
get
{
var claims = ((ClaimsIdentity)Context.User.Identity).Claims.ToArray();
var userIdClaim = claims.FirstOrDefault(x => x.Type.Equals("UserId"));
if (userIdClaim == null) return Guid.Empty;
return Guid.Parse(userIdClaim.Value);
}
}
/// <summary>
/// Consructor
/// </summary>
/// <param name="lifetimeScope"></param>
/// <param name="context"></param>
public NotificationHub(ILifetimeScope lifetimeScope, IHubContext<INotificationHub> context)
{
GlobalContext = context;
try
{
var childScope = lifetimeScope.BeginLifetimeScope();
_chatMessagingService = childScope.Resolve<IChatMessagingService>();
_userService = childScope.Resolve<IUserService>();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Notifications
/// </summary>
public void Notifications()
{
Clients.All.pushNotification("AllyHr" + LoggedInUserId);
}
/// <summary>
/// Send Message
/// </summary>
/// <param name="model"></param>
public void SendMessage(SendChatMessageBindingModel model)
{
var chatMessage = _chatMessagingService.SendMessageToGroup(LoggedInUserId, model.GroupId, model.Message);
var recipientIds = _chatMessagingService.GetChatMembersByGroupId(LoggedInUserId, model.GroupId);
var stringUserIds = new List<string>();
var chatGroup = _chatMessagingService.GetChatGroupById(model.GroupId);
foreach (var recipientId in recipientIds)
{
stringUserIds.Add(recipientId.ToString());
}
Clients.Users(stringUserIds).getNotification(new
{
message = "A new Message is Recieved in Chat Group: " + chatGroup.Name,
groupId = chatGroup.Id
});
var chatMessageVm = chatMessage.Map<ChatMessage, ChatMessageViewModel>();
chatMessageVm.Sender = _userService.Get(chatMessageVm.SenderId).Map<User, UserViewModel>();
stringUserIds.Add(LoggedInUserId.ToString());
Clients.Users(stringUserIds).getMessage(chatMessageVm);
}
}
signalRhelper.cs use to call from api or from Hangfire services
public class SignalRHelper
{
public IConnectionManager ConnectionManager { get; set; }
public IHubContext<INotificationHub> HubContext { get; set; }
/// <summary>
/// Send Notifications to Users
/// </summary>
/// <param name="message"></param>
/// <param name="userIds"></param>
public void GetNotification(object message, IList<string> userIds)
{
HubContext.Clients.Users(userIds).getNotification(message);
}
/// <summary>
/// Get LoggedInUser Id for SignalR
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public static Guid GetLoggedInUserId(IPrincipal user)
{
var claim = GetLoggedinUserClaim(user);
if (claim == null) return Guid.Empty;
return Guid.Parse(claim.Value);
}
private static Claim GetLoggedinUserClaim(IPrincipal user)
{
var claim = ((ClaimsIdentity)user.Identity).Claims.ToArray();
return claim.FirstOrDefault(x => x.Type.Equals("UserId"));
}
}
Could this be related to Autofac creating a new lifetimescope for your call, but you were expecting to continue using the existing scope? Maybe check your autofac registrations for singleinstance / instanceperlifetimescope
Just saying, but have you registered any static classes? They can keep your scope alive for far too long.
I see you're using multiple containerbuilders - that's not something we do over here, we have one 'massive' containerbuilder for each app. I'm curious why you're doing that? To satisfy my curiosity, could you try using a single containerbuilder and registering everything on that single builder? (Although it looks like this is a pattern for SignalR and autofac)
The documentation says: " a common error in OWIN integration is the use of GlobalHost."
It looks like you're doing exactly that.
Related
I have the following code for a MQTT Subscriber in a Background Task:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using PickByLight.BackgroundTask.Models;
using PickByLight.Database.Wrapper.Interfaces;
using PickByLight.Logic;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;
namespace PickByLight.BackgroundTask
{
/// <summary>
/// Hosted MQTT Background Service
/// </summary>
public class HostedMQTTService : IHostedService, IDisposable
{
private readonly Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
/// <summary>
/// MQTT Client
/// </summary>
private MqttClient MqttClient { get; set; }
/// <summary>
/// Name of the Pick by Light
/// </summary>
private string PickByLight_Name { get; set; }
/// <summary>
/// MQTT is activated
/// </summary>
private bool MqttIsActive { get; set; }
/// <summary>
/// IP Adress of the MQTT URL
/// </summary>
private string MqttURL { get; set; }
/// <summary>
/// Storage Process for an material
/// </summary>
private MaterialStorageProcess StorageProcess { get; set; }
/// <summary>
/// Service Scope Factory
/// </summary>
private IServiceScopeFactory ServiceScopeFactory { get; set; }
/// <summary>
/// Configuration
/// </summary>
private IConfiguration Configuration { get; set; }
/// <summary>
/// Logger
/// </summary>
private readonly ILogger<HostedMQTTService> _logger;
/// <summary>
/// Constructor
/// </summary>
/// <param name="configuration"></param>
public HostedMQTTService(IConfiguration configuration, ILogger<HostedMQTTService> logger, IServiceScopeFactory serviceScopeFactory)
{
this.PickByLight_Name = configuration.GetValue<string>("PickByLight_Name");
this.MqttURL = configuration.GetValue<string>("MQTTUrl");
this.MqttIsActive = configuration.GetValue<bool>("MQTTConnection");
this.ServiceScopeFactory = serviceScopeFactory;
this.Configuration = configuration;
this._logger = logger;
}
/// <summary>
/// Start the Task of the Background Service
/// </summary>
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Background-Service started...");
while (true)
{
try
{
//No Object is created
if (this.MqttClient == null)
{
_logger.LogInformation("Try to establishe new MQTT Client");
this.MqttClient = CreateNewMqttConnection();
}
else if (this.MqttClient.IsConnected == false)
{
_logger.LogInformation("MQTT Client is disconnected... Try to reconnect!");
this.MqttClient = CreateNewMqttConnection();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Ein schwerwiegender Fehler im MQTT Background-Service ist aufgetreten.");
}
}
}
/// <summary>
/// Prints out all received messages
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Mqtt_Message_Received(object sender, MqttMsgPublishEventArgs e)
{
try
{
var agcMessage = Encoding.UTF8.GetString(e.Message);
_logger.LogInformation("Topic: " + e.Topic + " | Nachricht: " + agcMessage + " | QOS: " + e.QosLevel);
var resultString = Encoding.UTF8.GetString(e.Message);
MqttReadTopicClass mqttContent = JsonConvert.DeserializeObject<MqttReadTopicClass>(resultString);
using (var scope = this.ServiceScopeFactory.CreateScope())
{
var storageConfigurationManager = scope.ServiceProvider.GetService<IStorageConfigurationManager>();
var storageElementManager = scope.ServiceProvider.GetService<IStorageElementManager>();
this.StorageProcess = new MaterialStorageProcess(storageConfigurationManager, storageElementManager, this.Configuration);
StorageProcess.Remove(mqttContent.storageLocation);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Schwerwiegender Fehler beim Lesen von MQTT Nachrichten");
}
}
/// <summary>
/// Create new MQTT connection if connection is lost or doesn't exist
/// </summary>
private MqttClient CreateNewMqttConnection()
{
_logger.LogInformation("Create MQTT Client");
MqttClient client = new MqttClient(this.MqttURL, 32005, false, null, null, MqttSslProtocols.None);
string clientId = Guid.NewGuid().ToString();
client.MqttMsgPublishReceived += Mqtt_Message_Received;
client.Connect(clientId);
client.Subscribe(new string[] { "buttonpress_sepioo_pdi/" + this.PickByLight_Name }, new byte[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
_logger.LogInformation("MQTT Client created");
return client;
}
/// <summary>
/// Stop the Task of the Background Service
/// </summary>
public async Task StopAsync(CancellationToken cancellationToken)
{
//Stop called without start
if (_executingTask == null)
{
return;
}
try
{
//Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
//wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
/// <summary>
/// Dispose the Background Service
/// </summary>
public void Dispose()
{
_stoppingCts.Cancel();
}
}
}
In my startup.cs File i am doing the following:
//Register Background Task
services.AddHostedService<HostedMQTTService>();
The problem is, that it seems to me that the hosted service is blocking the user-interface/webserver threads because i can not access the url of the .net 6 mvc application.
Could you give me a hint or a solution to this problem?
Thanks.
You will need to change your StartAsync-method to something like this:
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.Run(() =>
{
_logger.LogInformation("Background-Service started...");
while (!cancellationToken.IsCancellationRequested)
{
try
{
//No Object is created
if (this.MqttClient == null)
{
_logger.LogInformation("Try to establish new MQTT Client");
this.MqttClient = CreateNewMqttConnection();
}
else if (this.MqttClient.IsConnected == false)
{
_logger.LogInformation("MQTT Client is disconnected... Try to reconnect!");
this.MqttClient = CreateNewMqttConnection();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Ein schwerwiegender Fehler im MQTT Background-Service ist aufgetreten.");
}
}
});
}
You are blocking the process, because your hosted service never starts.
Remove the while(true) loop from the Start method.
Ensure base.StartAsync(cancellationToken); is always called.
Edit: I saw you implement the IHostedService interface. Try to inherits from the BackgroundService class
I have tried to migration my project from v2 to v3 and I think i have updated everything, but for some reason i can login fine and i get an access_token returned, but this access_token fails to authorize a request. Not sure why, but if someone can see anything obvious that would be great. My application is a web Api with a angular2 web application for the client.
In v2 i was using JWT, but i understand that this is the default in v3 so i may have some code that needs to removed for this to work.
To restrict a controllers methods or a single method, i am using [Authorize] attribute.
Extension method to be called in Startup.cs for adding Authentication, done to keep Startup.cs file from getting to big and keeps things in one place
/// <summary>
/// Add authentication
/// </summary>
/// <param name="services"></param>
/// <param name="appSettings"></param>
public static void AddAuthentication(this IServiceCollection services, AppSettings appSettings)
{
// appSettings.ApiUrl = "http://localhost:5000"
// appSettings.WebsiteUrl = "http://localhost:4200"
// the default value for AllowuserNameCharacters is "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+"
// here we have just added some additional characters
services.AddIdentity<User, Role>(options => { options.User.AllowedUserNameCharacters += "'&"; })
.AddDefaultTokenProviders();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
// return unauthorized message instead of url
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return System.Threading.Tasks.Task.CompletedTask;
};
});
// configure all tokens generated from aspnet to expire in 3 days (create password, forget password etc)
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(3));
// Authentication
var authenticationBuilder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.Authority = appSettings.ApiUrl;
cfg.Audience = appSettings.ApiUrl;
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
});
// OpenIddict
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFramework(e => e.UseDbContext<ApplicationDbContext>());
})
.AddServer(options =>
{
// For token lifetimes look at the below link
// https://github.com/openiddict/openiddict-core/wiki/Configuration-and-options
// Enable the authorization, logout, token endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token");
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(Scopes.Email,
Scopes.Profile,
Scopes.OpenId,
Scopes.OfflineAccess,
Scopes.Roles);
// code to allow requests without client_id
options.AcceptAnonymousClients();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.DisableTransportSecurityRequirement(); // Never use https because we use load balancer
})
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
}
Startup.cs
/// <summary>
/// Configure services
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
// application settings
var appSettings = new AppSettings();
Configuration.Bind(appSettings);
// mvc
services.AddMvc(appSettings);
// identity / saml etc
services.AddAuthentication(appSettings);
}
/// <summary>
/// Configure application
/// </summary>
/// <param name="applicationBuilder"></param>
/// <param name="webHostEnvironment"></param>
/// <param name="loggerFactory"></param>
public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment, ILoggerFactory loggerFactory)
{
// get current appSettings and output to logger
var appSettings = applicationBuilder.ApplicationServices.GetRequiredService<AppSettings>();
var logger = loggerFactory.CreateLogger(appSettings.ApiUrl);
logger.LogInformation(string.Format("{0}\n{1}", appSettings.Environment, appSettings.Data.DefaultConnection.ConnectionString));
// environment
webHostEnvironment.EnvironmentName = appSettings.Environment;
// CORS
applicationBuilder.UseCors("Default");
// static files in root
applicationBuilder.UseStaticFiles();
// routing
applicationBuilder.UseRouting();
// comment this out and you get an error saying
// InvalidOperationException: No authentication handler is configured to handle the scheme: Microsoft.AspNet.Identity.External
applicationBuilder.UseAuthentication();
// for authorization headers
applicationBuilder.UseAuthorization();
// response caching
applicationBuilder.UseResponseCaching();
// routes
applicationBuilder.UseEndpoints(options =>
{
// default goes to Home, and angular will deal with client side routing
options.MapControllerRoute(
name: "default",
pattern: "{*url}",
defaults: new { controller = "home", action = "index" });
});
}
I have updated my AuthorizationController.cs as well
/// <summary>
/// Handles authorization requests
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class AuthorizationController : Controller
{
private IOptions<IdentityOptions> _identityOptions;
private OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> _applicationManager;
private SignInManager<MyUser> _signInManager;
private UserManager<MyUser> _userManager;
private AppSettings _appSettings;
private IEncryptionService _encryptionService;
/// <summary>
/// Create a new authorization controller
/// </summary>
/// <param name="identityOptions"></param>
/// <param name="applicationManager"></param>
/// <param name="signInManager"></param>
/// <param name="userManager"></param>
/// <param name="appSettings"></param>
/// <param name="encryptionService"></param>
public AuthorizationController(
IOptions<IdentityOptions> identityOptions,
OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> applicationManager,
SignInManager<MyUser> signInManager, UserManager<MyUser> userManager,
AppSettings appSettings, IEncryptionService encryptionService)
{
_identityOptions = identityOptions;
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
_appSettings = appSettings;
_encryptionService = encryptionService;
}
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
/// <summary>
/// Authorize an openId request
/// </summary>
/// <param name="connectRequest"></param>
/// <returns></returns>
[Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId, new System.Threading.CancellationToken());
if (application == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidClient,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Details concerning the calling client application cannot be found in the database"
}
)
);
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
/// <summary>
/// Accept an openId request
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the profile of the logged in user
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ServerError,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "An internal error has occurred"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
// returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Deny an openId request
/// </summary>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public IActionResult Deny()
{
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Logout
/// </summary>
/// <returns></returns>
[HttpPost("~/connect/logout")]
public async Task<IActionResult> Logout()
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
/// <summary>
/// Exchange request for valid openId token
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid"
}
)
);
}
// Ensure the user is still allowed to sign in
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new NotImplementedException("The specified grant type is not implemented");
}
/// <summary>
/// Creates a principal based on the openId request
/// </summary>
/// <param name="request"></param>
/// <param name="user"></param>
/// <param name="properties"></param>
/// <returns></returns>
private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, MyUser user, AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
principal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.OfflineAccess,
Scopes.Roles
}.Intersect(request.GetScopes()));
}
// Set resource
principal.SetResources(new string[] { _appSettings.ApiUrl });
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return principal;
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
Also here is my applicationDbContext
/// <summary>
/// Application context to hold our openIddict entities
/// </summary>
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
/// <summary>
/// Create context with connection string
/// </summary>
/// <param name="connectionString"></param>
public ApplicationDbContext(string connectionString) : base(connectionString)
{
}
/// <summary>
/// When creating models
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.UseOpenIddict();
}
/// <summary>
/// Authorizations
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkAuthorization> Authorization { get; set; }
/// <summary>
/// Applications
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkApplication> Application { get; set; }
/// <summary>
/// Tokens
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkToken> Token { get; set; }
/// <summary>
/// Scopes
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkScope> Scope { get; set; }
}
As i said, i am getting a token returned, like below
{
"access_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoiYXQrand0In0.TxmA-Lv60Lwvr5eXdu2fgG95Bx4AUoPvp5XniqSYbBgmpDrVXCUpSjnEv7tWQ5QLPjks6TAy8hGAGfgQPFB1xDM05mZyu2WDPBsoEQ2VtxuabaPhj-uK2KjnWEccjS-n-YAY7OlSDFXrmiDsFCOf3jaPGZ43gUCPyTVl_WVE_KBGVXa19RDZFf9Ger-TsHY3evYbsUchOKMco-CD2IGt6Xg6DttxFcKF3SUPwMXhn6AGdkNesGVIoVSeDd_CnM3MyHtJIbGbkRkmwpMWsbwaWGI38itUJ0XeUqn45B_GsZfaZllytuMHbuqt1GgL0gdOaCfBUxaqy7AaYsY9UKlaEw.LS4FnIOCRESdeo1c5K2_5Q.Ic8fssDScEu7lAomL8d_7JZMVho5hLphg269UIETZui7DUB2gund7YCtGq7imdnDtN-wsEoZaLHJ4UQSJrpuIMCN2pW69J9kKcx-UT1e2Ma-JNj2G5CtxwRf_bszRRsgWC1ia85LU5TPsUFvhd_wDxyUax2GKowb0FVl8EFRoFdZF40h06LaofxDzD2BFdwSYaHaDm3icVNZ0CRpYCoZq-MK_c3Fl98l57zjZl1CNscs1w0trApMDvQ7UeFcez3zelN24H6TCaXqTRP3lixMiv9Rtm3Kqkv67HEUAFD-vXIMyWQTo28oJMMAQz9zQeTp88JsfI4Pv7euUECEkwK7Fe5rbxhD4oicNNa_wmRo2vsjrpm4C7mmRKH2u3cX2CCTwahOsVHxu4kO3zliWmniW-krEAAsaW6BjoWqJiGN3ydoiHLIv3muM78jt85KzKxMIFsalpdf2F9Z8neQBglhzxZQFp9JiEecczxao6Y03WuFNX98kZh8Zr4D21CM_m634u3mf5-Gz-frVlSgnMrE9vgCG2eETI3fOnObf9pQ1XWpK7I9SE0AZvc2PciaKO4H7mKDqXuXFrzdiz6Tx9G9MMkXXbuKsuslZJ6q9wlE3xOl9mzd6shN9GvfSTQNrFoV-PS62xhnfCFKQDfYLzWF1fhQbq_RECp6IVJN960jaMAGIZ0yzIbRInIkbhdFEIbl__h9AYo6FfcZgYv4tfw-iB_unezgHCIPpbBPC0eRMiZh1wqpfwpiJ8zjvcXJXXCfSlu13KlBWD9tHVEHh6aHE258hGdFzApA-MTCkjHwMRjVgmv_-Ed-xwk0hjBpRhiZv-0kNwKI72YJVgZMX9CMpFBg2CO5z5hHSlc0pO9pc5ovuCZ_dYYRiqqcIDUJ5Wl6dDt1JCPI084C4yssC7MC4e94OtvfRtrI096qMI34qrLWi-jn7UOMnWMUunNepQLwQ07DK-ubXsS-m0xvSxPYxtE6XM6QebmvCcXj__vELGscDu8mWmvP1Y0L9SoSpWlErHvlCPpkfVeMnkdP74cKTYgpXSQcGduqcdfU86leI9oUYgnrcPwvMgUq3jwgHGWn_0d4Bo8CsChWatYWmSNKN88h3WfASSl6SyqeVbSSYvIp-0HBGxGOwODgO-YsOlOgeKmm8oSIhnELVNMVEY1uiUZpJ9DGfrUUXKMw7aIz6LK_zH_HxlMiBam0fxgAQwRyYlO0AFmuxFO62KEHFEjdDSEO9pcUP03_RNqDfAX-IAV_EoFT7CwVpOZMUvFLo78S4xkq2ss3CFbkA3J4ioud88T5SUfslnsZmY1dJYtW4HhlGF7SKVMN6GSwckz1YhyaxqlQpMbMRFA0uCbkM6u41K0-_toRQejKDX5juqFwqK8.m0bmPYOuAYYdc6WfjDWP2UneysP1G3FqwlmOzWOrTnM",
"token_type": "Bearer",
"expires_in": 3599,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwMzJBRUJBQjc4NkNCM0ExRkI3Nzk1NjdDQTRGRjU1ODg3OTFCNDEiLCJ4NXQiOiJjREt1dXJlR3l6b2Z0M2xXZktUX1ZZaDVHMEUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxIiwibmFtZSI6InBvd2VydXNlckBtZWFzdXJlMmltcHJvdmUuY29tIiwib2lfYXVfaWQiOiIxMDJjZTE0NC02NzkyLTRlYzAtOTlhNy0xZDBlMzdmNzkyZTYiLCJhdF9oYXNoIjoiV3BibExxNFdNR2dwdHdGVVVSMktRdyIsIm9pX3Rrbl9pZCI6IjQyMzBlYmJkLWUyNzItNDJlZS1iOTZiLTQ1ZGJmMTJlZWJmMiIsImV4cCI6MTYwNjQ3NzgzOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyIsImlhdCI6MTYwNjQ3NjYzOX0.WZkB63ZfZuJfigNVabPegu-B8TvMeS1DmgRQJS151XGR08Pw-fcCldb35oM7ZW9oQenj7059BAZMI1EveHWNVWOEFpabebi7TccGRoR1YKqWSNWTBDwyQgGMyehVmze_TPgsSjAJA0y1f_xtF3-ImfVx5Tzlcjg4XAmAhV3MRd-fEobdGk5540uto5hZJ7ieHrV_7U_FF4NgVT5nSw92bkFNjUokmNgMBpDWelZUEXmsb3MFGDMnQkP0oTGXeIcy0nuuIKpr1Liza_cvv1JfICQnSUKw_u3zdqSbsbXtzGg9GfunEhXf1zSF5dxfbNpr2E4bsnIxGJ-mOYxBsOQDJA",
"refresh_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoib2lfcmVmdCtqd3QifQ.E5xUDcqMK15spCgqoM8YrOCn_32wBX_X_XsG_f1nKOR-TcDI2AKi07vX62uMmbO1bgclGSGNynEUTuPt7KATf9UHXwpytXAK3_BnLxQLz7NbYIlfrja9t5z3gCRIGOZ-5gHbHcU6RdJrDpC73_V7CXYIfkVhfWeeS30_3GtLa-BUu4Nlr9YL7K22KdR0LJQsK6SXLMKobsj6PLrrywHnClaw6dG_O_SDezC5M_eIo_ErdHFqUBwWYhUHyntijJ9ekH4A2SiY0LGaQD6QyvmUNPC7E-LtLVO78saJYAgGW-CkQbwTBF7b_vOoEr_mPrFnpytwLnIvxSQCFVjclH-vzA.RGNxh09nh1RWb6XU6jNZSg.VRwdO_5UrB7tqaMHXwt6qZN8VucSlNXnBNoBxcvisDvjWhBOlhrJxEidGQPBeiIpbrYrWQXyPfSgFrmZAlqYcF-3cXTl0W-xR4oW1ZqRf95eMs-YTA9-bR6n8P2pV7WeYky7wkwa-i3Vs9rzasYjK18LTU6v71xMNVN2K0Z2HRgTXxPg3U4HUgkVQpZp4qetyhozdOhGBV37igAHSZHFUSYg-dlstz3DtEX7IXvi7GQbM_fLKVlDLWYZOtX1yH366C9TIjZbxGP3h-qPqfKKDNP-IhD_x2IpymmiKllfhfLKb62JQzy_ci-ICXGvs0z_ZLZirmzVSEotqrcKmwe1rlqRXbymP8llIXe3AzjLonN3I_35bLeqZz1KQE93pkLOpJGAJuhJ4dc8a7wJ5kxTAs9_CCvb6UBvhRxInUyZ9PQq026RaYJvN9i--x8JxAAtH58h9Y5zgm_M4kFbEoGgZshyGZ8QSQrp4JpPrGeW9ElvkyEmrGvEpm6zmy_Y6tEF3lXFfrGJL6FPGD5_q8m9lGQ162OCmZMGg2Sy46Hne0SPv04TpM43F-m82BTzXxGRbpvOoNye7Pl9dKbdOBno1WEmaesL-R-W_8qogNszW5c-gbhQTUjcQ1J5yuVL4r29qN9xhVDPKqxPYPzInCr30m8SH41NI6WusFVJbNoqVs43wymGfQwuDfJacVCIfuT1Lx2VMdVl3nvCAGOnEwiUDufjqMaA-Nh-Row4QIwTHMGwfzxbFkQjRfwq_NjVU0LZlVDX2BgSETw2ak3KpgAGoeDlVVwmXuQpbvZ3wRHVh4a3k3_JVGf1dOiHbXVcWx984-EARKmW6gLjIhe2t5tzIJ9NQqBrdBrCvMmQXWfBiP00-YH31yDfYRF9tNZoojnkcrkOewHFUsEcFuayBzP5ySR6RgGVVE_zkEXTnBXxeiynzfJn0D3XthpRPosLJTJz91tdmvo6CxPWuaaZoUlXq_EXqFrGCb09f11Hs_mKu7T7pHMbJioTAvj1Jy8jzduMU3Rth-w0A8Md2gdeKmoCXW51lvpuyFsP4R8AdthhXGUbAk6rNJXdkaUkRyygYU8ZQWI2WCyroDnBRpEIaBtjYT1Uf4zXyTUZ3jicKe5Rjr08tmgAQd_3pNu1Wb2dBQgvcoqnwXcnLvoMTp0yk7Osae94Pqh_Oyz45kz_oS_7fR6WvZm-avBPYLmW92eQWiMG6glxPPe_Vnq9ghwjYjS4KEvyFaDqusDWmxBEDwEzLgBTF3p0R2saedTbwpG4Epcey_T18KEuklAWbCwz-fvV3ip-_wGUNe6cuAOeyuXcUHm18Le0knZt6xNS_a8cxMz9RAEpBs3a-tOWQLrVELXxnNY03NL2szkNQuDUzY9JRtTxqngxcPpHVk058dRG1rwFZqiI4-6_yL03X_fbXsIR0ItBig9surYyB_crwyH3C6OZTnGgwxLKUU9qY0LNTzR0gatrrT1l8NrorLAvODzQqhrqqClHgZkVoQXANvz3mNZWIpxjCkOgDkH-YOaaR3egnwLVMk_clo3-gC76UQ-5T-NnZ7MJTv2twFBhUKHABEJsT9a3nR7ra2CbHFzJFNvRRCPHAGOVAY-y-Ek0xZn8mvd3Fvw4wlej4EdQvlnFUcaIH4Cl0MZD9G9t2W3A96KLRZctVfGK6W1yM846DWHwSfeTj3ZPN_6bYDqbWHoXrQxe5BGi9aN8PXfL1Bu9W5jhQpgrvTRvnjhYqwN24ta9r6BbrMzBL3cszf56dG3Ko6aA8gDwUmDSjyNVhk76sdoW.1xLMuQf7NjeYjAXDL5BUxaoFSR0EECEzPh7XxkE8Hj4"
}
Here is the token being passed successfully, but Unauthorized is returned, as you can see the next call fails as well.
Any help much appreciated
I know this is old, but just post my solution in case anyone has the same issue.
Just specificy the schema for the Authorize attribute:
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
I need to have both windows authentication and owin (forms) authentication but i can't get it to work.
Probably the best option is to have two sites that have different authentication methods.
I found a project that does what i want: MVC5-MixedAuth. But it uses IISExpress and i can't get it to work with Local IIS.
The error that occurs is:
Request filtering is configured on the Web server to deny the request because the query string is too long.
If i remove all my ConfigureAuth() method inside Startup.Auth.cs it doesn't throw the error but i can't login because it is needed to do CookieAuthentication.
Startup.Auth.cs:
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(dbEmployeePortal.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, UserMaster, int>
(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
)
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
Any idea?
UPDATE 1
The error
Request filtering is configured on the Web server to deny the request because the query string is too long.
appears because occurs a login loop when it tries to reach the login page.
Resolved!
I followed the example: MVC5-MixAuth
Credits: Mohammed Younes
UPDATE 1
Problem: I needed to have both Anonymous Authentication and Windows Authentication enabled.
But when you enable them both, you can only get NT AUTHORITY\IUSR.
Resolution: To get the current user (introduced with NTLM prompt), we need to create an handler that will execute when an user enter at login page.
When the user hits the login page, the handler will get the current windows identity cached in the browser and then set as the LogonUserIdentity.
Note: I needed to use windows first login, when the user hits the login page it will try to get the correspondent ASP.NET User.
Handler
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.AspNet.Identity;
namespace MixedAuth
{
/// <summary>
/// Managed handler for windows authentication.
/// </summary>
public class WindowsLoginHandler : HttpTaskAsyncHandler, System.Web.SessionState.IRequiresSessionState
{
public HttpContext Context { get; set; }
public override async Task ProcessRequestAsync(HttpContext context)
{
this.Context = context;
//if user is already authenticated, LogonUserIdentity will be holding the current application pool identity.
//to overcome this:
//1. save userId to session.
//2. log user off.
//3. request challenge.
//4. log user in.
if (context.User.Identity.IsAuthenticated)
{
this.SaveUserIdToSession(context.User.Identity.GetUserId());
await WinLogoffAsync(context);
context.RequestChallenge();
}
else if (!context.Request.LogonUserIdentity.IsAuthenticated)
{
context.RequestChallenge();
}
else
{
// true: user is trying to link windows login to an existing account
if (this.SessionHasUserId())
{
var userId = this.ReadUserIdFromSession();
this.SaveUserIdToContext(userId);
await WinLinkLoginAsync(context);
}
else // normal login.
await WinLoginAsync(context);
}
}
#region helpers
/// <summary>
/// Executes Windows login action against account controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLoginAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Login);
routeData.Values.Add("returnUrl", context.Request["returnUrl"]);
routeData.Values.Add("userName", context.Request.Form["UserName"]);
await ExecuteController(context, routeData);
}
/// <summary>
/// Execute Link Windows login action against account controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLinkLoginAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Link);
await ExecuteController(context, routeData);
}
/// <summary>
/// Executes Windows logoff action against controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLogoffAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Logoff);
await ExecuteController(context, routeData);
}
/// <summary>
/// Executes controller based on route data.
/// </summary>
/// <param name="context"></param>
/// <param name="routeData"></param>
/// <returns></returns>
private async Task ExecuteController(HttpContext context, RouteData routeData)
{
var wrapper = new HttpContextWrapper(context);
MvcHandler handler = new MvcHandler(new RequestContext(wrapper, routeData));
IHttpAsyncHandler asyncHandler = ((IHttpAsyncHandler)handler);
await Task.Factory.FromAsync(asyncHandler.BeginProcessRequest, asyncHandler.EndProcessRequest, context, null);
}
#endregion
}
}
Extensions
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
namespace MixedAuth
{
public enum Action { Login, Link, Logoff };
public static class MixedAuthExtensions
{
const string userIdKey = "windows.userId";
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
const int fakeStatusCode = 418;
const string controllerName = "Account";
const string loginActionName = "WindowsLogin";
const string linkActionName = "LinkWindowsLogin";
const string logoffActionName = "WindowsLogoff";
const string windowsLoginRouteName = "Windows/Login";
public static void RegisterWindowsAuthentication(this MvcApplication app)
{
app.EndRequest += (object sender, EventArgs e) =>
{
HttpContext.Current.ApplyChallenge();
};
}
/// <summary>
/// Registers ignore route for the managed handler.
/// </summary>
/// <param name="routes"></param>
public static void IgnoreWindowsLoginRoute(this RouteCollection routes)
{
routes.IgnoreRoute(windowsLoginRouteName);
}
/// <summary>
/// By pass all middleware and modules, by setting a fake status code.
/// </summary>
/// <param name="context"></param>
public static void RequestChallenge(this HttpContext context)
{
context.Response.StatusCode = fakeStatusCode;
}
/// <summary>
/// Invoke on end response only. Replaces the current response status code with 401.2
/// </summary>
/// <param name="context"></param>
public static void ApplyChallenge(this HttpContext context)
{
if (context.Response.StatusCode == fakeStatusCode)
{
context.Response.StatusCode = 401;
context.Response.SubStatusCode = 2;
//http://msdn.microsoft.com/en-us/library/system.web.httpresponse.tryskipiiscustomerrors(v=vs.110).aspx
//context.Response.TrySkipIisCustomErrors = true;
}
}
/// <summary>
///
/// </summary>
/// <param name="handler"></param>
/// <param name="action"></param>
/// <returns></returns>
public static RouteData CreateRouteData(this WindowsLoginHandler handler, Action action)
{
RouteData routeData = new RouteData();
routeData.RouteHandler = new MvcRouteHandler();
switch (action)
{
case Action.Login:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", loginActionName);
break;
case Action.Link:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", linkActionName);
break;
case Action.Logoff:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", logoffActionName);
break;
default:
throw new NotSupportedException(string.Format("unknonw action value '{0}'.", action));
}
return routeData;
}
/// <summary>
/// Saves userId to the items collection inside <see cref="HttpContext"/>.
/// </summary>
public static void SaveUserIdToContext(this WindowsLoginHandler handler, string userId)
{
if (handler.Context.Items.Contains(userIdKey))
throw new ApplicationException("Id already exists in context.");
handler.Context.Items.Add("windows.userId", userId);
}
/// <summary>
/// Reads userId from item collection inside <see cref="HttpContext"/>.
/// </summary>
/// <remarks>The item will removed before this method returns</remarks>
/// <param name="context"></param>
/// <returns></returns>
public static int ReadUserId(this HttpContextBase context)
{
if (!context.Items.Contains(userIdKey))
throw new ApplicationException("Id not found in context.");
int userId = Convert.ToInt32(context.Items[userIdKey] as string);
context.Items.Remove(userIdKey);
return userId;
}
/// <summary>
/// Returns true if the session contains an entry for userId.
/// </summary>
public static bool SessionHasUserId(this WindowsLoginHandler handler)
{
return handler.Context.Session[userIdKey] != null;
}
/// <summary>
/// Save a session-state value with the specified userId.
/// </summary>
public static void SaveUserIdToSession(this WindowsLoginHandler handler, string userId)
{
if (handler.SessionHasUserId())
throw new ApplicationException("Id already exists in session.");
handler.Context.Session[userIdKey] = userId;
}
/// <summary>
/// Reads userId value from session-state.
/// </summary>
/// <remarks>The session-state value removed before this method returns.</remarks>
/// <param name="session"></param>
/// <returns></returns>
public static string ReadUserIdFromSession(this WindowsLoginHandler handler)
{
string userId = handler.Context.Session[userIdKey] as string;
if (string.IsNullOrEmpty(userIdKey))
throw new ApplicationException("Id not found in session.");
handler.Context.Session.Remove(userIdKey);
return userId;
}
/// <summary>
/// Creates a form for windows login, simulating external login providers.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="htmlAttributes"></param>
/// <returns></returns>
public static MvcForm BeginWindowsAuthForm(this HtmlHelper htmlHelper, object htmlAttributes)
{
return htmlHelper.BeginForm("Login", "Windows", FormMethod.Post, htmlAttributes);
}
/// <summary>
/// Creates a form for windows login, simulating external login providers.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="htmlAttributes"></param>
/// <returns></returns>
public static MvcForm BeginWindowsAuthForm(this HtmlHelper htmlHelper, object routeValues, object htmlAttributes)
{
return htmlHelper.BeginForm("Login", "Windows", FormMethod.Post, htmlAttributes);
}
}
}
Note
You need to have AccountController.cs as partial.
AccountController.Windows.cs
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.AspNet.Identity;
using MixedAuth;
namespace EmployeePortal.Web.Controllers
{
[Authorize]
public partial class AccountController : BaseController
{
//
// POST: /Account/WindowsLogin
[AllowAnonymous]
[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]
public ActionResult WindowsLogin(string userName, string returnUrl)
{
if (!Request.LogonUserIdentity.IsAuthenticated)
{
return RedirectToAction("Login");
}
var loginInfo = GetWindowsLoginInfo();
// Sign in the user with this external login provider if the user already has a login
var user = UserManager.Find(loginInfo);
if (user != null)
{
SignIn(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
return RedirectToAction("Login", new RouteValueDictionary(new { controller = "Account", action = "Login", returnUrl = returnUrl }));
}
}
//
// POST: /Account/WindowsLogOff
[HttpPost]
[ValidateAntiForgeryToken]
public void WindowsLogOff()
{
AuthenticationManager.SignOut();
}
//
// POST: /Account/LinkWindowsLogin
[AllowAnonymous]
[HttpPost]
public async Task<ActionResult> LinkWindowsLogin()
{
int userId = HttpContext.ReadUserId();
//didn't get here through handler
if (userId <= 0)
return RedirectToAction("Login");
HttpContext.Items.Remove("windows.userId");
//not authenticated.
var loginInfo = GetWindowsLoginInfo();
if (loginInfo == null)
return RedirectToAction("Manage");
//add linked login
var result = await UserManager.AddLoginAsync(userId, loginInfo);
//sign the user back in.
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
await SignInAsync(user, false);
if (result.Succeeded)
return RedirectToAction("Manage");
return RedirectToAction("Manage", new { Message = ManageMessageId.Error });
}
#region helpers
private UserLoginInfo GetWindowsLoginInfo()
{
if (!Request.LogonUserIdentity.IsAuthenticated)
return null;
return new UserLoginInfo("Windows", Request.LogonUserIdentity.User.ToString());
}
#endregion
}
public class WindowsLoginConfirmationViewModel
{
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }
}
}
Then, you need to add the handler:
<add name="Windows Login Handler" path="Login" verb="GET,POST" type="MixedAuth.WindowsLoginHandler" preCondition="integratedMode" />
Startup.cs
app.CreatePerOwinContext(dbEmployeePortal.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
PathString path = new PathString("/Account/Login");
if (GlobalExtensions.WindowsAuthActive)
path = new PathString("/Windows/Login");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
//LoginPath = new PathString("/Account/Login")
LoginPath = path
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
Then you need to configure Local IIS to use WindowsAuthentication and AnonymousAuthentication. You can do this in the Authentication Module.
Note If you don't have WindowsAuthentication go to Control Panel then Programs and Features then "Turn Windows features on or off":
select "Internet Information Services" > "World Wide Web" > "Security"
and select Windows Authentication.
I didn't see this in your answer, so for anyone looking on how to capture Windows Auth in your Owin pipeline, you also can add the following to your ConfigureAuth method:
public void ConfigureAuth(IAppBuilder app)
{
HttpListener listener = (HttpListener)app.Properties["System.Net.HttpListener"];
listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;
}
I am using the newest version of ASP.NET Identity, using the sample project as a base template.
My changes are simple: Adding a FIRM Properyt to the application user.
Thus, the application User looks like this:
public class ApplicationUser : IdentityUser
{
/// <summary>
/// Gets or sets the firm.
/// </summary>
/// <value>
/// The firm.
/// </value>
public virtual Firm Firm { get; set; }
/// <summary>
/// Generates the user identity asynchronous.
/// </summary>
/// <param name="manager">The manager.</param>
/// <returns>A claims identity</returns>
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
//// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
//// Add custom user claims here
return userIdentity;
}
}
Also, I do have a Unit Of Work implented, which is responsible for Firm and other entities.
/// <summary>
/// This contains a Unit of Work pattern Implementation. Provides access to all Elements used in this Application.
/// </summary>
public class UnitOfWork : IDisposable, IUnitOfWork
{
/// <summary>
/// The database context
/// </summary>
private PriorityDBContext dbContext = new PriorityDBContext();
public UnitOfWork(ApplicationDbContext dbContext)
{
this.dbContext = (PriorityDBContext)ApplicationDbContext;
}
private GenericRepository<Firm> firmRepository;
public IGenericRepository<Firm> FirmRepository
{
get
{
if (this.firmRepository == null)
{
this.firmRepository = new GenericRepository<Firm>(this.dbContext);
}
return this.firmRepository;
}
}
}
Now to my problem: I have an Account Controller, which can create new Users (among other things). When a new User is created, the Firm is silently doubled. E.g. I had a Firm called "Example1" in the database before. Now I create a new User and suddenly the Database contains "Example1" two times.
As far I can tell, the root cause is that Asp.net Identity uses one DbContext, my UnitOfWork another. I am unsure why, I tried to set up Ninject in a way that the same DbContext is used in all parts. But it simply have enormous problems getting the same db Context into the UserManager and the unit of work :(
[Authorize]
public class AccountController : Controller
{
/// <summary>
/// The Unit of Work
/// </summary>
private IUnitOfWork uoW;
/// <summary>
/// The _user manager
/// </summary>
private ApplicationUserManager userManager;
public AccountController(IUnitOfWork uow)
{
this.uoW = uow;
}
/// <summary>
/// Gets the user manager.
/// </summary>
/// <value>
/// The user manager.
/// </value>
public ApplicationUserManager UserManager
{
get
{
if (this.userManager == null)
{
this.userManager = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
return this.userManager;
}
private set
{
this.userManager = value;
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
bool success = true;
if (ModelState.IsValid)
{
var firm = this.uoW.FirmRepository.GetRaw().Where(f => f.FirmName == newUser.Firm).First();
var user = new ApplicationUser { FirstName = newUser.FirstName, LastName = newUser.LastName, Email = newUser.Email, UserName = newUser.Email, PreferredLanguage = newUser.Language, Firm = firm };
var password = Guid.NewGuid().ToString();
var result = await this.UserManager.CreateAsync(user, password);
if (result.Succeeded)
{
this.logger.Info("Created user for {0} successfully", newUser.Email);
}
}
}
}
}
}
Ninject, registering the Bindings:
private static void RegisterServices(IKernel kernel)
{
/// loading the ApplicationDbContext here because the UserManager uses it from the OwinContext and if the
/// Unit of Work defines it's own DBContext, bad things happen, e.g. duplicated objects when working with users AND our own pocos here.
kernel.Bind<IUnitOfWork>().To<UnitOfWork>().WithConstructorArgument("dbContext", ninjectContext => HttpContext.Current.GetOwinContext().Get<ApplicationDbContext>());
kernel.Bind<IGenericRepository<Firm>>().To<GenericRepository<Firm>>();
//kernel.Bind<ApplicationUserManager>().To(HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>());
}
The ApplicationUserManager:
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
//// Konfigurieren der Überprüfungslogik für Benutzernamen.
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// snipped
return manager;
}
}
Following the advice I have been given in this thread [Ninject UOW pattern, new ConnectionString after user is authenticated I now understand that I should not use the following line...
var applicationConfiguration =
(IApplicationConfiguration)
DependencyResolver.Current.GetService(typeof(IApplicationConfiguration));
...as a Service Locator is an anti-pattern.
But in the case of the following procedure how can I instantiate my concrete object that implements "IApplicationConfiguration" so that I can use that object to get the unknown user role name, or use it to assign to the "ApplicationConfiguration" property of my principle?
Global.asax
public class MvcApplication : NinjectHttpApplication
{
/// <summary>
/// Handles the PostAuthenticateRequest event of the Application control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
String[] roles;
var applicationConfiguration =
(IApplicationConfiguration)
DependencyResolver.Current.GetService(typeof(IApplicationConfiguration));
var identity = HttpContext.Current.User.Identity;
if (Request.IsAuthenticated)
{
roles = Roles.GetRolesForUser(identity.Name);
}
else
{
roles = new[] { applicationConfiguration.UnknownUserRoleName };
}
var webIdentity = new WebIdentity(identity, roles);
var principal = new WebsitePrincipal(webIdentity)
{
ApplicationConfiguration = applicationConfiguration
};
HttpContext.Current.User = principal;
}
.
.
.
}
Resolution Mapping Code
public class ApplicationConfigurationContractMapping : NinjectModule
{
public override void Load()
{
Bind<IApplicationConfiguration>()
.To<ApplicationConfiguration>();
}
}
ApplicationConfiguration
public class ApplicationConfiguration : IApplicationConfiguration
{
.
.
.
.
}
I am using Ninject as my Dependency Injection framework. Any suggestions appreciated.
EDIT: Full code can be seen here:
https://github.com/dibley1973/Dibware.Template.Presentation.Web
You can't prevent having to call either the DI Container or an abstraction over it in your Application_PostAuthenticateRequest, but that shouldn't be a problem, since this Application_PostAuthenticateRequest can be considered to be part of your Composition Root. Or in other words: you have to resolve it somewhere.
The problem in your case, however, is that this method contains an awful lot of code, and the real problem is that you are missing an abstraction. To resolve this, extract all logic of this method into a new class and hide it behind an abstraction. What will be left is the following code:
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
var provider = (IPostAuthenticateRequestProvider)
DependencyResolver.Current.GetService(typeof(IPostAuthenticateRequestProvider));
provider.ApplyPrincipleToCurrentRequest();
}
The code can be built up by your DI Container, and will have the following signature:
public class MvcPostAuthenticateRequestProvider : IPostAuthenticateRequestProvider
{
private readonly IApplicationConfiguration configuration;
public MvcPostAuthenticateRequestProvider(IApplicationConfiguration configuration)
{
this.configuration = configuration;
}
public void ApplyPrincipleToCurrentRequest()
{
// ...
}
}
Following Steven's suggestion, the final code was:
A new interface "IPostAuthenticateRequestProvider"
/// <summary>
/// Defines the expected members of a PostAuthenticateRequestProvider
/// </summary>
internal interface IPostAuthenticateRequestProvider
{
/// <summary>
/// Applies a correctly setup principle to the Http request
/// </summary>
/// <param name="httpContext"></param>
void ApplyPrincipleToHttpRequest(HttpContext httpContext);
}
A concrete class that implements "IPostAuthenticateRequestProvider"
/// <summary>
/// Provides PostAuthenticateRequest functionality
/// </summary>
public class MvcPostAuthenticateRequestProvider : IPostAuthenticateRequestProvider
{
#region Declarations
private readonly IApplicationConfiguration _configuration;
#endregion
#region Constructors
public MvcPostAuthenticateRequestProvider(IApplicationConfiguration configuration)
{
_configuration = configuration;
}
#endregion
#region IPostAuthenticateRequestProvider Members
/// <summary>
/// Applies a correctly setup principle to the Http request
/// </summary>
/// <param name="httpContext"></param>
public void ApplyPrincipleToHttpRequest(HttpContext httpContext)
{
// declare a collection to hold roles for the current user
String[] roles;
// Get the current identity
var identity = HttpContext.Current.User.Identity;
// Check if the request is authenticated...
if (httpContext.Request.IsAuthenticated)
{
// ...it is so load the roles collection for the user
roles = Roles.GetRolesForUser(identity.Name);
}
else
{
// ...it isn't so load the collection with the unknown role
roles = new[] { _configuration.UnknownUserRoleName };
}
// Create a new WebIdenty from the current identity
// and using the roles collection just populated
var webIdentity = new WebIdentity(identity, roles);
// Create a principal using the web identity and load it
// with the app configuration
var principal = new WebsitePrincipal(webIdentity)
{
ApplicationConfiguration = _configuration
};
// Set the user for the specified Http context
httpContext.User = principal;
}
#endregion
}
And in global.asax...
public class MvcApplication : NinjectHttpApplication
{
/// <summary>
/// Handles the PostAuthenticateRequest event of the Application control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
// Get a PostAuthenticateRequestProvider and use this to apply a
// correctly configured principal to the current http request
var provider = (IPostAuthenticateRequestProvider)
DependencyResolver.Current.GetService(typeof(IPostAuthenticateRequestProvider));
provider.ApplyPrincipleToHttpRequest(HttpContext.Current);
}
.
.
}