Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/API/API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
10 changes: 0 additions & 10 deletions backend/API/JwtOptions.cs

This file was deleted.

72 changes: 51 additions & 21 deletions backend/API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using API;
using API.Filters;
using Business;
using Business.Authorization;
using Business.Services;
using Database.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
Expand All @@ -26,29 +29,54 @@



// Bearer Token Authentication
var jwtOptions = builder.Configuration.GetSection("Jwt").Get<JwtOptions>();
// At the top where your services are registered, add:
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IJwtService, JwtService>();
builder.Services.AddScoped<IAuthorizationHandler, ClubAuthorizationHandler>();

// Replace your current JWT authentication setup with:
var jwtSection = builder.Configuration.GetSection("Jwt");
builder.Services.Configure<JwtOptions>(jwtSection);
var jwtOptions = jwtSection.Get<JwtOptions>();

if (jwtOptions == null)
{
throw new InvalidOperationException("JWT configuration is missing in appsettings.json");
}

builder.Services.AddSingleton(jwtOptions);
builder.Services.AddAuthentication().
AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
// to keep the token string after getting the info
// then it can be accessed using HttpContext
options.SaveToken = true;

options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtOptions.Issuer,

ValidateAudience = true,
ValidAudience = jwtOptions.Audience,

ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey))
};
});
ValidateIssuer = true,
ValidIssuer = jwtOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey))
};
});

// Add authorization after authentication
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ClubMember", policy =>
policy.Requirements.Add(new ClubAuthorizationRequirement("", ClubRole.Member)));
options.AddPolicy("ClubModerator", policy =>
policy.Requirements.Add(new ClubAuthorizationRequirement("", ClubRole.Moderator)));
options.AddPolicy("ClubOwner", policy =>
policy.Requirements.Add(new ClubAuthorizationRequirement("", ClubRole.Owner)));
});

// In the middleware section (after app.UseHttpsRedirection()):

//
builder.Services.AddScoped<ClubMemberFilter>();
Expand All @@ -66,6 +94,8 @@
app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();
app.UseCors(static builder =>
builder.AllowAnyMethod()
Expand Down
6 changes: 6 additions & 0 deletions backend/API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,11 @@
"ConnectionStrings": {
"DefaultConnection": "Server=.; Database = StudyCircle; Integrated Security = SSPI; TrustServerCertificate = True;"
},
"Jwt": {
"Issuer": "StudyCircle",
"Audience": "StudyCircleClient",
"Lifetime": 60,
"SigningKey": "StudyCircle_SuperSecretKey_12345!@#$%^&*()_+QWERTY"
},
"AllowedHosts": "*"
}
41 changes: 41 additions & 0 deletions backend/Business/Authorization/ClubAuthorizationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Database.Models;
using Microsoft.AspNetCore.Authorization;

namespace Business.Authorization
{
public class ClubAuthorizationHandler : AuthorizationHandler<ClubAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ClubAuthorizationRequirement requirement)
{
var user = context.User;
var clubId = requirement.ClubId;
var requiredRole = requirement.RequiredRole;

if (user.IsInRole($"{requiredRole}#{clubId}"))
{
context.Succeed(requirement);
return Task.CompletedTask;
}

if (requiredRole == ClubRole.Member)
{
if (user.IsInRole($"{ClubRole.Moderator}#{clubId}") ||
user.IsInRole($"{ClubRole.Owner}#{clubId}"))
{
context.Succeed(requirement);
}
}
else if (requiredRole == ClubRole.Moderator)
{
if (user.IsInRole($"{ClubRole.Owner}#{clubId}"))
{
context.Succeed(requirement);
}
}

return Task.CompletedTask;
}
}
}
17 changes: 17 additions & 0 deletions backend/Business/Authorization/ClubAuthorizationRequirement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Database.Models;
using Microsoft.AspNetCore.Authorization;

namespace Business.Authorization
{
public class ClubAuthorizationRequirement : IAuthorizationRequirement
{
public string ClubId { get; }
public ClubRole RequiredRole { get; }

public ClubAuthorizationRequirement(string clubId, ClubRole requiredRole)
{
ClubId = clubId;
RequiredRole = requiredRole;
}
}
}
1 change: 1 addition & 0 deletions backend/Business/Business.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
Expand Down
38 changes: 38 additions & 0 deletions backend/Business/DTOs/AuthenticationDTOs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Business.DTOs
{
public class RegisterDto
{
public string Username { get; set; } = null!;
public string Password { get; set; } = null!;
public string Email { get; set; } = null!;
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
public string? City { get; set; }
public string? Country { get; set; }
}

public class LoginDto
{
public string Username { get; set; } = null!;
public string Password { get; set; } = null!;
}

public class AuthResponseDto
{
public bool Success { get; set; }
public string? Token { get; set; }
public string? Message { get; set; }
public UserDto? User { get; set; }
}

public class UserDto
{
public int Id { get; set; }
public string Username { get; set; } = null!;
public string Email { get; set; } = null!;
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
public string? City { get; set; }
public string? Country { get; set; }
}
}
59 changes: 59 additions & 0 deletions backend/Business/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using API;
using Business.Authorization;
using Business.Services;
using Database.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace Business.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddClubAuthorization(
this IServiceCollection services,
IConfiguration configuration)
{
var jwtOptions = configuration.GetSection("Jwt").Get<JwtOptions>();
services.Configure<JwtOptions>(configuration.GetSection("Jwt"));

services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtOptions.SigningKey))
};
});

services.AddAuthorization(options =>
{
options.AddPolicy("ClubMember", policy =>
policy.Requirements.Add(new ClubAuthorizationRequirement("", ClubRole.Member)));
options.AddPolicy("ClubModerator", policy =>
policy.Requirements.Add(new ClubAuthorizationRequirement("", ClubRole.Moderator)));
options.AddPolicy("ClubOwner", policy =>
policy.Requirements.Add(new ClubAuthorizationRequirement("", ClubRole.Owner)));
});

services.AddScoped<IAuthorizationHandler, ClubAuthorizationHandler>();
services.AddScoped<IJwtService, JwtService>();

return services;
}
}
}
Loading