From 3e7b04df1c3f6aa97b7b14a23df14d694c589d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Thu, 22 May 2025 18:25:56 +0200 Subject: [PATCH] Basic login an authorize in backend --- USEntryCoach.Server/Data/LoginCredentials.cs | 7 ++ USEntryCoach.Server/Data/User.cs | 9 ++ USEntryCoach.Server/Data/UserRole.cs | 7 ++ USEntryCoach.Server/Program.cs | 105 +++++++++++++++++- USEntryCoach.Server/Services/TokenService.cs | 56 ++++++++++ .../USEntryCoach.Server.csproj | 1 + .../appsettings.Development.json | 6 + 7 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 USEntryCoach.Server/Data/LoginCredentials.cs create mode 100644 USEntryCoach.Server/Data/User.cs create mode 100644 USEntryCoach.Server/Data/UserRole.cs create mode 100644 USEntryCoach.Server/Services/TokenService.cs diff --git a/USEntryCoach.Server/Data/LoginCredentials.cs b/USEntryCoach.Server/Data/LoginCredentials.cs new file mode 100644 index 0000000..5b551f3 --- /dev/null +++ b/USEntryCoach.Server/Data/LoginCredentials.cs @@ -0,0 +1,7 @@ +namespace USEntryCoach.Server.Data; + +class LoginCredentials +{ + public string Username { get; set; } + public string Password { get; set; } +} diff --git a/USEntryCoach.Server/Data/User.cs b/USEntryCoach.Server/Data/User.cs new file mode 100644 index 0000000..e3fdb9a --- /dev/null +++ b/USEntryCoach.Server/Data/User.cs @@ -0,0 +1,9 @@ +namespace USEntryCoach.Server.Data; + +public class User +{ + public Guid Id { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public UserRole Role { get; set; } +} diff --git a/USEntryCoach.Server/Data/UserRole.cs b/USEntryCoach.Server/Data/UserRole.cs new file mode 100644 index 0000000..5efa0e4 --- /dev/null +++ b/USEntryCoach.Server/Data/UserRole.cs @@ -0,0 +1,7 @@ +namespace USEntryCoach.Server.Data; + +public enum UserRole +{ + Developer, + User +} diff --git a/USEntryCoach.Server/Program.cs b/USEntryCoach.Server/Program.cs index e44345f..d553cc2 100644 --- a/USEntryCoach.Server/Program.cs +++ b/USEntryCoach.Server/Program.cs @@ -1,13 +1,64 @@ +using System.Security.Claims; +using System.Text; using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.IdentityModel.Tokens; +using USEntryCoach.Server.Data; +using USEntryCoach.Server.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +builder.Services.AddSingleton(); + +// Configure JWT token generation. +builder.Services.AddAuthentication(config => +{ + config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}).AddJwtBearer(config => +{ + // TODO: Use Microsoft.Extensions.Options? + // var myOptions = new MyOptions(); // Your settings class + // builder.Configuration.GetSection("MySection").Bind(myOptions); + // myOptions now has the values + // + // TODO: This is identical in TokenService + string? secretToken = builder.Configuration.GetValue("Authentication:Secret"); + + if (secretToken == null) + { + throw new Exception("No Authentication Secret Token set! Please define a value for \"Authentication:SecretToken\" in appsettings.json."); + } + + byte[] secretKey = Encoding.ASCII.GetBytes(secretToken); + + // TODO: Only for debug! + config.RequireHttpsMetadata = false; + config.SaveToken = true; + config.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(secretKey), + ValidateIssuer = false, + ValidateAudience = false + }; +}); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy(nameof(UserRole.Developer), policy => policy.RequireRole(nameof(UserRole.Developer))); + options.AddPolicy(nameof(UserRole.User), policy => policy.RequireRole(nameof(UserRole.User))); +}); var app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); + app.UseDefaultFiles(); app.MapStaticAssets(); @@ -24,13 +75,61 @@ string? apiKey = app.Configuration.GetValue("API:OpenAI"); client.DefaultRequestHeaders.Clear(); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); +app.MapPost("/login", Login); + +app.MapGet("/hello", () => "Hello World!").RequireAuthorization("admin_greetings"); + +async Task Login(LoginCredentials credentials, TokenService tokenService, HttpContext context) +{ + // TODO: Check with database + User? user = null; + + if (credentials is {Username:"developer", Password:"dev"}) + { + user = new User() + { + Username = credentials.Username, + Password = credentials.Password, + Id = Guid.CreateVersion7(), + Role = UserRole.Developer + }; + } + else if (credentials is {Username:"user", Password:"us"}) + { + user = new User() + { + Username = credentials.Username, + Password = credentials.Password, + Id = Guid.CreateVersion7(), + Role = UserRole.User + }; + } + + if (user == null) + { + return Results.Unauthorized(); + } + + var token = tokenService.GenerateToken(user); + + return Results.Ok(new { user = user, token = token }); +} + +app.MapGet("/developer", (ClaimsPrincipal user) => +{ + Results.Ok(new { message = $"Authenticated as { user?.Identity?.Name }" }); +}).RequireAuthorization(nameof(UserRole.Developer)); + +app.MapGet("/user", (ClaimsPrincipal user) => +{ + Results.Ok(new { message = $"Authenticated as { user?.Identity?.Name }" }); +}).RequireAuthorization(nameof(UserRole.User)); + app.MapGet("/ephemeral_token", async () => { if (apiKey == null) throw new Exception("API key not set"); -#pragma warning disable OPENAI002 - var options = new { model = "gpt-4o-mini-realtime-preview", @@ -44,8 +143,6 @@ app.MapGet("/ephemeral_token", async () => //}//ConversationTurnDetectionOptions.CreateServerVoiceActivityTurnDetectionOptions(0.5f, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(500), true) }; -#pragma warning restore OPENAI002 - try { JsonContent content = JsonContent.Create(options); diff --git a/USEntryCoach.Server/Services/TokenService.cs b/USEntryCoach.Server/Services/TokenService.cs new file mode 100644 index 0000000..a69cee3 --- /dev/null +++ b/USEntryCoach.Server/Services/TokenService.cs @@ -0,0 +1,56 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using USEntryCoach.Server.Data; + +namespace USEntryCoach.Server.Services; + +public class TokenService +{ + private byte[] _secretToken; + private double _jwtExpiryMinutes; + private const double DefaultJwtExpiryMinutes = 15; + + public TokenService(IConfiguration configuration) + { + string? secretToken = configuration.GetValue("Authentication:Secret"); + + if (secretToken == null) + { + throw new Exception("No Authentication Secret Token set! Please define a value for \"Authentication:SecretToken\" in appsettings.json."); + } + + _secretToken = Encoding.ASCII.GetBytes(secretToken); + + double? jwtExpiryMinutes = configuration.GetValue("Authentication:JwtExpiryMinutes"); + + if (jwtExpiryMinutes == null) + { + // TODO: Use logger + Console.WriteLine($"Warning: No expiry time for jwt session tokens defined. Using {DefaultJwtExpiryMinutes} minutes."); + } + + _jwtExpiryMinutes = jwtExpiryMinutes ?? DefaultJwtExpiryMinutes; + } + + public string GenerateToken(User user) + { + JwtSecurityTokenHandler tokenHandler = new(); + + SecurityTokenDescriptor tokenDescriptor = new() + { + Subject = new ClaimsIdentity([ + new Claim(ClaimTypes.Name, user.Username), + //new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Role, user.Role.ToString()) + ]), + Expires = DateTime.UtcNow.AddMinutes(_jwtExpiryMinutes), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(_secretToken), SecurityAlgorithms.HmacSha256Signature) + }; + + SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); + + return tokenHandler.WriteToken(token); + } +} \ No newline at end of file diff --git a/USEntryCoach.Server/USEntryCoach.Server.csproj b/USEntryCoach.Server/USEntryCoach.Server.csproj index f570bc4..91646ea 100644 --- a/USEntryCoach.Server/USEntryCoach.Server.csproj +++ b/USEntryCoach.Server/USEntryCoach.Server.csproj @@ -13,6 +13,7 @@ + 9.*-* diff --git a/USEntryCoach.Server/appsettings.Development.json b/USEntryCoach.Server/appsettings.Development.json index 0c208ae..28abe05 100644 --- a/USEntryCoach.Server/appsettings.Development.json +++ b/USEntryCoach.Server/appsettings.Development.json @@ -4,5 +4,11 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "API": { + "OpenAI": "Please set the key in secrets.json! NEVER HERE!!!" + }, + "Authentication": { + "Secret": "Please provide a GUID (without dashes) as secret." } }