Реализовать авторизацию для Persistence.Client

This commit is contained in:
Roman Efremov 2024-11-25 10:09:38 +05:00
parent 6518aeabf1
commit 3806e395eb
18 changed files with 297 additions and 298 deletions

View File

@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.API.Controllers
{
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class SetpointController : ControllerBase, ISetpointApi
{

View File

@ -1,7 +1,13 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Persistence.Models;
using Persistence.Models.Configurations;
using System.Data.Common;
using System.Text;
using System.Text.Json.Nodes;
namespace Persistence.API;
@ -30,6 +36,10 @@ public static class DependencyInjection
});
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" });
var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>();
if (needUseKeyCloak)
{
c.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
@ -62,6 +72,36 @@ public static class DependencyInjection
new List<string>()
}
});
}
else
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
}
//var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
//var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
@ -73,16 +113,67 @@ public static class DependencyInjection
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
var needUseKeyCloak = configuration
.GetSection("NeedUseKeyCloak")
.Get<bool>();
if (needUseKeyCloak) services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
o.RequireHttpsMetadata = false;
o.Audience = configuration["Authentication:Audience"];
o.MetadataAddress = configuration["Authentication:MetadataAddress"]!;
o.TokenValidationParameters = new TokenValidationParameters
options.RequireHttpsMetadata = false;
options.Audience = configuration["Authentication:Audience"];
options.MetadataAddress = configuration["Authentication:MetadataAddress"]!;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = configuration["Authentication:ValidIssuer"],
};
});
else services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = JwtParams.Issuer,
ValidateAudience = true,
ValidAudience = JwtParams.Audience,
ValidateLifetime = true,
IssuerSigningKey = JwtParams.SecurityKey,
ValidateIssuerSigningKey = false
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Headers["Authorization"]
.ToString()
.Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty)
.Trim();
context.Token = accessToken;
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var username = context.Principal?.Claims
.FirstOrDefault(e => e.Type == "username")?.Value;
var password = context.Principal?.Claims
.FirstOrDefault(e => e.Type == "password")?.Value;
var keyCloakUser = configuration
.GetSection(nameof(AuthUser))
.Get<AuthUser>()!;
if (username != keyCloakUser.Username || password != keyCloakUser.Password)
{
context.Fail("username or password did not match");
}
return Task.CompletedTask;
}
};
});
}
}

View File

@ -8,7 +8,7 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5032"
"applicationUrl": "http://localhost:13616"
},
"IIS Express": {
"commandName": "IISExpress",

View File

@ -4,5 +4,6 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
},
"NeedUseKeyCloak": false
}

View File

@ -1,11 +1,12 @@
{
{
"DbConnection": {
"Host": "localhost",
"Port": 5432,
"Username": "postgres",
"Password": "q"
},
"KeycloakTestUser": {
"NeedUseKeyCloak": false,
"AuthUser": {
"username": "myuser",
"password": 12345,
"clientId": "webapi",

View File

@ -1,43 +1,72 @@
namespace Persistence.Client.Helpers;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Persistence.Models.Configurations;
using RestSharp;
namespace Persistence.Client.Helpers;
public static class ApiTokenHelper
{
public static string GetAdminUserToken()
public static void Authorize(this HttpClient httpClient, IConfiguration configuration)
{
//var user = new User()
//{
// Id = 1,
// IdCompany = 1,
// Login = "test_user"
//};
//var roles = new[] { "root" };
var authUser = configuration
.GetSection(nameof(AuthUser))
.Get<AuthUser>()!;
var needUseKeyCloak = configuration
.GetSection("NeedUseKeyCloak")
.Get<bool>()!;
var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get<string>() ?? string.Empty;
return string.Empty;
var jwtToken = needUseKeyCloak
? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl)
: authUser.CreateDefaultJwtToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
}
//private static string CreateToken(User user, IEnumerable<string> roles)
//{
// var claims = new List<Claim>
// {
// new("id", user.Id.ToString()),
// new(ClaimsIdentity.DefaultNameClaimType, user.Login),
// new("idCompany", user.IdCompany.ToString()),
// };
private static string CreateDefaultJwtToken(this AuthUser authUser)
{
var claims = new List<Claim>()
{
new("client_id", authUser.ClientId),
new("username", authUser.Username),
new("password", authUser.Password),
new("grant_type", authUser.GrantType)
};
// claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
// const string secret = "супер секретный ключ для шифрования";
// var key = Encoding.ASCII.GetBytes(secret);
// var tokenDescriptor = new SecurityTokenDescriptor
// {
// Issuer = "a",
// Audience = "a",
// Subject = new ClaimsIdentity(claims),
// Expires = DateTime.UtcNow.AddHours(1),
// SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
// };
// var tokenHandler = new JwtSecurityTokenHandler();
// var token = tokenHandler.CreateToken(tokenDescriptor);
// return tokenHandler.WriteToken(token);
//}
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = JwtParams.Issuer,
Audience = JwtParams.Audience,
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl)
{
var restClient = new RestClient();
var request = new RestRequest(keycloakGetTokenUrl, Method.Post);
request.AddParameter("username", authUser.Username);
request.AddParameter("password", authUser.Password);
request.AddParameter("client_id", authUser.ClientId);
request.AddParameter("grant_type", authUser.GrantType);
var keyCloackResponse = restClient.Post(request);
if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content))
{
var token = JsonSerializer.Deserialize<JwtToken>(keyCloackResponse.Content)!;
return token.AccessToken;
}
return String.Empty;
}
}

View File

@ -7,12 +7,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Persistence\Persistence.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>
</Project>

View File

@ -1,48 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Refit;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Persistence.Client.Helpers;
using Persistence.Models.Configurations;
using Refit;
namespace Persistence.Client
{
public static class PersistenceClientFactory
public class PersistenceClientFactory
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions));
public static T GetClient<T>(HttpClient client)
private HttpClient httpClient;
public PersistenceClientFactory(IHttpClientFactory httpClientFactory, IConfiguration configuration)
{
return RestService.For<T>(client, RefitSettings);
this.httpClient = httpClientFactory.CreateClient();
httpClient.Authorize(configuration);
}
public static T GetClient<T>(string baseUrl)
public T GetClient<T>()
{
var client = new HttpClient();
client.BaseAddress = new Uri(baseUrl);
return RestService.For<T>(client, RefitSettings);
}
private static HttpClient GetAuthorizedClient()
{
var httpClient = new HttpClient();
var jwtToken = ApiTokenHelper.GetAdminUserToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
return httpClient;
return RestService.For<T>(httpClient, RefitSettings);
}
}
}

View File

@ -1,43 +0,0 @@
namespace Persistence.IntegrationTests;
public static class ApiTokenHelper
{
//public static string GetAdminUserToken()
//{
// var user = new User()
// {
// Id = 1,
// IdCompany = 1,
// Login = "test_user"
// };
// var roles = new[] { "root" };
// return CreateToken(user, roles);
//}
//private static string CreateToken(User user, IEnumerable<string> roles)
//{
// var claims = new List<Claim>
// {
// new("id", user.Id.ToString()),
// new(ClaimsIdentity.DefaultNameClaimType, user.Login),
// new("idCompany", user.IdCompany.ToString()),
// };
// claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
// const string secret = "супер секретный ключ для шифрования";
// var key = Encoding.ASCII.GetBytes(secret);
// var tokenDescriptor = new SecurityTokenDescriptor
// {
// Issuer = "a",
// Audience = "a",
// Subject = new ClaimsIdentity(claims),
// Expires = DateTime.UtcNow.AddHours(1),
// SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
// };
// var tokenHandler = new JwtSecurityTokenHandler();
// var token = tokenHandler.CreateToken(tokenDescriptor);
// return tokenHandler.WriteToken(token);
//}
}

View File

@ -17,11 +17,10 @@ namespace Persistence.IntegrationTests.Controllers
public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory)
{
var scope = factory.Services.CreateScope();
var httpClient = scope.ServiceProvider
.GetRequiredService<IHttpClientFactory>()
.CreateClient();
var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>();
setpointClient = PersistenceClientFactory.GetClient<ISetpointClient>(httpClient);
setpointClient = persistenceClientFactory.GetClient<ISetpointClient>();
}
[Fact]

View File

@ -17,11 +17,10 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
dbContext.CleanupDbSet<TEntity>();
var scope = factory.Services.CreateScope();
var httpClient = scope.ServiceProvider
.GetRequiredService<IHttpClientFactory>()
.CreateClient();
var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>();
timeSeriesClient = PersistenceClientFactory.GetClient<ITimeSeriesClient<TDto>>(httpClient);
timeSeriesClient = persistenceClientFactory.GetClient<ITimeSeriesClient<TDto>>();
}
public async Task InsertRangeSuccess(TDto dto)

View File

@ -1,8 +0,0 @@
using System.Text.Json.Serialization;
namespace Persistence.IntegrationTests;
public class JwtToken
{
[JsonPropertyName("access_token")]
public required string AccessToken { get; set; }
}

View File

@ -1,27 +0,0 @@
namespace Persistence.IntegrationTests;
/// <summary>
/// настройки credentials для пользователя в KeyCloak
/// </summary>
public class KeyCloakUser
{
/// <summary>
///
/// </summary>
public required string Username { get; set; }
/// <summary>
///
/// </summary>
public required string Password { get; set; }
/// <summary>
///
/// </summary>
public required string ClientId { get; set; }
/// <summary>
///
/// </summary>
public required string GrantType { get; set; }
}

View File

@ -3,57 +3,33 @@ using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Persistence.API;
using Persistence.Client;
using Persistence.Database.Model;
using Persistence.Database.Postgres;
using Refit;
using RestSharp;
using System.Net.Http.Headers;
using System.Text.Json;
using Persistence.Database.Postgres;
using System.Net.Http.Headers;
using Persistence.Client;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Persistence.IntegrationTests;
public class WebAppFactoryFixture : WebApplicationFactory<Startup>
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
//Converters = { new ValidationResultConverter() }
};
private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions));
private readonly string connectionString;
private readonly KeyCloakUser keycloakTestUser;
public readonly string KeycloakGetTokenUrl;
public WebAppFactoryFixture()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.Tests.json")
.Build();
var dbConnection = configuration.GetSection("DbConnection").Get<DbConnection>()!;
connectionString = dbConnection.GetConnectionString();
keycloakTestUser = configuration.GetSection("KeycloakTestUser").Get<KeyCloakUser>()!;
KeycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Value!;
}
private string connectionString = string.Empty;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("appsettings.Tests.json");
var dbConnection = config.Build().GetSection("DbConnection").Get<DbConnection>()!;
connectionString = dbConnection.GetConnectionString();
});
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistenceDbContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<PersistenceDbContext>(options =>
options.UseNpgsql(connectionString));
@ -63,6 +39,8 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>
return new TestHttpClientFactory(this);
});
services.AddSingleton<PersistenceClientFactory>();
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
@ -83,57 +61,4 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>
await dbContext.Database.EnsureDeletedAsync();
}
public T GetHttpClient<T>(string uriSuffix)
{
var httpClient = CreateClient();
if (string.IsNullOrEmpty(uriSuffix))
return RestService.For<T>(httpClient, RefitSettings);
if (httpClient.BaseAddress is not null)
httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix);
return RestService.For<T>(httpClient, RefitSettings);
}
public async Task<T> GetAuthorizedHttpClient<T>(string uriSuffix)
{
var httpClient = await GetAuthorizedHttpClient();
if (string.IsNullOrEmpty(uriSuffix))
return RestService.For<T>(httpClient, RefitSettings);
if (httpClient.BaseAddress is not null)
httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix);
return RestService.For<T>(httpClient, RefitSettings);
}
private async Task<HttpClient> GetAuthorizedHttpClient()
{
var httpClient = CreateClient();
var token = await GetTokenAsync();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return httpClient;
}
private async Task<string> GetTokenAsync()
{
var restClient = new RestClient();
var request = new RestRequest(KeycloakGetTokenUrl, Method.Post);
request.AddParameter("username", keycloakTestUser.Username);
request.AddParameter("password", keycloakTestUser.Password);
request.AddParameter("client_id", keycloakTestUser.ClientId);
request.AddParameter("grant_type", keycloakTestUser.GrantType);
var keyCloackResponse = await restClient.PostAsync(request);
if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content))
{
var token = JsonSerializer.Deserialize<JwtToken>(keyCloackResponse.Content)!;
return token.AccessToken;
}
return String.Empty;
}
}

View File

@ -0,0 +1,12 @@
namespace Persistence.Models.Configurations;
/// <summary>
/// Настройки credentials для авторизации
/// </summary>
public class AuthUser
{
public required string Username { get; set; }
public required string Password { get; set; }
public required string ClientId { get; set; }
public required string GrantType { get; set; }
}

View File

@ -0,0 +1,18 @@
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace Persistence.Models.Configurations
{
public static class JwtParams
{
private static readonly string KeyValue = "супер секретный ключ для шифрования";
public static SymmetricSecurityKey SecurityKey
{
get { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KeyValue)); }
}
public static readonly string Issuer = "a";
public static readonly string Audience = "a";
}
}

View File

@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace Persistence.Models.Configurations
{
public class JwtToken
{
[JsonPropertyName("access_token")]
public required string AccessToken { get; set; }
}
}

View File

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
</ItemGroup>
</Project>