Home > Mobile >  ASP.NET Core with React.js API authorization
ASP.NET Core with React.js API authorization

Time:11-25

I am trying to create an ASP.NET Core with React.js project with API authorization but struggling to find documentation/instructions that make sense.

https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-7.0 seems like a good reference, but it is using Entity Framework which I am not. My goal is to manage user authentication without EF.

The React template created by dotnet new react -au Individual provides AuthorizeService.js and OidcConfigurationController.cs which I have linked here: https://gist.github.com/julesx/d3daa6ed5a7f905c984a3fedf02004c0

My program.cs is as follows:

using Duende.IdentityServer.Models;
using Microsoft.AspNetCore.Authentication;

var ApiScopes = new List<ApiScope> {
    new ApiScope("api1", "My API")
};

var Clients = new List<Client> {
    new Client {
        ClientId = "client",

        // no interactive user, use the clientid/secret for authentication
        AllowedGrantTypes = GrantTypes.ClientCredentials,

        // secret for authentication
        ClientSecrets =
        {
            new Secret("secret".Sha256())
        },

        // scopes that client has access to
        AllowedScopes = { "api1" }
    }
};

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllersWithViews();

builder.Services.AddIdentityServer()
    .AddDeveloperSigningCredential()
    .AddInMemoryApiScopes(ApiScopes)
    .AddInMemoryClients(Clients);

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");

app.MapFallbackToFile("index.html");

app.Run();

After struggling to get this far, the app starts successfully (dev environment).

My fetch from the front end is as follows:

export async function getKpiData(): Promise<IRawKpi[]> {

    const token = await authService.getAccessToken();

    const response = await fetch('/kpidata', {
        headers: !token ? {} : { 'Authorization': `Bearer ${token}` }
    });

    if (response.status == 200) {
        return response.json();
    }

    return [];
}

this causes a get request to the OidcConfigurationController which fails with following error: Unable to resolve service for type 'Microsoft.AspNetCore.ApiAuthorization.IdentityServer.IClientRequestParametersProvider' while attempting to activate 'MyNamespace.Controllers.OidcConfigurationController'.

I know this is occurring because I am not registering the IClientRequestParametersProvider injected into the OidcConfigurationController, however when I look at the sample code I don't see it being injected there either. I also do not see anything obvious I should be injecting into the Program.cs builder.Services.

Am I on the right track at all? The amount of "arcane" knowledge required to configure this seems overwhelming. Is there a quality example somewhere I can refer to? What is the bare minimum requirement for Program.cs to achieve some super basic authentication?

CodePudding user response:

IClientRequestParametersProvider is registred with the call to

builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

The problem is that there is no "in memory" store for users per default. Clients in this case is the application and not the user. The default authentication template uses "IdentityServer" which is based on "MS Identity" which in itself is using EF Core. So to get this working you kind of have to use EF Core. What you can do is instead of using sqlserver to have an inmemory database to store the users.

To do that in the csproj replace the SqlServer PackageReference for "Microsoft.EntityFrameworkCore.SqlServer" with "Microsoft.EntityFrameworkCore.Sqlite"

Then in Program.cs replace

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

with

var connection = new Microsoft.Data.Sqlite.SqliteConnection(connectionString);
// Open the inmemory database to make sure every instance of DbContext gets the same database all the time
connection.Open();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connection));

this ensures that the database is always the same and not recreated.

In appsettings.json set the DefaultConnectionString to "DataSource=:memory:" for an inmemory database based on sqlite (which is more recommended then the actual "InMemory" EF-Core datasource).

Starting now will give you a clean database on each start but keeps the database open between different instances of DbContext. Now you need to "seed" the users you want on each start. For that you can create an entry in appsettings.json:

  "Users": {
    "[email protected]": "Password1!" 
  },

And then in Program.cs under var app = builder.Build(); make sure the database is created and then loop over that users and seed them to the database like that:

// Create a new service scope for seeding the database
using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    // Make sure we create the in-memory database first
    await dbContext.Database.EnsureCreatedAsync();
    await dbContext.SaveChangesAsync();

    // Get the usermanager from that scope. UserManager configures MS Identity Users.
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();

    // Loop over the key value pairs in the "Users" Section of the appsettings.json
    foreach (var (user, password) in app.Configuration.GetSection("Users").Get<Dictionary<string, string>>())
    {
        // Create the user and make sure the user can log in
        var result = await userManager.CreateAsync(new ApplicationUser()
        {
            UserName = user,
            EmailConfirmed = true
        }, password);

        // Throw if anything goes wrong (e.g. password not safe enough)
        if (!result.Succeeded) throw new Exception(string.Join(", ", result.Errors.Select(x => x.Description)));
    }
}

Here is the gist for the full Program.cs and appsettings.json: https://gist.github.com/WolfspiritM/cf74430e4178cdaea94de5109413e796

  • Related