ASP.NET CORE MVC - Identity Bearer Tokens
Secure REST API Access with ASP.NET Core Identity Bearer Tokens
Continue from Part 12 and test the complete owner-protected REST API locally with curl.
This tutorial continues from Part 12. Identity login and Product ownership must already work.
1. Open the Project
cd ~
cd aspnet-mvc-tutorial/ProductManagement
pwd
dotnet build
code .
Expected path:
/home/xubuntu/aspnet-mvc-tutorial/ProductManagement
2. Understand the Two Authentication Methods
3. Update Program.cs
Open:
code Program.cs
Replace the old AddDefaultIdentity block with:
builder.Services
.AddIdentityApiEndpoints<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
Then add this after app.MapRazorPages();:
app.MapGroup("/api/auth")
.MapIdentityApi<IdentityUser>();
4. Full Program.cs Check
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
builder.Configuration.GetConnectionString(
"DefaultConnection")));
builder.Services
.AddIdentityApiEndpoints<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.MapGroup("/api/auth")
.MapIdentityApi<IdentityUser>();
app.Run();
5. Build the Identity Change
cd ~/aspnet-mvc-tutorial/ProductManagement
dotnet build
6. Create ProductApiViewModels.cs
mkdir -p ViewModels
touch ViewModels/ProductApiViewModels.cs
code ViewModels/ProductApiViewModels.cs
Paste:
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.ViewModels;
public class ProductApiRequestViewModel
{
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[StringLength(500)]
public string? Description { get; set; }
[Range(0.01, 1000000)]
public decimal Price { get; set; }
[Range(0, 1000000)]
public int Quantity { get; set; }
public int? CategoryId { get; set; }
}
public class ProductApiResponseViewModel
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public int? CategoryId { get; set; }
public string? CategoryName { get; set; }
}
7. Create ProductsApiController.cs
mkdir -p Controllers
touch Controllers/ProductsApiController.cs
code Controllers/ProductsApiController.cs
Paste:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
using ProductManagement.Models;
using ProductManagement.ViewModels;
namespace ProductManagement.Controllers;
[ApiController]
[Authorize]
[Route("api/products")]
public class ProductsApiController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly UserManager<IdentityUser> _userManager;
public ProductsApiController(
ApplicationDbContext context,
UserManager<IdentityUser> userManager)
{
_context = context;
_userManager = userManager;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductApiResponseViewModel>>> GetAll()
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var products = await _context.Products
.Where(p => p.OwnerId == userId)
.Include(p => p.Category)
.OrderBy(p => p.Name)
.Select(p => new ProductApiResponseViewModel
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Quantity = p.Quantity,
CategoryId = p.CategoryId,
CategoryName = p.Category != null ? p.Category.Name : null
})
.ToListAsync();
return Ok(products);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ProductApiResponseViewModel>> GetById(int id)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var product = await _context.Products
.Where(p => p.Id == id && p.OwnerId == userId)
.Include(p => p.Category)
.Select(p => new ProductApiResponseViewModel
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Quantity = p.Quantity,
CategoryId = p.CategoryId,
CategoryName = p.Category != null ? p.Category.Name : null
})
.FirstOrDefaultAsync();
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
public async Task<ActionResult<ProductApiResponseViewModel>> Create(
ProductApiRequestViewModel request)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
if (request.CategoryId.HasValue &&
!await _context.Categories.AnyAsync(c => c.Id == request.CategoryId.Value))
{
return BadRequest(new { message = "Category does not exist." });
}
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = request.Price,
Quantity = request.Quantity,
CategoryId = request.CategoryId,
OwnerId = userId
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
new ProductApiResponseViewModel
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Quantity = product.Quantity,
CategoryId = product.CategoryId
});
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(
int id,
ProductApiRequestViewModel request)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Id == id && p.OwnerId == userId);
if (product == null)
return NotFound();
if (request.CategoryId.HasValue &&
!await _context.Categories.AnyAsync(c => c.Id == request.CategoryId.Value))
{
return BadRequest(new { message = "Category does not exist." });
}
product.Name = request.Name;
product.Description = request.Description;
product.Price = request.Price;
product.Quantity = request.Quantity;
product.CategoryId = request.CategoryId;
await _context.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Id == id && p.OwnerId == userId);
if (product == null)
return NotFound();
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return NoContent();
}
}
8. Create ApiAccessViewModel.cs
touch ViewModels/ApiAccessViewModel.cs
code ViewModels/ApiAccessViewModel.cs
Paste:
namespace ProductManagement.ViewModels;
public class ApiAccessViewModel
{
public string Email { get; set; } = string.Empty;
public string BaseUrl { get; set; } = string.Empty;
}
9. Create ApiAccessController.cs
touch Controllers/ApiAccessController.cs
code Controllers/ApiAccessController.cs
Paste:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ProductManagement.ViewModels;
namespace ProductManagement.Controllers;
[Authorize]
public class ApiAccessController : Controller
{
public IActionResult Index()
{
return View(new ApiAccessViewModel
{
Email = User.Identity?.Name ?? string.Empty,
BaseUrl = $"{Request.Scheme}://{Request.Host}"
});
}
}
10. Create Views/ApiAccess
mkdir -p Views/ApiAccess
touch Views/ApiAccess/Index.cshtml
code Views/ApiAccess/Index.cshtml
Paste:
@model ProductManagement.ViewModels.ApiAccessViewModel
@{
ViewData["Title"] = "API Access";
}
<h1>API Access</h1>
<div class="alert alert-warning">
<strong>Training only.</strong>
This page deliberately displays an access token for curl practice.
</div>
<p>Signed-in user: <strong>@Model.Email</strong></p>
<p>Base URL: <code>@Model.BaseUrl</code></p>
<h2>Generate Bearer Token</h2>
<p>
Your browser login uses an Identity cookie.
Re-enter your password to request a separate bearer token.
</p>
<div class="mb-3">
<label for="apiEmail" class="form-label">Email</label>
<input id="apiEmail"
class="form-control"
value="@Model.Email"
readonly />
</div>
<div class="mb-3">
<label for="apiPassword" class="form-label">Password</label>
<input id="apiPassword"
type="password"
class="form-control"
autocomplete="current-password" />
</div>
<button id="generateToken"
type="button"
class="btn btn-primary">
Generate Bearer Token
</button>
<div id="tokenError"
class="alert alert-danger mt-3 d-none"></div>
<div id="tokenPanel" class="mt-4 d-none">
<h3>Access Token</h3>
<textarea id="accessToken"
class="form-control"
rows="7"
readonly></textarea>
<button id="copyToken"
type="button"
class="btn btn-outline-secondary mt-2">
Copy Access Token
</button>
<p class="mt-3">
Token type: <strong id="tokenType"></strong><br />
Expires in: <strong id="expiresIn"></strong> seconds
</p>
<details>
<summary>Show refresh token</summary>
<textarea id="refreshToken"
class="form-control mt-2"
rows="5"
readonly></textarea>
</details>
</div>
<hr />
<h2>REST Endpoints</h2>
<table class="table table-striped">
<thead>
<tr><th>Method</th><th>Endpoint</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td>GET</td><td><code>@Model.BaseUrl/api/products</code></td><td>List your Products</td></tr>
<tr><td>GET</td><td><code>@Model.BaseUrl/api/products/1</code></td><td>Get one Product</td></tr>
<tr><td>POST</td><td><code>@Model.BaseUrl/api/products</code></td><td>Create a Product</td></tr>
<tr><td>PUT</td><td><code>@Model.BaseUrl/api/products/1</code></td><td>Update a Product</td></tr>
<tr><td>DELETE</td><td><code>@Model.BaseUrl/api/products/1</code></td><td>Delete a Product</td></tr>
</tbody>
</table>
<h2>Authorization Header</h2>
<pre><code>Authorization: Bearer YOUR_ACCESS_TOKEN</code></pre>
@section Scripts {
<script>
(() => {
const generateButton = document.getElementById("generateToken");
const errorBox = document.getElementById("tokenError");
const tokenPanel = document.getElementById("tokenPanel");
generateButton.addEventListener("click", async () => {
errorBox.classList.add("d-none");
tokenPanel.classList.add("d-none");
const email = document.getElementById("apiEmail").value;
const password = document.getElementById("apiPassword").value;
try {
const response = await fetch(
"/api/auth/login?useCookies=false",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: email,
password: password
})
});
if (!response.ok) {
throw new Error(
"Token request failed. Check your password.");
}
const data = await response.json();
document.getElementById("accessToken").value =
data.accessToken ?? "";
document.getElementById("refreshToken").value =
data.refreshToken ?? "";
document.getElementById("tokenType").textContent =
data.tokenType ?? "Bearer";
document.getElementById("expiresIn").textContent =
data.expiresIn ?? "";
tokenPanel.classList.remove("d-none");
document.getElementById("apiPassword").value = "";
}
catch (error) {
errorBox.textContent = error.message;
errorBox.classList.remove("d-none");
}
});
document.getElementById("copyToken")
.addEventListener("click", async () => {
const token =
document.getElementById("accessToken").value;
await navigator.clipboard.writeText(token);
});
})();
</script>
}
11. Add the API Access Navigation Link
Open:
code Views/Shared/_Layout.cshtml
Add this inside the main navigation list:
@if (User.Identity?.IsAuthenticated == true)
{
<li class="nav-item">
<a class="nav-link text-dark"
asp-area=""
asp-controller="ApiAccess"
asp-action="Index">
API Access
</a>
</li>
}
12. Build the Completed Application
cd ~/aspnet-mvc-tutorial/ProductManagement
dotnet build
Continue only when the build succeeds.
13. Run the Application
dotnet run
Keep this terminal running. Note the HTTPS URL, for example https://localhost:7001. Your port may differ.
14. Log In and Generate a Token
In the browser, open /Identity/Account/Login, log in, then open /ApiAccess. Enter the password again and click Generate Bearer Token. Copy the access token.
15. Open a Second Terminal
Do not stop dotnet run. Open another Xubuntu terminal and run:
cd ~
16. Store the Base URL
Use the exact HTTPS port from the first terminal:
BASE_URL="https://localhost:7001"
Check:
echo "$BASE_URL"
17. Store the Access Token
Paste it between single quotes:
TOKEN='PASTE_ACCESS_TOKEN_HERE'
Do not paste the token into screenshots, blog posts, or shared terminals.
18. GET — List Your Products
curl -k -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products"
19. Install jq for Easier JSON Reading
sudo apt update
sudo apt install jq
Then:
curl -ks -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products" | jq
20. GET — Retrieve One Product
Choose one ID returned above:
PRODUCT_ID=1
Run:
curl -ki -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products/$PRODUCT_ID"
21. Create a curl Lab Folder
cd ~
mkdir -p api-curl-lab
cd api-curl-lab
22. Create the POST JSON File
touch create-product.json
code create-product.json
Paste:
{
"name": "USB Hub",
"description": "Seven-port USB hub",
"price": 89.90,
"quantity": 12,
"categoryId": 2
}
23. POST — Create a Product
curl -ki -X POST "$BASE_URL/api/products" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data @create-product.json
Expected status:
HTTP/1.1 201 Created
24. Find the New Product ID
curl -ks -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products" | jq
Set:
PRODUCT_ID=REPLACE_WITH_NEW_ID
25. Create the PUT JSON File
touch update-product.json
code update-product.json
Paste:
{
"name": "USB Hub Pro",
"description": "Updated through the REST API",
"price": 99.90,
"quantity": 15,
"categoryId": 2
}
26. PUT — Update the Product
curl -ki -X PUT "$BASE_URL/api/products/$PRODUCT_ID" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data @update-product.json
Expected:
HTTP/1.1 204 No Content
27. Verify the Update
curl -ks -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products/$PRODUCT_ID" | jq
28. DELETE — Delete the Product
curl -ki -X DELETE -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products/$PRODUCT_ID"
Expected:
HTTP/1.1 204 No Content
29. Verify 404 After Delete
curl -ki -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/products/$PRODUCT_ID"
30. Test Without a Token
curl -ki "$BASE_URL/api/products"
The protected API should reject the request.
31. Test an Invalid Token
curl -ki -H "Authorization: Bearer invalid-token" "$BASE_URL/api/products"
32. Test Ownership with Two Users
- Log in as User A and generate Token A.
- Create a Product with Token A and record its ID.
- Log out of the browser.
- Log in as User B and generate Token B.
- In the curl terminal, replace
TOKENwith Token B. - GET User A's Product ID.
- Try PUT and DELETE against that ID.
- Confirm User B cannot retrieve, update, or delete User A's Product.
33. Refresh the Token
Copy the refresh token shown on the API Access page.
cd ~/api-curl-lab
touch refresh.json
code refresh.json
Paste:
{
"refreshToken": "PASTE_REFRESH_TOKEN_HERE"
}
Then:
curl -ks -X POST "$BASE_URL/api/auth/refresh" -H "Content-Type: application/json" --data @refresh.json | jq
34. Why curl Uses -k
The local ASP.NET Core development certificate may not be trusted by curl. -k skips certificate validation for this local lab only. Do not make this a production habit.
35. Migration Check
No EF Core migration is required because no database entity or table changed.
36. Troubleshooting
curl cannot connect
Confirm dotnet run is still running and BASE_URL uses the correct port.
Token generation fails
Confirm the current user's email/password and verify the Identity API route was mapped in Program.cs.
Login Razor Pages disappear
Verify .AddDefaultUI(), AddRazorPages(), and MapRazorPages() are still present.
POST returns Category does not exist
Use an existing Category ID or set categoryId to null.
37. Part 17A Summary
- continued from Part 12 ownership;
- added Identity API bearer-token support;
- kept normal browser Identity login;
- created a protected API Access page;
- created owner-protected Product REST endpoints;
- used curl for GET, POST, PUT, and DELETE;
- used JSON request files;
- tested missing and invalid bearer tokens;
- tested ownership with two users; and
- used the refresh-token endpoint.
Appendix — Full Code for Final Verification
Program.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
builder.Configuration.GetConnectionString(
"DefaultConnection")));
builder.Services
.AddIdentityApiEndpoints<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.MapGroup("/api/auth")
.MapIdentityApi<IdentityUser>();
app.Run();
ProductApiViewModels.cs
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.ViewModels;
public class ProductApiRequestViewModel
{
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[StringLength(500)]
public string? Description { get; set; }
[Range(0.01, 1000000)]
public decimal Price { get; set; }
[Range(0, 1000000)]
public int Quantity { get; set; }
public int? CategoryId { get; set; }
}
public class ProductApiResponseViewModel
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public int? CategoryId { get; set; }
public string? CategoryName { get; set; }
}
ProductsApiController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
using ProductManagement.Models;
using ProductManagement.ViewModels;
namespace ProductManagement.Controllers;
[ApiController]
[Authorize]
[Route("api/products")]
public class ProductsApiController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly UserManager<IdentityUser> _userManager;
public ProductsApiController(
ApplicationDbContext context,
UserManager<IdentityUser> userManager)
{
_context = context;
_userManager = userManager;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductApiResponseViewModel>>> GetAll()
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var products = await _context.Products
.Where(p => p.OwnerId == userId)
.Include(p => p.Category)
.OrderBy(p => p.Name)
.Select(p => new ProductApiResponseViewModel
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Quantity = p.Quantity,
CategoryId = p.CategoryId,
CategoryName = p.Category != null ? p.Category.Name : null
})
.ToListAsync();
return Ok(products);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ProductApiResponseViewModel>> GetById(int id)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var product = await _context.Products
.Where(p => p.Id == id && p.OwnerId == userId)
.Include(p => p.Category)
.Select(p => new ProductApiResponseViewModel
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Quantity = p.Quantity,
CategoryId = p.CategoryId,
CategoryName = p.Category != null ? p.Category.Name : null
})
.FirstOrDefaultAsync();
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
public async Task<ActionResult<ProductApiResponseViewModel>> Create(
ProductApiRequestViewModel request)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
if (request.CategoryId.HasValue &&
!await _context.Categories.AnyAsync(c => c.Id == request.CategoryId.Value))
{
return BadRequest(new { message = "Category does not exist." });
}
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = request.Price,
Quantity = request.Quantity,
CategoryId = request.CategoryId,
OwnerId = userId
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
new ProductApiResponseViewModel
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Quantity = product.Quantity,
CategoryId = product.CategoryId
});
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(
int id,
ProductApiRequestViewModel request)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Id == id && p.OwnerId == userId);
if (product == null)
return NotFound();
if (request.CategoryId.HasValue &&
!await _context.Categories.AnyAsync(c => c.Id == request.CategoryId.Value))
{
return BadRequest(new { message = "Category does not exist." });
}
product.Name = request.Name;
product.Description = request.Description;
product.Price = request.Price;
product.Quantity = request.Quantity;
product.CategoryId = request.CategoryId;
await _context.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized();
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Id == id && p.OwnerId == userId);
if (product == null)
return NotFound();
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return NoContent();
}
}
ApiAccessViewModel.cs
namespace ProductManagement.ViewModels;
public class ApiAccessViewModel
{
public string Email { get; set; } = string.Empty;
public string BaseUrl { get; set; } = string.Empty;
}
ApiAccessController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ProductManagement.ViewModels;
namespace ProductManagement.Controllers;
[Authorize]
public class ApiAccessController : Controller
{
public IActionResult Index()
{
return View(new ApiAccessViewModel
{
Email = User.Identity?.Name ?? string.Empty,
BaseUrl = $"{Request.Scheme}://{Request.Host}"
});
}
}
Views/ApiAccess/Index.cshtml
@model ProductManagement.ViewModels.ApiAccessViewModel
@{
ViewData["Title"] = "API Access";
}
<h1>API Access</h1>
<div class="alert alert-warning">
<strong>Training only.</strong>
This page deliberately displays an access token for curl practice.
</div>
<p>Signed-in user: <strong>@Model.Email</strong></p>
<p>Base URL: <code>@Model.BaseUrl</code></p>
<h2>Generate Bearer Token</h2>
<p>
Your browser login uses an Identity cookie.
Re-enter your password to request a separate bearer token.
</p>
<div class="mb-3">
<label for="apiEmail" class="form-label">Email</label>
<input id="apiEmail"
class="form-control"
value="@Model.Email"
readonly />
</div>
<div class="mb-3">
<label for="apiPassword" class="form-label">Password</label>
<input id="apiPassword"
type="password"
class="form-control"
autocomplete="current-password" />
</div>
<button id="generateToken"
type="button"
class="btn btn-primary">
Generate Bearer Token
</button>
<div id="tokenError"
class="alert alert-danger mt-3 d-none"></div>
<div id="tokenPanel" class="mt-4 d-none">
<h3>Access Token</h3>
<textarea id="accessToken"
class="form-control"
rows="7"
readonly></textarea>
<button id="copyToken"
type="button"
class="btn btn-outline-secondary mt-2">
Copy Access Token
</button>
<p class="mt-3">
Token type: <strong id="tokenType"></strong><br />
Expires in: <strong id="expiresIn"></strong> seconds
</p>
<details>
<summary>Show refresh token</summary>
<textarea id="refreshToken"
class="form-control mt-2"
rows="5"
readonly></textarea>
</details>
</div>
<hr />
<h2>REST Endpoints</h2>
<table class="table table-striped">
<thead>
<tr><th>Method</th><th>Endpoint</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td>GET</td><td><code>@Model.BaseUrl/api/products</code></td><td>List your Products</td></tr>
<tr><td>GET</td><td><code>@Model.BaseUrl/api/products/1</code></td><td>Get one Product</td></tr>
<tr><td>POST</td><td><code>@Model.BaseUrl/api/products</code></td><td>Create a Product</td></tr>
<tr><td>PUT</td><td><code>@Model.BaseUrl/api/products/1</code></td><td>Update a Product</td></tr>
<tr><td>DELETE</td><td><code>@Model.BaseUrl/api/products/1</code></td><td>Delete a Product</td></tr>
</tbody>
</table>
<h2>Authorization Header</h2>
<pre><code>Authorization: Bearer YOUR_ACCESS_TOKEN</code></pre>
@section Scripts {
<script>
(() => {
const generateButton = document.getElementById("generateToken");
const errorBox = document.getElementById("tokenError");
const tokenPanel = document.getElementById("tokenPanel");
generateButton.addEventListener("click", async () => {
errorBox.classList.add("d-none");
tokenPanel.classList.add("d-none");
const email = document.getElementById("apiEmail").value;
const password = document.getElementById("apiPassword").value;
try {
const response = await fetch(
"/api/auth/login?useCookies=false",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: email,
password: password
})
});
if (!response.ok) {
throw new Error(
"Token request failed. Check your password.");
}
const data = await response.json();
document.getElementById("accessToken").value =
data.accessToken ?? "";
document.getElementById("refreshToken").value =
data.refreshToken ?? "";
document.getElementById("tokenType").textContent =
data.tokenType ?? "Bearer";
document.getElementById("expiresIn").textContent =
data.expiresIn ?? "";
tokenPanel.classList.remove("d-none");
document.getElementById("apiPassword").value = "";
}
catch (error) {
errorBox.textContent = error.message;
errorBox.classList.remove("d-none");
}
});
document.getElementById("copyToken")
.addEventListener("click", async () => {
const token =
document.getElementById("accessToken").value;
await navigator.clipboard.writeText(token);
});
})();
</script>
}
Navigation Block
@if (User.Identity?.IsAuthenticated == true)
{
<li class="nav-item">
<a class="nav-link text-dark"
asp-area=""
asp-controller="ApiAccess"
asp-action="Index">
API Access
</a>
</li>
}
Part 17A is complete when a logged-in user can generate a bearer token and curl can list, create, update and delete only that user's Products.