Skip to content

Commit ad8be9b

Browse files
committed
2.24.0 Initial
1 parent 454a252 commit ad8be9b

17 files changed

+303
-142
lines changed

NpgsqlRest/Auth/AuthHandler.cs

+169-104
Large diffs are not rendered by default.

NpgsqlRest/Auth/NpgsqlRestAuthenticationOptions.cs

+14
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,18 @@ public class NpgsqlRestAuthenticationOptions
8989
/// This is used to verify the password from the password parameter when login endpoint returns a hash of the password (see HashColumnName).
9090
/// </summary>
9191
public string PasswordParameterNameContains { get; set; } = "pass";
92+
93+
/// <summary>
94+
/// Default password hasher object. Inject custom password hasher object to add default password hasher.
95+
/// </summary>
96+
public IPasswordHasher? PasswordHasher { get; set; } = new PasswordHasher();
97+
98+
/// <summary>
99+
/// Command that is executed when the password verification fails. There are three text parameters:
100+
/// - scheme: authentication scheme used for the login (if exists)
101+
/// - user_name: user id used for the login (if exists)
102+
/// - user_id: user id used for the login (if exists)
103+
/// Please use PostgreSQL parameter placeholders for the parameters ($1, $2, $3).
104+
/// </summary>
105+
public string? PasswordVerificationFailedCommand { get; set; } = null;
92106
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace NpgsqlRest.Auth;
4+
5+
public partial class PostgreSqlParameterCounter
6+
{
7+
[GeneratedRegex(@"\$\d+")]
8+
public static partial Regex PostgreSqlParameterPattern();
9+
10+
public static int CountParameters(string sql)
11+
{
12+
return PostgreSqlParameterPattern().Matches(sql).Count;
13+
}
14+
}

NpgsqlRest/Log.cs

+3
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,7 @@ public static partial class Log
178178

179179
[LoggerMessage(Level = LogLevel.Information, Message = "{description} has set to use multiple UPLOAD HANDLERES {handlers} by the comment annotation.")]
180180
public static partial void CommentUploadHandlers(this ILogger logger, string description, string[]? handlers);
181+
182+
[LoggerMessage(Level = LogLevel.Warning, Message = "Login endpoint {endpoint} failed to locate the password parameter in parameter collection {parameters}. Password parameter is the first that contains \"{contains}\" text in parameter name.")]
183+
public static partial void CantFindPasswordParameter(this ILogger logger, string endpoint, string?[]? parameters, string contains);
181184
}

NpgsqlRest/NpgsqlRest.csproj

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
<GenerateDocumentationFile>true</GenerateDocumentationFile>
3030
<PackageReadmeFile>README.MD</PackageReadmeFile>
3131
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
32-
<Version>2.23.0</Version>
33-
<AssemblyVersion>2.23.0</AssemblyVersion>
34-
<FileVersion>2.23.0</FileVersion>
35-
<PackageVersion>2.23.0</PackageVersion>
32+
<Version>2.24.0</Version>
33+
<AssemblyVersion>2.24.0</AssemblyVersion>
34+
<FileVersion>2.24.0</FileVersion>
35+
<PackageVersion>2.24.0</PackageVersion>
3636
</PropertyGroup>
3737

3838
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">

NpgsqlRest/NpgsqlRestMiddleware.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ public async Task InvokeAsync(HttpContext context)
311311
}
312312
else
313313
{
314-
parameter.Value = options.PasswordHasher?.HashPassword(hashValueQueryDict) as object ?? DBNull.Value;
314+
parameter.Value = options.AuthenticationOptions.PasswordHasher?.HashPassword(hashValueQueryDict) as object ?? DBNull.Value;
315315
}
316316
}
317317
if (parameter.UploadMetadata is true)
@@ -749,7 +749,7 @@ await options.ValidateParametersAsync(new ParameterValidationValues(
749749
}
750750
else
751751
{
752-
parameter.Value = options.PasswordHasher?.HashPassword(hashValueBodyDict) as object ?? DBNull.Value;
752+
parameter.Value = options.AuthenticationOptions.PasswordHasher?.HashPassword(hashValueBodyDict) as object ?? DBNull.Value;
753753
}
754754
}
755755
if (parameter.UploadMetadata is true)

NpgsqlRest/NpgsqlRestOptions.cs

-5
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,6 @@ public NpgsqlRestOptions(NpgsqlDataSource dataSource)
316316
/// </summary>
317317
public IResponseParser? DefaultResponseParser { get; set; }
318318

319-
/// <summary>
320-
/// Default password hasher object. Inject custom password hasher object to add default password hasher.
321-
/// </summary>
322-
public IPasswordHasher PasswordHasher { get; set; } = new PasswordHasher();
323-
324319
/// <summary>
325320
/// Default upload handler options.
326321
/// Set this option to null to disable upload handlers or use this to modify upload handler options.

NpgsqlRestClient/ExternalAuth.cs

+20-14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using static NpgsqlRestClient.Config;
1313
using static NpgsqlRestClient.Builder;
1414
using static NpgsqlRest.Auth.ClaimsDictionary;
15+
using NpgsqlRest.Auth;
1516

1617
namespace NpgsqlRestClient;
1718

@@ -41,8 +42,8 @@ public static class ExternalAuthConfig
4142
public static string ClientAnaliticsIpKey { get; private set; } = default!;
4243

4344
private static readonly Dictionary<string, ExternalAuthClientConfig> DefaultClientConfigs =
44-
new Dictionary<string, ExternalAuthClientConfig>
45-
{
45+
new()
46+
{
4647
{ "google", new ExternalAuthClientConfig
4748
{
4849
AuthUrl = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id={0}&redirect_uri={1}&scope=openid profile email&state={2}",
@@ -190,7 +191,7 @@ public static void Configure(WebApplication app, NpgsqlRestOptions options, Post
190191
string code = (node["code"]?.ToString()) ??
191192
throw new ArgumentException("code retrieved from the external provider is null");
192193

193-
await ProcessAsync(code, body, config, context, options);
194+
await ProcessAsync(code, config, context, options);
194195
return;
195196
}
196197
catch (Exception e)
@@ -216,15 +217,14 @@ private static void PrepareResponse(string contentType, HttpContext context)
216217
context.Response.Headers.Expires = "0";
217218
}
218219

219-
private static readonly HttpClient _httpClient = new();
220+
private static HttpClient? _httpClient = null;
220221
private static readonly string _agent = $"{Guid.NewGuid().ToString()[..8]}";
221222

222223
private const string browserSessionStateKey = "__external_state";
223224
private const string browserSessionParamsKey = "__external_params";
224225

225226
private static async Task ProcessAsync(
226227
string code,
227-
string body,
228228
ExternalAuthClientConfig config,
229229
HttpContext context,
230230
NpgsqlRestOptions options)
@@ -233,6 +233,8 @@ private static async Task ProcessAsync(
233233
string? name;
234234
string token;
235235

236+
_httpClient ??= new();
237+
236238
using var requestTokenMessage = new HttpRequestMessage(HttpMethod.Post, config.TokenUrl);
237239
requestTokenMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(Application.Json));
238240
requestTokenMessage.Content = new FormUrlEncodedContent(new Dictionary<string, string>
@@ -273,6 +275,7 @@ private static async Task ProcessAsync(
273275
infoRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
274276
using var infoResponse = await _httpClient.SendAsync(infoRequest);
275277
var infoContent = await infoResponse.Content.ReadAsStringAsync();
278+
JsonNode infoNode;
276279
if (!infoResponse.IsSuccessStatusCode)
277280
{
278281
throw new HttpRequestException($"Info endpoint {config.InfoUrl} returned {infoResponse.StatusCode} with following content: {infoContent}");
@@ -284,18 +287,18 @@ private static async Task ProcessAsync(
284287

285288
try
286289
{
287-
JsonNode node = JsonNode.Parse(infoContent) ??
290+
infoNode = JsonNode.Parse(infoContent) ??
288291
throw new ArgumentException("info json node is null");
289292

290293
// Email parsing
291-
email = node["email"]?.ToString() ?? // Google, GitHub, Facebook, Microsoft
292-
node["userPrincipalName"]?.ToString() ?? // Microsoft (organizational accounts)
293-
node?["elements"]?[0]?["handle~"]?["emailAddress"]?.ToString(); // LinkedIn
294+
email = infoNode["email"]?.ToString() ?? // Google, GitHub, Facebook, Microsoft
295+
infoNode["userPrincipalName"]?.ToString() ?? // Microsoft (organizational accounts)
296+
infoNode?["elements"]?[0]?["handle~"]?["emailAddress"]?.ToString(); // LinkedIn
294297

295298
#pragma warning disable CS8602 // Dereference of a possibly null reference.
296-
name = node["localizedLastName"] is not null ?
297-
$"{node["localizedFirstName"]} {node["localizedLastName"]}".Trim() : // linkedin format
298-
node["name"]?.ToString(); // normal format
299+
name = infoNode["localizedLastName"] is not null ?
300+
$"{infoNode["localizedFirstName"]} {infoNode["localizedLastName"]}".Trim() : // linkedin format
301+
infoNode["name"]?.ToString(); // normal format
299302
#pragma warning restore CS8602 // Dereference of a possibly null reference.
300303
}
301304
catch (Exception e)
@@ -323,6 +326,7 @@ private static async Task ProcessAsync(
323326
{
324327
JsonNode node = JsonNode.Parse(emailContent) ??
325328
throw new ArgumentException("email json node is null");
329+
infoNode["emailRequest"] = node;
326330

327331
email = node["email"]?.ToString() ??
328332
node?["elements"]?[0]?["handle~"]?["emailAddress"]?.ToString(); // linkedin format
@@ -351,8 +355,8 @@ private static async Task ProcessAsync(
351355
await connection.OpenAsync();
352356
using var command = connection.CreateCommand();
353357
command.CommandText = ExternalAuthConfig.LoginCommand;
354-
var paramCount = command.CommandText[command.CommandText.IndexOf(Consts.OpenParenthesis)..].Split(Consts.Comma).Length;
355358

359+
var paramCount = PostgreSqlParameterCounter.CountParameters(command.CommandText);
356360
if (paramCount >= 1) command.Parameters.Add(new NpgsqlParameter()
357361
{
358362
Value = config.ExternalType,
@@ -368,9 +372,11 @@ private static async Task ProcessAsync(
368372
Value = name is not null ? name : DBNull.Value,
369373
NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text
370374
});
375+
//emailContent
376+
371377
if (paramCount >= 4) command.Parameters.Add(new NpgsqlParameter()
372378
{
373-
Value = body is not null ? body : DBNull.Value,
379+
Value = infoNode is not null ? infoContent.ToString() : DBNull.Value,
374380
NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Json
375381
});
376382
if (paramCount >= 5)

NpgsqlRestClient/NpgsqlRestClient.csproj

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<InvariantGlobalization>true</InvariantGlobalization>
99
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
1010
<PublishAot>true</PublishAot>
11-
<Version>2.18.0</Version>
11+
<Version>2.19.0</Version>
1212
</PropertyGroup>
1313

1414
<ItemGroup>
@@ -22,5 +22,9 @@
2222
<ProjectReference Include="..\plugins\NpgsqlRest.HttpFiles\NpgsqlRest.HttpFiles.csproj" />
2323
<ProjectReference Include="..\plugins\NpgsqlRest.TsClient\NpgsqlRest.TsClient.csproj" />
2424
</ItemGroup>
25+
26+
<ItemGroup>
27+
<Folder Include="wwwroot\" />
28+
</ItemGroup>
2529

2630
</Project>

NpgsqlRestClient/Program.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
Configure(app, () =>
3838
{
3939
sw.Stop();
40-
var message = GetConfigStr("StartupMessage", Cfg);
40+
var message = GetConfigStr("StartupMessage", Cfg) ?? "Started in {0}, listening on {1}, version {2}";
4141
if (string.IsNullOrEmpty(message) is false)
4242
{
4343
Logger?.Information(message,
@@ -115,7 +115,8 @@
115115

116116
AuthenticationOptions = new()
117117
{
118-
DefaultAuthenticationType = GetConfigStr("DefaultAuthenticationType", AuthCfg)
118+
DefaultAuthenticationType = GetConfigStr("DefaultAuthenticationType", AuthCfg) ?? GetConfigStr("ApplicationName", Cfg),
119+
PasswordVerificationFailedCommand = GetConfigStr("PasswordVerificationFailedCommand", AuthCfg),
119120
},
120121

121122
EndpointCreateHandlers = CreateCodeGenHandlers(connectionString),

NpgsqlRestClient/appsettings.json

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
2.18.0.0
2+
2.19.0.0
33
*/
44
{
55
//
@@ -224,8 +224,14 @@
224224
//
225225
"ReturnToPathQueryStringKey": "return_to",
226226
//
227-
// Login command to execute after the external auth process is completed.
228-
// The first parameter is the email returned from the provider, the second parameter is the name returned from the provider and third parameter is the collection of the parameters that will include original query string.
227+
// Login command to execute after the external auth process is completed. Parameters:
228+
// - external login provider (if param exists)
229+
// - external login email (if param exists)
230+
// - external login name (if param exists)
231+
// - external login json data received (if param exists)
232+
// - client browser analytics json data (if param exists)
233+
// Please use PostgreSQL parameter placeholders for the parameters ($1, $2, $3).
234+
//
229235
// The command uses the same rules as the login enabled routine.
230236
// See: https://vb-consulting.github.io/npgsqlrest/login-endpoints and https://vb-consulting.github.io/npgsqlrest/login-endpoints/#external-logins
231237
//
@@ -238,7 +244,9 @@
238244
// Client IP address that will be added to the client analytics data under this JSON key.
239245
//
240246
"ClientAnaliticsIpKey": "ip",
241-
247+
//
248+
// External providers
249+
//
242250
"Google": {
243251
//
244252
// visit https://console.cloud.google.com/apis/ to configure your google app and get your client id and client secret
@@ -648,6 +656,16 @@
648656
//
649657
"DefaultAuthenticationType": null,
650658
//
659+
// Command that is executed when the password verification fails. There are three text parameters:
660+
// - scheme: authentication scheme used for the login (if exists)
661+
// - user_name: user id used for the login (if exists)
662+
// - user_id: user id used for the login (if exists)
663+
// Please use PostgreSQL parameter placeholders for the parameters ($1, $2, $3).
664+
//
665+
// See https://vb-consulting.github.io/npgsqlrest/options/#authenticationoptionsdefaultauthenticationtype
666+
//
667+
"PasswordVerificationFailedCommand": null,
668+
//
651669
// Name of the PostgreSQL text parameter that will be used to pass the authenticated user id to the PostgreSQL routine (function or query) automatically (supplied values are rewritten).
652670
//
653671
"UserIdParameterName": null,

NpgsqlRestTests/AuthTests/AuthPasswordLoginTests.cs

+22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Npgsql;
2+
13
namespace NpgsqlRestTests;
24

35
public static partial class Database
@@ -42,6 +44,16 @@ language sql as $$
4244
$$;
4345
comment on function password_protected_login2(text) is 'login';
4446
47+
create table failed_logins(scheme text, user_id text, user_name text);
48+
49+
create procedure failed_login(
50+
_scheme text,
51+
_user_id text,
52+
_user_name text
53+
)
54+
language sql as $$
55+
insert into failed_logins(scheme, user_id, user_name) values (_scheme, _user_id, _user_name);
56+
$$;
4557
""");
4658
}
4759
}
@@ -94,5 +106,15 @@ public async Task Test_password_protected_login1_wrong_password()
94106
using var content = new StringContent($"{{\"pass\": \"{password}\", \"hashed\": \"{hashed}\"}}", Encoding.UTF8, "application/json");
95107
using var login = await client.PostAsync(requestUri: "/api/password-protected-login1/", content);
96108
login.StatusCode.Should().Be(HttpStatusCode.NotFound);
109+
110+
using var connection = Database.CreateConnection();
111+
await connection.OpenAsync();
112+
using var command = new NpgsqlCommand("select * from failed_logins", connection);
113+
using var reader = await command.ExecuteReaderAsync();
114+
(await reader.ReadAsync()).Should().BeTrue(); // there is a record
115+
116+
reader.IsDBNull(0).Should().Be(true);
117+
reader.GetString(1).Should().Be("passwordprotected"); // username
118+
reader.GetString(2).Should().Be("999"); // user id
97119
}
98120
}

NpgsqlRestTests/AuthTests/HashedParameterTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ public async Task Test_post_hashed_in_new_parameter1_3()
160160
response.Should().Be("");
161161
}
162162

163-
164163
[Fact]
165164
public async Task Test_post_hashed_parameter1()
166165
{

NpgsqlRestTests/AuthTests/PasswordHasherTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace NpgsqlRestTests;
1+
namespace NpgsqlRestTests.AuthTests;
22

33
public class PasswordHasherTests
44
{

NpgsqlRestTests/Setup/Program.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,12 @@ public static void Main()
116116
CustomRequestHeaders = new()
117117
{
118118
{ "custom-header1", "custom-header1-value" }
119-
}
119+
},
120+
121+
AuthenticationOptions = new()
122+
{
123+
PasswordVerificationFailedCommand = "call failed_login($1,$2,$3)"
124+
},
120125
});
121126
app.Run();
122127
}

0 commit comments

Comments
 (0)