How can I set the CookieDOmain in the CookieAuthenticationOptions at runtime if i want to pull this value from the Request.Url or from some settings stored in my database?
I want to support sub-domains, but also support multi-tenants too which each have different domains.
At the moment this is configured I don't have access to either of these.
Paul
You can assign your own cookie provider:
CookieAuthProvider myProvider = new CookieAuthProvider();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = myProvider
});
Either implement your own, or simply inherit from the existing provider:
public class CookieAuthProvider : CookieAuthenticationProvider
{
public override void ResponseSignIn(CookieResponseSignInContext context)
{
//Alter you cookie options
//context.CookieOptions.Domain = "www...";
base.ResponseSignIn(context);
}
}
And implement ResponseSignIn, it is called when an endpoint has provided sign in information before it is converted into a cookie. By implementing this method the claims and extra information that go into the ticket may be altered.
You'll be passed a CookieResponseSignInContext, which exposes CookieOptions property that can be replaced or altered during the ResponseSignIn call.
Code references from Katana project:
ICookieAuthenticationProvider
CookieResponseSignInContext
CookieAuthenticationHandler
Do you already try this:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
LoginPath = "/Account/Login",
CookieDomain = ".myDomain.com"
});
It looks like MK. answer does not allow proper handling of token renewal when using SlidingExpiration option.
As a workaround, instead of supplying a custom cookie provider, it appears you can supply a custom cookie manager, and define your own methods for adding/removing the cookie.
To keep it simple in my case, I reuse the default cookie manager under the hood. (I can not extend it, its methods are not overridable.)
Here is the code I have ended up with:
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
public class Startup
{
public void Configuration(IAppBuilder app)
{
var options = new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
SlidingExpiration = true,
CookieManager = new CustomCookieManager()
};
app.UseCookieAuthentication(options);
}
}
public class CustomCookieManager : ICookieManager
{
private readonly ICookieManager ConcreteManager;
public CustomCookieManager()
{
ConcreteManager = new ChunkingCookieManager();
}
string ICookieManager.GetRequestCookie(IOwinContext context, string key)
{
return ConcreteManager.GetRequestCookie(context, key);
}
void ICookieManager.AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
SetupDomain(context, options);
ConcreteManager.AppendResponseCookie(context, key, value, options);
}
void ICookieManager.DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
SetupDomain(context, options);
ConcreteManager.DeleteCookie(context, key, options);
}
private void SetupDomain(IOwinContext context, CookieOptions options)
{
// custom logic for assigning something to options.Domain
}
}
Related
I have multi tenant application where each tenant can use different IdP to authenticate. Below code correctly redirects to IdP but problem is to get back the response to ACS endpoint.
Key is the Configuration method which configures the paths and their authentication:
[assembly: OwinStartup(typeof(SSOSamlDemoASPNET.App_Start.Startup))]
namespace SSOSamlDemoASPNET.App_Start
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/client/okta", (appx) =>
{
ConfigureAuthentication(appx, "/client/okta/Saml2", ...);
});
app.Map("/client/azuread", (appx) =>
{
ConfigureAuthentication(appx, "/client/azuread/Saml2", ...);
});
}
private static void ConfigureAuthentication(IAppBuilder app, string modulePath, string audience, string issuer, string metadataUrl)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
CookieName = "LoggedUser",
CookiePath = "/",
CookieManager = new SystemWebCookieManager(),
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
ConfigureSaml(app, modulePath, audience, issuer, metadataUrl);
}
private static void ConfigureSaml(IAppBuilder app, string modulePath, string audience, string issuer, string metadataUrl)
{
var saml2options = new Saml2AuthenticationOptions(false);
var spOptions = new SPOptions
{
EntityId = new EntityId(audience),
ModulePath = modulePath,
PublicOrigin = new Uri("https://localhost:44340/"),
};
spOptions.Logger = new ConsoleLoggerAdapter();
saml2options.SPOptions = spOptions;
saml2options.IdentityProviders.Add(new IdentityProvider(new EntityId(issuer), spOptions)
{
AllowUnsolicitedAuthnResponse = true,
MetadataLocation = metadataUrl,
LoadMetadata = true,
Binding = Saml2BindingType.HttpPost,
});
app.UseSaml2Authentication(saml2options);
}
}
}
Authenticating against individual IdP is done like this:
authProperties.Dictionary["idp"] = "https://sts.windows.net/xxx/";
authProperties.RedirectUri = "https://localhost:44340/client/azuread/ExternalLoginCallback";
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(authProperties, "Saml2");
When inspecting code of the Sustainsys.Saml2 library (especially Saml2AuthenticationHandler). I found the conditions do not take into account OwinRequest.PathBase and therefore the identity is not coming back to the application.
An example can be (Saml2AuthenticationHandler.Invoke method).
Options.SPOptions.ModulePath = /client/azuread/Saml2
Request.Path = /Saml2/Acs
==> therefore the code inside the condition is not executed.
public override async Task<bool> InvokeAsync()
{
var Saml2Path = new PathString(Options.SPOptions.ModulePath);
if (Request.Path.StartsWithSegments(Saml2Path, out PathString remainingPath))
{
if (remainingPath == new PathString("/" + CommandFactory.AcsCommandName))
{
var ticket = (MultipleIdentityAuthenticationTicket)await AuthenticateAsync();
if (ticket.Identities.Any())
{
Context.Authentication.SignIn(ticket.Properties, ticket.Identities.ToArray());
// No need to redirect here. Command result is applied in AuthenticateCoreAsync.
}
else
{
Response.Redirect(ticket.Properties.RedirectUri);
}
return true;
}
Is there any way to change this behavioral? e.g. saml2Options.Notifications to get this working?
That is obviously a bug/lack of feature, but nothing that will be fixed on the Owin module - it's on life support.
The solution for a multi tenancy owin app is to register one Saml2 middleware and add multiple IdentityProviders to that one. The middleware will handle all responses on the same endpoint and use the configuration from the right IdentityProvider based on where the response came from.
I am new to ASP.NET core itself. However, I am creating WebAPIs in ASP.NET Core 2.0. I have configured JWT Bearer Token based authentication. Below is my Controller which return token.
[AllowAnonymous]
[Route("api/[controller]")]
public class TokenController : Controller
{
private readonly UserManager<UserEntity> userManager;
private readonly SignInManager<UserEntity> signInManager;
public TokenController(UserManager<UserEntity> userManager, SignInManager<UserEntity> signInManager)
{
this.userManager = userManager;
this.signInManager = signInManager;
}
// GET: api/values
[HttpGet]
public async Task<IActionResult> Get(string username, string password, string grant_type)
{
{
var user = await userManager.FindByEmailAsync(username);
if (user != null)
{
var result =await signInManager.CheckPasswordSignInAsync(user, password, false);
if (result.Succeeded)
{
var claims = new[]
{
new Claim( JwtRegisteredClaimNames.Sub, username),
new Claim( JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim( JwtRegisteredClaimNames.GivenName, "SomeUserID")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secretesecretesecretesecretesecretesecrete"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken( issuer: "test",
audience: "test",
claims: claims,
expires: DateTime.Now.AddDays(15),
signingCredentials: creds);
return Ok(new { access_token = new JwtSecurityTokenHandler().WriteToken(token), expires_on=DateTime.Now.AddDays(15) });
}
}
}
return BadRequest("Could not create token");
}
}
But when calling ValuesController API which is decorated with [Authorize] attributes. I am getting User.Identity.Name is empty. I am not getting any information about user. I am not sure, My token controller is correctly written. As long as it is protecting my ValuesController, I assume, it is correct. However, I might be missing something. Please help.
Note: I am developing using Visual Studio 2017 with Mac Community
addition
Yes, you need to specify the claim for the unique name which is translated into the user.identity.name:
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
I've also been having this problem with ASP.Net Core 2, and I'm really surprised no one's discovered the other cause of this problem.
When my webapp is deployed to IIS, "User.Identity.Name" always returns null. The IIS site has anonymous access disabled, and windows authentication is enabled.
BUT.
I didn't realise that my ASP.Net Core 2 has a "launchSettings.json" file, quietly hidden under the Properties folder, and in there, there's also some iisSettings, and in here "windowsAuthentication" was, strangely, set as false by default.
Changing "windowsAuthentication" to true, and "anonymousAuthentication" to false solved the problem for me.
After doing this, "User.Identity.Name" did finally contain the correct username.
But what the heck is this setting ? Why would this get priority over the actual settings we've setup in IIS Manager ?!
Had this problem too (Core 3.1) using the "DefaultIdentity" (Individual User Accounts).
User.Identity.Name is null, User.Identity.IsAuthenticated = true.
By using httpContextAccessor you can get the userId an with that id you can find the user and the UserName.
In your controller add
using System.Security.Claims;
...
private readonly IHttpContextAccessor _httpContextAccessor;
public MyController(MyContext context, IHttpContextAccessor httpContextAccessor)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}
// Any method username needed
[HttpGet("{id}")]
public async Task<ActionResult<MyInfo>> GetMyInfo(int id)
{
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
var user = _context.AspNetUsers.Find(userId);
var userName = user.UserName;
...
}
In the Startup.cs add the following line:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
For Azure OAuth v2, use preferred_username instead of unique_name (see this and this).
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
serviceCollection.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.RoleClaimType = "roles";
options.TokenValidationParameters.NameClaimType = "preferred_username";
//options.TokenValidationParameters.NameClaimType = "email"; // or if you want to use user's email for User.Identity.Name
//below lines of code can be removed. just there if you want some code to be executed right after user is validated.
options.Events.OnTokenValidated = async context =>
{
var personFirstName = context.Principal.FindFirstValue("given_name") ?? string.Empty;
var personLastName = context.Principal.FindFirstValue("family_name") ?? string.Empty;
var personEmail = context.Principal.FindFirstValue("email")?.ToLower();
var personName = context.Principal.Identity.Name;
};
});
Then in your controllers, you will get username from User.Identity.Name
I have an ASP.NET Core app that uses Identity. It works, but when I am trying to add custom roles to the database I run into problems.
In Startup ConfigureServices I have added Identity and the role manager as a scoped service like this:
services.AddIdentity<Entities.DB.User, IdentityRole<int>>()
.AddEntityFrameworkStores<MyDBContext, int>();
services.AddScoped<RoleManager<IdentityRole>>();
and in Startup Configure I inject RoleManager and pass it to my custom class RolesData:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
RoleManager<IdentityRole> roleManager
)
{
app.UseIdentity();
RolesData.SeedRoles(roleManager).Wait();
app.UseMvc();
This is the RolesData class:
public static class RolesData
{
private static readonly string[] roles = new[] {
"role1",
"role2",
"role3"
};
public static async Task SeedRoles(RoleManager<IdentityRole> roleManager)
{
foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
{
var create = await roleManager.CreateAsync(new IdentityRole(role));
if (!create.Succeeded)
{
throw new Exception("Failed to create role");
}
}
}
}
}
The app builds without errors, but when trying to access it I get the following error:
Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IRoleStore`1[Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole]' while attempting to activate 'Microsoft.AspNetCore.Identity.RoleManager
What am I doing wrong? My gut says there's something wrong with how I add the RoleManager as a service.
PS: I have used "No authentication" when creating the project to learn Identity from scratch.
I was having this issue
No service for type 'Microsoft.AspNetCore.Identity.RoleManager`
And this page was the first result on Google. It did not answer my question, so I thought I would put my solution here, for anyone else that may be having this problem.
ASP.NET Core 2.2
The missing line for me was .AddRoles() in the Startup.cs file.
services.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
Hope this helps someone
Source: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 (at the bottom)
What am I doing wrong? My gut says there's something wrong with how I add the RoleManager as a service.
The registration part is actually fine, tho' you should remove services.AddScoped<RoleManager<IdentityRole>>(), as the role manager is already added for you by services.AddIdentity().
Your issue is most likely caused by a generic type mismatch: while you call services.AddIdentity() with IdentityRole<int>, you try to resolve RoleManager with IdentityRole, which is an equivalent of IdentityRole<string> (string being the default key type in ASP.NET Core Identity).
Update your Configure method to take a RoleManager<IdentityRole<int>> parameter and it should work.
This my solution seed User and Role ASP.NET Core 2.2
Startup.cs
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole<Guid>>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
...
...
SeedData.Initialize(app.ApplicationServices);
)
SeedData.cs
public static void Initialize(IServiceProvider serviceProvider)
{
using (var scope = serviceProvider.CreateScope())
{
var provider = scope.ServiceProvider;
var context = provider.GetRequiredService<ApplicationDbContext>();
var userManager = provider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = provider.GetRequiredService<RoleManager<IdentityRole<Guid>>>();
// automigration
context.Database.Migrate();
InstallUsers(userManager, roleManager);
}
}
private static void InstallUsers(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole<Guid>> roleManager)
{
const string USERNAME = "admin#mysite.com";
const string PASSWORD = "123456ABCD";
const string ROLENAME = "Admin";
var roleExist = roleManager.RoleExistsAsync(ROLENAME).Result;
if (!roleExist)
{
//create the roles and seed them to the database
roleManager.CreateAsync(new IdentityRole<Guid>(ROLENAME)).GetAwaiter().GetResult();
}
var user = userManager.FindByNameAsync(USERNAME).Result;
if (user == null)
{
var serviceUser = new ApplicationUser
{
UserName = USERNAME,
Email = USERNAME
};
var createPowerUser = userManager.CreateAsync(serviceUser, PASSWORD).Result;
if (createPowerUser.Succeeded)
{
var confirmationToken = userManager.GenerateEmailConfirmationTokenAsync(serviceUser).Result;
var result = userManager.ConfirmEmailAsync(serviceUser, confirmationToken).Result;
//here we tie the new user to the role
userManager.AddToRoleAsync(serviceUser, ROLENAME).GetAwaiter().GetResult();
}
}
}
I'm creating an MVC5 app. It is an intranet app. All users are already authenticated to the local Active Directory Domain.
We have an existing database that is currently used for a Windows app.
I want take the User's domain login name and use it to look up the roles and claims that are already configured in that database.
I will assume the the base project of ASP.NET / MVC5 / Authentication "Individual User Accounts" would be the starting point.
Please point me in the right direction.
Thanks
You do not want entire ASP.Net Identity. Instead, you can just use OWIN cookie authentication middleware.
Validate Credential via Active Directory
public bool ValidateCredentials(string userName, string password)
{
using (var context = new PrincipalContext(ContextType.Domain))
{
return context.ValidateCredentials(userName, password);
}
}
Authorize via Old Database
Once authenticated, you want to retrieve authorized role from old Database, and create claims.
private readonly HttpContextBase _context;
private const string AuthenticationType = "ApplicationCookie";
public OwinAuthenticationService(HttpContextBase context)
{
_context = context;
}
public void SignIn(User user)
{
IList<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
};
// Get authorized roles from old database
foreach (Role role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}
ClaimsIdentity identity = new ClaimsIdentity(claims, AuthenticationType);
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignIn(identity);
}
public void SignOut()
{
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignOut(AuthenticationType);
}
Startup.cs
You also need to configure Startup for all those to happen.
[assembly: OwinStartup(typeof(YourApplication.Startup))]
namespace YourApplication
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ApplicationCookie",
LoginPath = new PathString("/Account/Login")
});
}
}
}
I hope you get the starting point.
The .NET OWIN Identity classes require that you authenticate through the CheckPasswordAsync() method of the ApplicationUserManager class. This can be done by overriding the CheckPasswordAsync() method of class ApplicationUserManager. In your override you will need to call the ValidateCredentials() method of class System.DirectoryServices.AccountManagement to authenticate via Active Directory. This will require the user to login to the application with their Windows username and password. There are a few steps to get that to work though.
As you said, you start with a base project with "Individual User Accounts" authentication.
Step 1 - Update the ConfigureAuth() method in file App_Start\Startup.Auth.cs by adding the code below to the ConfigureAuth() method.
using System.DirectoryServices.AccountManagement;
//Add an Owin context for Active Directory principals
app.CreatePerOwinContext(() => new PrincipalContext(ContextType.Domain));
The rest of the updates are done in file App_Start\IdentityConfig.cs
Step 2 - Update the constructor for class ApplicationUserManager.
using System.DirectoryServices.AccountManagement;
//Add a PrincipalContext parameter to the constructor
public ApplicationUserManager(IUserStore<ApplicationUser> store, PrincipalContext principal) : base(store)
{
this.principal = principal;
}
Step 3 - In the Create() method update the call to the constructor for class ApplicationUserManager.
//Add the PrincipalContext parameter
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<PortalIdentityDbContext>()), context.Get<PrincipalContext>());
Step 4 - Override the CheckPasswordAsync() method of class ApplicationUserManager.
//Override CheckPasswordAsync to login via Active Directory.
public override async Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
{
return await Task.FromResult(this.principal.ValidateCredentials(user.UserName, password, ContextOptions.Negotiate));
}
As for using your existing database, you will have to incorporate the OWIN Identity tables in it or vice-versa. The Identity functionality requires those tables and you can't change that. I would create a test project and get familiar with those tables. Then figure out how you want to incorporate them into your existing database or vice-versa. I heavily modify those tables for my custom functionality. But the core tables and columns have to exist.
I created two projects from templates.
The first contains Membership and MVC4. And the second uses MVC5 and Asp.Net Identity.
Then I added signalR to both projects using same code.
In Membership project I can access HttpContext.User both in controllers and SignalR connection class(OnConnected method). But in Identity project I have proper value of HttpContext.User only in controllers. In OnConnected method HttpContext.User returns null.
Code of signalR is same in both projects:
1) SynchronizationConnection.cs
public class SynchronizationConnection : PersistentConnection
{
public SynchronizationConnection()
{
}
protected override Task OnReceived(IRequest request, string connectionId, string data)
{
Debugger.Break();
return base.OnReceived(request, connectionId, data);
}
protected override Task OnConnected(IRequest request, string connectionId)
{
Debugger.Break(); //HttpContext.Current.User == null
return base.OnConnected(request, connectionId);
}
protected override Task OnDisconnected(IRequest request, string connectionId)
{
Debugger.Break();
return base.OnDisconnected(request, connectionId);
}
}
2) Startup.cs
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
ConfigureAuth(app);
}
}
3) Startup.Auth.cs
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.MapSignalR<Services.Realtime.SynchronizationConnection>("/test");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
}
4) Client javascript
var connection = $.connection('/test');
connection.logging = true;
console.log('Receiving connection');
connection.received(function (data) {
console.log('received');
});
connection.disconnected(function () {
console.log('disconnected');
});
connection.error(function (data) {
console.log('error');
});
connection.start().done(function () {
console.log('Connection started');
});
I've seen questions about null User.Identity.Name because of missing [Authorize] attribute. In my case I cannot access even User.Identity. Also I have [Authorize] attribute on my action, that contains client javascript.
Inside of the OnConnected method, you can use:
request.User
That will carry the IPrincipal that was in the HttpContext.Current.User at the time of connection, and it is the one you should mind.
If you need to alter that IPrincipal (like removing the default one generated by Forms authentication and set your own), I recommend you to use an IHttpModule. All the transports in SignalR at least start as an HTTP (even WebSockets), so all transports are going to hit the module at least during connection.
Cheers.
I had the same problem: request.User was null in the PersistentConnection's OnConnected() method. Changing the order in which I initialized SignalR and my authentication provider fixed it. I changed it so that my SignalR endpoints were mapped after my authentication was configured:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
ExpireTimeSpan = new TimeSpan(999, 0, 0, 0),
SlidingExpiration = true
});
GlobalConfiguration.Configuration.UseSqlServerStorage("MyDatabase", new SqlServerStorageOptions() { PrepareSchemaIfNecessary = false });
app.MapSignalR<MyConnection>("/MyConnection");