-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/auth #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/auth #133
Changes from all commits
34ade5e
0160fbc
6e7eb77
8a45c9a
5288901
fe2975f
4037132
410275c
6ab7856
fa128f6
eee42b6
a2b7473
c6315a5
fc77fea
f9049b9
94dc100
afefa0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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]; | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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." }); | ||
| } | ||
| } | ||
| 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); | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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>(); | ||
| } | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -59,7 +73,7 @@ | |
| // CORS stuff | ||
| builder.Services.AddCors(options => | ||
| { | ||
| options.AddPolicy("AllowReactApp", policy => | ||
| options.AddPolicy("AllowSvelteApp", policy => | ||
| { | ||
| policy.WithOrigins("http://localhost:3000") | ||
| .AllowAnyHeader() | ||
|
|
@@ -70,7 +84,7 @@ | |
|
|
||
| var app = builder.Build(); | ||
|
|
||
| app.UseCors("AllowReactApp"); | ||
| app.UseCors("AllowSvelteApp"); | ||
|
|
||
| // Configure the HTTP request pipeline. | ||
| if (app.Environment.IsDevelopment()) | ||
|
|
@@ -80,6 +94,7 @@ | |
| } | ||
|
|
||
| // app.UseHttpsRedirection(); | ||
| app.UseAuthentication(); | ||
| app.UseAuthorization(); | ||
| app.MapControllers(); | ||
| // using (var scope = app.Services.CreateScope()) | ||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this part is now out of my paygrade; what do you think |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.