14
14
15
15
namespace AspNetCore . Authentication . Basic
16
16
{
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 >
21
21
{
22
22
private readonly IBasicUserValidationService _basicUserValidationService ;
23
23
@@ -32,76 +32,231 @@ public class BasicHandler : AuthenticationHandler<BasicOptions>
32
32
public BasicHandler ( IOptionsMonitor < BasicOptions > options , ILoggerFactory logger , UrlEncoder encoder , ISystemClock clock , IBasicUserValidationService basicUserValidationService )
33
33
: base ( options , logger , encoder , clock )
34
34
{
35
- _basicUserValidationService = basicUserValidationService ;
35
+ _basicUserValidationService = basicUserValidationService ?? throw new ArgumentNullException ( nameof ( basicUserValidationService ) ) ;
36
36
}
37
37
38
38
private string Challenge => $ "{ BasicDefaults . AuthenticationScheme } realm=\" { Options . Realm } \" , charset=\" UTF-8\" ";
39
39
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 ; }
42
44
43
45
/// <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 "/>.
45
47
/// </summary>
46
48
/// <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>
47
55
protected override async Task < AuthenticateResult > HandleAuthenticateAsync ( )
48
56
{
49
57
if ( ! Request . Headers . ContainsKey ( HeaderNames . Authorization ) )
50
58
{
51
- // No 'Authorization' header found in the request.
59
+ Logger . LogInformation ( " No 'Authorization' header found in the request." ) ;
52
60
return AuthenticateResult . NoResult ( ) ;
53
61
}
54
62
55
63
if ( ! AuthenticationHeaderValue . TryParse ( Request . Headers [ HeaderNames . Authorization ] , out var headerValue ) )
56
64
{
57
- // No valid 'Authorization' header found in the request.
65
+ Logger . LogInformation ( " No valid 'Authorization' header found in the request." ) ;
58
66
return AuthenticateResult . NoResult ( ) ;
59
67
}
60
-
61
68
62
69
if ( ! headerValue . Scheme . Equals ( BasicDefaults . AuthenticationScheme , StringComparison . OrdinalIgnoreCase ) )
63
70
{
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." ) ;
65
72
return AuthenticateResult . NoResult ( ) ;
66
73
}
67
74
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
72
86
{
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 ;
74
173
}
75
- var username = usernameAndPasswordSplit [ 0 ] ;
76
- var password = usernameAndPasswordSplit [ 1 ] ;
174
+ }
77
175
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 )
81
183
{
82
- return AuthenticateResult . Fail ( "Invalid username or password" ) ;
184
+ return ;
83
185
}
84
186
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
+ }
92
189
93
190
/// <summary>
94
191
/// Handles the un-authenticated requests.
95
192
/// 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'
97
195
/// to let the client know that 'Basic' authentication scheme is being used by the system.
98
196
/// </summary>
99
- /// <param name="properties"></param>
100
- /// <returns></returns>
197
+ /// <param name="properties"><see cref="AuthenticationProperties"/>< /param>
198
+ /// <returns>A Task. </returns>
101
199
protected override async Task HandleChallengeAsync ( AuthenticationProperties properties )
102
200
{
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
+ }
104
213
await base . HandleChallengeAsync ( properties ) ;
105
214
}
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
+ }
106
261
}
107
262
}
0 commit comments