diff --git a/api/Program.cs b/api/Program.cs index e553957..d4f8478 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,4 +1,5 @@ +// system usings using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -7,23 +8,27 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using System.Text; +// homeburger usings using agologumApi.Models; using agologumApi.Services; var builder = WebApplication.CreateBuilder(args); +// make sure the jwt key exists or else abort, security issue var key = builder.Configuration["Jwt:Key"]; if(key == null) return; +// connect to the sql database builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddControllers(); -// services +// add our services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +// if this grows sufficiently large we can put elsewhere // configuration for jwt authentication builder.Services.AddIdentity() @@ -46,6 +51,8 @@ builder.Services.AddAuthentication(options => { }; }); +// authorization configurations; here's where we register our permissions to policies +// TODO: this suspiciously looks able to be automated through a for loop, only if we can have a static dictionary maybe though? builder.Services.AddAuthorization(options => { options.AddPolicy(Permission.SensitiveData_Read, policy => @@ -70,6 +77,7 @@ builder.Services.Configure(options => // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +// cors; scary needs to be fixed builder.Services.AddCors(options => { options.AddPolicy("dev", @@ -78,17 +86,14 @@ builder.Services.AddCors(options => policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); - }); + }); // TODO: scary please fix this }); +// more middleware; probably uncessary at this stage builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// https://www.reddit.com/r/dotnet/comments/1h7vzbs/how_do_you_guys_handle_authorization_on_a_web_api/ -// add authorization here -// controllers will have endpoints based on authorization -// frontend is a different story - +// build app var app = builder.Build(); app.UseForwardedHeaders(); @@ -121,17 +126,6 @@ using (var scope = app.Services.CreateScope()) { } } - // TODO: abstract this away - // auto seed Identity roles - var roleManager = scope.ServiceProvider.GetRequiredService>(); - string[] roles = { "guest", "user", "dev", "mod", "admin", "superuser", "role1", "role2" }; - foreach(string role in roles) { - if(!await roleManager.RoleExistsAsync(role)) { - await roleManager.CreateAsync(new IdentityRole(role)); - } - } - - } app.Run(); diff --git a/api/src/Controllers/AuthController.cs b/api/src/Controllers/AuthController.cs index 3364fc3..0ecf3da 100644 --- a/api/src/Controllers/AuthController.cs +++ b/api/src/Controllers/AuthController.cs @@ -13,10 +13,11 @@ public class AuthController : ControllerBase { // identity things private readonly UserManager userManager_; private readonly SignInManager signInManager_; - + // services private readonly JwtService jwt_; private readonly UserService userService_; + // class constructor (where are my initializer lists man) public AuthController(UserManager userManager, SignInManager signInManager, JwtService jwt, UserService userService) { userManager_ = userManager; @@ -25,43 +26,49 @@ public class AuthController : ControllerBase { userService_ = userService; } + // register endpoint [HttpPost("register")] public async Task Register(RegisterDto dto) { + // create a new user out of the dto from the request User user = new User { UserName = dto.UserName, Email = dto.Email, CreatedAt = DateTime.UtcNow // yeah why not utc }; - // assigning roles to user + // assigning roles to user. create a user starting with x to give it permissions to read sensitive data if(dto.UserName.StartsWith("x")) { user.Permissions = new List { Permission.SensitiveData_Read }; - } else if(dto.UserName == "bard") { - user.Permissions = new List { Permission.SensitiveData_Read, Permission.SensitiveData_Modify }; - } + } + // use Identity's user manager to add to db; error check if failed var result = await userManager_.CreateAsync(user, dto.Password); if(!result.Succeeded) return BadRequest(result.Errors); + // respond to post as necessary return CreatedAtAction( nameof(Register), new { id = user.Id } ); } + // login endpoint [HttpPost("login")] public async Task Login(LoginDto dto) { + // get the user from the database given the username var user = await userManager_.FindByNameAsync(dto.UserName); + // user not found with that name + if (user == null) return Unauthorized(); // unauthorized instead of not found to not give away info - if (user == null) return Unauthorized(); - + // use identity's password validation var result = await signInManager_.CheckPasswordSignInAsync(user, dto.Password, false); - + // if failed then youre not real ! if(!result.Succeeded) return Unauthorized(); + // login sucess, give you an authentication token var accessToken = await jwt_.GenerateJwt(user); - var refreshToken = jwt_.GenerateRefreshToken(); + var refreshToken = jwt_.GenerateRefreshToken(); // the refresh token is good enough to refresh your access token RefreshToken newTokenObject = new RefreshToken { Token = refreshToken, UserId = user.Id, @@ -70,43 +77,44 @@ public class AuthController : ControllerBase { IsRevoked = false }; await jwt_.AddRefreshToken(newTokenObject); + // the jwt says we trust who you are and can substitute it for login + // contains permissions claims too - if(user.UserName == "bard") { // tempory because im silly - await userManager_.AddToRoleAsync(user, "superuser"); - } // eventually ill have an endpoint for adding/removing roles - - if(dto.UserName == "bard") { - user.Permissions = new List { Permission.SensitiveData_Read, Permission.SensitiveData_Modify }; - await userService_.Update(user.Id, user); - } - + // return both access and refresh token return Ok(new { accessToken, refreshToken }); } + // logout endpoint [Authorize] // authorize is handled by middleware [HttpPost("logout")] public async Task Logout(string refreshTokenString) { // revoke refresh token bool success = await jwt_.RevokeRefreshToken(refreshTokenString); if(!success) return NotFound(); + // frontend refreshes page and detects logout return Ok(); } + // refresh token endpoint [HttpPost("refresh")] // allow-anonymous by default public async Task Refresh(TokenDto request) { + // reached when the frontend gets an unauthorized response and autoattempts to refresh if available + // get token from request and check if its valid RefreshToken? storedToken = await jwt_.GetRefreshToken(request.RefreshToken); if (storedToken == null) return Unauthorized(); bool valid = (storedToken.IsRevoked) || (storedToken.ExpiresAt < DateTime.UtcNow); if(!valid) return Unauthorized(); // TODO: delete the invalid token + // get user from the token and give them new tokens User? user = await jwt_.GetUser(storedToken.UserId); if(user == null) return NotFound(); string? newAccessToken = await jwt_.GenerateJwt(user); if(newAccessToken == null) return NotFound(); string newRefreshToken = jwt_.GenerateRefreshToken(); + // construct new token storedToken.IsRevoked = true; RefreshToken newTokenObject = new RefreshToken { Token = newRefreshToken, @@ -117,10 +125,9 @@ public class AuthController : ControllerBase { }; await jwt_.AddRefreshToken(newTokenObject); - + // return new tokens return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken }); - } // TODO diff --git a/api/src/Controllers/UsersController.cs b/api/src/Controllers/UsersController.cs index 0ec74bf..24339b0 100644 --- a/api/src/Controllers/UsersController.cs +++ b/api/src/Controllers/UsersController.cs @@ -68,6 +68,7 @@ public class UsersController : ControllerBase { [HttpDelete("{id}/{permission}")] public async Task removePermission(string id, string permission) { + // get the user this request comes from. since it passed identity auth we can trust it var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if(permission == Permission.SensitiveData_Modify && userId == id) return BadRequest(); // dont allow permission removal of whats allowing us to re-add premissions diff --git a/api/src/Models/User.cs b/api/src/Models/User.cs index 8857f71..a542617 100644 --- a/api/src/Models/User.cs +++ b/api/src/Models/User.cs @@ -10,7 +10,7 @@ public class User : IdentityUser { // TODO: make this a list of UserPermissions // where a userpermission has an Id, Permission (string), and userId string // then we can do something like: get all users with this permission - public List? Permissions { get; set; } = new(); + public List? Permissions { get; set; } = new(); // because this isnt very relational database happy // properties inherited from IdentityUser: /* @@ -54,12 +54,14 @@ public class LoginDto { public class UserDto { - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // gets compressed to a string' + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // datetimes get compressed to a string public List? Permissions { get; set; } = []; public string? Email { get; set; } = ""; public string Id { get; set; } = ""; public string? UserName { get; set; } = ""; + // constructor out of a full User object + // REMEMBER: when adding fields to UserDto they must also be set in this constructor or else stuff breaks public UserDto(User user) { CreatedAt = user.CreatedAt; Email = user.Email; diff --git a/api/src/Services/ItemService.cs b/api/src/Services/ItemService.cs index 8fab9ff..8535a6f 100644 --- a/api/src/Services/ItemService.cs +++ b/api/src/Services/ItemService.cs @@ -5,6 +5,7 @@ using agologumApi.Models; namespace agologumApi.Services; +// basic CRUD operations for items in the database public class ItemService { private readonly AppDbContext db_; diff --git a/api/src/Services/JwtService.cs b/api/src/Services/JwtService.cs index a43788d..b2c7c19 100644 --- a/api/src/Services/JwtService.cs +++ b/api/src/Services/JwtService.cs @@ -21,14 +21,16 @@ public class JwtService { userManager_ = userManager; } + // create a jwt string given a user (user contains permissions which go into claims) public async Task GenerateJwt(User user) { + // security stuff string? jwtKey = config_["Jwt:Key"]; if(jwtKey == null) return null; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - + // make sure the user is real if(user.UserName == null) return null; // not too sure @@ -37,13 +39,15 @@ public class JwtService { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) }; + // add each permission that the user has into the claims List? permissions = user.Permissions; if(permissions != null) { - foreach(var perm in permissions) { + foreach(string perm in permissions) { claims.Add(new Claim("permission", perm)); } } + // construct that token var token = new JwtSecurityToken( issuer: "agologum", audience: "agologum", @@ -56,6 +60,7 @@ public class JwtService { } + // generating a refresh token is just like a long random password public string GenerateRefreshToken() { byte[] randomBytes = new byte[64]; @@ -64,10 +69,12 @@ public class JwtService { } + // we store refresh tokens on our side to check against when a user requests a refresh public async Task GetRefreshToken(string refreshTokenString) { return await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString); } + // add a refresh token to the token db store public async Task AddRefreshToken(RefreshToken refreshToken) { db_.RefreshTokens.Add(refreshToken); await db_.SaveChangesAsync(); @@ -77,8 +84,9 @@ public class JwtService { // helper to get the User from the id that exists in a refresh token object public async Task GetUser(string id) { return await db_.Users.FindAsync(id); - } + } // since other places aren't good for having references to db contexts + // remove refresh token from our store; called when user logs out public async Task RevokeRefreshToken(string refreshTokenString) { var refreshToken = await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString); if(refreshToken == null) return false; diff --git a/api/src/Services/UserService.cs b/api/src/Services/UserService.cs index 68c1e82..1800523 100644 --- a/api/src/Services/UserService.cs +++ b/api/src/Services/UserService.cs @@ -13,18 +13,22 @@ public class UserService { db_ = db; } + // get all users public async Task> GetAll() { return await db_.Users.ToListAsync(); } + // get one user with id of id public async Task GetById(string id) { return await db_.Users.FindAsync(id); } + // get one user with username of name public async Task GetByName(string name) { return await db_.Users.FirstOrDefaultAsync(u => u.UserName == name); } + // delete one user with id of id public async Task Delete(string id) { User? User = await db_.Users.FindAsync(id); if(User != null) { @@ -36,6 +40,7 @@ public class UserService { } } + // update user of id with user public async Task Update(string id, User user) { User? oldUser = await db_.Users.FindAsync(id); diff --git a/client/src/api/axios.ts b/client/src/api/axios.ts index 601abe3..8a48736 100644 --- a/client/src/api/axios.ts +++ b/client/src/api/axios.ts @@ -88,6 +88,6 @@ api.interceptors.response.use(response => response, async error => { // mainly f }) // TODO: if you get a 403 while navigating then redirect to the last authenticated page -// if you gert a 403 on a form submissio nthen do like an unauthorized popup +// if you gert a 403 on a form submissio nthen do like an unauthorized popup (message: stale session ) (or redirect to login like i said elsewhere) export default api;