I have setup a donet core (v6) C# API, with Entity Framework (SQLExpress backend). I want the API to:
- serve some application data (as an example, I've a "todo items" table).
- 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:
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:
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:
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!