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
120 changes: 120 additions & 0 deletions backend/TRFSAE.MemberPortal.API/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Microsoft.AspNetCore.Mvc;
using Supabase;
using TRFSAE.MemberPortal.API.Interfaces;
using TRFSAE.MemberPortal.API.Models;

namespace TRFSAE.MemberPortal.API.Controllers;

[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
private readonly Client _supabaseClient;

public AuthController(IAuthService authService, Client supabaseClient)
{
_authService = authService;
_supabaseClient = supabaseClient;
}

[HttpGet("google")]
public async Task<IActionResult> GoogleLogin()
{
var callbackUrl = "http://127.0.0.1:5096/api/auth/callback"; //change to actual auth frontend url when deployed
var verifierBytes = new byte[32];
Comment thread
liangricky7 marked this conversation as resolved.
System.Security.Cryptography.RandomNumberGenerator.Fill(verifierBytes);
var pkceVerifier = Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(verifierBytes);

var challengeBytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(pkceVerifier));
var pkceChallenge = Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(challengeBytes);

var options = new Supabase.Gotrue.SignInOptions
{
RedirectTo = callbackUrl,
QueryParams = new Dictionary<string, string>
{
{ "code_challenge", pkceChallenge },
{ "code_challenge_method", "s256" }
}
};

var providerState = await _supabaseClient.Auth.SignIn(Supabase.Gotrue.Constants.Provider.Google, options);

Response.Cookies.Append("pkce_verifier", pkceVerifier, new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps, // Set to true in production
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.UtcNow.AddMinutes(5)
});

return Redirect(providerState.Uri.ToString());
}

[HttpGet("callback")]
public async Task<IActionResult> Callback([FromQuery] string code)
{
if (string.IsNullOrEmpty(code)) return BadRequest("Missing authorization code.");

if (!Request.Cookies.TryGetValue("pkce_verifier", out var pkceVerifier))
{
return BadRequest("PKCE verifier missing or expired. Please try logging in again.");
}

try
{
var session = await _supabaseClient.Auth.ExchangeCodeForSession(pkceVerifier, code);

Response.Cookies.Append("access_token", session.AccessToken, new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.UtcNow.AddSeconds(session.ExpiresIn)
});

Response.Cookies.Delete("pkce_verifier");

await _authService.SyncUserToDatabase(session.AccessToken);

return Redirect("http://127.0.0.1:3000/");
}
catch (Exception ex)
{
return StatusCode(500, "Authentication failed: " + ex.Message);
}
Comment on lines +83 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copilot suggestion is right; but we don't really have to worry about it for now, we should probably just revisit this later before we release/not even bother bc this is member portal

}

[HttpGet("me")]
public async Task<IActionResult> Me()
{
if (!Request.Cookies.TryGetValue("access_token", out var token))
{
return Unauthorized();
}

if (!_authService.ValidateSupabaseToken(token)) return Unauthorized();

var user = await _authService.GetUserFromToken(token);
if (user == null) return NotFound(new { message = "User not found" });

return Ok(user);
}

[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
try
{
await _supabaseClient.Auth.SignOut();
}
catch
{
//ensures that cookie is deleted even when waiting for supabase
}

Response.Cookies.Delete("access_token");
return Ok(new { message = "Successfully logged out." });
}
}
3 changes: 2 additions & 1 deletion backend/TRFSAE.MemberPortal.API/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public enum Role
Admin,
SystemLead,
SubsystemLead,
Member
Member,
Unverified
Comment thread
liangricky7 marked this conversation as resolved.
}

[JsonConverter(typeof(StringEnumConverter))]
Expand Down
15 changes: 15 additions & 0 deletions backend/TRFSAE.MemberPortal.API/Interfaces/IAuthService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Supabase.Gotrue;
using TRFSAE.MemberPortal.API.DTOs;
using TRFSAE.MemberPortal.API.Models;

namespace TRFSAE.MemberPortal.API.Interfaces;

public interface IAuthService
{
bool ValidateSupabaseToken(string token);
Task<UserModel?> GetUserFromToken(string token);
Task SyncUserToDatabase(string token);

}
41 changes: 28 additions & 13 deletions backend/TRFSAE.MemberPortal.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
builder.Services.AddScoped<IRoleService, RoleService>();
builder.Services.AddScoped<ITaskService, TaskService>();
builder.Services.AddScoped<IProjectService, ProjectService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IOrderService, OrderService>();
// builder.Services.AddScoped<IGoogleSheetsService, GoogleSheetsService>();
}
Expand All @@ -36,19 +37,32 @@
return client;
});

// register Supabase client as a singleton for reuse across project
builder.Services.AddScoped(provider =>
{
var options = new SupabaseOptions
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
AutoConnectRealtime = true,
AutoRefreshToken = true,
};
var supabaseUrl = builder.Configuration["SupabaseUrl"];
var jwtSecret = builder.Configuration["SupabaseJwtSecret"];

var url = builder.Configuration["SupabaseUrl"] ?? throw new InvalidOperationException("Supabase URL is not configured.");
var key = builder.Configuration["SupabaseKey"] ?? throw new InvalidOperationException("Supabase Key is not configured.");
return new Client(url, key, options);
});
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"{supabaseUrl}/auth/v1",

ValidateAudience = false,

ValidateLifetime = true,

ValidateIssuerSigningKey = true,
IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(jwtSecret!)
),
Comment on lines +43 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bah


ClockSkew = TimeSpan.Zero
};
});


builder.Services.AddAuthorization();

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
Expand All @@ -59,7 +73,7 @@
// CORS stuff
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowReactApp", policy =>
options.AddPolicy("AllowSvelteApp", policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
Expand All @@ -70,7 +84,7 @@

var app = builder.Build();

app.UseCors("AllowReactApp");
app.UseCors("AllowSvelteApp");

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
Expand All @@ -80,6 +94,7 @@
}

// app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// using (var scope = app.Services.CreateScope())
Expand Down
72 changes: 72 additions & 0 deletions backend/TRFSAE.MemberPortal.API/Services/AuthService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using TRFSAE.MemberPortal.API.DTOs;
using TRFSAE.MemberPortal.API.Enums;
using TRFSAE.MemberPortal.API.Interfaces;
using TRFSAE.MemberPortal.API.Models;
using Supabase;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

namespace TRFSAE.MemberPortal.API.Services;

public class AuthService: IAuthService
{
private readonly Client _supabaseClient;
private readonly IUserService _userService;

public AuthService(Client supabaseClient, IUserService userService)
{
_supabaseClient = supabaseClient;
_userService = userService;
}

public bool ValidateSupabaseToken(string token)
{
if (string.IsNullOrEmpty(token)) return false;

try
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
return jwtToken.ValidTo > DateTime.UtcNow;
}
Comment on lines +22 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what copilot said (man ts is gonna take my job)

catch
{
return false;
}
}

public async Task<UserModel?> GetUserFromToken(string token)
{
if (string.IsNullOrEmpty(token)) return null;

try
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
var userIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
if (userIdClaim == null) return null;

var userDto = await _userService.GetUserAsync(Guid.Parse(userIdClaim));
if (userDto == null) return null;
Comment on lines +44 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hes right but probablyj ust implement it differently


return new UserModel
{
Id = userDto.Id,
Name = userDto.Name,
Email = userDto.Email,
Subsystem = userDto.Subsystem ?? Subsystem.Frame,
GradYear = userDto.GradYear
};
}
catch
{
return null;
}
}

public async Task SyncUserToDatabase(string token)
{
// Sync functionality removed to avoid changes outside auth
await Task.CompletedTask;
Comment on lines +69 to +70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part is now out of my paygrade; what do you think

}
}
2 changes: 1 addition & 1 deletion frontend/src/routes/login/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<div class="flex flex-col justify-around items-center mt-30">
<div class="flex flex-col gap-6">
<img src={Logo} alt="TigerRacing logo" />
<a class="{buttonVariants({ variant: "ghost" })} bg-background text-muted-foreground font-black! text-2xl! p-8! hover:bg-primary hover:text-background active:bg-foreground motion-safe:active:scale-95" href={resolve("/api/auth/microsoft", {})}>
<a class="{buttonVariants({ variant: "ghost" })} bg-background text-muted-foreground font-black! text-2xl! p-8! hover:bg-primary hover:text-background active:bg-foreground motion-safe:active:scale-95" href={resolve("/api/auth/google", {})}>
<img src={Google} alt="Google logo" />
Sign in with Google
</a>
Expand Down