I created an ASP.NET Core Web Api project with Visual Studio. It uses .NET 6.0, controllers and Swagger. The default structure is the below:
Right now I have two controllers more:
UsersController
[ApiController] [Authorize] // <------- all endpoints are protected with cookie auth [Route("api/[controller]")] public class UsersController : ControllerBase { ... }
That class contains the below endpoint with a simple get operation:
/GET /api/users
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetAll()
{
var users = await _userService.GetAll();
return Ok(users);
}
AuthenticationController
[HttpPost] public async Task<IActionResult> AuthenticateAsync([FromBody] Credentials credentials) { ... }
/POST /api/authenticate
[AllowAnonymous] // <------ so we can call it with no cookie present in the request
[HttpPost]
public async Task<IActionResult> AuthenticateAsync([FromBody] Credentials credentials)
{
var isAuthenticated = _cookieService.Authenticate(credentials.Username, credentials.Password);
if (isAuthenticated)
{
var userId = _cookieService.CreateCookie(userData.Username, Request.HttpContext);
var completeUserData = await _userService.GetById(userId);
var partialUserData = _mapper.Map<PartialUserDataDto>(completeUserData );
return Ok(partialUserData);
}
return Unauthorized();
}
_cookeService.Authenticate(...)
just checks against an in memory dictionary if username and password exists (ex. { username: test1, password: pass1 }).
partialUserData
is an object containing some user fields to show them in the frontend (ex. username, role, email, etc.).
In Program.cs
I use authentication and authorization middleware with cookies like this:
builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(x =>
{
x.Cookie.SameSite = SameSiteMode.None; // only for dev mode
x.SlidingExpiration = true;
x.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
...
app.UseCookiePolicy(new CookiePolicyOptions
{
Secure = CookieSecurePolicy.Always
}
);
app.UseAuthentication();
app.UseAuthorization();
- The browser makes a
/POST /authentication { username: test1, password: pass1 }
- The server checks whether the user exists in a dictionary in memory
- If it exists, it returns a cookie along some user data
- In my Angular frontend, I save that user data in a
currentUser
object that contains, among other data, the user role - An auth guard renders the view based on the user role.
- I can naviagate to other views and make requests to the API because the browser sends automatically the cookie in step 3 to the server
My problem
- If I refresh the browser, I lose my
currentUser
object. I don't want to consider this a logout since I still have the cookie in the browser. But the user data was stored in memory, so obviously I lost it. Should I create a new protected endpoint likeGET /user/<id>
to get again the user data? Is it a good practice?
My goal is that, as long as I have the cookie in the browser, I consider the user is logged in so they don't need to enter their credentials in the login box each time they enter to the app or navigate to a different view. Also, as long I have the same cookie, the user can make any API request to the server. This is, I use the cookie for authentication and also for authorization.
I'm quite confused and don't know if this is a good approach.