aboutsummaryrefslogtreecommitdiff
path: root/VPNAuth.Server/Api
diff options
context:
space:
mode:
Diffstat (limited to 'VPNAuth.Server/Api')
-rw-r--r--VPNAuth.Server/Api/OAuth2.cs112
-rw-r--r--VPNAuth.Server/Api/Oidc.cs99
-rw-r--r--VPNAuth.Server/Api/UserInterface.cs45
3 files changed, 256 insertions, 0 deletions
diff --git a/VPNAuth.Server/Api/OAuth2.cs b/VPNAuth.Server/Api/OAuth2.cs
new file mode 100644
index 0000000..b7bdcf7
--- /dev/null
+++ b/VPNAuth.Server/Api/OAuth2.cs
@@ -0,0 +1,112 @@
+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;
+ }
+
+ // -> https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
+ 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("/", "_");
+ }
+
+ // -> Request: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
+ // -> Response: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
+ 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.AuthRequests.Remove(authRequest);
+ db.SaveChanges();
+
+ await context.Response.WriteAsJsonAsync(new Token
+ {
+ AccessToken = accessTokenEntry.Entity.Token,
+ TokenType = "Bearer",
+ ExpiresIn = 604800 // 7 days
+ });
+ }
+}
diff --git a/VPNAuth.Server/Api/Oidc.cs b/VPNAuth.Server/Api/Oidc.cs
new file mode 100644
index 0000000..6c15113
--- /dev/null
+++ b/VPNAuth.Server/Api/Oidc.cs
@@ -0,0 +1,99 @@
+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;
+ }
+
+ if (!tokenDbEntry.Scopes.Contains("openid"))
+ {
+ 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;
+ }
+
+ var userInfoResponse = new UserInfo();
+
+ if (tokenDbEntry.Scopes.Contains("profile"))
+ {
+ userInfoResponse.GivenName = userInformation.GivenName;
+ userInfoResponse.FamilyName = userInformation.FamilyName;
+ userInfoResponse.Name = userInformation.Name;
+ userInfoResponse.Picture = userInformation.Picture;
+ userInfoResponse.PreferredUsername = userInformation.PreferredUsername;
+ }
+
+ if (tokenDbEntry.Scopes.Contains("email"))
+ userInfoResponse.Email = userInformation.Email;
+
+ userInfoResponse.Sub = userInformation.Sub;
+
+ await context.Response.WriteAsJsonAsync(userInfoResponse);
+ }
+
+ public static async Task DiscoveryHandler(HttpContext context)
+ {
+ if (!context.Request.Host.HasValue)
+ {
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ return;
+ }
+
+ var serverAddress = context.Request.IsHttps ? "https://" : "http://" + context.Request.Host.Value;
+
+ await context.Response.WriteAsJsonAsync(new OidcDiscovery
+ {
+ Issuer = serverAddress + "/",
+ AuthorizationEndpoint = $"{serverAddress}/auth",
+ TokenEndpoint = $"{serverAddress}/access-token",
+ UserInfoEndpoint = $"{serverAddress}/user-info",
+ JwksUri = "",
+ ResponseTypesSupported = ["code"],
+ SubjectTypesSupported = [],
+ IdTokenSigningAlgValuesSupported = ["RS256"]
+ });
+ }
+}
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();
+ }
+}