1414
1515namespace 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}
0 commit comments