2

I have setup a donet core (v6) C# API, with Entity Framework (SQLExpress backend). I want the API to:

  1. serve some application data (as an example, I've a "todo items" table).
  2. manage users - register, login (get access token) etc via individual accounts stored in my database (just using the AspNet identity tables setup for me automatically).

Additional context - I'll eventually hook this up to a Single Page App (SPA). API example is here: https://github.com/rmccabe24/AuthTest.

EF packages used:

enter image description here

I'd like to lock down API endpoints using the [Authorize] tag so that those endpoints are only accessible to valid users (and later users with specific roles). To do this I've setup AspNetCore.Identity. Packages used:

enter image description here

My Program.cs file has the following setup for both Entity Framework and AspNetCore Identity:

...
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<ApplicationUser>(options => {
    //Disable account confirmation.
    options.SignIn.RequireConfirmedAccount = false;
    options.SignIn.RequireConfirmedEmail = false;
    options.SignIn.RequireConfirmedPhoneNumber = false;
}).AddEntityFrameworkStores<ApplicationDbContext>();

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

Running the EF migration commands (Enable-Migrations, Add-Migration, Update-Database), I get the following database tables generated:

enter image description here

My ApplicationDbContext inherits from ApiAuthorizationDbContext and I've specified my own ApplicationUser.

public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>
{
    public DbSet<TodoItem> TodoItems { get; set; } = null!;

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IOptions<OperationalStoreOptions> operationalStoreOptions)
        : base(options, operationalStoreOptions)
    {
    }
}

public class ApplicationUser : IdentityUser
{
    public string CustomTag { get; set; }
}

Finally, I have a CRUD test controller for my TodoItems as follows:

[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
    private readonly ApplicationDbContext _context;

    public TodoController(ApplicationDbContext context)
    {
        _context = context;
    }

    // GET: api/<TodoController>
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return Ok(await _context.TodoItems.ToListAsync());
    }

    // GET api/<TodoController>/5
    [HttpGet("{id}")]
    public async Task<IActionResult> GetValue(long? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var todoItem = await _context.TodoItems
            .FirstOrDefaultAsync(m => m.Id == id);
        if (todoItem == null)
        {
            return NotFound();
        }

        return Ok(todoItem);
    }

    // POST api/<TodoController>
    [HttpPost]
    public async Task<IActionResult> Post([FromBody] UpsertTodoItem todoItem)
    {
        if (ModelState.IsValid)
        {
            var item = _context.Add(new TodoItem { 
                IsComplete = todoItem.IsComplete,
                Name = todoItem.Name
            });
            await _context.SaveChangesAsync();

            return CreatedAtAction(nameof(GetValue), new { id = item.Entity.Id }, item.Entity);
        }
        return BadRequest();
    }

    // PUT api/<TodoController>/5
    [HttpPut("{id}")]
    public async Task<IActionResult> Put(int id, [FromBody] UpsertTodoItem todoItem)
    {
        var foundItem = _context.TodoItems.FirstOrDefault(t => t.Id == id);
        if (foundItem == null)
        {
            return NotFound();
        }

        if (ModelState.IsValid)
        {
            try
            {
                foundItem.Name = todoItem.Name;
                foundItem.IsComplete = todoItem.IsComplete;

                _context.Update(foundItem);
                await _context.SaveChangesAsync();
                return Ok(todoItem);
            }
            catch (DbUpdateConcurrencyException)
            {
                throw;
            }
        }
        return BadRequest();
    }

    // DELETE api/<TodoController>/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(long? id)
    {
        if (id == null || !TodoItemExists(id.Value))
        {
            return NotFound();
        }

        var todoItem = await _context.TodoItems
            .FirstOrDefaultAsync(m => m.Id == id);
        if (todoItem == null)
        {
            return NotFound();
        }

        _context.Remove(todoItem);
        await _context.SaveChangesAsync();

        return Ok(todoItem);
    }

    private bool TodoItemExists(long id)
    {
        return _context.TodoItems.Any(e => e.Id == id);
    }
}

This all works fine - I can run the code and get data back as expected. What I'd like to do now is introduce Authentication. I've added this to the TodoItems controller:

[HttpGet("test")]
[Authorize]
public IActionResult TestAuthGet()
{
    return Ok("Hello world!");
}

As expected I get a 401 response calling this without a token.

I'm not sure how to expose the Register/login functions that come as part of the AspNetCore Identity services. How do I get a token to attach to my API calls to successfully call the test endpoint? Ideally adding a controller that lets me authenticate (username/pass) or register a user.

The only examples I've seen for this is using the AspNetCore MVC application but I want to incorporate the functionality into my SPA. I was reading this article from Microsoft but it's using a very outdated demo. https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/individual-accounts-in-web-api#get-an-access-token

Any advice on how to expose this functionality is greatly appreciated! Thanks in advance!

Rob
  • 6,819
  • 17
  • 71
  • 131
  • 2
    When token involve, we should not couple authentication with a single api project, make an identity provider instead. Anyway, does [this](https://stackoverflow.com/questions/70272798/authorization-and-authentication-in-web-application-with-asp-net-core-backend-an) give the idea ? – Gordon Khanh Ng. Feb 11 '22 at 02:02
  • 1
    Wresting with similar issue. Many answers point to using [IdentityServer4](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-6.0&tabs=visual-studio) for APIs. However, to use `[Authorize]` I've also seen an older app that has its own OWin OAuthAuthorizationServerProvider - overriding the methods to generate and refresh tokens. I have no idea if that route is a good idea, so I'm not doing it. (Yet??) – Ian W Feb 14 '22 at 16:55
  • I read a similar article talking about Owin. IdentityServer4 is changing and becoming a paid for service. The example I got going on that github is where the Identity server and resource server where one and the same. – Rob Feb 15 '22 at 09:37

0 Answers0