From a930e972917575c46ff9f59ab69d62be657ecac8 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 21 Apr 2025 15:36:15 +0200 Subject: Move api handler out of Program.cs --- VPNAuth.Server/Api/OAuth2.cs | 107 ++++++++++++++++++++ VPNAuth.Server/Api/Oidc.cs | 63 ++++++++++++ VPNAuth.Server/Api/UserInterface.cs | 45 +++++++++ VPNAuth.Server/Program.cs | 195 ++---------------------------------- 4 files changed, 221 insertions(+), 189 deletions(-) create mode 100644 VPNAuth.Server/Api/OAuth2.cs create mode 100644 VPNAuth.Server/Api/Oidc.cs create mode 100644 VPNAuth.Server/Api/UserInterface.cs diff --git a/VPNAuth.Server/Api/OAuth2.cs b/VPNAuth.Server/Api/OAuth2.cs new file mode 100644 index 0000000..63dc115 --- /dev/null +++ b/VPNAuth.Server/Api/OAuth2.cs @@ -0,0 +1,107 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using VPNAuth.Server.Database; +using VPNAuth.Server.Responses; + +namespace VPNAuth.Server.Api; + +public static class OAuth2 +{ + public static async Task AcceptAuthHandler(HttpContext context, int id) + { + using var db = new Database.Database(); + var authRequest = db.AuthRequests.Find(id); + if (authRequest == null || authRequest.Accepted) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + if (authRequest.Username != context.GetUser()?.Username) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + + authRequest.Accepted = true; + db.SaveChanges(); + + var config = Config.Read(); + context.Response.StatusCode = StatusCodes.Status302Found; + context.Response.Headers["Location"] = config.FindApp(authRequest.ClientId)!.RedirectUri! + + "?code=" + authRequest.Code + + "&state=" + authRequest.State; + } + + private static string HashCodeVerifier(string codeVerifier) + { + using var sha256 = SHA256.Create(); + var removeCodeChallengeEnd = new Regex("=$"); + + var verifierBytes = Encoding.ASCII.GetBytes(codeVerifier); + var hashedVerifierBytes = sha256.ComputeHash(verifierBytes); + return removeCodeChallengeEnd.Replace(Convert.ToBase64String(hashedVerifierBytes), "") + .Replace("+", "-") + .Replace("/", "_"); + } + + public static async Task AccessTokenHandler(HttpContext context) + { + var config = Config.Read(); + if (context.Request.Form["grant_type"] != "authorization_code") + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + var clientSecret = config.FindApp(context.Request.Form["client_id"]!)!.Secret; // FIXME: null pointer + if (clientSecret != null && clientSecret != context.Request.Form["client_secret"]) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + + using var db = new Database.Database(); + var authRequest = db.AuthRequests + .Where(request => request.Code == context.Request.Form["code"].ToString()) + .ToList() + .FirstOrDefault(); + if (authRequest == null) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + if (!context.Request.Form.ContainsKey("code_verifier")) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + var expectedCodeChallenge = HashCodeVerifier(context.Request.Form["code_verifier"].ToString()); + + if (expectedCodeChallenge != authRequest.CodeChallenge) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + + var accessTokenEntry = db.AccessTokens.Add(new AccessToken + { + ClientId = authRequest.ClientId, + Scopes = authRequest.Scopes, + CreationTime = DateTime.Now, + Token = PkceUtils.GenerateToken(), + Username = authRequest.Username + }); + db.SaveChanges(); + + await context.Response.WriteAsJsonAsync(new Token + { + AccessToken = accessTokenEntry.Entity.Token, + TokenType = "Bearer", + Expires = 0 // TODO: change to actual value + }); + } +} diff --git a/VPNAuth.Server/Api/Oidc.cs b/VPNAuth.Server/Api/Oidc.cs new file mode 100644 index 0000000..8b984c7 --- /dev/null +++ b/VPNAuth.Server/Api/Oidc.cs @@ -0,0 +1,63 @@ +using VPNAuth.Server.Responses; + +namespace VPNAuth.Server.Api; + +public static class Oidc +{ + public static async Task UserInfoHandler(HttpContext context) + { + if (context.Request.Method != "GET" && context.Request.Method != "POST") + { + context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; + return; + } + + var tokenHeader = context.Request.Headers["Authorization"].First()?.Split(" "); + + if (tokenHeader?.Length == 1 || tokenHeader?[0] != "Bearer") + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + if (tokenHeader.Length < 2) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } + + using var db = new Database.Database(); + var tokenDbEntry = db.AccessTokens + .Where(tokenEntry => tokenEntry.Token == tokenHeader[1]) + .ToList() + .FirstOrDefault(); + + if (tokenDbEntry == null) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + + var userInformation = db.UserInformation + .Where(entry => entry.Sub == tokenDbEntry.Username) + .ToList() + .FirstOrDefault(); + + if (userInformation == null) + { + context.Response.StatusCode = StatusCodes.Status204NoContent; + return; + } + + context.Response.WriteAsJsonAsync(new UserInfo + { + Email = userInformation.Email, + GivenName = userInformation.GivenName, + FamilyName = userInformation.FamilyName, + Name = userInformation.Name, + Picture = userInformation.Picture, + PreferredUsername = userInformation.PreferredUsername, + Sub = userInformation.Sub + }); + } +} diff --git a/VPNAuth.Server/Api/UserInterface.cs b/VPNAuth.Server/Api/UserInterface.cs new file mode 100644 index 0000000..274f9b1 --- /dev/null +++ b/VPNAuth.Server/Api/UserInterface.cs @@ -0,0 +1,45 @@ +using VPNAuth.Server.Database; + +namespace VPNAuth.Server.Api; + +public static class UserInterface +{ + public static async Task UserSettingsHandler(HttpContext context) + { + using var db = new Database.Database(); + + ConfigUser? configUser = context.GetUser(); + + if (configUser == null) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + } + + UserInformation? userInformation = db.UserInformation + .Where(user => user.Sub == configUser!.Username) + .ToList() + .FirstOrDefault() ?? db.Add(new UserInformation + { + Sub = configUser!.Username + }).Entity; + + if (context.Request.Form.ContainsKey("given-name")) + userInformation.GivenName = context.Request.Form["given-name"]!; + + if (context.Request.Form.ContainsKey("family-name")) + userInformation.FamilyName = context.Request.Form["family-name"]!; + + if (context.Request.Form.ContainsKey("preferred-username")) + userInformation.PreferredUsername = context.Request.Form["preferred-username"]!; + + if (context.Request.Form.ContainsKey("email")) + userInformation.Email = context.Request.Form["email"]!; + + if (context.Request.Form.ContainsKey("picture")) + userInformation.Picture = context.Request.Form["picture"]!; + + userInformation.Name = userInformation.GivenName + " " + userInformation.FamilyName; + + db.SaveChanges(); + } +} diff --git a/VPNAuth.Server/Program.cs b/VPNAuth.Server/Program.cs index e9dc036..b52abd8 100644 --- a/VPNAuth.Server/Program.cs +++ b/VPNAuth.Server/Program.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using VPNAuth.Server; +using VPNAuth.Server.Api; using VPNAuth.Server.Database; using VPNAuth.Server.Responses; @@ -27,196 +28,12 @@ app.UseStaticFiles(new StaticFileOptions { RequestPath = "/static" }); -app.UseRouting(); - -app.MapGet("/accept-auth/{id}", async (HttpContext context, int id) => -{ - using var db = new Database(); - var authRequest = db.AuthRequests.Find(id); - if (authRequest == null || authRequest.Accepted) - { - context.Response.StatusCode = StatusCodes.Status404NotFound; - return; - } - - if (authRequest.Username != context.GetUser()?.Username) - { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; - } - - authRequest.Accepted = true; - db.SaveChanges(); - - var config = Config.Read(); - context.Response.StatusCode = StatusCodes.Status302Found; - context.Response.Headers["Location"] = config.FindApp(authRequest.ClientId)!.RedirectUri! - + "?code=" + authRequest.Code - + "&state=" + authRequest.State; -}); - -app.MapPost("/access-token", async (HttpContext context) => -{ - var config = Config.Read(); - if (context.Request.Form["grant_type"] != "authorization_code") - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } - - var clientSecret = config.FindApp(context.Request.Form["client_id"]!)!.Secret; // FIXME: null pointer - if (clientSecret != null && clientSecret != context.Request.Form["client_secret"]) - { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; - } - - using var db = new Database(); - var authRequest = db.AuthRequests - .Where(request => request.Code == context.Request.Form["code"].ToString()) - .ToList() - .FirstOrDefault(); - if (authRequest == null) - { - context.Response.StatusCode = StatusCodes.Status404NotFound; - return; - } - - if (!context.Request.Form.ContainsKey("code_verifier")) - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } - - using var sha256 = SHA256.Create(); - var removeCodeChallengeEnd = new Regex("=$"); - - var verifier = context.Request.Form["code_verifier"]; - var verifierBytes = Encoding.ASCII.GetBytes(verifier.ToString()); - var hashedVerifierBytes = sha256.ComputeHash(verifierBytes); - var expectedCodeChallenge = removeCodeChallengeEnd.Replace(Convert.ToBase64String(hashedVerifierBytes), "") - .Replace("+", "-") - .Replace("/", "_"); - - if (expectedCodeChallenge != authRequest.CodeChallenge) - { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; - } - - var accessTokenEntry = db.AccessTokens.Add(new AccessToken - { - ClientId = authRequest.ClientId, - Scopes = authRequest.Scopes, - CreationTime = DateTime.Now, - Token = PkceUtils.GenerateToken(), - Username = authRequest.Username - }); - db.SaveChanges(); - - await context.Response.WriteAsJsonAsync(new Token - { - AccessToken = accessTokenEntry.Entity.Token, - TokenType = "Bearer", - Expires = 0 // TODO: change to actual value - }); -}); - -app.MapPost("/user-info-settings", async (HttpContext context) => -{ - using var db = new Database(); - - ConfigUser? configUser = context.GetUser(); - - if (configUser == null) - { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - } - UserInformation? userInformation = db.UserInformation - .Where(user => user.Sub == configUser!.Username) - .ToList() - .FirstOrDefault() ?? db.Add(new UserInformation - { - Sub = configUser!.Username - }).Entity; - - if (context.Request.Form.ContainsKey("given-name")) - userInformation.GivenName = context.Request.Form["given-name"]!; - - if (context.Request.Form.ContainsKey("family-name")) - userInformation.FamilyName = context.Request.Form["family-name"]!; - - if (context.Request.Form.ContainsKey("preferred-username")) - userInformation.PreferredUsername = context.Request.Form["preferred-username"]!; - - if (context.Request.Form.ContainsKey("email")) - userInformation.Email = context.Request.Form["email"]!; - - if (context.Request.Form.ContainsKey("picture")) - userInformation.Picture = context.Request.Form["picture"]!; - - userInformation.Name = userInformation.GivenName + " " + userInformation.FamilyName; - - db.SaveChanges(); -}); - -app.Map("/user-info", (HttpContext context) => -{ - if (context.Request.Method != "GET" && context.Request.Method != "POST") - { - context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; - return; - } - - var tokenHeader = context.Request.Headers["Authorization"].First()?.Split(" "); - - if (tokenHeader?.Length == 1 || tokenHeader?[0] != "Bearer") - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } - - if (tokenHeader.Length < 2) - { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - return; - } - - using var db = new Database(); - var tokenDbEntry = db.AccessTokens - .Where(tokenEntry => tokenEntry.Token == tokenHeader[1]) - .ToList() - .FirstOrDefault(); - - if (tokenDbEntry == null) - { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; - } - - var userInformation = db.UserInformation - .Where(entry => entry.Sub == tokenDbEntry.Username) - .ToList() - .FirstOrDefault(); - - if (userInformation == null) - { - context.Response.StatusCode = StatusCodes.Status204NoContent; - return; - } - - context.Response.WriteAsJsonAsync(new UserInfo - { - Email = userInformation.Email, - GivenName = userInformation.GivenName, - FamilyName = userInformation.FamilyName, - Name = userInformation.Name, - Picture = userInformation.Picture, - PreferredUsername = userInformation.PreferredUsername, - Sub = userInformation.Sub - }); -}); +app.UseRouting(); +app.MapGet("/accept-auth/{id}", OAuth2.AcceptAuthHandler); +app.MapPost("/access-token", OAuth2.AccessTokenHandler); +app.MapPost("/user-info-settings", UserInterface.UserSettingsHandler); +app.Map("/user-info", Oidc.UserInfoHandler); app.MapStaticAssets(); app.MapRazorPages() -- cgit v1.2.3