comments galore
This commit is contained in:
@@ -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<AppDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// services
|
||||
// add our services
|
||||
builder.Services.AddScoped<UserService>();
|
||||
builder.Services.AddScoped<ItemService>();
|
||||
builder.Services.AddScoped<JwtService>();
|
||||
// if this grows sufficiently large we can put elsewhere
|
||||
|
||||
// configuration for jwt authentication
|
||||
builder.Services.AddIdentity<User, IdentityRole>()
|
||||
@@ -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<ForwardedHeadersOptions>(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<RoleManager<IdentityRole>>();
|
||||
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();
|
||||
|
||||
@@ -13,10 +13,11 @@ public class AuthController : ControllerBase {
|
||||
// identity things
|
||||
private readonly UserManager<User> userManager_;
|
||||
private readonly SignInManager<User> signInManager_;
|
||||
|
||||
// services
|
||||
private readonly JwtService jwt_;
|
||||
private readonly UserService userService_;
|
||||
|
||||
// class constructor (where are my initializer lists man)
|
||||
public AuthController(UserManager<User> userManager, SignInManager<User> signInManager, JwtService jwt, UserService userService) {
|
||||
|
||||
userManager_ = userManager;
|
||||
@@ -25,43 +26,49 @@ public class AuthController : ControllerBase {
|
||||
userService_ = userService;
|
||||
}
|
||||
|
||||
// register endpoint
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult> 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<string> { Permission.SensitiveData_Read };
|
||||
} else if(dto.UserName == "bard") {
|
||||
user.Permissions = new List<string> { 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<ActionResult> 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<string> { 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<ActionResult> 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<ActionResult> 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
|
||||
|
||||
@@ -68,6 +68,7 @@ public class UsersController : ControllerBase {
|
||||
[HttpDelete("{id}/{permission}")]
|
||||
public async Task<ActionResult> 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
|
||||
|
||||
|
||||
@@ -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<string>? Permissions { get; set; } = new();
|
||||
public List<string>? 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<string>? 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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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<string?> 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<string>? 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<RefreshToken?> GetRefreshToken(string refreshTokenString) {
|
||||
return await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString);
|
||||
}
|
||||
|
||||
// add a refresh token to the token db store
|
||||
public async Task<RefreshToken> 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<User?> 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<bool> RevokeRefreshToken(string refreshTokenString) {
|
||||
var refreshToken = await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString);
|
||||
if(refreshToken == null) return false;
|
||||
|
||||
@@ -13,18 +13,22 @@ public class UserService {
|
||||
db_ = db;
|
||||
}
|
||||
|
||||
// get all users
|
||||
public async Task<List<User>> GetAll() {
|
||||
return await db_.Users.ToListAsync();
|
||||
}
|
||||
|
||||
// get one user with id of id
|
||||
public async Task<User?> GetById(string id) {
|
||||
return await db_.Users.FindAsync(id);
|
||||
}
|
||||
|
||||
// get one user with username of name
|
||||
public async Task<User?> GetByName(string name) {
|
||||
return await db_.Users.FirstOrDefaultAsync(u => u.UserName == name);
|
||||
}
|
||||
|
||||
// delete one user with id of id
|
||||
public async Task<bool> 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<User?> Update(string id, User user) {
|
||||
|
||||
User? oldUser = await db_.Users.FindAsync(id);
|
||||
|
||||
Reference in New Issue
Block a user