Skip to content

Commit 60c4c67

Browse files
committed
improve authentication and authorization
1 parent dc5072b commit 60c4c67

12 files changed

Lines changed: 190 additions & 164 deletions

File tree

NpgsqlRest/Auth/AuthHandler.cs

Lines changed: 101 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
using System.Net;
1+
using System;
2+
using System.Net;
23
using System.Security.Claims;
34
using Microsoft.AspNetCore.Http.HttpResults;
45
using Npgsql;
5-
66
using static NpgsqlRest.Auth.ClaimsDictionary;
77

88
namespace NpgsqlRest.Auth;
99

10-
internal static class AuthHandler
10+
public static class AuthHandler
1111
{
1212
public static async Task HandleLoginAsync(
1313
NpgsqlCommand command,
@@ -21,13 +21,12 @@ public static async Task HandleLoginAsync(
2121
string? message = null;
2222
string? userId = null;
2323
string? userName = null;
24-
var authenticationType = options.AuthenticationOptions.DefaultAuthenticationType;
25-
var claims = new List<Claim>(10);
24+
var opts = options.AuthenticationOptions;
25+
List<Claim> claims = new(10);
2626
var verificationPerformed = false;
2727
var verificationFailed = false;
2828

29-
30-
await using (var reader = await command.ExecuteReaderAsync())
29+
await using (NpgsqlDataReader reader = await command.ExecuteReaderAsync())
3130
{
3231
if (await reader.ReadAsync() is false)
3332
{
@@ -44,12 +43,12 @@ public static async Task HandleLoginAsync(
4443
{
4544
var name1 = routine.OriginalColumnNames[i];
4645
var name2 = routine.ColumnNames[i];
47-
var descriptor = routine.ColumnsTypeDescriptor[i];
46+
TypeDescriptor descriptor = routine.ColumnsTypeDescriptor[i];
4847

49-
if (options.AuthenticationOptions.StatusColumnName is not null)
48+
if (opts.StatusColumnName is not null)
5049
{
51-
if (string.Equals(name1, options.AuthenticationOptions.StatusColumnName, StringComparison.OrdinalIgnoreCase) ||
52-
string.Equals(name2, options.AuthenticationOptions.StatusColumnName, StringComparison.OrdinalIgnoreCase))
50+
if (string.Equals(name1, opts.StatusColumnName, StringComparison.OrdinalIgnoreCase) ||
51+
string.Equals(name2, opts.StatusColumnName, StringComparison.OrdinalIgnoreCase))
5352
{
5453
if (descriptor.IsBoolean)
5554
{
@@ -78,88 +77,47 @@ public static async Task HandleLoginAsync(
7877
}
7978
}
8079

81-
if (options.AuthenticationOptions.SchemeColumnName is not null)
80+
if (opts.SchemeColumnName is not null)
8281
{
83-
if (string.Equals(name1, options.AuthenticationOptions.SchemeColumnName, StringComparison.OrdinalIgnoreCase) ||
84-
string.Equals(name2, options.AuthenticationOptions.SchemeColumnName, StringComparison.OrdinalIgnoreCase))
82+
if (string.Equals(name1, opts.SchemeColumnName, StringComparison.OrdinalIgnoreCase) ||
83+
string.Equals(name2, opts.SchemeColumnName, StringComparison.OrdinalIgnoreCase))
8584
{
8685
scheme = reader?.GetValue(i).ToString();
8786
continue;
8887
}
8988
}
9089

91-
if (options.AuthenticationOptions.MessageColumnName is not null)
90+
if (opts.MessageColumnName is not null)
9291
{
93-
if (string.Equals(name1, options.AuthenticationOptions.MessageColumnName, StringComparison.OrdinalIgnoreCase) ||
94-
string.Equals(name2, options.AuthenticationOptions.MessageColumnName, StringComparison.OrdinalIgnoreCase))
92+
if (string.Equals(name1, opts.MessageColumnName, StringComparison.OrdinalIgnoreCase) ||
93+
string.Equals(name2, opts.MessageColumnName, StringComparison.OrdinalIgnoreCase))
9594
{
9695
message = reader?.GetValue(i).ToString();
9796
continue;
9897
}
9998
}
100-
101-
string? claimType;
102-
if (options.AuthenticationOptions.UseActiveDirectoryFederationServicesClaimTypes)
103-
{
104-
if (ClaimTypesDictionary.TryGetValue(name1.ToLowerInvariant(), out claimType) is false)
105-
{
106-
if (ClaimTypesDictionary.TryGetValue(name2.ToLowerInvariant(), out claimType) is false)
107-
{
108-
claimType = name1;
109-
}
110-
}
111-
}
112-
else
99+
var (userNameCurrent, userIdCurrent) = AddClaimFromReader(opts, i, descriptor, reader!, claims, name1, name2);
100+
if (userNameCurrent is not null)
113101
{
114-
claimType = name1;
102+
userName = userNameCurrent;
115103
}
116-
117-
if (reader?.IsDBNull(i) is true)
104+
if (userIdCurrent is not null)
118105
{
119-
claims.Add(new Claim(claimType, ""));
120-
if (string.Equals(claimType, options.AuthenticationOptions.DefaultNameClaimType, StringComparison.Ordinal))
121-
{
122-
userName = null;
123-
}
124-
else if (string.Equals(claimType, options.AuthenticationOptions.DefaultUserIdClaimType, StringComparison.Ordinal))
125-
{
126-
userId = null;
127-
}
128-
}
129-
else if (descriptor.IsArray)
130-
{
131-
object[]? values = reader?.GetValue(i) as object[];
132-
for (int j = 0; j < values?.Length; j++)
133-
{
134-
claims.Add(new Claim(claimType, values[j]?.ToString() ?? ""));
135-
}
136-
}
137-
else
138-
{
139-
string? value = reader?.GetValue(i)?.ToString();
140-
claims.Add(new Claim(claimType, value ?? ""));
141-
if (string.Equals(claimType, options.AuthenticationOptions.DefaultNameClaimType, StringComparison.Ordinal))
142-
{
143-
userName = value;
144-
}
145-
else if (string.Equals(claimType, options.AuthenticationOptions.DefaultUserIdClaimType, StringComparison.Ordinal))
146-
{
147-
userId = value;
148-
}
106+
userId = userIdCurrent;
149107
}
150108
}
151109

152110
// hash verification last
153-
if (options.AuthenticationOptions.HashColumnName is not null &&
154-
options.AuthenticationOptions.PasswordHasher is not null &&
155-
options.AuthenticationOptions.PasswordParameterNameContains is not null)
111+
if (opts.HashColumnName is not null &&
112+
opts.PasswordHasher is not null &&
113+
opts.PasswordParameterNameContains is not null)
156114
{
157115
for (int i = 0; i < routine.ColumnCount; i++)
158116
{
159117
var name1 = routine.OriginalColumnNames[i];
160118
var name2 = routine.ColumnNames[i];
161-
if (string.Equals(name1, options.AuthenticationOptions.HashColumnName, StringComparison.OrdinalIgnoreCase) ||
162-
string.Equals(name2, options.AuthenticationOptions.HashColumnName, StringComparison.OrdinalIgnoreCase))
119+
if (string.Equals(name1, opts.HashColumnName, StringComparison.OrdinalIgnoreCase) ||
120+
string.Equals(name2, opts.HashColumnName, StringComparison.OrdinalIgnoreCase))
163121
{
164122
if (reader?.IsDBNull(i) is false)
165123
{
@@ -173,15 +131,15 @@ options.AuthenticationOptions.PasswordHasher is not null &&
173131
{
174132
var parameter = command.Parameters[j];
175133
var name = (parameter as NpgsqlRestParameter)?.ActualName;
176-
if (name is not null && name.Contains(options.AuthenticationOptions.PasswordParameterNameContains, // found password parameter
134+
if (name is not null && name.Contains(opts.PasswordParameterNameContains, // found password parameter
177135
StringComparison.OrdinalIgnoreCase))
178136
{
179137
foundPasswordParameter = true;
180138
var pass = parameter?.Value?.ToString();
181139
if (pass is not null && parameter?.Value != DBNull.Value)
182140
{
183141
verificationPerformed = true;
184-
if (options.AuthenticationOptions.PasswordHasher?.VerifyHashedPassword(hash, pass) is false)
142+
if (opts.PasswordHasher?.VerifyHashedPassword(hash, pass) is false)
185143
{
186144
logger?.VerifyPasswordFailed(endpoint, userId, userName);
187145
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
@@ -196,7 +154,7 @@ options.AuthenticationOptions.PasswordHasher is not null &&
196154
{
197155
logger?.CantFindPasswordParameter(endpoint,
198156
command.Parameters.Select(p => (p as NpgsqlRestParameter)?.ActualName)?.ToArray(),
199-
options.AuthenticationOptions.PasswordParameterNameContains);
157+
opts.PasswordParameterNameContains);
200158
}
201159
}
202160
}
@@ -209,12 +167,12 @@ options.AuthenticationOptions.PasswordHasher is not null &&
209167
{
210168
if (verificationFailed is true)
211169
{
212-
if (string.IsNullOrEmpty(options.AuthenticationOptions.PasswordVerificationFailedCommand) is false)
170+
if (string.IsNullOrEmpty(opts.PasswordVerificationFailedCommand) is false)
213171
{
214172
using var failedCommand = connection?.CreateCommand();
215173
if (failedCommand is not null)
216174
{
217-
failedCommand.CommandText = options.AuthenticationOptions.PasswordVerificationFailedCommand;
175+
failedCommand.CommandText = opts.PasswordVerificationFailedCommand;
218176
var paramCount = failedCommand.CommandText.PgCountParams();
219177
if (paramCount >= 1)
220178
{
@@ -235,12 +193,12 @@ options.AuthenticationOptions.PasswordHasher is not null &&
235193
}
236194
else
237195
{
238-
if (string.IsNullOrEmpty(options.AuthenticationOptions.PasswordVerificationSucceededCommand) is false)
196+
if (string.IsNullOrEmpty(opts.PasswordVerificationSucceededCommand) is false)
239197
{
240198
using var succeededCommand = connection?.CreateCommand();
241199
if (succeededCommand is not null)
242200
{
243-
succeededCommand.CommandText = options.AuthenticationOptions.PasswordVerificationSucceededCommand;
201+
succeededCommand.CommandText = opts.PasswordVerificationSucceededCommand;
244202
var paramCount = succeededCommand.CommandText.PgCountParams();
245203

246204
if (paramCount >= 1)
@@ -265,9 +223,9 @@ options.AuthenticationOptions.PasswordHasher is not null &&
265223
{
266224
var principal = new ClaimsPrincipal(new ClaimsIdentity(
267225
claims,
268-
scheme ?? authenticationType,
269-
nameType: options.AuthenticationOptions.DefaultNameClaimType,
270-
roleType: options.AuthenticationOptions.DefaultRoleClaimType));
226+
scheme ?? opts.DefaultAuthenticationType,
227+
nameType: opts.GetUserNameClaimType(),
228+
roleType: opts.GetRoleClaimType()));
271229

272230
if (Results.SignIn(principal: principal, authenticationScheme: scheme) is not SignInHttpResult result)
273231
{
@@ -283,6 +241,71 @@ options.AuthenticationOptions.PasswordHasher is not null &&
283241
}
284242
}
285243

244+
public static (string? userName, string? userId) AddClaimFromReader(
245+
NpgsqlRestAuthenticationOptions options,
246+
int i,
247+
TypeDescriptor descriptor,
248+
NpgsqlDataReader reader,
249+
List<Claim> claims,
250+
string name1,
251+
string name2)
252+
{
253+
string? claimType;
254+
string? userName = null;
255+
string? userId = null;
256+
257+
if (options.UseActiveDirectoryFederationServicesClaimTypes)
258+
{
259+
if (ClaimTypesDictionary.TryGetValue(name1.ToLowerInvariant(), out claimType) is false)
260+
{
261+
if (ClaimTypesDictionary.TryGetValue(name2.ToLowerInvariant(), out claimType) is false)
262+
{
263+
claimType = name1.Replace("_", "");
264+
}
265+
}
266+
}
267+
else
268+
{
269+
claimType = name1.Replace("_", "");
270+
}
271+
272+
if (reader?.IsDBNull(i) is true)
273+
{
274+
claims.Add(new Claim(claimType, ""));
275+
if (string.Equals(claimType, options.GetUserNameClaimType(), StringComparison.Ordinal))
276+
{
277+
userName = null;
278+
}
279+
else if (string.Equals(claimType, options.GetUserIdClaimType(), StringComparison.Ordinal))
280+
{
281+
userId = null;
282+
}
283+
}
284+
else if (descriptor.IsArray)
285+
{
286+
object[]? values = reader?.GetValue(i) as object[];
287+
for (int j = 0; j < values?.Length; j++)
288+
{
289+
claims.Add(new Claim(claimType, values[j]?.ToString() ?? ""));
290+
}
291+
}
292+
else
293+
{
294+
string? value = reader?.GetValue(i)?.ToString();
295+
claims.Add(new Claim(claimType, value ?? ""));
296+
if (string.Equals(claimType, options.GetUserNameClaimType(), StringComparison.Ordinal))
297+
{
298+
userName = value;
299+
}
300+
else if (string.Equals(claimType, options.GetUserIdClaimType(), StringComparison.Ordinal))
301+
{
302+
userId = value;
303+
}
304+
}
305+
306+
return (userName, userId);
307+
}
308+
286309
public static async Task HandleLogoutAsync(NpgsqlCommand command, Routine routine, HttpContext context)
287310
{
288311
if (routine.IsVoid)

NpgsqlRest/NpgsqlRestAuthenticationOptions.cs renamed to NpgsqlRest/Auth/NpgsqlRestAuthenticationOptions.cs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
1-
using System.Security.Claims;
2-
3-
namespace NpgsqlRest;
1+

2+
namespace NpgsqlRest.Auth;
43

54
/// <summary>
65
/// Authentication options for the NpgsqlRest middleware.
76
/// </summary>
87
public class NpgsqlRestAuthenticationOptions
98
{
9+
private string? _defaultUserIdClaimType = null;
10+
private string? _defaultNameClaimType = null;
11+
private string? _defaultRoleClaimType = null;
12+
13+
public string GetUserIdClaimType()
14+
{
15+
_defaultUserIdClaimType ??= DefaultUserIdClaimType.ToLowerInvariant().Replace("_", "") ?? string.Empty;
16+
if (UseActiveDirectoryFederationServicesClaimTypes is true &&
17+
ClaimsDictionary.ClaimTypesDictionary.TryGetValue(_defaultUserIdClaimType, out var claimType))
18+
{
19+
return claimType;
20+
}
21+
return DefaultUserIdClaimType;
22+
}
23+
24+
public string GetUserNameClaimType()
25+
{
26+
_defaultNameClaimType ??= DefaultNameClaimType.ToLowerInvariant().Replace("_", "") ?? string.Empty;
27+
if (UseActiveDirectoryFederationServicesClaimTypes is true &&
28+
ClaimsDictionary.ClaimTypesDictionary.TryGetValue(DefaultNameClaimType.ToLowerInvariant(), out var claimType))
29+
{
30+
return claimType;
31+
}
32+
return DefaultNameClaimType;
33+
}
34+
35+
public string GetRoleClaimType()
36+
{
37+
_defaultRoleClaimType ??= DefaultRoleClaimType.ToLowerInvariant().Replace("_", "") ?? string.Empty;
38+
if (UseActiveDirectoryFederationServicesClaimTypes is true &&
39+
ClaimsDictionary.ClaimTypesDictionary.TryGetValue(DefaultRoleClaimType.ToLowerInvariant(), out var claimType))
40+
{
41+
return claimType;
42+
}
43+
return DefaultRoleClaimType;
44+
}
45+
1046
/// <summary>
1147
/// Authentication type used with the Login endpoints to set the authentication type for the new `ClaimsIdentity` created by the login.
1248
///
@@ -46,33 +82,22 @@ public class NpgsqlRestAuthenticationOptions
4682
///
4783
/// For example, column name `NameIdentifier` or `name_identifier` (when transformed by the default name transformer) will match the key `NameIdentifier` which translates to this: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
4884
/// </summary>
49-
public bool UseActiveDirectoryFederationServicesClaimTypes { get; set; } = true;
85+
public bool UseActiveDirectoryFederationServicesClaimTypes { get; set; } = false;
5086

5187
/// <summary>
5288
/// Default claim type for user id.
5389
/// </summary>
54-
public string DefaultUserIdClaimType { get; set; } = ClaimTypes.NameIdentifier;
55-
56-
private string _defaultNameClaimType = ClaimTypes.Name;
57-
internal bool UsingDefaultNameClaimType = true;
90+
public string DefaultUserIdClaimType { get; set; } = "nameidentifier"; // ClaimTypes.NameIdentifier;
5891

5992
/// <summary>
6093
/// Default claim type for user name.
6194
/// </summary>
62-
public string DefaultNameClaimType
63-
{
64-
get => _defaultNameClaimType;
65-
set
66-
{
67-
_defaultNameClaimType = value;
68-
UsingDefaultNameClaimType = string.Equals(value, ClaimTypes.Name, StringComparison.Ordinal);
69-
}
70-
}
95+
public string DefaultNameClaimType { get; set; } = "name"; // ClaimTypes.Name;
7196

7297
/// <summary>
7398
/// Default claim type for user roles.
7499
/// </summary>
75-
public string DefaultRoleClaimType { get; set; } = ClaimTypes.Role;
100+
public string DefaultRoleClaimType { get; set; } = "role"; // ClaimTypes.Role;
76101

77102
/// <summary>
78103
/// If true, return any response from auth endpoints (login and logout) if response hasn't been written by auth handler.

NpgsqlRest/Defaults/DefaultCommentParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ internal static class DefaultCommentParser
2525
];
2626
private static readonly string[] authorizeKey = [
2727
"authorize",
28-
"requires_authorization"
28+
"authorized",
29+
"requires_authorization",
2930
];
3031
private static readonly string[] allowAnonymousKey = [
3132
"allow_anonymous",

0 commit comments

Comments
 (0)