User creation with IdentityServer4 from multiple API's
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.