diff --git a/Api/Api.csproj b/Api/Api.csproj index bc5025d..4edeb20 100644 --- a/Api/Api.csproj +++ b/Api/Api.csproj @@ -24,7 +24,9 @@ + + @@ -37,6 +39,7 @@ + diff --git a/Api/Authorization/AuthorizeMembersAttribute.cs b/Api/Authorization/AuthorizeMembersAttribute.cs new file mode 100644 index 0000000..8fbd4a7 --- /dev/null +++ b/Api/Authorization/AuthorizeMembersAttribute.cs @@ -0,0 +1,12 @@ +using Api.Entities; +using Microsoft.AspNetCore.Authorization; + +namespace Api.Authorization; + +public class AuthorizeMembersAttribute: AuthorizeAttribute +{ + public AuthorizeMembersAttribute(params UserMemberType[] memberTypes) + { + Roles = string.Join(",", memberTypes); + } +} \ No newline at end of file diff --git a/Api/Authorization/ClaimsTransformation.cs b/Api/Authorization/ClaimsTransformation.cs new file mode 100644 index 0000000..803b62b --- /dev/null +++ b/Api/Authorization/ClaimsTransformation.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using Api.Context; +using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; + +namespace Api.Authorization; + +public class ClaimsTransformation(AppDbContext dbContext): IClaimsTransformation +{ + public async Task TransformAsync(ClaimsPrincipal principal) + { + var sub = principal.Claims.SingleOrDefault(c => c.Type == "user_id"); + if (sub is null) + { + return principal; + } + + var user = await dbContext.Users.SingleOrDefaultAsync(u => u.FirebaseId == sub.Value); + if (user is null) + { + return principal; + } + + var ci = new ClaimsIdentity(); + ci.AddClaim(new Claim(ClaimTypes.Role, user.MemberType.ToString())); + + principal.AddIdentity(ci); + + return principal; + } +} \ No newline at end of file diff --git a/Api/Program.cs b/Api/Program.cs index 16a674a..82ca29c 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -1,6 +1,10 @@ +using Api.Authorization; using Api.Context; using Api.Services.V1; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Npgsql; using OpenTelemetry.Trace; @@ -9,10 +13,27 @@ builder.Services.AddOpenTelemetry() .WithTracing(tracerProviderBuilder => tracerProviderBuilder - .AddAspNetCoreInstrumentation() .AddNpgsql() + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation() .AddConsoleExporter()); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + var projectId = builder.Configuration.GetValue("Firebase:ProjectId"); + options.Authority = $"https://securetoken.google.com/{projectId}"; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = $"https://securetoken.google.com/{projectId}", + ValidAudience = projectId, + ValidateIssuerSigningKey = true, + ValidateTokenReplay = true + }; + }); + +builder.Services.AddAuthorization(); + builder.Services .AddGrpc() .AddJsonTranscoding(); @@ -21,6 +42,33 @@ { c.SwaggerDoc("v1", new OpenApiInfo { Title = "SST Alumni Association API", Version = "v1" }); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Firebase ID Token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = JwtBearerDefaults.AuthenticationScheme + }); + + c.AddSecurityRequirement( + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + } + ); + var filePath = Path.Combine(AppContext.BaseDirectory, "Api.xml"); c.IncludeXmlComments(filePath); c.IncludeGrpcXmlComments(filePath, includeControllerXmlComments: true); @@ -32,6 +80,8 @@ builder.Configuration.GetConnectionString("Postgres") ); +builder.Services.AddTransient(); + var app = builder.Build(); using (var scope = app.Services.CreateScope()) @@ -40,6 +90,9 @@ await db.Database.MigrateAsync(); } +app.UseAuthentication(); +app.UseAuthorization(); + app.MapGrpcService(); app.MapGrpcService(); app.MapGrpcService(); diff --git a/Api/Services/V1/UserService.cs b/Api/Services/V1/UserService.cs index 9d26e50..94609a8 100644 --- a/Api/Services/V1/UserService.cs +++ b/Api/Services/V1/UserService.cs @@ -1,15 +1,20 @@ +using Api.Authorization; using Api.Context; using Api.Extensions; using Google.Protobuf.WellKnownTypes; using Grpc.Core; +using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using User.V1; +using Enum = System.Enum; +using UserMemberType = Api.Entities.UserMemberType; namespace Api.Services.V1; /// public class UserServiceV1(ILogger logger, AppDbContext dbContext) : UserService.UserServiceBase { + [AuthorizeMembers(UserMemberType.Exco)] public override async Task ListUsers(ListUsersRequest request, ServerCallContext context) { return new ListUsersResponse diff --git a/Api/appsettings.json b/Api/appsettings.json index 10f68b8..dec13cb 100644 --- a/Api/appsettings.json +++ b/Api/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Firebase": { + "ProjectId": "sstaa-app" + } }