Skip to content

Commit 58f13c0

Browse files
committed
Events added
1 parent bd16da4 commit 58f13c0

12 files changed

+595
-54
lines changed

src/AspNetCore.Authentication.Basic/AspNetCore.Authentication.Basic.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
<PackageTags>aspnetcore, security, authentication, microsoft, microsoft.aspnetcore.authentication, microsoft-aspnetcore-authentication, microsoft.aspnetcore.authentication.basic, microsoft-aspnetcore-authentication-basic, asp-net-core, netstandard, netstandard20, basic-authentication, basicauthentication, dotnetcore, dotnetcore3.1, asp-net-core-basic-authentication, aspnetcore-basic-authentication, asp-net-core-authentication, aspnetcore-authentication, asp, aspnet, basic, authentication-scheme</PackageTags>
99
<PackageReleaseNotes>- Multitarget framework support added
1010
- Source Link support added
11-
- Strong Name Key support added</PackageReleaseNotes>
11+
- Strong Name Key support added
12+
- SuppressWWWAuthenticateHeader added to configure options
13+
- Events added to configure options
14+
</PackageReleaseNotes>
1215
<Description>Easy to use and very light weight Microsoft style Basic Scheme Authentication implementation for ASP.NET Core.</Description>
1316
<Authors>Mihir Dilip</Authors>
1417
<Company>Mihir Dilip</Company>

src/AspNetCore.Authentication.Basic/BasicExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ public static class BasicExtensions
2323
public static AuthenticationBuilder AddBasic<TBasicUserValidationService>(this AuthenticationBuilder builder, Action<BasicOptions> configureOptions)
2424
where TBasicUserValidationService : class, IBasicUserValidationService
2525
{
26-
// Adds post configure options to the pipeline.
27-
builder.Services.AddSingleton<IPostConfigureOptions<BasicOptions>, BasicPostConfigureOptions>();
28-
2926
// Adds implementation of IBasicUserValidationService to the dependency container.
3027
builder.Services.AddTransient<IBasicUserValidationService, TBasicUserValidationService>();
28+
29+
// Adds post configure options to the pipeline.
30+
builder.Services.AddSingleton<IPostConfigureOptions<BasicOptions>, BasicPostConfigureOptions>();
3131

3232
// Adds basic authentication scheme to the pipeline.
3333
return builder.AddScheme<BasicOptions, BasicHandler>(BasicDefaults.AuthenticationScheme, configureOptions);

src/AspNetCore.Authentication.Basic/BasicHandler.cs

Lines changed: 189 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515
namespace AspNetCore.Authentication.Basic
1616
{
17-
/// <summary>
18-
/// Inherited from <see cref="AuthenticationHandler{TOptions}"/> for basic authentication.
19-
/// </summary>
20-
public class BasicHandler : AuthenticationHandler<BasicOptions>
17+
/// <summary>
18+
/// Inherited from <see cref="AuthenticationHandler{TOptions}"/> for basic authentication.
19+
/// </summary>
20+
internal class BasicHandler : AuthenticationHandler<BasicOptions>
2121
{
2222
private readonly IBasicUserValidationService _basicUserValidationService;
2323

@@ -32,76 +32,231 @@ public class BasicHandler : AuthenticationHandler<BasicOptions>
3232
public BasicHandler(IOptionsMonitor<BasicOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicUserValidationService basicUserValidationService)
3333
: base(options, logger, encoder, clock)
3434
{
35-
_basicUserValidationService = basicUserValidationService;
35+
_basicUserValidationService = basicUserValidationService ?? throw new ArgumentNullException(nameof(basicUserValidationService));
3636
}
3737

3838
private string Challenge => $"{BasicDefaults.AuthenticationScheme} realm=\"{Options.Realm}\", charset=\"UTF-8\"";
3939

40-
//protected new BasicEvents Events { get => (BasicEvents)base.Events; set => base.Events = value; }
41-
//protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new BasicEvents());
40+
/// <summary>
41+
/// Get or set <see cref="BasicEvents"/>.
42+
/// </summary>
43+
protected new BasicEvents Events { get => (BasicEvents)base.Events; set => base.Events = value; }
4244

4345
/// <summary>
44-
/// Searches the 'Authorization' header for 'Basic' scheme with base64 encoded username:password string value of which is validated using implementation of <see cref="IBasicUserValidationService"/> passed as type parameter when setting up basic authentication in the Startup.cs
46+
/// Create an instance of <see cref="BasicEvents"/>.
4547
/// </summary>
4648
/// <returns></returns>
49+
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new BasicEvents());
50+
51+
/// <summary>
52+
/// Searches the 'Authorization' header for 'Basic' scheme with base64 encoded username:password string value of which is validated using implementation of <see cref="IBasicUserValidationService"/> passed as type parameter when setting up basic authentication in the Startup.cs
53+
/// </summary>
54+
/// <returns><see cref="AuthenticateResult"/></returns>
4755
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
4856
{
4957
if (!Request.Headers.ContainsKey(HeaderNames.Authorization))
5058
{
51-
// No 'Authorization' header found in the request.
59+
Logger.LogInformation("No 'Authorization' header found in the request.");
5260
return AuthenticateResult.NoResult();
5361
}
5462

5563
if (!AuthenticationHeaderValue.TryParse(Request.Headers[HeaderNames.Authorization], out var headerValue))
5664
{
57-
// No valid 'Authorization' header found in the request.
65+
Logger.LogInformation("No valid 'Authorization' header found in the request.");
5866
return AuthenticateResult.NoResult();
5967
}
60-
6168

6269
if (!headerValue.Scheme.Equals(BasicDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
6370
{
64-
// 'Authorization' header found but the scheme is not a basic scheme.
71+
Logger.LogInformation($"'Authorization' header found but the scheme is not a '{BasicDefaults.AuthenticationScheme}' scheme.");
6572
return AuthenticateResult.NoResult();
6673
}
6774

68-
// Convert the base64 encoded 'username:password' to normal string and parse username and password from colon(:) separated string.
69-
var usernameAndPassword = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue.Parameter));
70-
var usernameAndPasswordSplit = usernameAndPassword.Split(':');
71-
if (usernameAndPasswordSplit.Length != 2)
75+
BasicCredentials credentials;
76+
try
77+
{
78+
credentials = DecodeBasicCredentials(headerValue.Parameter);
79+
}
80+
catch (Exception exception)
81+
{
82+
return AuthenticateResult.Fail(exception);
83+
}
84+
85+
try
7286
{
73-
return AuthenticateResult.Fail("Invalid Basic authentication header");
87+
// Raise validate credentials event.
88+
// It can either have a result set or a principal set or just a bool? validation result set.
89+
var validateCredentialsContext = new BasicValidateCredentialsContext(Context, Scheme, Options, credentials.Username, credentials.Password);
90+
await Events.ValidateCredentialsAsync(validateCredentialsContext).ConfigureAwait(false);
91+
92+
if (validateCredentialsContext.Result != null)
93+
{
94+
return validateCredentialsContext.Result;
95+
}
96+
97+
if (validateCredentialsContext.Principal?.Identity != null && validateCredentialsContext.Principal.Identity.IsAuthenticated)
98+
{
99+
// If claims principal is set and is authenticated then build a ticket by calling and return success.
100+
validateCredentialsContext.Success();
101+
return validateCredentialsContext.Result;
102+
}
103+
104+
var hasValidationSucceeded = false;
105+
var validationFailureMessage = "Invalid username or password.";
106+
107+
if (validateCredentialsContext.ValidationResult.HasValue)
108+
{
109+
hasValidationSucceeded = validateCredentialsContext.ValidationResult.Value;
110+
111+
// If validation result was not successful return failure.
112+
if (!hasValidationSucceeded)
113+
{
114+
return AuthenticateResult.Fail(
115+
validateCredentialsContext.ValidationFailureException ?? new Exception(validationFailureMessage)
116+
);
117+
}
118+
}
119+
120+
// If validation result was not set then validate using the implementation of IBasicUserValidationService.
121+
if (!hasValidationSucceeded)
122+
{
123+
if (_basicUserValidationService is DefaultBasicUserValidationService)
124+
{
125+
throw new InvalidOperationException($"Either {nameof(Options.Events.OnValidateCredentials)} delegate on configure options {nameof(Options.Events)} should be set or an implementaion of {nameof(IBasicUserValidationService)} should be registered in the dependency container.");
126+
}
127+
hasValidationSucceeded = await _basicUserValidationService.IsValidAsync(credentials.Username, credentials.Password);
128+
}
129+
130+
// Return fail if validation not succeeded.
131+
if (!hasValidationSucceeded)
132+
{
133+
return AuthenticateResult.Fail(validationFailureMessage);
134+
}
135+
136+
// Create claims principal.
137+
var claims = new[]
138+
{
139+
new Claim(ClaimTypes.NameIdentifier, credentials.Username, ClaimValueTypes.String, ClaimsIssuer),
140+
new Claim(ClaimTypes.Name, credentials.Username, ClaimValueTypes.String, ClaimsIssuer)
141+
};
142+
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
143+
144+
// Raise authentication succeeded event.
145+
var authenticationSucceededContext = new BasicAuthenticationSucceededContext(Context, Scheme, Options, principal);
146+
await Events.AuthenticationSucceededAsync(authenticationSucceededContext).ConfigureAwait(false);
147+
148+
if (authenticationSucceededContext.Result != null)
149+
{
150+
return authenticationSucceededContext.Result;
151+
}
152+
153+
if (authenticationSucceededContext.Principal?.Identity != null && authenticationSucceededContext.Principal.Identity.IsAuthenticated)
154+
{
155+
// If claims principal is set and is authenticated then build a ticket by calling and return success.
156+
authenticationSucceededContext.Success();
157+
return authenticationSucceededContext.Result;
158+
}
159+
160+
return AuthenticateResult.Fail("No authenticated prinicipal set.");
161+
}
162+
catch (Exception exception)
163+
{
164+
var authenticationFailedContext = new BasicAuthenticationFailedContext(Context, Scheme, Options, exception);
165+
await Events.AuthenticationFailedAsync(authenticationFailedContext).ConfigureAwait(false);
166+
167+
if (authenticationFailedContext.Result != null)
168+
{
169+
return authenticationFailedContext.Result;
170+
}
171+
172+
throw;
74173
}
75-
var username = usernameAndPasswordSplit[0];
76-
var password = usernameAndPasswordSplit[1];
174+
}
77175

78-
// Validate username and password by using the implementation of IBasicUserValidationService.
79-
var isValidUser = await _basicUserValidationService.IsValidAsync(username, password);
80-
if (!isValidUser)
176+
/// <inheritdoc/>
177+
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
178+
{
179+
// Raise handle forbidden event.
180+
var handleForbiddenContext = new BasicHandleForbiddenContext(Context, Scheme, Options, properties);
181+
await Events.HandleForbiddenAsync(handleForbiddenContext).ConfigureAwait(false);
182+
if (handleForbiddenContext.IsHandled)
81183
{
82-
return AuthenticateResult.Fail("Invalid username or password");
184+
return;
83185
}
84186

85-
// Create 'AuthenticationTicket' and return as success if the above validation was successful.
86-
var claims = new[] { new Claim(ClaimTypes.Name, username) };
87-
var identity = new ClaimsIdentity(claims, Scheme.Name);
88-
var principal = new ClaimsPrincipal(identity);
89-
var ticket = new AuthenticationTicket(principal, Scheme.Name);
90-
return AuthenticateResult.Success(ticket);
91-
}
187+
await base.HandleForbiddenAsync(properties);
188+
}
92189

93190
/// <summary>
94191
/// Handles the un-authenticated requests.
95192
/// Returns 401 status code in response.
96-
/// Adds 'WWW-Authenticate' with 'Basic' authentication scheme and 'Realm' in the response header
193+
/// If <see cref="BasicOptions.SuppressWWWAuthenticateHeader"/> is not set then,
194+
/// adds 'WWW-Authenticate' response header with 'Basic' authentication scheme and 'Realm'
97195
/// to let the client know that 'Basic' authentication scheme is being used by the system.
98196
/// </summary>
99-
/// <param name="properties"></param>
100-
/// <returns></returns>
197+
/// <param name="properties"><see cref="AuthenticationProperties"/></param>
198+
/// <returns>A Task.</returns>
101199
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
102200
{
103-
Response.Headers[HeaderNames.WWWAuthenticate] = Challenge;
201+
// Raise handle challenge event.
202+
var handleChallengeContext = new BasicHandleChallengeContext(Context, Scheme, Options, properties);
203+
await Events.HandleChallengeAsync(handleChallengeContext).ConfigureAwait(false);
204+
if (handleChallengeContext.IsHandled)
205+
{
206+
return;
207+
}
208+
209+
if (!Options.SuppressWWWAuthenticateHeader)
210+
{
211+
Response.Headers[HeaderNames.WWWAuthenticate] = Challenge;
212+
}
104213
await base.HandleChallengeAsync(properties);
105214
}
215+
216+
private BasicCredentials DecodeBasicCredentials(string credentials)
217+
{
218+
string username;
219+
string password;
220+
try
221+
{
222+
// Convert the base64 encoded 'username:password' to normal string and parse username and password from colon(:) separated string.
223+
var usernameAndPassword = Encoding.UTF8.GetString(Convert.FromBase64String(credentials));
224+
var usernameAndPasswordSplit = usernameAndPassword.Split(':');
225+
if (usernameAndPasswordSplit.Length != 2)
226+
{
227+
throw new Exception("Invalid Basic authentication header.");
228+
}
229+
username = usernameAndPasswordSplit[0];
230+
password = usernameAndPasswordSplit[1];
231+
}
232+
catch (Exception)
233+
{
234+
throw new Exception($"Problem decoding '{BasicDefaults.AuthenticationScheme}' scheme credentials.");
235+
}
236+
237+
if (string.IsNullOrWhiteSpace(username))
238+
{
239+
throw new Exception("Username cannot be empty.");
240+
}
241+
242+
if (password == null)
243+
{
244+
password = string.Empty;
245+
}
246+
247+
return new BasicCredentials(username, password);
248+
}
249+
250+
private struct BasicCredentials
251+
{
252+
public BasicCredentials(string username, string password)
253+
{
254+
Username = username;
255+
Password = password;
256+
}
257+
258+
public string Username { get; }
259+
public string Password { get; }
260+
}
106261
}
107262
}

src/AspNetCore.Authentication.Basic/BasicOptions.cs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,49 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using Microsoft.AspNetCore.Authentication;
5+
using System;
56

67
namespace AspNetCore.Authentication.Basic
78
{
8-
/// <summary>
9-
/// Inherited from <see cref="AuthenticationSchemeOptions"/> to allow extra option properties for 'Basic' authentication.
10-
/// </summary>
11-
public class BasicOptions : AuthenticationSchemeOptions
9+
/// <summary>
10+
/// Options used to configure basic authentication.
11+
/// </summary>
12+
public class BasicOptions : AuthenticationSchemeOptions
1213
{
13-
/// <summary>
14-
/// This is required property. It is used when challenging un-authenticated requests.
15-
/// </summary>
16-
public string Realm { get; set; }
14+
/// <summary>
15+
/// Gets or sets the realm property. It is used with WWW-Authenticate response header when challenging un-authenticated requests.
16+
/// <see href="https://tools.ietf.org/html/rfc7235#section-2.2"/>
17+
/// </summary>
18+
public string Realm { get; set; }
1719

18-
//public new BasicEvents Events
19-
//{
20-
// get => (BasicEvents)base.Events;
21-
// set => base.Events = value;
22-
//}
23-
}
20+
/// <summary>
21+
/// Default value is false.
22+
/// When set to true, it will NOT return WWW-Authenticate response header when challenging un-authenticated requests.
23+
/// When set to false, it will return WWW-Authenticate response header when challenging un-authenticated requests.
24+
/// It is normally used to disable browser prompt when doing ajax calls.
25+
/// <see href="https://tools.ietf.org/html/rfc7235#section-4.1"/>
26+
/// </summary>
27+
public bool SuppressWWWAuthenticateHeader { get; set; }
28+
29+
/// <summary>
30+
/// The object provided by the application to process events raised by the basic authentication middleware.
31+
/// The application may implement the interface fully, or it may create an instance of BasicEvents
32+
/// and assign delegates only to the events it wants to process.
33+
/// </summary>
34+
public new BasicEvents Events
35+
{
36+
get => (BasicEvents)base.Events;
37+
set => base.Events = value;
38+
}
39+
40+
/// <inheritdoc/>
41+
public override void Validate()
42+
{
43+
base.Validate();
44+
if (!SuppressWWWAuthenticateHeader && string.IsNullOrWhiteSpace(Realm))
45+
{
46+
throw new InvalidOperationException("Realm must be set in basic options");
47+
}
48+
}
49+
}
2450
}

src/AspNetCore.Authentication.Basic/BasicPostConfigureOptions.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@ namespace AspNetCore.Authentication.Basic
99
/// <summary>
1010
/// This post configure options checks whether the required option property <see cref="BasicOptions.Realm" /> is set or not on <see cref="BasicOptions"/>.
1111
/// </summary>
12-
class BasicPostConfigureOptions : IPostConfigureOptions<BasicOptions>
12+
internal class BasicPostConfigureOptions : IPostConfigureOptions<BasicOptions>
1313
{
14+
private readonly IBasicUserValidationService _basicUserValidationService;
15+
16+
public BasicPostConfigureOptions(IBasicUserValidationService basicUserValidationService)
17+
{
18+
_basicUserValidationService = basicUserValidationService ?? throw new ArgumentNullException(nameof(basicUserValidationService));
19+
}
20+
1421
public void PostConfigure(string name, BasicOptions options)
1522
{
16-
if (string.IsNullOrWhiteSpace(options.Realm))
23+
if (!options.SuppressWWWAuthenticateHeader && string.IsNullOrWhiteSpace(options.Realm))
1724
{
1825
throw new InvalidOperationException("Realm must be set in basic options");
1926
}
27+
28+
if (options.Events?.OnValidateCredentials == null && options.EventsType == null && _basicUserValidationService is DefaultBasicUserValidationService)
29+
{
30+
throw new InvalidOperationException($"Either {nameof(options.Events.OnValidateCredentials)} delegate on configure options {nameof(options.Events)} should be set or an implementaion of {nameof(IBasicUserValidationService)} should be registered in the dependency container.");
31+
}
2032
}
2133
}
2234
}

0 commit comments

Comments
 (0)