diff --git a/Auths.sln b/Auths.sln index dd66bc1..02b112d 100644 --- a/Auths.sln +++ b/Auths.sln @@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jwt.Server", "Jwt.Server\Jw EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RsaKeyLoader", "RsaKeyLoader\RsaKeyLoader.csproj", "{579C8783-7E95-40F3-96F4-BEBFB40F2D38}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jwt.Application", "Jwt.Application\Jwt.Application.csproj", "{E1DC6422-489C-424C-BC3A-3844599B43C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,11 +42,16 @@ Global {579C8783-7E95-40F3-96F4-BEBFB40F2D38}.Debug|Any CPU.Build.0 = Debug|Any CPU {579C8783-7E95-40F3-96F4-BEBFB40F2D38}.Release|Any CPU.ActiveCfg = Release|Any CPU {579C8783-7E95-40F3-96F4-BEBFB40F2D38}.Release|Any CPU.Build.0 = Release|Any CPU + {E1DC6422-489C-424C-BC3A-3844599B43C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1DC6422-489C-424C-BC3A-3844599B43C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1DC6422-489C-424C-BC3A-3844599B43C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1DC6422-489C-424C-BC3A-3844599B43C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3684B6DC-9301-496C-9832-FFC26084C496} = {A7B682CD-CF61-4684-977B-82E48A4051D8} {63CE5DC9-F976-430B-8B12-50FB3DA3F872} = {A7B682CD-CF61-4684-977B-82E48A4051D8} {BAD4810F-B60A-42F1-8176-649855B93794} = {29AFF1AF-FE18-491A-AFE1-CE2786187166} {579C8783-7E95-40F3-96F4-BEBFB40F2D38} = {29AFF1AF-FE18-491A-AFE1-CE2786187166} + {E1DC6422-489C-424C-BC3A-3844599B43C9} = {29AFF1AF-FE18-491A-AFE1-CE2786187166} EndGlobalSection EndGlobal diff --git a/Jwt.Application/Jwt.Application.csproj b/Jwt.Application/Jwt.Application.csproj new file mode 100644 index 0000000..b1f5538 --- /dev/null +++ b/Jwt.Application/Jwt.Application.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Jwt.Application/Jwt.Application.http b/Jwt.Application/Jwt.Application.http new file mode 100644 index 0000000..1f54de8 --- /dev/null +++ b/Jwt.Application/Jwt.Application.http @@ -0,0 +1,6 @@ +@Jwt.Application_HostAddress = http://localhost:5064 + +GET {{Jwt.Application_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Jwt.Application/Program.cs b/Jwt.Application/Program.cs new file mode 100644 index 0000000..f7d5897 --- /dev/null +++ b/Jwt.Application/Program.cs @@ -0,0 +1,76 @@ + +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; + +var builder = WebApplication.CreateBuilder(args); + +var jwKey = await new HttpClient().GetStringAsync("https://localhost:5000/jwk"); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddCors(); +builder.Services.AddSwaggerGen(); +builder.Services.AddAuthentication() + .AddJwtBearer(b => + { + b.TokenValidationParameters = new TokenValidationParameters() + { + // для облегчения дебага + ValidateAudience = false, + ValidateIssuer = false, + }; + + // important + b.Configuration = new OpenIdConnectConfiguration() + { + SigningKeys = + { + JsonWebKey.Create(jwKey) + } + }; + + // b.Events = new JwtBearerEvents() + // { + // OnMessageReceived = (ctx) => + // { + // if (ctx.Request.Query.ContainsKey("token")) + // { + // ctx.Token = ctx.Request.Query["token"]; + // } + // + // return Task.CompletedTask; + // } + // }; + }); +builder.Services.AddAuthorization(); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseCors(p => +{ + p.AllowAnyOrigin(); + p.AllowAnyMethod(); + p.AllowAnyHeader(); +}); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/me", (HttpContext ctx) => + { + return ctx.User.FindFirst("name").Value; + }) + .RequireAuthorization() + .WithName("GetWeatherForecast") + .WithOpenApi(); + +app.Run(); \ No newline at end of file diff --git a/Jwt.Application/Properties/launchSettings.json b/Jwt.Application/Properties/launchSettings.json new file mode 100644 index 0000000..bb99045 --- /dev/null +++ b/Jwt.Application/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:2141", + "sslPort": 44353 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:7001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7000;http://localhost:7001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Jwt.Application/appsettings.Development.json b/Jwt.Application/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Jwt.Application/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Jwt.Application/appsettings.json b/Jwt.Application/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Jwt.Application/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Jwt.Server/Jwt.Server.csproj b/Jwt.Server/Jwt.Server.csproj index 48e39a5..6ee9f30 100644 --- a/Jwt.Server/Jwt.Server.csproj +++ b/Jwt.Server/Jwt.Server.csproj @@ -7,8 +7,13 @@ + + + + + diff --git a/Jwt.Server/Program.cs b/Jwt.Server/Program.cs index 161f695..965eefc 100644 --- a/Jwt.Server/Program.cs +++ b/Jwt.Server/Program.cs @@ -1,3 +1,19 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using RsaKeyLoader; + +var _users = new List<(string Name, string Id)>() +{ + new ValueTuple("Danil", Guid.NewGuid().ToString()) +}; + +var _rsaKey = KeyLoader.GetGeneratedKey(); + var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -5,6 +21,42 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// todo: package Microsoft.AspNetCore.Authentication.JwtBearer; +builder.Services.AddAuthentication() + .AddJwtBearer(b => + { + b.TokenValidationParameters = new TokenValidationParameters() + { + // для облегчения дебага + ValidateAudience = false, + ValidateIssuer = false, + }; + + // important + b.Configuration = new OpenIdConnectConfiguration() + { + SigningKeys = + { + new RsaSecurityKey(_rsaKey) + } + }; + + // b.Events = new JwtBearerEvents() + // { + // OnMessageReceived = (ctx) => + // { + // if (ctx.Request.Query.ContainsKey("token")) + // { + // ctx.Token = ctx.Request.Query["token"]; + // } + // + // return Task.CompletedTask; + // } + // }; + }); +builder.Services.AddAuthorization(); + + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -15,6 +67,8 @@ if (app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); var summaries = new[] { @@ -33,12 +87,55 @@ app.MapGet("/weatherforecast", () => .ToArray(); return forecast; }) + .RequireAuthorization() .WithName("GetWeatherForecast") .WithOpenApi(); +app.MapGet("/login", (string userName) => +{ + var user = _users.First(x => x.Name == userName); + var handler = new JwtSecurityTokenHandler(); + var key = new RsaSecurityKey(_rsaKey); + var token = handler.CreateToken(new SecurityTokenDescriptor() + { + Issuer = "https://localhost:5000", + Subject = new ClaimsIdentity(new List() + { + new Claim("sub", user.Id), + new Claim("name", user.Name) + }), + // important + SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256) + }); + return handler.WriteToken(token); +}); + +app.MapGet("/jwk", () => +{ + var publicKey = RSA.Create(); + publicKey.ImportRSAPublicKey(_rsaKey.ExportRSAPublicKey(), out _); + var key = new RsaSecurityKey(publicKey); + return JsonWebKeyConverter.ConvertFromRSASecurityKey(key); +}); + app.Run(); record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} \ No newline at end of file +} + + +/* script: + +const token = ''; + +fetch("/weatherforecast", { + method: "GET", + headers: { + "Accept": "application/json", + "Authorization": "Bearer " + token + } + }).then(r => r.json()).then(r => console.log(r)); + +*/ \ No newline at end of file diff --git a/Jwt.Server/Properties/launchSettings.json b/Jwt.Server/Properties/launchSettings.json index c9f4735..7fb8b23 100644 --- a/Jwt.Server/Properties/launchSettings.json +++ b/Jwt.Server/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5079", + "applicationUrl": "http://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -24,7 +24,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7256;http://localhost:5079", + "applicationUrl": "https://localhost:5000;http://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/RsaKeyLoader/KeyLoader.cs b/RsaKeyLoader/KeyLoader.cs index 7d65518..716d2a6 100644 --- a/RsaKeyLoader/KeyLoader.cs +++ b/RsaKeyLoader/KeyLoader.cs @@ -4,10 +4,32 @@ namespace RsaKeyLoader; public class KeyLoader { - public void Generate(string path) + public static string PathString = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "auth-key"); + public static RSA GetGeneratedKey() + { + if (File.Exists(PathString)) + { + return LoadKey(PathString); + } + else + { + Generate(PathString); + return LoadKey(PathString); + } + } + + public static void Generate(string path) { var rsaKey = RSA.Create(); var privateKey = rsaKey.ExportRSAPrivateKey(); File.WriteAllBytes(path, privateKey); } + + public static RSA LoadKey(string path) + { + var rsaKey = RSA.Create(); + rsaKey.ImportRSAPrivateKey(File.ReadAllBytes(path), out _); + return rsaKey; + } } \ No newline at end of file