.NET Core route based authentication with multiple B2C environments - c#

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...

Related

Multiple Authentication Schemes ASPNET Core 3

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.

Implement two authentication options (Token and Certificate) in ASP Net Core

[Target netcoreapp3.1]
Hi there! So I have this Web Api that is protected by a middleware of this form in my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
//other services configuration
services.AddProtectedWebApi(options => { /* config */};
//other services configuration
}
This verifies Jwt Tokens issued by Azure and grants access to the API; it works fine.
At present, I have a front-end angular client website where a user signs in via Azure AD. Angular sends the token to my web API and everything works.
I would now like to use the same webapp to handle query requests from a user without credentials, but with a client certificate that would have been provided in advance. So basically, I'd like to authenticate on my Angular WebSite via Azure OR via a client cert. Angular would then follow up the information to my webapp, which would in turn authenticate the user with the appropriate method.
To be clear, I still want someone to be able to log in without a certificate by using his Azure account.
Is there a simple way to have two authentication options in this case without having to create a separate webapp? I read a bit there : https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.1#optional-client-certificates
But it seems it'd only work on the preview of ASP.NET Core 5, which I can't use in my situation.
Hope what follows will help someone!
I eventually found this link : https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1
It explains how to implement multiple authorization policies that both have a chance to succeed. Below is the solution I found using IIS after a bit more research:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//other services configuration
services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = true;
});
services.Configure<CertificateForwardingOptions>(options =>
{
options.CertificateHeader = {/*your header present in client request*/};
});
//other services configuration
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes =/*Whatever you need*/;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
if ({/*CertValidationClass*/}.ValidateCertificate(context.ClientCertificate))
{
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
}
};
});
services.AddProtectedWebApi(options => { /* config */};
//other services configuration
}
{CertValidationClass} being a service or helper class custom made to verify all I have to verify to approve the certificate. Obviously you can add a lot more verifying and actions on your own to this template.
I already had app.UseAuthentication(); app.UseAuthorization(); in my middleware pipeline, no need to change that, but you do have to add app.UseCertificateForwarding(); before these two.
Now I just had to specify above the controller I wanted to protect that I wanted to use both Authorization methods, and just like that, if one fails, it falls back on the other and it works perfectly, I tested by making requests via Insomnia with/without tokens and with/without certficates.
MyApiController.cs
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class MyApiController
{
//Just add the schemes you want used here
private const string AuthSchemes =
CertificateAuthenticationDefaults.AuthenticationScheme; + "," +
JwtBearerDefaults.AuthenticationScheme;

User creation with IdentityServer4 from multiple API's

So I have been bashing my head for a while with this problem.
We have one web app that is using IdentityServer4 and AspNetIdentity to authenticate and register users (this is working as intended).
In addition, we have an other API (inside the same solution) that is able to use IdentityServer4 to authenticate users accessing the API.
However, the problem is, that besides authentication we cannot use the API to create new users.
For instance, users should be able to create other users through the web API and not only from the web app, because in our case, users are linked to other users (think of it as multiple profiles).
I am not really familiar with all the configuration services that come up with .Net Core framework and I have tried multiple ways of accessing the user manager of the web app through the API to register my users through classic POST requests but nothing seems to be working. Searching online is tricky because our problem is kind of very specific, that's why I am posting here.
API Startup.cs - ConfigureServices:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = Configuration["IdentityServer:Url"];
// name of the API resource
options.ApiName = Configuration["IdentityServer:APIName"];
options.ApiSecret = Configuration["IdentityServer:APISecret"];
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10); // that's the default
options.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityServer:RequireHttpsMetadata"]);
});
API Startup.cs - Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("AllowAllOrigins");
app.UseAuthentication();
app.UseMvc();
}
API UsersController.cs - Constructor:
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _context;
public UsersController(IUserService service,
ApplicationDbContext context,
UserManager<ApplicationUser> userManager)
{
_service = service;
_userManager = userManager;
_context = context;
}
Now the problem is that when I start the API and try to access the UsersController I get the following error:
System.InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UserManager`1[XXXXX.Data.Models.ApplicationUser]' while attempting to activate 'XXXXXXX.Api.Controllers.UsersController'.
I sincerely hope I can find at least some advice on how to proceed with it.
Please if something is unclear reply and I will be more than happy to add more information or make things clear.
Kind regards,
Marios.
EDIT:
Thanks all for replying. The code snippet provided below by #Vidmantas did the trick.
Due to my limited knowledge of .net core I did a lot of trial and error in the configure services function which, as you can imagine, didn't work. I strongly believe that using .net core is kind of easy (e.g. API), but when it comes to configuring services the complexity (puzzling/confusing mostly) explodes.
As for the architecture, you gave me good ideas for future refactoring. Notes taken.
Marios.
If I understand you correctly, then you are not really supposed to create users through the API - that is why you have Identity Server 4 in place - to provide central authority for authentication for your user base. What you actually need:
a set of API endpoints on the Identity Server 4 side to manage AspNetIdentity
completely new API but one that shares the same database with Identity Server 4 for your AspNetIdentity
have your API share the database for AspNet Identity
If you go with the last option then you probably need something like below to add the:
services.AddDbContext<IdentityContext>(); //make sure it's same database as IdentityServer4
services.AddIdentityCore<ApplicationUser>(options => { });
new IdentityBuilder(typeof(ApplicationUser), typeof(IdentityRole), services)
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>()
.AddEntityFrameworkStores<IdentityContext>();
This will give you enough services to use the UserManager and it won't set up any unnecessary authentication schemes.
I would not recommend the last approach due to the separation of concerns - your API should be concerned about providing resources, not creating users and providing resources. First and second approach are alright in my opinion, but I would always lean for clean separate service for AspNetIdentity management.
An example architecture from one of my projects where we implemented such approach:
auth.somedomain.com - IdentityServer4 web app with AspNetIdentity for user authentication.
accounts.somedomain.com - AspNetCore web app with AspNetIdentity (same database as Identity Server 4) for AspNetIdentity user management
webapp1.somedomain.com - a web app where all your front end logic resides (can ofcourse have a backend as well if AspNetCore MVC or something like that)
api1.somedomain.com - a web app purely for API purposes (if you go single app for front end and backend then you can combine the last two)
I have a similar situation as you do.
Identity server with asp .net identity users. (DB contains clients and user data)
API (database contains access to application data) .net Framework
Application .net Framework.
Our use case was that normally new users would be created though the identity server. However we also wanted the ability for the application to invite users. So i could be logged into the application and i wanted to invite my friend. The idea was that the invite would act the same as if a user was creating themselves.
So it would send an email to my friend with a code attached and the user would then be able to supply their password and have an account.
To do this i created a new action on my account controller.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Invited([FromQuery] InviteUserRequest request)
{
if (request.Code == null)
{
RedirectToAction(nameof(Login));
}
var user = await _userManager.FindByIdAsync(request.UserId.ToString());
if (user == null)
{
return View("Error");
}
var validateCode = await _userManager.VerifyUserTokenAsync(user, _userManager.Options.Tokens.PasswordResetTokenProvider, "ResetPassword", Uri.UnescapeDataString(request.Code));
if (!validateCode)
{
return RedirectToAction(nameof(Login), new { message = ManageMessageId.PasswordResetFailedError, messageAttachment = "Invalid code." });
}
await _userManager.EnsureEmailConfirmedAsync(user);
await _userManager.EnsureLegacyNotSetAsync(user);
return View(new InvitedViewModel { Error = string.Empty, Email = user.Email, Code = request.Code, UserId = user.Id });
}
When the user accepts the email we add them.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Invited([FromForm] InvitedViewModel model)
{
if (!ModelState.IsValid)
{
model.Error = "invalid model";
return View(model);
}
if (!model.Password.Equals(model.ConfirmPassword))
{
model.Error = "Passwords must match";
return View(model);
}
if (model.Terms != null && !model.Terms.All(t => t.Accept))
{
return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction(nameof(Login), new { message = ManageMessageId.InvitedFailedError, messageAttachment = "User Not invited please invite user again." });
}
var result = await _userManager.ResetPasswordAsync(user, Uri.UnescapeDataString(model.Code), model.Password);
if (result.Succeeded)
{
return Redirect(_settings.Settings.XenaPath);
}
var errors = AddErrors(result);
return RedirectToAction(nameof(Login), new { message = ManageMessageId.InvitedFailedError, messageAttachment = errors });
}
The reason for doing it this way is that only the identity server should be reading and writing to its database. The api and the third party applications should never need to directly change the database controlled by another application. so in this manner the API tells the identity server to invite a user and then the identity server controls everything else itself.
Also by doing it this way it removes your need for having the user manager in your API :)
I would not recommend you to use shared database between different API's.
If you need to extend Identity Server 4 with additional API you can use LocalApiAuthentication for your controllers.

Multiple Google Authentication scopes in .NET Core depending on controller

I have a web based app. This app allows users to sign up/in using Google Auth as per this code in Startup.cs
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["ClientId"];
googleOptions.ClientSecret = Configuration["CliSecret"];
...
});
This all works nicely with the out-of-the-box Identity system so I can register users.
However, I also want users to be able to 'connect' to other Google services with separate accounts after the sign up in a separate area of the site.
For example, I might want a user to connect their AdWords account.
They will authenticate with Google via a non-Identity flow and the relevant info (token, refresh token etc) will be stored independantly in the db (i.e it won't store a 'User' in the AspNetUSers table).
Can I change the authentication scope in the controller before I make my initial call to google?
It'd be nice to utilize the same Authentication service but with some extra scope in this case. Is that possible?
Alternatively, have another Google section in Startup.cs...maybe like:
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["ClientId"];
googleOptions.ClientSecret = Configuration["CliSecret"];
googleOptions.Scope.Add("https://www.googleapis.com/auth/adwords"); //*** THIS IS THE EXTRA SCOPE NEEDED ***
...
});
We had similar problem, our Identity Provider should be able to login users of defferent clients with different Google account
We decided to add multiple Google areas as you suggested. The main point here is that each area (which defines some google account) uses unique cookie scheme.
When we create login URL, we get google account needed for that client, get it's cookie scheme and create correct URL for Google Authenticate button
code example:
public static class AuthenticationBuilderGoogleAdder
{
public static AuthenticationBuilder AddGoogleAuth(this AuthenticationBuilder authenticationBuilder, IServiceCollection services)
{
var serviceProvider = services.BuildServiceProvider();
// create IThirdPartyProvidersProvider realization with GetByProviderCode method
var authThirdPartyProvidersProvider = serviceProvider.GetService<IThirdPartyProvidersProvider>();
var googleProviders = authThirdPartyProvidersProvider.GetByProviderCode("google");
googleProviders.ForEach(p =>
{
authenticationBuilder = authenticationBuilder.AddGoogle(p.CookieScheme, options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = p.ClientId;
options.ClientSecret = p.ClientSecret;
});
});
return authenticationBuilder;
}
}
register it as
services.AddAuthentication()
.AddGoogleAuth(services)
We call services.BuildServiceProvider() in order to create another container with services which were already registered in DI, in order to get Google accounts with different cookie schemas from the DB

ASP.NET 5 Identity 3.0 scalability with CookieAuthentication

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.

Categories

Resources