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"
+ }
}