I'm using ASP.NET 5 with MVC6. I am working with Identity 3.0, but I need to know how to make it works with many webservers.
Is possible to store the session in other place? Database? In MVC5 you did that in the web.config, but I don't found information about it in MVC6.
This is my code in Startup.cs
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.LoginPath = new PathString("/Account/Login");
options.AutomaticChallenge = true;
});
Thanks!!
By default, authentication tickets stored in cookies are self-contained: knowing the encryption key is enough to retrieve the original ticket (there's no store or database involved in this process).
To make sure your authentication cookies are readable by all your servers, you need to synchronize the key ring they use to encrypt and decrypt authentication tickets. This can be done using an UNC share, as mentioned by the documentation: http://docs.asp.net/en/latest/security/data-protection/configuration/overview.html.
public void ConfigureServices(IServiceCollection services) {
services.AddDataProtection();
services.ConfigureDataProtection(options => {
options.PersistKeysToFileSystem(new DirectoryInfo(#"\\server\share\directory\"));
});
}
Alternatively, you could also provide your own TicketDataFormat to override the serialization/encryption logic, but it's definitely not the recommended approach.
Related
Situation
We have clients that should be able to login into our application. Our clients do also have clients, who also may login. Therefore we have an Azure AD B2C environment per client.
So, we want to have one single application that can be used to authenticate against multiple Azure B2C environments. We want to have this route-based. So:
/client1 goes to B2C environment Client1B2C, with user flow B2C_1_Client1
/client2 goes to B2C environment Client2B2C, with user flow B2C_1_Client2
Challenge
So, we need to define multiple instances of AddOpenIdConnect. I do this inside a specific builder, so my Startup.cs keeps clean:
Startup.cs
...
var AzureAdB2CSettings = new List<AzureAdB2COptions>();
Configuration.GetSection("Authentication:AzureAdB2C").Bind(AzureAdB2CSettings, c => c.BindNonPublicProperties = true);
services.AddAuthentication(sharedOptions =>
{
...
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options), AzureAdB2CSettings)
...
And there is the builder:
AzureAdB2CAuthenticationBuilderExtensions.cs
...
public static string policyToUse;
public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action<AzureAdB2COptions> configureOptions, List<AzureAdB2COptions> openIdOptions)
{
...
foreach(var b2c in openIdOptions)
{
builder.AddOpenIdConnect(b2c.SignUpSignInPolicyId, b2c.SignUpSignInPolicyId, options =>
{
options.Authority = b2c.Authority;
options.ClientId = b2c.ClientId;
options.CallbackPath = b2c.CallbackPath;
options.SignedOutCallbackPath = b2c.SignedOutCallbackPath;
options.ClientSecret = b2c.ClientSecret;
});
}
return builder;
}
...
public Task OnRedirectToIdentityProvider(RedirectContext context)
{
...
string policyToUse = "B2C_1_" + context.Request.Query["area"];
...
var b2cSettings = AzureAdB2CSettings.Find(x => x.SignUpSignInPolicyId.ToLower().Equals(policyToUse.ToLower()));
AzureAdB2CAuthenticationBuilderExtensions.policyToUse = b2cSettings.DefaultPolicy;
...
Yippee ya yeeey! We can have a dynamic amount of add AddOpenIdConnect, based on a configuration file. The chosen authentication scheme has been set to the static string "AzureAdB2CAuthenticationBuilderExtension.policyToUse".
But now it comes... how to define the Authorization header?
BackofficeController.cs
...
[Authorize(AuthenticationSchemes = AzureAdB2CAuthenticationBuilderExtensions.policyToUse)]
public async Task<IActionResult> ChooseBackoffice()
{
...
}
...
AUTCH!! You can't use dynamic attributes... Have tried to set the chosen scheme as a default, but it seems we can only define a default at startup, not during runtime...
Any suggestions how to solve this challenge?
One suggestion is to set all possible values of AzureAdB2CAuthenticationBuilderExtensions.policyToUse in config and read from there.
For each action method/controller (as per your use case), define the attribute value from these configs.
It seems indeed impossible at the moment to have multiple B2C environments connected to one Azure App Service.
Therefore there is a choice:
Don't do it. Just create one giant B2C environment.
Make a multi-instance application instead of a multi-tenant application.
Our partner came with another solution. We haven't explored this route. Who knows does this help somebody:
Orchard core. Seems like a multi-tenant .NET Core solution. Looks like a complete application, where this multi-tenant question will be handled.
We did choose option 2. This makes sure we have a good separation of data. There are more hosting costs, although with a multi-tenant application all the traffic does to one application. This does require better hardware, so is also more expensive. I do not know which option is more expensive.
Now comes the question how to deploy this efficiently, but that's another question...
I'm new to using Identity Server for SPA auth but I started following this example: Authentication and authorization for SPAs and with some tinkering I've now also added Google auth. However, I'm having trouble getting the external Google claims merged into my application's claims (for example: given_name).
I've verified that Google does send back the appropriate claims but nothing seems to map those claims, e.g. options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");. When I access one of my protected endpoints my claims do not include any of the additional google claims.
I did find some additional documentation Persist additional claims... which tells me to add the claim in OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) but since this is an SPA that page doesn't exist. Is there another approach to this? I haven't been able to find much that doesn't use the Page / OnPostConfirmationAsync.
Thanks
Including relevant details from my Startup.cs in case I'm doing something wrong here:
I've tried a few different variants from other examples I've found but
services
.AddDefaultIdentity<AppUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<AppRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddApiAuthorization<AppUser, ApplicationDbContext>();
services
.AddAuthentication()
.AddIdentityServerJwt()
.AddGoogle(options =>
{
options.ClientId = Configuration["Auth:Google:ClientId"];
options.ClientSecret = Configuration["Auth:Google:ClientSecret"];
options.AuthorizationEndpoint += "?prompt=consent"; // Hack so we always get a refresh token, it only comes on the first authorization response
options.AccessType = "offline";
options.SaveTokens = true;
options.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
options.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
})
And my api is simply:
[Authorize()]
[Route("test")]
public IActionResult Test()
{
var all = User.Claims.Select(s => $"{s.Type}: {s.Value}");
return Ok(all);
}
The only way I've been able to handle this is to scaffold the needed ExternalLogin page (like #d_f mentioned in the question's comments) and then continue following the Persist additional claims steps. I was hoping I could just set a collection of claims to keep in my Startup file and it would work but Identity Server just uses its internal ExternalLogin (Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal) and the code there doesn't handle adding external claims. This works for now but I would prefer a way of not needing to scaffold the ExternalLogin page.
I tried asking for clarification on the official docs but my question was closed (instead they suggested I ask on StackOverflow - lol). However there seems to be some work being done on improving the docs and flowing the claims through, if interested you can dig through this GitHub issue: https://github.com/dotnet/AspNetCore.Docs/issues/16488
See Update below
I'm using Azure AD B2C and I'd like my users to be able to log in thru my web app as well as be able to utilize JWT bearer tokens and call Web API methods from a mobile app.
I can get either authentication scheme to work by itself. For example, in my startup.cs I can do the following:
services
.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
which works as expected (a user can login on the web site, but JWT doesn't work).
Alternatively, I can instead use the following and then only JWT bearer tokens will work:
services
.AddAuthentication(AzureADB2CDefaults.JwtBearerAuthenticationScheme)
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
If I want either to work, I can do the following (with the help of https://stackoverflow.com/a/49706390)
services
.AddAuthentication()
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(AzureADB2CDefaults.AuthenticationScheme, AzureADB2CDefaults.JwtBearerAuthenticationScheme)
.Build();
}
And now either will work. (edit: actually, they don't work completely)
HOWEVER, I also have this code:
app.UseAuthentication();
app.UseMiddleware<MyAfterAuthenticatedMiddleware>();
app.UseAuthorization();
The problem is that when I use the combination of either authentication, when my middleware code runs, my user is not authenticated (in the middleware code) and has no claims, etc. but obtains them later in the pipeline.
What's happening here? And how can this be fixed?
It seems that when I don't specify a default authentication scheme--in order to have multiple schemes--the authentication is not happening until the authorization step in the pipeline.
I need my middleware to run after authentication and before authorization.
How can I make that happen with the multiple authentication schemes?
UPDATE -- Solved! But there must be a better way!??
First of all, to the people who have created the .NET security stuff, I say kudos. It's important and it's difficult. However I do think there may be a lot of room for improvement.
Most developers dabble in security when they have to, and then go back to their "regular" job". Unless you work with it every day, it's tough to keep on top of. Every time you go back to it, everything's changed yet again.
It must be a common scenario: I want my users to be able to log in to my web site and interact with various web API methods. I would like them to also be able to access those same API methods via another means, such as a mobile app--where I'd be using JWT tokens, or equivalent.
This shouldn't be hard to make work.
However, I was tying myself into knots creating handlers for this and policies for that. One thing would work, but another thing wouldn't. Then when I thought things were right--I discovered some of the challenge and forbid logic didn't work as expected.
The built-in Authorization middleware has the ability to do authentication -- this was one of the early roads I went down, only to discover that it didn't fully work -- and it caused other problems for me, as described above.
In my opinion, authentication should not happen during authorization. Authentication should happen where it is expected--in authentication middleware. (My guess is that it was added in authorization in order to work around some other problem that presented itself years ago -- and perhaps still exists today)
Anyway--here is how I finally got things to work. It could be a lot cleaner and slicker and more flexible, but it works for my needs. And it is less of a hack than anything else I have seen. But is there a nicer, built-in class that could have done this for me?
My new question is this: is there a better way to get this done than how I've done it as described below?. It's hard to believe this is the best way.
In Startup.ConfigureServices I have now have the following:
services
.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
I then also have:
services.AddHttpContextAccessor();
services.AddSingleton<IAuthenticationSchemeProvider, MyAuthenticationSchemeProvider>();
And finally, I have a new class:
public class MyAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
public MyAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IHttpContextAccessor httpContextAccessor) : base(options)
{
HttpContextAccessor = httpContextAccessor;
}
protected MyAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes, IHttpContextAccessor httpContextAccessor) : base(options, schemes)
{
HttpContextAccessor = httpContextAccessor;
}
private IHttpContextAccessor HttpContextAccessor { get; }
private bool IsBearerRequest()
{
var httpContext = HttpContextAccessor.HttpContext;
return httpContext.Request.Headers.ContainsKey("Authorization")
&& httpContext.Request.Headers["Authorization"].Any(x => x.ToLower().Contains("bearer"));
}
public async Task<AuthenticationScheme> GetMySchemeAsync()
{
return IsBearerRequest()
? await GetSchemeAsync(AzureADB2CDefaults.BearerAuthenticationScheme)
: await base.GetDefaultAuthenticateSchemeAsync();
}
public override async Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
{
return await GetMySchemeAsync();
}
public override Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
{
return GetMySchemeAsync();
}
public override Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
{
return GetMySchemeAsync();
}
}
Now I can use both kinds of authentication, the challenge & forbid work as expected. Why isn't there a built-in class that allows for switching between authentication schemes? Why does the authorization middleware attempt to authenticate with multiple schemes (I say it shouldn't do it at all), but not the authentication middleware?
Now this is here for anyone else who struggles with a similar issue.
As described in https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-3.1#session-state , one can add session to one's web app like so in Start.ConfigureServices
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
and in Startup.Configure
App.UseSession()
There is also the possibility to use cookie authentication without identity via the authentication middleware, as described here https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
...
});
My question is, if I, in Startup.Configure, use both
App.UseSession()
App.UseAuthentication()
Which Cookie settings will be used? Will the Cookie settings in services.AddSession be completely irrelevant (because the Authentication middleware also uses Session Cookies to keep track of users, right? Or am I completely wrong about that)? Or will they just be two different sessions/services running at the same time?
I am aware that Startup.Configure (the HTTP pipeline) is order-sensitive, as described my Microsoft "Adding the middleware to the app processing pipeline is order sensitive—it only affects downstream components registered in the pipeline." My second question is thus, if I put App.UseCookiePolicy(options) before the above, would it override the settings?
App.UseCookiePolicy()
Thanks in advance for any answers!
In ASP .NET Webforms, the MachineKey (a guid) was an important part of database security as well as application security. It was used as an encryption key for passwords for forms authentication, so that if the machine key was changed, existing passwords would no longer be recognised.
This did provide some level of safety around linking the password store in the db to a token on the webserver.
If there were multiple webservers using a single database authentication store, it was necessary for them all to have the same machine ID.
In .NET Core MVC we now have several services we declare in Startup.cs:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options => {
options.ExpireTimeSpan = new TimeSpan(0, 45, 0);
options.LoginPath = new PathString("/Account/Login");
options.AccessDeniedPath = new PathString("/Account/AccessDenied");
}
);
services.AddDataProtection()
.SetApplicationName("MyApplicationName")
.PersistKeysToFileSystem(new System.IO.DirectoryInfo(#"C:\server_config\my-app-keys"));
I've had a look at the methods for each service, and none jumps out as providing a data store encryption key. I thought perhaps the ApplicationName might be used as a key, but if I change it, I can still log in with the old passwords, so it's clearly not being used to encrypt.
This article stipulates isolation as one of the key requirements of the API:
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-3.0
and this one has a simple example using a 'Purpose String' to provide encryption isolation between different logical stores:
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/using-data-protection?view=aspnetcore-3.0
however I can't seem to put this together to tie the password encryption in .NET Core to a specific key.
What am I missing in regard to setting a specific unique encryption key for the standard Asp Net Identity injection, to be used for passwords stored in the database?
If I remember correctly you can do this using the following :
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.AspNet.DataProtection.Repositories;
namespace MySpace
{
public class MyXmlRepository : IXmlRepository
{
public MyXmlRepository()
{
// Whatever I wanted injected in.
}
public IReadOnlyCollection<XElement> GetAllElements()
{
return null;
}
public void StoreElement(XElement element, string friendlyName)
{
// Persist something
}
}
}
If you register this in the IOC it'll start using it for key persistence, you can then do whatever you like.