From 6a9657a10dc5ef3c4dfddf222284eec6c933ac83 Mon Sep 17 00:00:00 2001
From: Tim <contact@bytim.eu>
Date: Sat, 19 Apr 2025 19:33:04 +0200
Subject: Add OIDC user-information endpoint

---
 VPNAuth.Server/Database/AccessToken.cs             |   1 +
 VPNAuth.Server/Database/AuthRequest.cs             |   1 +
 .../20250419123149_AddUsernameFields.Designer.cs   | 131 +++++++++++++++++++++
 .../Migrations/20250419123149_AddUsernameFields.cs |  40 +++++++
 VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs |   8 ++
 VPNAuth.Server/Pages/Auth.cshtml.cs                |   3 +-
 VPNAuth.Server/Pages/Dashboard.cshtml              |   2 +-
 VPNAuth.Server/Program.cs                          |  78 +++++++++++-
 8 files changed, 256 insertions(+), 8 deletions(-)
 create mode 100644 VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.Designer.cs
 create mode 100644 VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.cs

diff --git a/VPNAuth.Server/Database/AccessToken.cs b/VPNAuth.Server/Database/AccessToken.cs
index 3cdc3ba..bb8fe7d 100644
--- a/VPNAuth.Server/Database/AccessToken.cs
+++ b/VPNAuth.Server/Database/AccessToken.cs
@@ -7,4 +7,5 @@ public class AccessToken
     public string ClientId { get; set; }
     public DateTime CreationTime { get; set; }
     public List<string> Scopes { get; set; }
+    public string Username { get; set; }
 }
diff --git a/VPNAuth.Server/Database/AuthRequest.cs b/VPNAuth.Server/Database/AuthRequest.cs
index 98fe001..11c05dc 100644
--- a/VPNAuth.Server/Database/AuthRequest.cs
+++ b/VPNAuth.Server/Database/AuthRequest.cs
@@ -11,4 +11,5 @@ public class AuthRequest
     public string CodeChallenge { get; set; }
     public string CodeChallengeMethod { get; set; }
     public bool Accepted { get; set; }
+    public string Username { get; set; }
 }
diff --git a/VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.Designer.cs b/VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.Designer.cs
new file mode 100644
index 0000000..8409c25
--- /dev/null
+++ b/VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.Designer.cs
@@ -0,0 +1,131 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using VPNAuth.Server.Database;
+
+#nullable disable
+
+namespace VPNAuth.Server.Migrations
+{
+    [DbContext(typeof(Database.Database))]
+    [Migration("20250419123149_AddUsernameFields")]
+    partial class AddUsernameFields
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "9.0.4");
+
+            modelBuilder.Entity("VPNAuth.Server.Database.AccessToken", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ClientId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("CreationTime")
+                        .HasColumnType("TEXT");
+
+                    b.PrimitiveCollection<string>("Scopes")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("AccessTokens");
+                });
+
+            modelBuilder.Entity("VPNAuth.Server.Database.AuthRequest", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Accepted")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ClientId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodeChallenge")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodeChallengeMethod")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("InitTime")
+                        .HasColumnType("TEXT");
+
+                    b.PrimitiveCollection<string>("Scopes")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("State")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("AuthRequests");
+                });
+
+            modelBuilder.Entity("VPNAuth.Server.Database.UserInformation", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Email")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("FamilyName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("GivenName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Picture")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredUsername")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Sub")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("UserInformation");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.cs b/VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.cs
new file mode 100644
index 0000000..3d649bb
--- /dev/null
+++ b/VPNAuth.Server/Migrations/20250419123149_AddUsernameFields.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace VPNAuth.Server.Migrations
+{
+    /// <inheritdoc />
+    public partial class AddUsernameFields : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<string>(
+                name: "Username",
+                table: "AuthRequests",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: "");
+
+            migrationBuilder.AddColumn<string>(
+                name: "Username",
+                table: "AccessTokens",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: "");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "Username",
+                table: "AuthRequests");
+
+            migrationBuilder.DropColumn(
+                name: "Username",
+                table: "AccessTokens");
+        }
+    }
+}
diff --git a/VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs b/VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs
index e4643df..4dcce6b 100644
--- a/VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs
+++ b/VPNAuth.Server/Migrations/DatabaseModelSnapshot.cs
@@ -38,6 +38,10 @@ namespace VPNAuth.Server.Migrations
                         .IsRequired()
                         .HasColumnType("TEXT");
 
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
                     b.HasKey("Id");
 
                     b.ToTable("AccessTokens");
@@ -78,6 +82,10 @@ namespace VPNAuth.Server.Migrations
                     b.Property<string>("State")
                         .HasColumnType("TEXT");
 
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
                     b.HasKey("Id");
 
                     b.ToTable("AuthRequests");
diff --git a/VPNAuth.Server/Pages/Auth.cshtml.cs b/VPNAuth.Server/Pages/Auth.cshtml.cs
index bdcbc59..1f75492 100644
--- a/VPNAuth.Server/Pages/Auth.cshtml.cs
+++ b/VPNAuth.Server/Pages/Auth.cshtml.cs
@@ -44,7 +44,8 @@ public class Auth : PageModel
                 Scopes = Request.Query["scope"].ToString().Split(" ").ToList(),
                 CodeChallenge = Request.Query["code_challenge"]!,
                 CodeChallengeMethod = Request.Query["code_challenge_method"]!,
-                Accepted = false
+                Accepted = false,
+                Username = User!.Username!
             });
             db.SaveChanges();
         }
diff --git a/VPNAuth.Server/Pages/Dashboard.cshtml b/VPNAuth.Server/Pages/Dashboard.cshtml
index 38f9c7e..78f6846 100644
--- a/VPNAuth.Server/Pages/Dashboard.cshtml
+++ b/VPNAuth.Server/Pages/Dashboard.cshtml
@@ -35,7 +35,7 @@
             <h2>VPNAuth</h2>
             <p>Hey, @configUser.Username!</p>
             <h3>User settings</h3>
-            <form hx-post="/user-info" hx-swap="none" hx-trigger="change">
+            <form hx-post="/user-info-settings" hx-swap="none" hx-trigger="change">
                 <table style="margin-left: auto; margin-right: auto;">
                     <tbody>
                         <tr>
diff --git a/VPNAuth.Server/Program.cs b/VPNAuth.Server/Program.cs
index 822aba7..beae428 100644
--- a/VPNAuth.Server/Program.cs
+++ b/VPNAuth.Server/Program.cs
@@ -36,6 +36,12 @@ app.MapGet("/accept-auth/{id}", async (HttpContext context, int id) =>
         return;
     }
 
+    if (authRequest.Username != context.GetUser()?.Username)
+    {
+        context.Response.StatusCode = StatusCodes.Status403Forbidden;
+        return;
+    }
+
     authRequest.Accepted = true;
     db.SaveChanges();
 
@@ -80,7 +86,8 @@ app.MapPost("/access-token", async (HttpContext context) =>
         ClientId = authRequest.ClientId,
         Scopes = authRequest.Scopes,
         CreationTime = DateTime.Now,
-        Token = PkceUtils.GenerateToken()
+        Token = PkceUtils.GenerateToken(),
+        Username = authRequest.Username
     });
     db.SaveChanges();
 
@@ -92,7 +99,7 @@ app.MapPost("/access-token", async (HttpContext context) =>
     });
 });
 
-app.MapPost("/user-info", async (HttpContext context) =>
+app.MapPost("/user-info-settings", async (HttpContext context) =>
 {
     using var db = new Database();
 
@@ -103,8 +110,10 @@ app.MapPost("/user-info", async (HttpContext context) =>
         context.Response.StatusCode = StatusCodes.Status401Unauthorized;
     }
 
-    UserInformation? userInformation = db.UserInformation.Where(user => user.Sub == configUser!.Username)
-        .ToList().FirstOrDefault() ?? db.Add(new UserInformation
+    UserInformation? userInformation = db.UserInformation
+        .Where(user => user.Sub == configUser!.Username)
+        .ToList()
+        .FirstOrDefault() ?? db.Add(new UserInformation
     {
         Sub = configUser!.Username
     }).Entity;
@@ -120,15 +129,72 @@ app.MapPost("/user-info", async (HttpContext context) =>
 
     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.MapStaticAssets();
 app.MapRazorPages()
     .WithStaticAssets();
-- 
cgit v1.2.3