User creation with IdentityServer4 from multiple API's - c#

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.

Related

Authentication/Authorization with ASP .NET Core and React based on Identity

I've been trying to create a SPA web application using React with ASP .NET Core 3.1 as backend, and now I need to restrict users going to certain pages. I know that for API methods I can do the following:
[Authorize(Roles="admin")]
[HttpGet]
public async Task<Whatever> Get(){ ... }
But that would only block users from using the API methods, which is good, but I also want to not let them go into the pages themselves.
Since I do not want to use their Blazor pages (because it breaks the separation of the client app and the backend), I cannot scaffold their login page, so I created mine using React and then implemented login and logout methods, which work, since when I'm logged in, the AspNetCore.Identity.Application cookie appears.
In order to do this, I added the following lines to Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddIdentityCore<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
services.AddAuthentication (o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
}).AddIdentityCookies(o => {});
...
}
public void Configure(IApplicationBuilder app)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}
Now I've read (source: https://stackoverflow.com/a/40055744/14806778) that, in React, you can define an onEnter method and check authentication there. So, to check if user is logged in or not, I implemented the simple method:
public bool IsUserLoggedIn()
{
return User.Identity.IsAuthenticated;
}
This also works, so I guess I could call this method in the onEnter method of React routing. I don't know if that's efficient or not, though.
Last, to check if user is in role, I could do something like this:
public async Task<bool> IsUserInRole(string requiredRole)
{
if(User.Identity.IsAuthenticated)
{
var user = await _userManager.GetUserAsync(User);
return await _userManager.IsInRoleAsync(user, requiredRole);
}
return false;
}
And call this onEnter instead.
My question is, is this approach valid? Is it safe? Does it have a huge performance impact? What are the alternatives? I've looked around a bit but I haven't seen a lot. I've read about JWT but I don't know how is it so different to this.
Also, I don't need Google/Apple/Facebook login, this is for an app which will be most likely running on localhost inside a VPN. I am using .NET Core 3.1, React 16.14 and React router dom 5.2.0.
Thank you.

.NET Core route based authentication with multiple B2C environments

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

ASP.Net Core WebAPI Authorization Policy for User or Admin

I have a controller that returns data about users. I want to set the authorization such that an admin can access this controller and retrieve data for any user, and a non-admin user can access the controller and retrieve data for themselves.
I've ruled out using [Authorize (Roles = "Admin")] because this means users can't get their own data. So I've inserted the following logic into the controller action:
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Name).Value;
var roles = _httpContextAccessor.HttpContext.User.FindAll(ClaimTypes.Role);
var query = roles.Select(r => r.Value).Contains("Admin");
Customer customer =await _context.Customers.FindAsync(id);
if (!(customer.EmailAddress == userId || query))
return Unauthorized();
This is roughly equivalent to this Stack Overflow answer, but for ASP.Net Core rather than MVC.
My question is, is there a way to do this with an Authorization Policy? Adding the RequireRole check is straightforward and covered in the MS Documentation as well as countless blogs, but I couldn't find or figure out a way to use a policy to check that the data the user is trying to access is their own.
I'm sure this isn't an uncommon requirement, is there a way to do this, or is what I'm currently doing OK? The only other approach I could think of was to have two separate endpoints, but both options seem inelegant.
The policy is for authorization , but either Admin or A normal user can access the controller , they are all authorized .
That is your custom logic to determine which data should be returned , that is nothing related to authorization . If you insist on using policy , you can put the logic to handler but that is nothing change when logic is in controller :
public class CustomerHandler : AuthorizationHandler<CustomerRequirement>
{
IHttpContextAccessor _httpContextAccessor = null;
public CustomerHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
CustomerRequirement requirement)
{
HttpContext httpContext = _httpContextAccessor.HttpContext;
//your logic
httpContext.Items["message"] = "ownData";
context.Succeed(requirement);
return Task.CompletedTask;
}
}
And read in controller so that you can know whether read his own data or all users' data :
var message = HttpContext.Items["message"];
In my option ,set two endpoints/function in your web api , one for admin , one for user is the clean way . In addition , that is your client app's responsibility to determine that current user wants to return his own data or all user's data . That seems not quite correct to send request to web api and let api to determine by logic . Webapi should include the clean functions/endpoint to map each request from client .

Populating User.Identity in ASP.NET MVC 5

I'm writing a simple chat application using ASP.NET MVC 5 and SignalR. The application doesn't require any complicated authentication logic. User simply enters their login and enters the system (if there was no such user in the db before, it's created).
My intent was to use Session to hold the logged in user and their information (id from the database and login/username) and write a global filter to check if user is authenticated on each request. I've got some problems with SignalR though. It's not possible to access the Session from the SignalR Hub, while I need it to find out the login of the user who sent the message.
As fas as I found out, it's possible to work with the User.Identity using SignalR's Context. However, in my case Uder.Identity is completely empty. Presumably because I've created the app as 'no authentication' and the mechanism that User.Identity uses to get user data is not aware of my manipulation with session.
The question is, is it possible to elegantly intergate User.Identity into my application and make it aware of the Session? Creating ASP.NET MVC project with individual user accounts creates a mess with stuff like
public AccountController() :
this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
{
}
and that's what I don't want to have in my application by any means, since I want to write it as clean as possible and not use any solutions I am not familiar with. I also don't need any external login providers, cookies, etc.
I was thinking about implementing some in-memory storage on my own. However, I would still have to clean this store up at some point of time. I though of cleaning it up when the Session_End event is fired. However, this event will only be fired if there is data in Session which I don't want to have since it would be quite awkward to have standalone in-memory storage and rely on Session events to clean it up and, moreover, to set some data in Session just to make sure Session_End will fire.
Here's the solution I came up with. It's still not as clear as I would like it to be and it uses cookies, so any additions are welcome.
First of all, I had to install Microsoft.AspNet.Identity.Owin package and all its dependencies.
Then I registered my auth as follows:
private void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
This method is then called in Configuration method of Startup.cs file.
In order to work with the authentication, an instance of IAuthenticationManager is required. I inject it into my controller and use Ninject to resolve the dependency
kernel.Bind<IAuthenticationManager>().ToMethod(_ => HttpContext.Current.GetOwinContext().Authentication).InRequestScope();
Here's the Login method of Account controller which user is redirected to when auth is required (thanks to LoginPath in ConfigureAuth method):
[HttpPost]
public ActionResult Login(LoginViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = authenticationService.AuthenticateUser(model.Login);
IdentitySignIn(user.Id, user.Login);
return RedirectToAction("Index", "Home");
}
AuthenticationService is my own class which communicates with the database and performs the login to create or return a user.
IdentitySignIn is declared as follows:
private void IdentitySignIn(int userId, string userLogin)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.PrimarySid, userId.ToString()));
claims.Add(new Claim(ClaimTypes.Name, userLogin));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties()
{
ExpiresUtc = DateTime.UtcNow.AddDays(200),
IsPersistent = true
}, identity);
}
This method creates a cookie with appropriate info. There is one thing, though. When I check the cookie expiration date, it's not the current date plus 200 days, which is kinda awkward.
SignOut method is quite simple:
public void IdentitySignout()
{
authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
}
So, User.Identity is now accessible in the SignalR hub with the Identity.Name property.
To do: it would be also nice to get access to the Id property via something like User.Identity.Id. As far as I know, it requires implementing custom Principal.
I am also still thinking of implementing some sort of session of my own using cookies to store the session id on client side, though it will definitely take more time than using Identity.
Addition:
in order to get user id, one might use the extension method of IdentityExtensions:
(Inside the Hub)
Context.User.Identity.GetUserId()
In order for this to work, the Claim with the value of user's id should have the type ClaimTypes.NameIdentifier.
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString()));
claims.Add(new Claim(ClaimTypes.Name, userLogin));
Update 2:
Here are some additional links on the subject that greatly helped me. I do not include links to MS guides since they are quite easy to find.
http://leastprivilege.com/2015/07/21/the-state-of-security-in-asp-net-5-and-mvc-6-claims-authentication/
http://weblog.west-wind.com/posts/2015/Apr/29/Adding-minimal-OWIN-Identity-Authentication-to-an-Existing-ASPNET-MVC-Application

ASP.NET Web Api Authentication Methods

I am trying to implement authentication for my web api.
I have read about different techniques of api authentication and the token technique is the most reasonable for me.
I read different articles about how to implement token based authentication in asp.net but they all rely on different libraries such as OAuth or Owin which also provide their own method of database interactions.
The thing is that I have already implemented database interaction with abstract repositories and entities and I would like to find out how can I implement api authentication easily and simply without interfering with my current design.
(By the way, my project is built on top of an empty web api project, so it doesn't come with all the bootstrap and authentication classes).
Thank you
One solution I've seen is to use .NET's HttpApplicationState class and store tokens in appstate; this way you're not directly messing with Session (which would be a REST antipattern), but you can still track all currently logged in users and use HttpContext/HttpActionContext to x-ref active tokens in the app. The benefit to using HttpActionContext is that it is thread-safe, whereas HttpContext is not, so you can lock the appstate, mess with the HttpContext of an individual request, and then unlock the appstate to allow other threads in.
Since locking/unlocking appstate does tie up the app, I'm not sure how well this solution scales, but here it is anyway . . .
General outline:
When a user first logs in, a token is generated for him/her and stored in appstate. Then you can tag any API calls that require authentication (or that need other information stored on that user) with a custom attribute that checks for that token in the appstate, sending the token name as a header in the API call (e.g. "{token-name: TOKEN}").
Here's a brief example:
[in Controller method first activated at login:]
CustomUserObject user = new CustomUserObject();
//store user props
string token = Guid.NewGuid().ToString();
//create AppState instance, mine's called _appState
//...
_appState.Lock();
_appState[token] = user;
_appState.UnLock();
//...
[Then in global.asax:]
public class CustomAuthorize : System.Web.Http.AuthorizeAttribute
{
HttpRequestMessage request = actionContext.ControllerContext.Request;
string token = string.Empty;
if (request.Headers.GetValues("token-name") != null)
{
token = request.Headers.GetValues("token-name").FirstOrDefault().ToString();
IAppStateService appService; //<--- I've created a custom service tier class for appstate stuff
//Get appState instance, however makes sense for you.
//I'm using repo pattern with UnitOfWork, so mine looks like this...
//"IContainer ioc = DependencyResolution.IoC.Initialize();"
//"IAppStateService appService = ioc.GetInstance<IAppStateService>();"
appService.SetHttpApplicationState(HttpContext.Current.Application);
bool isAuthorized = appService.CheckTokenAndDoStuff(token);
//inside that method ^^^ you'll do stuff like
//"_appState.Lock();"
//"if (_appState[token] == null) return false" (or whatever)
//"_appState.Unlock();"
}
if (isAuthorized)
{
HttpResponseMessage resp = request.CreateResponse(HttpStatusCode.OK);
resp.Headers.Add("AuthenticationToken", token);
resp.Headers.Add("WWW-Authenticate", "Basic");
resp.Headers.Add("AuthenticationStatus", "Authorized");
}
return isAuthorized;
}
[then in webapi]
[HttpPost]
[CustomAuthorize]
public HttpResponseMessage NameOfMethod(...)...
...and that should x-check your appstate for your user token for you. Just make sure to include your token in your request header, and make sure to include the Basic Auth info in your response header.

Categories

Resources