From 6a9657a10dc5ef3c4dfddf222284eec6c933ac83 Mon Sep 17 00:00:00 2001 From: Tim 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 (limited to 'VPNAuth.Server') 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 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 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("Scopes") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AccessTokens"); + }); + + modelBuilder.Entity("VPNAuth.Server.Database.AuthRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Accepted") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodeChallenge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodeChallengeMethod") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InitTime") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("Scopes") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AuthRequests"); + }); + + modelBuilder.Entity("VPNAuth.Server.Database.UserInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("FamilyName") + .HasColumnType("TEXT"); + + b.Property("GivenName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Picture") + .HasColumnType("TEXT"); + + b.Property("PreferredUsername") + .HasColumnType("TEXT"); + + b.Property("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 +{ + /// + public partial class AddUsernameFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Username", + table: "AuthRequests", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Username", + table: "AccessTokens", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + 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("Username") + .IsRequired() + .HasColumnType("TEXT"); + b.HasKey("Id"); b.ToTable("AccessTokens"); @@ -78,6 +82,10 @@ namespace VPNAuth.Server.Migrations b.Property("State") .HasColumnType("TEXT"); + b.Property("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 @@

VPNAuth

Hey, @configUser.Username!

User settings

-
+ 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