Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 774
mkanakis Avatar asked Feb 02 '19 14:02

mkanakis


3 Answers

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)
like image 129
Vidmantas Blazevicius Avatar answered Oct 16 '22 17:10

Vidmantas Blazevicius


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 :)

like image 24
DaImTo Avatar answered Oct 16 '22 16:10

DaImTo


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.

like image 40
R.Titov Avatar answered Oct 16 '22 15:10

R.Titov