From 0f706c79ab15f2927f110e2ee2a9df8047612ca0 Mon Sep 17 00:00:00 2001 From: Olivier Spinelli Date: Mon, 7 Oct 2024 08:23:51 +0200 Subject: [PATCH] Applying Code Cleanup. --- .editorconfig | 38 +- CK.AspNet.Auth/AuthenticationCookieMode.cs | 45 +- .../AuthenticationInfoTokenService.cs | 196 +- .../CKAspNetAuthHttpContextExtensions.cs | 45 +- .../IWebFrontAuthAutoBindingAccountContext.cs | 137 +- .../IWebFrontAuthAutoBindingAccountService.cs | 47 +- .../IWebFrontAuthAutoCreateAccountContext.cs | 123 +- .../IWebFrontAuthAutoCreateAccountService.cs | 47 +- .../IWebFrontAuthDynamicScopeProvider.cs | 45 +- .../IWebFrontAuthImpersonationService.cs | 65 +- ...bFrontAuthUnsafeDirectLoginAllowService.cs | 35 +- .../IWebFrontAuthValidateLoginContext.cs | 131 +- .../IWebFrontAuthValidateLoginService.cs | 55 +- CK.AspNet.Auth/FrontAuthenticationInfo.cs | 99 +- CK.AspNet.Auth/IErrorContext.cs | 32 +- CK.AspNet.Auth/IWebFrontAuthLoginService.cs | 133 +- CK.AspNet.Auth/InternalExtensions.cs | 101 +- ...teAuthenticationEventsContextExtensions.cs | 211 +- .../SecureData/ExtraDataSecureDataFormat.cs | 70 +- ...FrontAuthenticationInfoSecureDataFormat.cs | 72 +- CK.AspNet.Auth/UserLoginResult.cs | 143 +- CK.AspNet.Auth/WebFrontAuthExtensions.cs | 225 +- CK.AspNet.Auth/WebFrontAuthHandler.cs | 797 ++++--- CK.AspNet.Auth/WebFrontAuthLoginContext.cs | 580 +++-- CK.AspNet.Auth/WebFrontAuthLoginMode.cs | 49 +- CK.AspNet.Auth/WebFrontAuthOptions.cs | 357 ++- .../WebFrontAuthOptionsInstaller.cs | 24 +- CK.AspNet.Auth/WebFrontAuthService.cs | 1943 ++++++++--------- .../WebFrontAuthStartLoginContext.cs | 298 ++- ...AuthenticationDatabaseServiceExtensions.cs | 45 +- .../AuthenticationTypeSystemExtensions.cs | 41 +- .../DefaultAutoBindingAccountService.cs | 107 +- .../SqlWebFrontAuthLoginService.cs | 217 +- .../AspNetAuthServerTestHelperExtensions.cs | 77 +- .../AuthServerResponse.cs | 91 +- .../AuthenticationCookieValues.cs | 35 +- .../FakeUserDatabase.cs | 64 +- .../FakeWebFrontAuthLoginService.cs | 202 +- .../RunningAspNetAuthServerExtensions.cs | 553 +++-- CodeCakeBuilder/Build.cs | 135 +- CodeCakeBuilder/StandardGlobalInfo.cs | 2 +- .../dotnet/Build.NuGetArtifactType.cs | 7 +- .../AuthenticationInfoInjectionTests.cs | 66 +- .../CriticalLevelTests.cs | 149 +- Tests/CK.AspNet.Auth.Tests/DeviceIdTests.cs | 257 ++- .../ImpersonateCurrentActorTests.cs | 295 ++- .../ImpersonationTests.cs | 289 ++- Tests/CK.AspNet.Auth.Tests/LocalHelper.cs | 146 +- Tests/CK.AspNet.Auth.Tests/RememberMeTests.cs | 71 +- .../Services/AllDirectLoginAllower.cs | 10 +- .../ImpersonationForEverybodyService.cs | 35 +- .../Services/NoSchemeLoginService.cs | 45 +- Tests/CK.AspNet.Auth.Tests/UserDataTests.cs | 69 +- .../WebFrontAuthServiceTests.cs | 31 +- .../WebFrontHandlerTests.cs | 537 +++-- .../BasicAuthenticationTests.cs | 402 ++-- Tests/CK.DB.AspNet.Auth.This.Tests/DBSetup.cs | 9 +- .../RefreshTests.cs | 143 +- .../Services/DirectLoginAllower.cs | 38 +- .../ImpersonationForEveryBodyService.cs | 40 +- .../Services/NoEvilZoneForPaula.cs | 26 +- 61 files changed, 5165 insertions(+), 5212 deletions(-) diff --git a/.editorconfig b/.editorconfig index 38ed0fc0..351c4f27 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,9 +7,12 @@ root = true [*] charset = utf-8 indent_style = space -indent_size = 2 +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf [*.{cs,js,ts,sql,tql}] @@ -21,15 +24,42 @@ csharp_space_between_method_call_parameter_list_parentheses = true csharp_space_between_method_declaration_parameter_list_parentheses = true csharp_space_after_keywords_in_control_flow_statements = false csharp_space_between_parentheses = control_flow_statements +csharp_space_around_binary_operators = before_and_after # Motive: May be weird at first, but it improves readability. csharp_style_prefer_primary_constructors = false:suggestion # Primary constructors should be used only for very simple classes. May be record is a good choice. +csharp_indent_labels = no_change +# When using goto, labels should be explicitly positioned based on the algorithm. + +csharp_using_directive_placement = outside_namespace:silent +# Rather standard placement of using in C#. + +csharp_indent_case_contents_when_block = false; +# switch case block don't need another indent. + +csharp_prefer_braces = true:silent + +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent + +csharp_style_prefer_top_level_statements = true:suggestion +# Applies to Main(). + +csharp_style_namespace_declarations=file_scoped:suggestion +#Motive: Less useless space. + # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal @@ -38,9 +68,6 @@ dotnet_naming_style.camel_case_underscore_style.required_prefix = _ dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case # Motive: It follow the C# style guideline. -csharp_style_namespace_declarations=file_scoped:suggestion -#Motive: Less useless space. - # CA1063: Implement IDisposable Correctly dotnet_diagnostic.CA1063.severity = none # CA1816: Dispose methods should call SuppressFinalize @@ -118,6 +145,5 @@ dotnet_diagnostic.VSTHRD101.severity = error # VSTHRD003: Avoid awaiting foreign Tasks dotnet_diagnostic.VSTHRD003.severity = none - # /Signature-Code .editorconfig diff --git a/CK.AspNet.Auth/AuthenticationCookieMode.cs b/CK.AspNet.Auth/AuthenticationCookieMode.cs index 287b6428..039eadd2 100644 --- a/CK.AspNet.Auth/AuthenticationCookieMode.cs +++ b/CK.AspNet.Auth/AuthenticationCookieMode.cs @@ -3,33 +3,32 @@ using System.Collections.Generic; using System.Text; -namespace CK.AspNet.Auth -{ +namespace CK.AspNet.Auth; + +/// +/// Describes the how the authentication cookie is managed. +/// +public enum AuthenticationCookieMode +{ /// - /// Describes the how the authentication cookie is managed. + /// The authentication cookie is set on the /c/. + /// This is the default mode. /// - public enum AuthenticationCookieMode - { - /// - /// The authentication cookie is set on the /c/. - /// This is the default mode. - /// - WebFrontPath = 0, + WebFrontPath = 0, - /// - /// The authentication cookie is set on the root path: - /// this enables the to act as a standard Cookie authentication - /// service (applies to classical, server rendered, web site). - /// - RootPath = 1, + /// + /// The authentication cookie is set on the root path: + /// this enables the to act as a standard Cookie authentication + /// service (applies to classical, server rendered, web site). + /// + RootPath = 1, - /// - /// No authentication cookie is set (and no challenge is done). - /// This also forces the to be false: this ensures that - /// the long term cookie is also removed. - /// - None = 2 + /// + /// No authentication cookie is set (and no challenge is done). + /// This also forces the to be false: this ensures that + /// the long term cookie is also removed. + /// + None = 2 - } } diff --git a/CK.AspNet.Auth/AuthenticationInfoTokenService.cs b/CK.AspNet.Auth/AuthenticationInfoTokenService.cs index e23ebd5e..192926b8 100644 --- a/CK.AspNet.Auth/AuthenticationInfoTokenService.cs +++ b/CK.AspNet.Auth/AuthenticationInfoTokenService.cs @@ -4,114 +4,112 @@ using System; using System.Diagnostics; -namespace CK.AspNet.Auth -{ - /// - /// Simple singleton service that offers tokens creation and restoration functionalities. - /// - /// This is not specific to the global DI container, it is available from all containers. - /// - /// - public sealed class AuthenticationInfoTokenService : ISingletonAutoService - { - readonly IAuthenticationTypeSystem _typeSystem; - readonly IDataProtector _baseDataProtector; - readonly IDataProtector _tokenDataProtector; - readonly FrontAuthenticationInfoSecureDataFormat _frontTokenFormat; +namespace CK.AspNet.Auth; - public AuthenticationInfoTokenService( IAuthenticationTypeSystem typeSystem, IDataProtectionProvider dataProtectionProvider ) - { - _typeSystem = typeSystem; - Throw.DebugAssert( typeof( WebFrontAuthHandler ).FullName == "CK.AspNet.Auth.WebFrontAuthHandler" ); - _baseDataProtector = dataProtectionProvider.CreateProtector( "CK.AspNet.Auth.WebFrontAuthHandler" ); - _tokenDataProtector = _baseDataProtector.CreateProtector( "Token", "v1" ); - _frontTokenFormat = new FrontAuthenticationInfoSecureDataFormat( _typeSystem, _tokenDataProtector ); - } +/// +/// Simple singleton service that offers tokens creation and restoration functionalities. +/// +/// This is not specific to the global DI container, it is available from all containers. +/// +/// +public sealed class AuthenticationInfoTokenService : ISingletonAutoService +{ + readonly IAuthenticationTypeSystem _typeSystem; + readonly IDataProtector _baseDataProtector; + readonly IDataProtector _tokenDataProtector; + readonly FrontAuthenticationInfoSecureDataFormat _frontTokenFormat; - /// - /// Gets the type system service. - /// - public IAuthenticationTypeSystem TypeSystem => _typeSystem; + public AuthenticationInfoTokenService( IAuthenticationTypeSystem typeSystem, IDataProtectionProvider dataProtectionProvider ) + { + _typeSystem = typeSystem; + Throw.DebugAssert( typeof( WebFrontAuthHandler ).FullName == "CK.AspNet.Auth.WebFrontAuthHandler" ); + _baseDataProtector = dataProtectionProvider.CreateProtector( "CK.AspNet.Auth.WebFrontAuthHandler" ); + _tokenDataProtector = _baseDataProtector.CreateProtector( "Token", "v1" ); + _frontTokenFormat = new FrontAuthenticationInfoSecureDataFormat( _typeSystem, _tokenDataProtector ); + } - /// - /// Gets the data protector to use for authentication tokens. - /// - public IDataProtector TokenDataProtector => _tokenDataProtector; + /// + /// Gets the type system service. + /// + public IAuthenticationTypeSystem TypeSystem => _typeSystem; - /// - /// Base data protector for authentication related protected data. - /// - public IDataProtector BaseDataProtector => _baseDataProtector; + /// + /// Gets the data protector to use for authentication tokens. + /// + public IDataProtector TokenDataProtector => _tokenDataProtector; - /// - /// Creates a token from a . - /// - /// The authentication info. - /// The url-safe secured authentication token string. - public string ProtectFrontAuthenticationInfo( FrontAuthenticationInfo info ) - { - Debug.Assert( info.Info != null ); - return _frontTokenFormat.Protect( info ); - } + /// + /// Base data protector for authentication related protected data. + /// + public IDataProtector BaseDataProtector => _baseDataProtector; - /// - /// Extracts a from a token previously created with . - /// - /// By default, the expiration is checked based on . - /// If expiration check must be skipped, use as the expiration date. - /// - /// - /// The token. - /// Optional check expiration date. Defaults to . - /// The information (possibly expired) or null if an error occurred. - public FrontAuthenticationInfo? UnprotectFrontAuthenticationInfo( string data, DateTime? checkExpirationDate = null ) - { - Throw.CheckNotNullArgument( data ); - var info = _frontTokenFormat.Unprotect( data )!; - if( info == null ) return null; - return info.SetInfo( info.Info.CheckExpiration( checkExpirationDate ?? DateTime.UtcNow ) ); - } + /// + /// Creates a token from a . + /// + /// The authentication info. + /// The url-safe secured authentication token string. + public string ProtectFrontAuthenticationInfo( FrontAuthenticationInfo info ) + { + Debug.Assert( info.Info != null ); + return _frontTokenFormat.Protect( info ); + } - /// - /// Direct generation of an authentication token from any . - /// is called with . - /// - /// By default, the expiration is checked based on . - /// If expiration check must be skipped, use as the expiration date. - /// - /// - /// This is to be used with caution: the authentication token should never be sent to any client and should be - /// used only for secure server to server temporary authentication. - /// - /// - /// The authentication info for which an authentication token must be obtained. - /// Optional check expiration date. Defaults to . - /// The url-safe secured authentication token string. - public string UnsafeCreateAuthenticationToken( IAuthenticationInfo info, DateTime? checkExpirationDate = null ) - { - Throw.CheckNotNullArgument( info ); - info = info.CheckExpiration( checkExpirationDate ?? DateTime.UtcNow ); - return ProtectFrontAuthenticationInfo( new FrontAuthenticationInfo( info, false ) ); - } + /// + /// Extracts a from a token previously created with . + /// + /// By default, the expiration is checked based on . + /// If expiration check must be skipped, use as the expiration date. + /// + /// + /// The token. + /// Optional check expiration date. Defaults to . + /// The information (possibly expired) or null if an error occurred. + public FrontAuthenticationInfo? UnprotectFrontAuthenticationInfo( string data, DateTime? checkExpirationDate = null ) + { + Throw.CheckNotNullArgument( data ); + var info = _frontTokenFormat.Unprotect( data )!; + if( info == null ) return null; + return info.SetInfo( info.Info.CheckExpiration( checkExpirationDate ?? DateTime.UtcNow ) ); + } - /// - /// Direct generation of an authentication token for a user. - /// - /// This is to be used with caution: the authentication token should never be sent to any client and should be - /// used only for secure server to server temporary authentication. - /// - /// - /// The user identifier. - /// The user name. - /// The validity time span: the shorter the better. - /// The url-safe secured authentication token string. - public string UnsafeCreateAuthenticationToken( int userId, string userName, TimeSpan validity ) - { - var u = _typeSystem.UserInfo.Create( userId, userName ); - var info = _typeSystem.AuthenticationInfo.Create( u, DateTime.UtcNow.Add( validity ) ); - return ProtectFrontAuthenticationInfo( new FrontAuthenticationInfo( info, false ) ); - } + /// + /// Direct generation of an authentication token from any . + /// is called with . + /// + /// By default, the expiration is checked based on . + /// If expiration check must be skipped, use as the expiration date. + /// + /// + /// This is to be used with caution: the authentication token should never be sent to any client and should be + /// used only for secure server to server temporary authentication. + /// + /// + /// The authentication info for which an authentication token must be obtained. + /// Optional check expiration date. Defaults to . + /// The url-safe secured authentication token string. + public string UnsafeCreateAuthenticationToken( IAuthenticationInfo info, DateTime? checkExpirationDate = null ) + { + Throw.CheckNotNullArgument( info ); + info = info.CheckExpiration( checkExpirationDate ?? DateTime.UtcNow ); + return ProtectFrontAuthenticationInfo( new FrontAuthenticationInfo( info, false ) ); + } + /// + /// Direct generation of an authentication token for a user. + /// + /// This is to be used with caution: the authentication token should never be sent to any client and should be + /// used only for secure server to server temporary authentication. + /// + /// + /// The user identifier. + /// The user name. + /// The validity time span: the shorter the better. + /// The url-safe secured authentication token string. + public string UnsafeCreateAuthenticationToken( int userId, string userName, TimeSpan validity ) + { + var u = _typeSystem.UserInfo.Create( userId, userName ); + var info = _typeSystem.AuthenticationInfo.Create( u, DateTime.UtcNow.Add( validity ) ); + return ProtectFrontAuthenticationInfo( new FrontAuthenticationInfo( info, false ) ); } } diff --git a/CK.AspNet.Auth/CKAspNetAuthHttpContextExtensions.cs b/CK.AspNet.Auth/CKAspNetAuthHttpContextExtensions.cs index f3a5a674..a435cd05 100644 --- a/CK.AspNet.Auth/CKAspNetAuthHttpContextExtensions.cs +++ b/CK.AspNet.Auth/CKAspNetAuthHttpContextExtensions.cs @@ -6,34 +6,33 @@ using System.Collections.Generic; using System.Text; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Exposes extension method on . +/// +static public class CKAspNetAuthHttpContextExtensions { /// - /// Exposes extension method on . + /// Obtains the current , either because it is already + /// in or by extracting authentication from request. + /// It is never null, but can be . /// - static public class CKAspNetAuthHttpContextExtensions + /// This context. + /// Never null, can be . + static public IAuthenticationInfo GetAuthenticationInfo( this HttpContext @this ) { - /// - /// Obtains the current , either because it is already - /// in or by extracting authentication from request. - /// It is never null, but can be . - /// - /// This context. - /// Never null, can be . - static public IAuthenticationInfo GetAuthenticationInfo( this HttpContext @this ) + IAuthenticationInfo? authInfo; + if( @this.Items.TryGetValue( typeof( FrontAuthenticationInfo ), out var o ) && o != null ) + { + authInfo = ((FrontAuthenticationInfo)o).Info; + } + else { - IAuthenticationInfo? authInfo; - if( @this.Items.TryGetValue( typeof( FrontAuthenticationInfo ), out var o ) && o != null ) - { - authInfo = ((FrontAuthenticationInfo)o).Info; - } - else - { - IActivityMonitor? monitor = null; - var s = @this.RequestServices.GetRequiredService(); - authInfo = s.ReadAndCacheAuthenticationHeader( @this, ref monitor ).Info; - } - return authInfo; + IActivityMonitor? monitor = null; + var s = @this.RequestServices.GetRequiredService(); + authInfo = s.ReadAndCacheAuthenticationHeader( @this, ref monitor ).Info; } + return authInfo; } } diff --git a/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountContext.cs b/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountContext.cs index 019cba56..99e3d051 100644 --- a/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountContext.cs +++ b/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountContext.cs @@ -5,86 +5,85 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Enables to +/// attempt to bind an account to a currently already logged in user. +/// +public interface IWebFrontAuthAutoBindingAccountContext { /// - /// Enables to - /// attempt to bind an account to a currently already logged in user. + /// Gets the current http context. /// - public interface IWebFrontAuthAutoBindingAccountContext - { - /// - /// Gets the current http context. - /// - HttpContext HttpContext { get; } + HttpContext HttpContext { get; } - /// - /// Gets the authentication type system. - /// - IAuthenticationTypeSystem AuthenticationTypeSystem { get; } + /// + /// Gets the authentication type system. + /// + IAuthenticationTypeSystem AuthenticationTypeSystem { get; } - /// - /// Gets the endpoint that started the authentication. - /// - WebFrontAuthLoginMode LoginMode { get; } + /// + /// Gets the endpoint that started the authentication. + /// + WebFrontAuthLoginMode LoginMode { get; } - /// - /// Gets the return url only if '/c/startLogin' has been called with a 'returnUrl' parameter. - /// Null otherwise. - /// - /// This url is always checked against the set of allowed prefixes. - /// - /// - string? ReturnUrl { get; } + /// + /// Gets the return url only if '/c/startLogin' has been called with a 'returnUrl' parameter. + /// Null otherwise. + /// + /// This url is always checked against the set of allowed prefixes. + /// + /// + string? ReturnUrl { get; } - /// - /// Gets the authentication provider on which .webfront/c/starLogin has been called. - /// This is "Basic" when is - /// and null when LoginMode is . - /// - string? InitialScheme { get; } + /// + /// Gets the authentication provider on which .webfront/c/starLogin has been called. + /// This is "Basic" when is + /// and null when LoginMode is . + /// + string? InitialScheme { get; } - /// - /// Gets the calling authentication scheme. - /// This is usually the same as the . - /// - string CallingScheme { get; } + /// + /// Gets the calling authentication scheme. + /// This is usually the same as the . + /// + string CallingScheme { get; } - /// - /// Gets the provider payload (type is provider -ie. - dependent). - /// This is never null but may be an empty object when unsafe login is used with no payload. - /// - object Payload { get; } + /// + /// Gets the provider payload (type is provider -ie. - dependent). + /// This is never null but may be an empty object when unsafe login is used with no payload. + /// + object Payload { get; } - /// - /// Gets the query parameters (for GET) or form data (when POST was used) of the - /// initial .webfront/c/starLogin call as a readonly list. - /// - IDictionary UserData { get; } + /// + /// Gets the query parameters (for GET) or form data (when POST was used) of the + /// initial .webfront/c/starLogin call as a readonly list. + /// + IDictionary UserData { get; } - /// - /// Gets the authentication information of the current authentication. - /// - IAuthenticationInfo InitialAuthentication { get; } + /// + /// Gets the authentication information of the current authentication. + /// + IAuthenticationInfo InitialAuthentication { get; } - /// - /// Sets an error and always returns null to easily return - /// from method. - /// - /// Error identifier (a dotted identifier string). Must not be null or empty. - /// The optional error message in clear text (typically in English). - /// Always null. - UserLoginResult? SetError( string errorId, string? errorText = null ); + /// + /// Sets an error and always returns null to easily return + /// from method. + /// + /// Error identifier (a dotted identifier string). Must not be null or empty. + /// The optional error message in clear text (typically in English). + /// Always null. + UserLoginResult? SetError( string errorId, string? errorText = null ); - /// - /// Sets an error and always returns null to easily return - /// from method. - /// The returned error has "errorId" set to the full name of the exception - /// and the "errorText" is the . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// The exception. Can not be null. - /// Always null. - UserLoginResult? SetError( Exception ex ); - } + /// + /// Sets an error and always returns null to easily return + /// from method. + /// The returned error has "errorId" set to the full name of the exception + /// and the "errorText" is the . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// The exception. Can not be null. + /// Always null. + UserLoginResult? SetError( Exception ex ); } diff --git a/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountService.cs b/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountService.cs index f2e80685..865ba416 100644 --- a/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountService.cs +++ b/CK.AspNet.Auth/Extensions/AutoBindingAccount/IWebFrontAuthAutoBindingAccountService.cs @@ -5,31 +5,30 @@ using CK.Auth; using CK.Core; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Optional service that, when registered, enables automatic account binding. +/// Implementation may consider that when current authentication is it is safe +/// to bind the account. +/// +[ContainerConfiguredSingletonService] +public interface IWebFrontAuthAutoBindingAccountService : ISingletonAutoService { /// - /// Optional service that, when registered, enables automatic account binding. - /// Implementation may consider that when current authentication is it is safe - /// to bind the account. + /// Called for each failed login when the user is currently logged in. /// - [ContainerConfiguredSingletonService] - public interface IWebFrontAuthAutoBindingAccountService : ISingletonAutoService - { - /// - /// Called for each failed login when the user is currently logged in. - /// - /// The monitor to use. - /// Account binding context. - /// - /// The login result where the may have its - /// updated with the new one (the current logged in user available on - /// may be returned but this is quite useless). - /// - /// Null to return the standard User.NoAutoBinding/"Automatic account binding is disabled." error - /// or the error identifier and error text have been set via - /// or . - /// - /// - Task BindAccountAsync( IActivityMonitor monitor, IWebFrontAuthAutoBindingAccountContext context ); - } + /// The monitor to use. + /// Account binding context. + /// + /// The login result where the may have its + /// updated with the new one (the current logged in user available on + /// may be returned but this is quite useless). + /// + /// Null to return the standard User.NoAutoBinding/"Automatic account binding is disabled." error + /// or the error identifier and error text have been set via + /// or . + /// + /// + Task BindAccountAsync( IActivityMonitor monitor, IWebFrontAuthAutoBindingAccountContext context ); } diff --git a/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountContext.cs b/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountContext.cs index 9abe7e83..22f13919 100644 --- a/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountContext.cs +++ b/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountContext.cs @@ -5,79 +5,78 @@ using System.Collections.Generic; using System.Text; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Enables to +/// attempt to create an account and log in the user based on any criteria exposed by this context. +/// +public interface IWebFrontAuthAutoCreateAccountContext { /// - /// Enables to - /// attempt to create an account and log in the user based on any criteria exposed by this context. + /// Gets the current http context. /// - public interface IWebFrontAuthAutoCreateAccountContext - { - /// - /// Gets the current http context. - /// - HttpContext HttpContext { get; } + HttpContext HttpContext { get; } - /// - /// Gets the authentication type system. - /// - IAuthenticationTypeSystem AuthenticationTypeSystem { get; } + /// + /// Gets the authentication type system. + /// + IAuthenticationTypeSystem AuthenticationTypeSystem { get; } - /// - /// Gets the endpoint that started the authentication. - /// - WebFrontAuthLoginMode LoginMode { get; } + /// + /// Gets the endpoint that started the authentication. + /// + WebFrontAuthLoginMode LoginMode { get; } - /// - /// Gets the return url only if '/c/startLogin' has been called with a 'returnUrl' parameter. - /// Null otherwise. - /// - string? ReturnUrl { get; } + /// + /// Gets the return url only if '/c/startLogin' has been called with a 'returnUrl' parameter. + /// Null otherwise. + /// + string? ReturnUrl { get; } - /// - /// Gets the authentication provider on which .webfront/c/starLogin has been called. - /// This is "Basic" when is - /// and null when LoginMode is . - /// - string? InitialScheme { get; } + /// + /// Gets the authentication provider on which .webfront/c/starLogin has been called. + /// This is "Basic" when is + /// and null when LoginMode is . + /// + string? InitialScheme { get; } - /// - /// Gets the calling authentication scheme. - /// This is usually the same as the . - /// - string CallingScheme { get; } + /// + /// Gets the calling authentication scheme. + /// This is usually the same as the . + /// + string CallingScheme { get; } - /// - /// Gets the provider payload (type is provider -ie. - dependent). - /// This is never null but may be an empty object when unsafe login is used with no payload. - /// - object Payload { get; } + /// + /// Gets the provider payload (type is provider -ie. - dependent). + /// This is never null but may be an empty object when unsafe login is used with no payload. + /// + object Payload { get; } - /// - /// Gets the query parameters (for GET) or form data (when POST was used) of the - /// initial .webfront/c/starLogin call as a readonly list. - /// - IDictionary UserData { get; } + /// + /// Gets the query parameters (for GET) or form data (when POST was used) of the + /// initial .webfront/c/starLogin call as a readonly list. + /// + IDictionary UserData { get; } - /// - /// Sets an error and always returns null to easily return - /// from method. - /// - /// Error identifier (a dotted identifier string). Must not be null or empty. - /// The optional error message in clear text (typically in english). - /// Always null. - UserLoginResult? SetError( string errorId, string? errorText = null ); + /// + /// Sets an error and always returns null to easily return + /// from method. + /// + /// Error identifier (a dotted identifier string). Must not be null or empty. + /// The optional error message in clear text (typically in english). + /// Always null. + UserLoginResult? SetError( string errorId, string? errorText = null ); - /// - /// Sets an error and always returns null to easily return - /// from method. - /// The returned error has "errorId" set to the full name of the exception - /// and the "errorText" is the . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// The exception. Can not be null. - /// Always null. - UserLoginResult? SetError( Exception ex ); + /// + /// Sets an error and always returns null to easily return + /// from method. + /// The returned error has "errorId" set to the full name of the exception + /// and the "errorText" is the . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// The exception. Can not be null. + /// Always null. + UserLoginResult? SetError( Exception ex ); - } } diff --git a/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountService.cs b/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountService.cs index 1bd152ea..c6e9a7c9 100644 --- a/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountService.cs +++ b/CK.AspNet.Auth/Extensions/AutoCreateAccount/IWebFrontAuthAutoCreateAccountService.cs @@ -7,31 +7,30 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Optional service that, when registered, enables automatic account creation. +/// This should be used with care. +/// The should typically +/// contain a special key (like an "InvitationToken") with a relatively short life timed and verifiable value that should be +/// required to actually create the account and log in the user. +/// Also, not all schemes should be systematically supported, nor all . +/// +[ContainerConfiguredSingletonService] +public interface IWebFrontAuthAutoCreateAccountService : ISingletonAutoService { /// - /// Optional service that, when registered, enables automatic account creation. - /// This should be used with care. - /// The should typically - /// contain a special key (like an "InvitationToken") with a relatively short life timed and verifiable value that should be - /// required to actually create the account and log in the user. - /// Also, not all schemes should be systematically supported, nor all . + /// Called for each failed login when is true and when there is + /// no current authentication. /// - [ContainerConfiguredSingletonService] - public interface IWebFrontAuthAutoCreateAccountService : ISingletonAutoService - { - /// - /// Called for each failed login when is true and when there is - /// no current authentication. - /// - /// The monitor to use. - /// Account creation context. - /// - /// The login result that may be automatically created AND logged in. - /// Null to return the standard User.NoAutoRegistration/"Automatic user registration is disabled." error - /// or the error identifier and error text have been set via - /// or . - /// - Task CreateAccountAndLoginAsync( IActivityMonitor monitor, IWebFrontAuthAutoCreateAccountContext context ); - } + /// The monitor to use. + /// Account creation context. + /// + /// The login result that may be automatically created AND logged in. + /// Null to return the standard User.NoAutoRegistration/"Automatic user registration is disabled." error + /// or the error identifier and error text have been set via + /// or . + /// + Task CreateAccountAndLoginAsync( IActivityMonitor monitor, IWebFrontAuthAutoCreateAccountContext context ); } diff --git a/CK.AspNet.Auth/Extensions/IWebFrontAuthDynamicScopeProvider.cs b/CK.AspNet.Auth/Extensions/IWebFrontAuthDynamicScopeProvider.cs index b56f307a..5cc1e07e 100644 --- a/CK.AspNet.Auth/Extensions/IWebFrontAuthDynamicScopeProvider.cs +++ b/CK.AspNet.Auth/Extensions/IWebFrontAuthDynamicScopeProvider.cs @@ -2,30 +2,29 @@ using Microsoft.AspNetCore.Authentication; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Optional service that can handle dynamic scopes. +/// This service provides the scopes that must be submitted to an authentication provider. +/// +/// Updating the actual scopes that have been accepted or rejected is a specific process +/// that must be implemented for each provider. +/// +/// +/// For instance: Facebook requires to use its GraphQL API to know which scopes have been +/// accepted or rejected by the user. +/// Others simply returns these informations in the . +/// +/// +[ContainerConfiguredSingletonService] +public interface IWebFrontAuthDynamicScopeProvider : ISingletonAutoService { /// - /// Optional service that can handle dynamic scopes. - /// This service provides the scopes that must be submitted to an authentication provider. - /// - /// Updating the actual scopes that have been accepted or rejected is a specific process - /// that must be implemented for each provider. - /// - /// - /// For instance: Facebook requires to use its GraphQL API to know which scopes have been - /// accepted or rejected by the user. - /// Others simply returns these informations in the . - /// + /// Called at the start of the external login flow. /// - [ContainerConfiguredSingletonService] - public interface IWebFrontAuthDynamicScopeProvider : ISingletonAutoService - { - /// - /// Called at the start of the external login flow. - /// - /// The monitor to use. - /// The context. - /// Scopes that should be submitted. - Task GetScopesAsync( IActivityMonitor m, WebFrontAuthStartLoginContext context ); - } + /// The monitor to use. + /// The context. + /// Scopes that should be submitted. + Task GetScopesAsync( IActivityMonitor m, WebFrontAuthStartLoginContext context ); } diff --git a/CK.AspNet.Auth/Extensions/IWebFrontAuthImpersonationService.cs b/CK.AspNet.Auth/Extensions/IWebFrontAuthImpersonationService.cs index f125c517..ae7bf994 100644 --- a/CK.AspNet.Auth/Extensions/IWebFrontAuthImpersonationService.cs +++ b/CK.AspNet.Auth/Extensions/IWebFrontAuthImpersonationService.cs @@ -6,41 +6,40 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Optional service that controls user impersonation either by user identifier or user name. +/// Impersonation is not an actual login, it must have no visible impact on the impersonated user data. +/// +[ContainerConfiguredSingletonService] +public interface IWebFrontAuthImpersonationService : ISingletonAutoService { /// - /// Optional service that controls user impersonation either by user identifier or user name. - /// Impersonation is not an actual login, it must have no visible impact on the impersonated user data. + /// Attempts to impersonate the current user into another one. + /// Should return the user information on success and null if impersonation is not allowed. /// - [ContainerConfiguredSingletonService] - public interface IWebFrontAuthImpersonationService : ISingletonAutoService - { - /// - /// Attempts to impersonate the current user into another one. - /// Should return the user information on success and null if impersonation is not allowed. - /// - /// The HttpContext. - /// The monitor to use. - /// The current user information. - /// The target user identifier. - /// The target impersonated user or null if impersonation is not possible. - Task ImpersonateAsync( HttpContext ctx, - IActivityMonitor monitor, - IAuthenticationInfo info, - int userId ); + /// The HttpContext. + /// The monitor to use. + /// The current user information. + /// The target user identifier. + /// The target impersonated user or null if impersonation is not possible. + Task ImpersonateAsync( HttpContext ctx, + IActivityMonitor monitor, + IAuthenticationInfo info, + int userId ); - /// - /// Attempts to impersonate the current user into another one. - /// Should return the user information on success and null if impersonation is not allowed. - /// - /// The HttpContext. - /// The monitor to use. - /// The current user information. - /// The target user name. - /// The target impersonated user or null if impersonation is not possible. - Task ImpersonateAsync( HttpContext ctx, - IActivityMonitor monitor, - IAuthenticationInfo info, - string userName ); - } + /// + /// Attempts to impersonate the current user into another one. + /// Should return the user information on success and null if impersonation is not allowed. + /// + /// The HttpContext. + /// The monitor to use. + /// The current user information. + /// The target user name. + /// The target impersonated user or null if impersonation is not possible. + Task ImpersonateAsync( HttpContext ctx, + IActivityMonitor monitor, + IAuthenticationInfo info, + string userName ); } diff --git a/CK.AspNet.Auth/Extensions/IWebFrontAuthUnsafeDirectLoginAllowService.cs b/CK.AspNet.Auth/Extensions/IWebFrontAuthUnsafeDirectLoginAllowService.cs index 22826673..f4fa09fd 100644 --- a/CK.AspNet.Auth/Extensions/IWebFrontAuthUnsafeDirectLoginAllowService.cs +++ b/CK.AspNet.Auth/Extensions/IWebFrontAuthUnsafeDirectLoginAllowService.cs @@ -2,25 +2,24 @@ using Microsoft.AspNetCore.Http; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Optional service that can allow calls to the dangerous '/c/unsafeDirectLogin'. +/// Enabling calls to to this endpoint must be explicit: by default "403 - Forbidden" +/// is always returned. +/// +[ContainerConfiguredSingletonService] +public interface IWebFrontAuthUnsafeDirectLoginAllowService : ISingletonAutoService { /// - /// Optional service that can allow calls to the dangerous '/c/unsafeDirectLogin'. - /// Enabling calls to to this endpoint must be explicit: by default "403 - Forbidden" - /// is always returned. + /// Predicate function that may allow calls to '/c/unsafeDirectLogin' for a + /// scheme and a payload. /// - [ContainerConfiguredSingletonService] - public interface IWebFrontAuthUnsafeDirectLoginAllowService : ISingletonAutoService - { - /// - /// Predicate function that may allow calls to '/c/unsafeDirectLogin' for a - /// scheme and a payload. - /// - /// The current context. - /// The monitor to use. - /// The authentication scheme. - /// The login payload for the scheme. - /// True if the call must be allowed, false otherwise. - Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ); - } + /// The current context. + /// The monitor to use. + /// The authentication scheme. + /// The login payload for the scheme. + /// True if the call must be allowed, false otherwise. + Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ); } diff --git a/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginContext.cs b/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginContext.cs index 8baa13f9..d4b8eb45 100644 --- a/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginContext.cs +++ b/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginContext.cs @@ -5,83 +5,82 @@ using System.Collections.Generic; using System.Text; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Enables to +/// cancel login based on any criteria exposed by this context. +/// +public interface IWebFrontAuthValidateLoginContext { /// - /// Enables to - /// cancel login based on any criteria exposed by this context. + /// Gets the current http context. /// - public interface IWebFrontAuthValidateLoginContext - { - /// - /// Gets the current http context. - /// - HttpContext HttpContext { get; } + HttpContext HttpContext { get; } - /// - /// Gets the authentication type system. - /// - IAuthenticationTypeSystem AuthenticationTypeSystem { get; } + /// + /// Gets the authentication type system. + /// + IAuthenticationTypeSystem AuthenticationTypeSystem { get; } - /// - /// Gets the endpoint that started the authentication. - /// - WebFrontAuthLoginMode LoginMode { get; } + /// + /// Gets the endpoint that started the authentication. + /// + WebFrontAuthLoginMode LoginMode { get; } - /// - /// Gets the return url only if '/c/startLogin' has been called with a 'returnUrl' parameter. - /// Null otherwise. - /// - string? ReturnUrl { get; } + /// + /// Gets the return url only if '/c/startLogin' has been called with a 'returnUrl' parameter. + /// Null otherwise. + /// + string? ReturnUrl { get; } - /// - /// Gets the authentication provider on which .webfront/c/starLogin has been called. - /// This is "Basic" when is - /// and null when LoginMode is . - /// - string? InitialScheme { get; } + /// + /// Gets the authentication provider on which .webfront/c/starLogin has been called. + /// This is "Basic" when is + /// and null when LoginMode is . + /// + string? InitialScheme { get; } - /// - /// Gets the calling authentication scheme. - /// This is usually the same as the . - /// - string CallingScheme { get; } + /// + /// Gets the calling authentication scheme. + /// This is usually the same as the . + /// + string CallingScheme { get; } - /// - /// Gets the current authentication when .webfront/c/starLogin has been called - /// or the current authentication when is - /// or . - /// - IAuthenticationInfo InitialAuthentication { get; } + /// + /// Gets the current authentication when .webfront/c/starLogin has been called + /// or the current authentication when is + /// or . + /// + IAuthenticationInfo InitialAuthentication { get; } - /// - /// Gets the query parameters (for GET) or form data (when POST was used) of the - /// initial .webfront/c/starLogin call as a readonly list. - /// - IDictionary UserData { get; } + /// + /// Gets the query parameters (for GET) or form data (when POST was used) of the + /// initial .webfront/c/starLogin call as a readonly list. + /// + IDictionary UserData { get; } - /// - /// Gets whether an error has already been set. - /// - bool HasError { get; } + /// + /// Gets whether an error has already been set. + /// + bool HasError { get; } - /// - /// Cancels the login and sets an error message. - /// The returned error contains the , the , - /// , and optionally the . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// Error identifier (a dotted identifier string). Can not be null or empty. - /// The error message in clear text. - void SetError( string errorId, string errorText ); + /// + /// Cancels the login and sets an error message. + /// The returned error contains the , the , + /// , and optionally the . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// Error identifier (a dotted identifier string). Can not be null or empty. + /// The error message in clear text. + void SetError( string errorId, string errorText ); - /// - /// Cancels the login and sets an error message. - /// The returned error has "errorId" set to the full name of the exception - /// and the "errorText" is the . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// The exception. Can not be null. - void SetError( Exception ex ); - } + /// + /// Cancels the login and sets an error message. + /// The returned error has "errorId" set to the full name of the exception + /// and the "errorText" is the . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// The exception. Can not be null. + void SetError( Exception ex ); } diff --git a/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginService.cs b/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginService.cs index b2393ead..13f79a02 100644 --- a/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginService.cs +++ b/CK.AspNet.Auth/Extensions/ValidateLogin/IWebFrontAuthValidateLoginService.cs @@ -7,35 +7,34 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Optional service that, when registered, enables login validations. +/// When this service is available, the login process follows the 3 steps below: +/// +/// +/// First, the is called +/// with a false actualLogin parameter. +/// +/// +/// On success, this is called. +/// +/// +/// Then, only if this validation succeeds, the +/// is called again with a true actualLogin parameter. +/// +/// +/// +[ContainerConfiguredSingletonService] +public interface IWebFrontAuthValidateLoginService : ISingletonAutoService { /// - /// Optional service that, when registered, enables login validations. - /// When this service is available, the login process follows the 3 steps below: - /// - /// - /// First, the is called - /// with a false actualLogin parameter. - /// - /// - /// On success, this is called. - /// - /// - /// Then, only if this validation succeeds, the - /// is called again with a true actualLogin parameter. - /// - /// + /// Called for each login. Any error set on the cancels the login. /// - [ContainerConfiguredSingletonService] - public interface IWebFrontAuthValidateLoginService : ISingletonAutoService - { - /// - /// Called for each login. Any error set on the cancels the login. - /// - /// The monitor to use. - /// The logged in user. - /// Validation context. - /// The awaitable. - Task ValidateLoginAsync( IActivityMonitor monitor, IUserInfo loggedInUser, IWebFrontAuthValidateLoginContext context ); - } + /// The monitor to use. + /// The logged in user. + /// Validation context. + /// The awaitable. + Task ValidateLoginAsync( IActivityMonitor monitor, IUserInfo loggedInUser, IWebFrontAuthValidateLoginContext context ); } diff --git a/CK.AspNet.Auth/FrontAuthenticationInfo.cs b/CK.AspNet.Auth/FrontAuthenticationInfo.cs index a2cedf76..aebdbe57 100644 --- a/CK.AspNet.Auth/FrontAuthenticationInfo.cs +++ b/CK.AspNet.Auth/FrontAuthenticationInfo.cs @@ -1,61 +1,60 @@ using CK.Auth; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Immutable capture of the core along with option. +/// This is the information that is stored in the token and the authentication cookie. +/// +/// It's a reference type since it is stored in the HttpContext's Items (a struct would be boxed 99% of the times). +/// +/// +public sealed class FrontAuthenticationInfo { /// - /// Immutable capture of the core along with option. - /// This is the information that is stored in the token and the authentication cookie. - /// - /// It's a reference type since it is stored in the HttpContext's Items (a struct would be boxed 99% of the times). - /// + /// The authentication information. /// - public sealed class FrontAuthenticationInfo - { - /// - /// The authentication information. - /// - public readonly IAuthenticationInfo Info; + public readonly IAuthenticationInfo Info; - /// - /// Whether this authentication info should be memorized or considered - /// as a transient one. - /// - public readonly bool RememberMe; + /// + /// Whether this authentication info should be memorized or considered + /// as a transient one. + /// + public readonly bool RememberMe; - /// - /// Initializes a new info. - /// - /// The info. - /// The option. - public FrontAuthenticationInfo( IAuthenticationInfo info, bool rememberMe ) - { - Info = info; - RememberMe = rememberMe; - } + /// + /// Initializes a new info. + /// + /// The info. + /// The option. + public FrontAuthenticationInfo( IAuthenticationInfo info, bool rememberMe ) + { + Info = info; + RememberMe = rememberMe; + } - /// - /// Immutable setter. - /// - /// The new info to consider. - /// The new front authentication info (or this). - public FrontAuthenticationInfo SetInfo( IAuthenticationInfo info ) => info == Info ? this : new FrontAuthenticationInfo( info, RememberMe ); + /// + /// Immutable setter. + /// + /// The new info to consider. + /// The new front authentication info (or this). + public FrontAuthenticationInfo SetInfo( IAuthenticationInfo info ) => info == Info ? this : new FrontAuthenticationInfo( info, RememberMe ); - /// - /// Immutable setter. - /// Ensures that is . - /// The user identifier and name is available (but at the unsafe level). The device identifier - /// and the flag are preserved. This is a kind of "soft logout". - /// - /// The new front authentication info (or this). - public FrontAuthenticationInfo SetUnsafeLevel() => Info.Level <= AuthLevel.Unsafe - ? this - : new FrontAuthenticationInfo( Info.SetExpires( null ), RememberMe ); + /// + /// Immutable setter. + /// Ensures that is . + /// The user identifier and name is available (but at the unsafe level). The device identifier + /// and the flag are preserved. This is a kind of "soft logout". + /// + /// The new front authentication info (or this). + public FrontAuthenticationInfo SetUnsafeLevel() => Info.Level <= AuthLevel.Unsafe + ? this + : new FrontAuthenticationInfo( Info.SetExpires( null ), RememberMe ); - /// - /// Immutable setter. - /// - /// The new remember me. - /// The new front authentication info (or this). - public FrontAuthenticationInfo SetRememberMe( bool rememberMe ) => rememberMe == RememberMe ? this : new FrontAuthenticationInfo( Info, rememberMe ); - } + /// + /// Immutable setter. + /// + /// The new remember me. + /// The new front authentication info (or this). + public FrontAuthenticationInfo SetRememberMe( bool rememberMe ) => rememberMe == RememberMe ? this : new FrontAuthenticationInfo( Info, rememberMe ); } diff --git a/CK.AspNet.Auth/IErrorContext.cs b/CK.AspNet.Auth/IErrorContext.cs index 643d94d3..bc8b18cb 100644 --- a/CK.AspNet.Auth/IErrorContext.cs +++ b/CK.AspNet.Auth/IErrorContext.cs @@ -1,22 +1,20 @@ -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Internal that unifies and . +/// +interface IErrorContext { /// - /// Internal that unifies and . + /// Sets an error identifier and message. + /// Can be called multiple times: new error information replaces the previous one. /// - interface IErrorContext - { - /// - /// Sets an error identifier and message. - /// Can be called multiple times: new error information replaces the previous one. - /// - /// Error identifier (a dotted identifier string). - /// The error message in clear text. - public void SetError( string errorId, string errorMessage ); - - /// - /// Gets whether an error has been set. - /// - bool HasError { get; } - } + /// Error identifier (a dotted identifier string). + /// The error message in clear text. + public void SetError( string errorId, string errorMessage ); + /// + /// Gets whether an error has been set. + /// + bool HasError { get; } } diff --git a/CK.AspNet.Auth/IWebFrontAuthLoginService.cs b/CK.AspNet.Auth/IWebFrontAuthLoginService.cs index e5c6df23..ab81ad25 100644 --- a/CK.AspNet.Auth/IWebFrontAuthLoginService.cs +++ b/CK.AspNet.Auth/IWebFrontAuthLoginService.cs @@ -6,80 +6,79 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Interface to the back-end login service. +/// This is the most important (and required) service that abstracts any persistence layer or gateway that +/// is able to handle login and authentication. +/// +/// This service is a endpoint service: it is available only in the global DI context, not from any other endpoints. +/// +/// +[ContainerConfiguredSingletonService] +public interface IWebFrontAuthLoginService : ISingletonAutoService { /// - /// Interface to the back-end login service. - /// This is the most important (and required) service that abstracts any persistence layer or gateway that - /// is able to handle login and authentication. - /// - /// This service is a endpoint service: it is available only in the global DI context, not from any other endpoints. - /// + /// Gets whether is supported. /// - [ContainerConfiguredSingletonService] - public interface IWebFrontAuthLoginService : ISingletonAutoService - { - /// - /// Gets whether is supported. - /// - bool HasBasicLogin { get; } + bool HasBasicLogin { get; } - /// - /// Gets the existing providers's name. - /// - IReadOnlyList Providers { get; } + /// + /// Gets the existing providers's name. + /// + IReadOnlyList Providers { get; } - /// - /// Attempts to login. must be true for this - /// to be called. Must never return null. - /// - /// Current Http context. - /// The activity monitor to use. - /// The user name. - /// The password. - /// - /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: - /// only checks are done. - /// - /// A non null . - Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin = true ); + /// + /// Attempts to login. must be true for this + /// to be called. Must never return null. + /// + /// Current Http context. + /// The activity monitor to use. + /// The user name. + /// The password. + /// + /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: + /// only checks are done. + /// + /// A non null . + Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin = true ); - /// - /// Creates a payload object for a given scheme that can be used to - /// call . - /// - /// Current Http context. - /// The activity monitor to use. - /// The login scheme (either the provider name to use or starts with the provider name and a dot). - /// A new, empty, provider dependent login payload. - object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ); + /// + /// Creates a payload object for a given scheme that can be used to + /// call . + /// + /// Current Http context. + /// The activity monitor to use. + /// The login scheme (either the provider name to use or starts with the provider name and a dot). + /// A new, empty, provider dependent login payload. + object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ); - /// - /// Attempts to login a user using an existing provider. - /// The provider derived from the scheme must exist and the payload must be compatible - /// otherwise an is thrown. - /// Must never return null. - /// - /// Current Http context. - /// The activity monitor to use. - /// The login scheme (either the provider name to use or starts with the provider name and a dotted suffix). - /// The provider dependent login payload. - /// - /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: - /// only checks are done. - /// - /// A non null . - Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload, bool actualLogin = true ); + /// + /// Attempts to login a user using an existing provider. + /// The provider derived from the scheme must exist and the payload must be compatible + /// otherwise an is thrown. + /// Must never return null. + /// + /// Current Http context. + /// The activity monitor to use. + /// The login scheme (either the provider name to use or starts with the provider name and a dotted suffix). + /// The provider dependent login payload. + /// + /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: + /// only checks are done. + /// + /// A non null . + Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload, bool actualLogin = true ); - /// - /// Refreshes a by reading the actual user and the impersonated user if any. - /// - /// The current http context. - /// The monitor to use. - /// The current authentication info that should be refreshed. Can be null (None authentication is returned). - /// New expiration date (can be the same as the current's one). - /// The refreshed information. Never null but may be the None authentication info. - Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ); + /// + /// Refreshes a by reading the actual user and the impersonated user if any. + /// + /// The current http context. + /// The monitor to use. + /// The current authentication info that should be refreshed. Can be null (None authentication is returned). + /// New expiration date (can be the same as the current's one). + /// The refreshed information. Never null but may be the None authentication info. + Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ); - } } diff --git a/CK.AspNet.Auth/InternalExtensions.cs b/CK.AspNet.Auth/InternalExtensions.cs index b748a572..f85b7c2a 100644 --- a/CK.AspNet.Auth/InternalExtensions.cs +++ b/CK.AspNet.Auth/InternalExtensions.cs @@ -10,58 +10,58 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +static class InternalExtensions { - static class InternalExtensions + static public JProperty ToJProperty( this IDictionary @this, string name = "userData" ) { - static public JProperty ToJProperty( this IDictionary @this, string name = "userData" ) - { - return new JProperty( name, - new JObject( @this.Select( d => new JProperty( d.Key, (string?)d.Value ) ) ) ); - } + return new JProperty( name, + new JObject( @this.Select( d => new JProperty( d.Key, (string?)d.Value ) ) ) ); + } - static public void SetNoCacheAndDefaultStatus( this HttpResponse @this, int defaultStatusCode ) - { - @this.Headers[HeaderNames.CacheControl] = "no-cache"; - @this.Headers[HeaderNames.Pragma] = "no-cache"; - @this.Headers[HeaderNames.Expires] = "-1"; - @this.StatusCode = defaultStatusCode; - } + static public void SetNoCacheAndDefaultStatus( this HttpResponse @this, int defaultStatusCode ) + { + @this.Headers[HeaderNames.CacheControl] = "no-cache"; + @this.Headers[HeaderNames.Pragma] = "no-cache"; + @this.Headers[HeaderNames.Expires] = "-1"; + @this.StatusCode = defaultStatusCode; + } - /// - /// Reads a limited number of characters from the request body (with an UTF8 encoding). - /// - /// This request. - /// The maximal number of characters to read. - /// The string or null on error. - static public async Task TryReadSmallBodyAsStringAsync( this HttpRequest @this, int maxLen ) + /// + /// Reads a limited number of characters from the request body (with an UTF8 encoding). + /// + /// This request. + /// The maximal number of characters to read. + /// The string or null on error. + static public async Task TryReadSmallBodyAsStringAsync( this HttpRequest @this, int maxLen ) + { + using( var s = new StreamReader( @this.Body, Encoding.UTF8, true, 1024, true ) ) { - using( var s = new StreamReader( @this.Body, Encoding.UTF8, true, 1024, true ) ) + char[] max = new char[maxLen + 1]; + int len = await s.ReadBlockAsync( max, 0, maxLen + 1 ); + if( len >= maxLen ) { - char[] max = new char[maxLen + 1]; - int len = await s.ReadBlockAsync( max, 0, maxLen + 1 ); - if( len >= maxLen ) - { - @this.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return null; - } - return new String( max, 0, len ); + @this.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return null; } + return new String( max, 0, len ); } + } - static public Task WriteAsync( this HttpResponse @this, JObject? o, int code = StatusCodes.Status200OK ) - { - @this.StatusCode = code; - @this.ContentType = "application/json"; - return @this.WriteAsync( o != null ? o.ToString( Newtonsoft.Json.Formatting.None ) : "{}" ); - } + static public Task WriteAsync( this HttpResponse @this, JObject? o, int code = StatusCodes.Status200OK ) + { + @this.StatusCode = code; + @this.ContentType = "application/json"; + return @this.WriteAsync( o != null ? o.ToString( Newtonsoft.Json.Formatting.None ) : "{}" ); + } - static public Task WriteWindowPostMessageAsync( this HttpResponse @this, JObject o, string? callerOrigin ) - { - @this.StatusCode = StatusCodes.Status200OK; - @this.ContentType = "text/html"; - var oS = o != null ? o.ToString( Newtonsoft.Json.Formatting.None ) : "{}"; - var r = $@" + static public Task WriteWindowPostMessageAsync( this HttpResponse @this, JObject o, string? callerOrigin ) + { + @this.StatusCode = StatusCodes.Status200OK; + @this.ContentType = "text/html"; + var oS = o != null ? o.ToString( Newtonsoft.Json.Formatting.None ) : "{}"; + var r = $@" @@ -78,15 +78,14 @@ static public Task WriteWindowPostMessageAsync( this HttpResponse @this, JObject "; - return @this.WriteAsync( r ); - } + return @this.WriteAsync( r ); + } - static string GetBreachPadding() - { - Random random = new Random(); - byte[] data = new byte[random.Next( 10, 256 )]; - random.NextBytes( data ); - return Convert.ToBase64String( data ); - } + static string GetBreachPadding() + { + Random random = new Random(); + byte[] data = new byte[random.Next( 10, 256 )]; + random.NextBytes( data ); + return Convert.ToBase64String( data ); } } diff --git a/CK.AspNet.Auth/RemoteAuthenticationEventsContextExtensions.cs b/CK.AspNet.Auth/RemoteAuthenticationEventsContextExtensions.cs index 0f22dd6e..fd0c976d 100644 --- a/CK.AspNet.Auth/RemoteAuthenticationEventsContextExtensions.cs +++ b/CK.AspNet.Auth/RemoteAuthenticationEventsContextExtensions.cs @@ -5,124 +5,123 @@ using System; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Helper on and . +/// +public static class RemoteAuthenticationEventsContextExtensions { /// - /// Helper on and . + /// Obsolete. /// - public static class RemoteAuthenticationEventsContextExtensions - { - /// - /// Obsolete. - /// - /// Type of the payload. - /// This ticket received context. - /// Action that must configure the payload. - /// The awaitable. - [Obsolete( "Use WebFrontAuthOnTicketReceivedAsync (renaming).", error: true )] - public static Task WebFrontAuthRemoteAuthenticateAsync( this TicketReceivedContext c, Action payloadConfigurator ) - => WebFrontAuthOnTicketReceivedAsync( c, payloadConfigurator ); + /// Type of the payload. + /// This ticket received context. + /// Action that must configure the payload. + /// The awaitable. + [Obsolete( "Use WebFrontAuthOnTicketReceivedAsync (renaming).", error: true )] + public static Task WebFrontAuthRemoteAuthenticateAsync( this TicketReceivedContext c, Action payloadConfigurator ) + => WebFrontAuthOnTicketReceivedAsync( c, payloadConfigurator ); - /// - /// Simple API used from to handle - /// external authentication: - /// is called. - /// - /// Type of the payload. - /// This ticket received context. - /// Action that must configure the payload. - /// The awaitable. - public static Task WebFrontAuthOnTicketReceivedAsync( this TicketReceivedContext c, Action payloadConfigurator ) - { - var authService = c.HttpContext.RequestServices.GetRequiredService(); - return authService.HandleRemoteAuthenticationAsync( c, payloadConfigurator ); - } + /// + /// Simple API used from to handle + /// external authentication: + /// is called. + /// + /// Type of the payload. + /// This ticket received context. + /// Action that must configure the payload. + /// The awaitable. + public static Task WebFrontAuthOnTicketReceivedAsync( this TicketReceivedContext c, Action payloadConfigurator ) + { + var authService = c.HttpContext.RequestServices.GetRequiredService(); + return authService.HandleRemoteAuthenticationAsync( c, payloadConfigurator ); + } - /// - /// Simple API used from to handle remote failure authentication: - /// the and are returned to the client. - /// (This method calls that ends any further response processing.) - /// - /// This remote failure context. - /// - /// True to downgrade the current authentication to . - /// By default the current authentication is kept as-is. - /// - /// - /// Error identifier: should be a dotted identifier that could easily be used as a resource - /// name (to map to translations in different languages). - /// - /// When null, 's is used. - /// The awaitable. - public static Task WebFrontAuthOnRemoteFailureAsync( this RemoteFailureContext f, bool setUnsafeLevel = false, string errorId = "RemoteFailure", string? errorText = null ) - { - return OnErrorAsync( f, f.Properties, setUnsafeLevel, errorId, errorText ?? f.Failure?.Message ?? "RemoteFailure" ); - } + /// + /// Simple API used from to handle remote failure authentication: + /// the and are returned to the client. + /// (This method calls that ends any further response processing.) + /// + /// This remote failure context. + /// + /// True to downgrade the current authentication to . + /// By default the current authentication is kept as-is. + /// + /// + /// Error identifier: should be a dotted identifier that could easily be used as a resource + /// name (to map to translations in different languages). + /// + /// When null, 's is used. + /// The awaitable. + public static Task WebFrontAuthOnRemoteFailureAsync( this RemoteFailureContext f, bool setUnsafeLevel = false, string errorId = "RemoteFailure", string? errorText = null ) + { + return OnErrorAsync( f, f.Properties, setUnsafeLevel, errorId, errorText ?? f.Failure?.Message ?? "RemoteFailure" ); + } - /// - /// Simple API used from to handle remote access denied: - /// the and are returned to the client. - /// (This method calls that ends any further response processing.) - /// - /// This remote failure context. - /// - /// True to downgrade the current authentication to . - /// By default the current authentication is kept as-is. - /// - /// - /// Error identifier: should be a dotted identifier that could easily be used as a resource - /// name (to map to translations in different languages). - /// - /// When null, is used. - /// The awaitable. - public static Task WebFrontAuthOnAccessDeniedAsync( this AccessDeniedContext d, - bool setUnsafeLevel = false, - string errorId = "AccessDenied", - string? errorText = null ) - { - return OnErrorAsync( d, d.Properties, setUnsafeLevel, errorId, errorText ?? errorId ); - } + /// + /// Simple API used from to handle remote access denied: + /// the and are returned to the client. + /// (This method calls that ends any further response processing.) + /// + /// This remote failure context. + /// + /// True to downgrade the current authentication to . + /// By default the current authentication is kept as-is. + /// + /// + /// Error identifier: should be a dotted identifier that could easily be used as a resource + /// name (to map to translations in different languages). + /// + /// When null, is used. + /// The awaitable. + public static Task WebFrontAuthOnAccessDeniedAsync( this AccessDeniedContext d, + bool setUnsafeLevel = false, + string errorId = "AccessDenied", + string? errorText = null ) + { + return OnErrorAsync( d, d.Properties, setUnsafeLevel, errorId, errorText ?? errorId ); + } - static Task OnErrorAsync( HandleRequestContext h, - AuthenticationProperties? properties, - bool setUnsafeLevel, - string errorId, - string errorText ) + static Task OnErrorAsync( HandleRequestContext h, + AuthenticationProperties? properties, + bool setUnsafeLevel, + string errorId, + string errorText ) + { + h.HandleResponse(); + var authService = h.HttpContext.RequestServices.GetRequiredService(); + authService.GetWFAData( h.HttpContext, properties, out var fAuth, out var impersonateActualUser, out var initialScheme, out var callerOrigin, out var returnUrl, out var userData ); + if( setUnsafeLevel ) { - h.HandleResponse(); - var authService = h.HttpContext.RequestServices.GetRequiredService(); - authService.GetWFAData( h.HttpContext, properties, out var fAuth, out var impersonateActualUser, out var initialScheme, out var callerOrigin, out var returnUrl, out var userData ); - if( setUnsafeLevel ) - { - fAuth = fAuth.SetUnsafeLevel(); - } - return authService.SendRemoteAuthenticationErrorAsync( h.HttpContext, fAuth, returnUrl, callerOrigin, errorId, errorText, initialScheme, h.Scheme.Name, userData ); + fAuth = fAuth.SetUnsafeLevel(); } + return authService.SendRemoteAuthenticationErrorAsync( h.HttpContext, fAuth, returnUrl, callerOrigin, errorId, errorText, initialScheme, h.Scheme.Name, userData ); + } - /// - /// Extracts the initial authentication from this context (from the "WFA-C" key of ). - /// - /// This ticket received context. - /// The initial authentication. - public static IAuthenticationInfo GetTicketAuthenticationInfo( this TicketReceivedContext @this ) => GetFrontAuthenticationInfo( @this.HttpContext, @this.Properties ).Info; + /// + /// Extracts the initial authentication from this context (from the "WFA-C" key of ). + /// + /// This ticket received context. + /// The initial authentication. + public static IAuthenticationInfo GetTicketAuthenticationInfo( this TicketReceivedContext @this ) => GetFrontAuthenticationInfo( @this.HttpContext, @this.Properties ).Info; - /// - /// Extracts the initial authentication from this context (from the "WFA-C" key of ). - /// - /// This failure context. - /// The initial authentication. - public static IAuthenticationInfo GetTicketAuthenticationInfo( this RemoteFailureContext @this ) => GetFrontAuthenticationInfo( @this.HttpContext, @this.Properties ).Info; + /// + /// Extracts the initial authentication from this context (from the "WFA-C" key of ). + /// + /// This failure context. + /// The initial authentication. + public static IAuthenticationInfo GetTicketAuthenticationInfo( this RemoteFailureContext @this ) => GetFrontAuthenticationInfo( @this.HttpContext, @this.Properties ).Info; - /// - /// Extracts the initial authentication from this context (from the "WFA-C" key of ). - /// - /// This failure context. - /// The initial authentication. - public static IAuthenticationInfo GetTicketAuthenticationInfo( this AccessDeniedContext d ) => GetFrontAuthenticationInfo( d.HttpContext, d.Properties ).Info; + /// + /// Extracts the initial authentication from this context (from the "WFA-C" key of ). + /// + /// This failure context. + /// The initial authentication. + public static IAuthenticationInfo GetTicketAuthenticationInfo( this AccessDeniedContext d ) => GetFrontAuthenticationInfo( d.HttpContext, d.Properties ).Info; - static FrontAuthenticationInfo GetFrontAuthenticationInfo( HttpContext httpContext, AuthenticationProperties? properties ) - { - return httpContext.RequestServices.GetRequiredService().GetFrontAuthenticationInfo( httpContext, properties ); - } + static FrontAuthenticationInfo GetFrontAuthenticationInfo( HttpContext httpContext, AuthenticationProperties? properties ) + { + return httpContext.RequestServices.GetRequiredService().GetFrontAuthenticationInfo( httpContext, properties ); } } diff --git a/CK.AspNet.Auth/SecureData/ExtraDataSecureDataFormat.cs b/CK.AspNet.Auth/SecureData/ExtraDataSecureDataFormat.cs index e9f0087e..3da3d17e 100644 --- a/CK.AspNet.Auth/SecureData/ExtraDataSecureDataFormat.cs +++ b/CK.AspNet.Auth/SecureData/ExtraDataSecureDataFormat.cs @@ -9,56 +9,54 @@ using System.IO; using System.Text; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Secure IQueryCollection and/or IFormCollection data serialization, using a binary serialization. +/// +public class ExtraDataSecureDataFormat : SecureDataFormat> { - /// - /// Secure IQueryCollection and/or IFormCollection data serialization, using a binary serialization. - /// - public class ExtraDataSecureDataFormat : SecureDataFormat> + class Serializer : IDataSerializer> { - class Serializer : IDataSerializer> + public IDictionary Deserialize( byte[] data ) { - public IDictionary Deserialize( byte[] data ) + var result = new Dictionary(); + using( var s = Util.RecyclableStreamManager.GetStream( data ) ) + using( var r = new CKBinaryReader( s ) ) { - var result = new Dictionary(); - using( var s = Util.RecyclableStreamManager.GetStream( data ) ) - using( var r = new CKBinaryReader( s ) ) + int c = r.ReadNonNegativeSmallInt32(); + while( --c >= 0 ) { - int c = r.ReadNonNegativeSmallInt32(); - while( --c >= 0 ) - { - result.Add( r.ReadString(), r.ReadNullableString() ); - } - return result; + result.Add( r.ReadString(), r.ReadNullableString() ); } + return result; } + } - public byte[] Serialize( IDictionary model ) + public byte[] Serialize( IDictionary model ) + { + using( var s = Util.RecyclableStreamManager.GetStream() ) + using( var w = new CKBinaryWriter( s ) ) { - using( var s = Util.RecyclableStreamManager.GetStream() ) - using( var w = new CKBinaryWriter( s ) ) + w.WriteNonNegativeSmallInt32( model.Count ); + foreach( var k in model ) { - w.WriteNonNegativeSmallInt32( model.Count ); - foreach( var k in model ) - { - w.Write( k.Key ); - w.WriteNullableString( k.Value ); - } - return s.ToArray(); + w.Write( k.Key ); + w.WriteNullableString( k.Value ); } + return s.ToArray(); } } + } - static readonly Serializer _serializer = new Serializer(); + static readonly Serializer _serializer = new Serializer(); - /// - /// Initialize a new AuthenticationInfoSecureDataFormat. - /// - /// Data protector to use. - public ExtraDataSecureDataFormat( IDataProtector p ) - : base( _serializer, p ) - { - } + /// + /// Initialize a new AuthenticationInfoSecureDataFormat. + /// + /// Data protector to use. + public ExtraDataSecureDataFormat( IDataProtector p ) + : base( _serializer, p ) + { } - } diff --git a/CK.AspNet.Auth/SecureData/FrontAuthenticationInfoSecureDataFormat.cs b/CK.AspNet.Auth/SecureData/FrontAuthenticationInfoSecureDataFormat.cs index d861d7b3..b1a2f423 100644 --- a/CK.AspNet.Auth/SecureData/FrontAuthenticationInfoSecureDataFormat.cs +++ b/CK.AspNet.Auth/SecureData/FrontAuthenticationInfoSecureDataFormat.cs @@ -7,54 +7,52 @@ using System.IO; using System.Text; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Secure data, using a binary serialization +/// thanks to . +/// +class FrontAuthenticationInfoSecureDataFormat : SecureDataFormat { - /// - /// Secure data, using a binary serialization - /// thanks to . - /// - class FrontAuthenticationInfoSecureDataFormat : SecureDataFormat + class Serializer : IDataSerializer { - class Serializer : IDataSerializer - { - readonly IAuthenticationInfoType _t; + readonly IAuthenticationInfoType _t; - public Serializer( IAuthenticationTypeSystem t ) - { - _t = t.AuthenticationInfo; - } + public Serializer( IAuthenticationTypeSystem t ) + { + _t = t.AuthenticationInfo; + } - public FrontAuthenticationInfo Deserialize( byte[] data ) + public FrontAuthenticationInfo Deserialize( byte[] data ) + { + using( var s = Util.RecyclableStreamManager.GetStream( data ) ) + using( var r = new BinaryReader( s ) ) { - using( var s = Util.RecyclableStreamManager.GetStream( data ) ) - using( var r = new BinaryReader( s ) ) - { - return new FrontAuthenticationInfo( _t.Read( r )!, r.ReadBoolean() ); - } + return new FrontAuthenticationInfo( _t.Read( r )!, r.ReadBoolean() ); } + } - public byte[] Serialize( FrontAuthenticationInfo model ) + public byte[] Serialize( FrontAuthenticationInfo model ) + { + using( var s = Util.RecyclableStreamManager.GetStream() ) + using( var w = new BinaryWriter( s ) ) { - using( var s = Util.RecyclableStreamManager.GetStream() ) - using( var w = new BinaryWriter( s ) ) - { - _t.Write( w, model.Info ); - w.Write( model.RememberMe ); - return s.ToArray(); - } + _t.Write( w, model.Info ); + w.Write( model.RememberMe ); + return s.ToArray(); } } + } - /// - /// Initialize a new AuthenticationInfoSecureDataFormat. - /// - /// Type system to use. - /// Data protector to use. - public FrontAuthenticationInfoSecureDataFormat( IAuthenticationTypeSystem t, IDataProtector p ) - : base( new Serializer( t ), p ) + /// + /// Initialize a new AuthenticationInfoSecureDataFormat. + /// + /// Type system to use. + /// Data protector to use. + public FrontAuthenticationInfoSecureDataFormat( IAuthenticationTypeSystem t, IDataProtector p ) + : base( new Serializer( t ), p ) - { - } + { } - } diff --git a/CK.AspNet.Auth/UserLoginResult.cs b/CK.AspNet.Auth/UserLoginResult.cs index 68fa9473..54ae3df8 100644 --- a/CK.AspNet.Auth/UserLoginResult.cs +++ b/CK.AspNet.Auth/UserLoginResult.cs @@ -4,91 +4,90 @@ using System.Diagnostics.CodeAnalysis; using System.Text; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Encapsulates login result information. +/// +public class UserLoginResult { /// - /// Encapsulates login result information. + /// Initializes a new login result. /// - public class UserLoginResult + /// The user info. When null or anonymous, failure code and reason must indicate an error. + /// + /// Failure code must be positive on failure, zero on success. + /// Standard implementation by CK.DB.AspNetAuth (the SqlWebFrontAuthLoginService class) uses + /// the CK.DB.Auth.KnownLoginFailureCode that is defined here: https://github.com/Invenietis/CK-DB/blob/develop/CK.DB.Auth/KnownLoginFailureCode.cs. + /// + /// Failure reason must be not null on failure, null on success. + /// + /// Indicates that the login failed because the user is not registered in the provider: this may be + /// corrected by registering the user for the provider. + /// This can be true only on failure otherwise an argument exception is thrown. + /// + public UserLoginResult( IUserInfo? info, int failureCode, string? failureReason, bool unregisteredUser ) { - /// - /// Initializes a new login result. - /// - /// The user info. When null or anonymous, failure code and reason must indicate an error. - /// - /// Failure code must be positive on failure, zero on success. - /// Standard implementation by CK.DB.AspNetAuth (the SqlWebFrontAuthLoginService class) uses - /// the CK.DB.Auth.KnownLoginFailureCode that is defined here: https://github.com/Invenietis/CK-DB/blob/develop/CK.DB.Auth/KnownLoginFailureCode.cs. - /// - /// Failure reason must be not null on failure, null on success. - /// - /// Indicates that the login failed because the user is not registered in the provider: this may be - /// corrected by registering the user for the provider. - /// This can be true only on failure otherwise an argument exception is thrown. - /// - public UserLoginResult( IUserInfo? info, int failureCode, string? failureReason, bool unregisteredUser ) + if( info == null || info.UserId == 0 ) { - if( info == null || info.UserId == 0 ) + if( failureReason == null ) { - if( failureReason == null ) - { - throw new ArgumentException( $"Null or anonymous: failure reason must be not null.", nameof(failureReason) ); - } - if( failureCode <= 0 ) - { - throw new ArgumentException( $"Null or anonymous: failure code must be positive (value: {failureCode}).", nameof(failureCode) ); - } - LoginFailureCode = failureCode; - LoginFailureReason = failureReason; - IsUnregisteredUser = unregisteredUser; + throw new ArgumentException( $"Null or anonymous: failure reason must be not null.", nameof( failureReason ) ); } - else + if( failureCode <= 0 ) { - if( failureReason != null ) - { - throw new ArgumentException( $"Valid user info: failure reason must be null (value: {failureReason}).", nameof( failureReason ) ); - } - if( failureCode != 0 ) - { - throw new ArgumentException( $"Valid user info: : failure code must be zero (value: {failureCode}).", nameof( failureCode ) ); - } - if( unregisteredUser ) - { - throw new ArgumentException( $"Valid user info: it can not be an unregistered user.", nameof( unregisteredUser ) ); - } - UserInfo = info; + throw new ArgumentException( $"Null or anonymous: failure code must be positive (value: {failureCode}).", nameof( failureCode ) ); } + LoginFailureCode = failureCode; + LoginFailureReason = failureReason; + IsUnregisteredUser = unregisteredUser; } + else + { + if( failureReason != null ) + { + throw new ArgumentException( $"Valid user info: failure reason must be null (value: {failureReason}).", nameof( failureReason ) ); + } + if( failureCode != 0 ) + { + throw new ArgumentException( $"Valid user info: : failure code must be zero (value: {failureCode}).", nameof( failureCode ) ); + } + if( unregisteredUser ) + { + throw new ArgumentException( $"Valid user info: it can not be an unregistered user.", nameof( unregisteredUser ) ); + } + UserInfo = info; + } + } - /// - /// Gets the user information. - /// Null if for any reason, login failed. - /// - public IUserInfo? UserInfo { get; } + /// + /// Gets the user information. + /// Null if for any reason, login failed. + /// + public IUserInfo? UserInfo { get; } - /// - /// Gets whether the login succeeded. - /// - [MemberNotNullWhen(true,nameof(UserInfo))] - public bool IsSuccess => UserInfo != null; + /// + /// Gets whether the login succeeded. + /// + [MemberNotNullWhen( true, nameof( UserInfo ) )] + public bool IsSuccess => UserInfo != null; - /// - /// Gets whether the failure may be corrected by registering the user - /// for the provider. - /// - public bool IsUnregisteredUser { get; } + /// + /// Gets whether the failure may be corrected by registering the user + /// for the provider. + /// + public bool IsUnregisteredUser { get; } - /// - /// Gets the login failure code. This value is positive if login failed. - /// Standard implementation by CK.DB.AspNetAuth (the SqlWebFrontAuthLoginService class) uses - /// the CK.DB.Auth.KnownLoginFailureCode that is defined here: https://github.com/Invenietis/CK-DB/blob/develop/CK.DB.Auth/KnownLoginFailureCode.cs. - /// - public int LoginFailureCode { get; } + /// + /// Gets the login failure code. This value is positive if login failed. + /// Standard implementation by CK.DB.AspNetAuth (the SqlWebFrontAuthLoginService class) uses + /// the CK.DB.Auth.KnownLoginFailureCode that is defined here: https://github.com/Invenietis/CK-DB/blob/develop/CK.DB.Auth/KnownLoginFailureCode.cs. + /// + public int LoginFailureCode { get; } - /// - /// Gets a string describing the reason of a login failure. - /// Null on success. - /// - public string? LoginFailureReason { get; } - } + /// + /// Gets a string describing the reason of a login failure. + /// Null on success. + /// + public string? LoginFailureReason { get; } } diff --git a/CK.AspNet.Auth/WebFrontAuthExtensions.cs b/CK.AspNet.Auth/WebFrontAuthExtensions.cs index cf06a451..42638621 100644 --- a/CK.AspNet.Auth/WebFrontAuthExtensions.cs +++ b/CK.AspNet.Auth/WebFrontAuthExtensions.cs @@ -12,135 +12,134 @@ using System; using System.Collections.Generic; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Offers support for WebFrontAuth on . +/// +public static class WebFrontAuthExtensions { /// - /// Offers support for WebFrontAuth on . + /// Idempotent registration of the , and + /// with the . + /// + /// When called more than once, all are applied to the final . + /// /// - public static class WebFrontAuthExtensions + /// This builder. + /// Optional option configuration. + /// This builder. + public static WebApplicationBuilder AddWebFrontAuth( this WebApplicationBuilder builder, + Action? authOptions = null ) { - /// - /// Idempotent registration of the , and - /// with the . - /// - /// When called more than once, all are applied to the final . - /// - /// - /// This builder. - /// Optional option configuration. - /// This builder. - public static WebApplicationBuilder AddWebFrontAuth( this WebApplicationBuilder builder, - Action? authOptions = null ) + var props = ((IHostApplicationBuilder)builder).Properties; + if( props.TryAdd( typeof( WebFrontAuthExtensions ), typeof( WebFrontAuthExtensions ) ) ) { - var props = ((IHostApplicationBuilder)builder).Properties; - if( props.TryAdd( typeof( WebFrontAuthExtensions ), typeof( WebFrontAuthExtensions ) ) ) - { - builder.Services.AddSingleton(); - builder.Services.AddScoped( sp => sp.GetRequiredService().HttpContext.GetAuthenticationInfo() ); - var authBuilder = builder.Services.AddAuthentication( WebFrontAuthOptions.OnlyAuthenticationScheme ); - authBuilder.AddScheme( WebFrontAuthOptions.OnlyAuthenticationScheme, "Web Front Authentication", authOptions ); - builder.AppendApplicationBuilder( app => app.UseAuthentication() ); - } - else if( authOptions != null ) - { - // Already called. If an option configurator is present, register the new one. - // The OptionsFactory will call all the registered IConfigureOptions. - var configurator = new ConfigureNamedOptions( WebFrontAuthOptions.OnlyAuthenticationScheme, authOptions ); - builder.Services.AddSingleton>( configurator ); - } - return builder; + builder.Services.AddSingleton(); + builder.Services.AddScoped( sp => sp.GetRequiredService().HttpContext.GetAuthenticationInfo() ); + var authBuilder = builder.Services.AddAuthentication( WebFrontAuthOptions.OnlyAuthenticationScheme ); + authBuilder.AddScheme( WebFrontAuthOptions.OnlyAuthenticationScheme, "Web Front Authentication", authOptions ); + builder.AppendApplicationBuilder( app => app.UseAuthentication() ); } - - /// - /// Add dangerous Cors support: this allows all orgigins, methods, headers AND supports credential. - /// This is unfortunately required in some testing scenario but should NEVER be used in production. - /// - /// This method, just like and - /// can be called multiple times: the last wins. - /// - /// - /// This builder. - /// This builder. - public static WebApplicationBuilder AddUnsafeAllowAllCors( this WebApplicationBuilder builder ) + else if( authOptions != null ) { - return AddCors( builder, CorsAllowAllBuilder ); - - static void CorsAllowAllBuilder( CorsPolicyBuilder o ) - { - o.AllowAnyMethod().AllowCredentials().AllowAnyHeader().SetIsOriginAllowed( _ => true ); - } + // Already called. If an option configurator is present, register the new one. + // The OptionsFactory will call all the registered IConfigureOptions. + var configurator = new ConfigureNamedOptions( WebFrontAuthOptions.OnlyAuthenticationScheme, authOptions ); + builder.Services.AddSingleton>( configurator ); } + return builder; + } - /// - /// Add Cors support for a single policy. - /// - /// This method, just like and - /// can be called multiple times: the last wins. - /// - /// - /// This builder. - /// The cors policy builder. - /// - public static WebApplicationBuilder AddCors( this WebApplicationBuilder builder, - Action policyBuilder ) - { - Throw.CheckNotNullArgument( policyBuilder ); - var props = ((IHostApplicationBuilder)builder).Properties; - if( !props.TryGetValue( typeof( CorsPolicyBuilder ), out var currentPolicy ) ) - { - props.Add( typeof( CorsPolicyBuilder ), policyBuilder ); - builder.Services.AddCors(); - builder.AppendApplicationBuilder( DoUseCors( props ) ); - } - else - { - props[typeof( CorsPolicyBuilder )] = policyBuilder; - } - return builder; + /// + /// Add dangerous Cors support: this allows all orgigins, methods, headers AND supports credential. + /// This is unfortunately required in some testing scenario but should NEVER be used in production. + /// + /// This method, just like and + /// can be called multiple times: the last wins. + /// + /// + /// This builder. + /// This builder. + public static WebApplicationBuilder AddUnsafeAllowAllCors( this WebApplicationBuilder builder ) + { + return AddCors( builder, CorsAllowAllBuilder ); + static void CorsAllowAllBuilder( CorsPolicyBuilder o ) + { + o.AllowAnyMethod().AllowCredentials().AllowAnyHeader().SetIsOriginAllowed( _ => true ); } + } - /// - /// Add Cors support for a named policy. This method can be called multiple times: the last wins. - /// - /// Named policy must be defined by using - /// and configuring the . - /// - /// - /// This method, just like and - /// can be called multiple times: the last wins. - /// - /// - /// This builder. - /// The policy name. - /// This builder. - public static WebApplicationBuilder AddCors( this WebApplicationBuilder builder, - string policyName ) + /// + /// Add Cors support for a single policy. + /// + /// This method, just like and + /// can be called multiple times: the last wins. + /// + /// + /// This builder. + /// The cors policy builder. + /// + public static WebApplicationBuilder AddCors( this WebApplicationBuilder builder, + Action policyBuilder ) + { + Throw.CheckNotNullArgument( policyBuilder ); + var props = ((IHostApplicationBuilder)builder).Properties; + if( !props.TryGetValue( typeof( CorsPolicyBuilder ), out var currentPolicy ) ) { - Throw.CheckNotNullOrWhiteSpaceArgument( policyName ); - var props = ((IHostApplicationBuilder)builder).Properties; - if( !props.TryGetValue( typeof( CorsPolicyBuilder ), out var currentPolicy ) ) - { - props.Add( typeof( CorsPolicyBuilder ), policyName ); - builder.Services.AddCors(); - builder.AppendApplicationBuilder( DoUseCors( props ) ); - } - else - { - props[typeof( CorsPolicyBuilder )] = policyName; - } - return builder; + props.Add( typeof( CorsPolicyBuilder ), policyBuilder ); + builder.Services.AddCors(); + builder.AppendApplicationBuilder( DoUseCors( props ) ); } + else + { + props[typeof( CorsPolicyBuilder )] = policyBuilder; + } + return builder; + + } - static Action DoUseCors( IDictionary props ) + /// + /// Add Cors support for a named policy. This method can be called multiple times: the last wins. + /// + /// Named policy must be defined by using + /// and configuring the . + /// + /// + /// This method, just like and + /// can be called multiple times: the last wins. + /// + /// + /// This builder. + /// The policy name. + /// This builder. + public static WebApplicationBuilder AddCors( this WebApplicationBuilder builder, + string policyName ) + { + Throw.CheckNotNullOrWhiteSpaceArgument( policyName ); + var props = ((IHostApplicationBuilder)builder).Properties; + if( !props.TryGetValue( typeof( CorsPolicyBuilder ), out var currentPolicy ) ) { - return app => - { - var p = props[typeof( CorsPolicyBuilder )]; - if( p is string name ) app.UseCors( name ); - else app.UseCors( (Action)p ); - props.Remove( typeof( CorsPolicyBuilder ) ); - }; + props.Add( typeof( CorsPolicyBuilder ), policyName ); + builder.Services.AddCors(); + builder.AppendApplicationBuilder( DoUseCors( props ) ); } + else + { + props[typeof( CorsPolicyBuilder )] = policyName; + } + return builder; + } + + static Action DoUseCors( IDictionary props ) + { + return app => + { + var p = props[typeof( CorsPolicyBuilder )]; + if( p is string name ) app.UseCors( name ); + else app.UseCors( (Action)p ); + props.Remove( typeof( CorsPolicyBuilder ) ); + }; } } diff --git a/CK.AspNet.Auth/WebFrontAuthHandler.cs b/CK.AspNet.Auth/WebFrontAuthHandler.cs index 5d863089..b277ad92 100644 --- a/CK.AspNet.Auth/WebFrontAuthHandler.cs +++ b/CK.AspNet.Auth/WebFrontAuthHandler.cs @@ -19,503 +19,502 @@ using System.Threading.Tasks; using ISystemClock = Microsoft.AspNetCore.Authentication.ISystemClock; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +sealed class WebFrontAuthHandler : AuthenticationHandler, IAuthenticationRequestHandler { - sealed class WebFrontAuthHandler : AuthenticationHandler, IAuthenticationRequestHandler + readonly static CSemVer.SVersion _version = CSemVer.InformationalVersion.ReadFromAssembly( Assembly.GetExecutingAssembly() ).Version ?? CSemVer.SVersion.ZeroVersion; + internal readonly static PathString _cSegmentPath = "/c"; + + readonly WebFrontAuthService _authService; + readonly IAuthenticationTypeSystem _typeSystem; + readonly IWebFrontAuthLoginService _loginService; + readonly IAuthenticationSchemeProvider _schemeProvider; + readonly IWebFrontAuthImpersonationService? _impersonationService; + readonly IWebFrontAuthUnsafeDirectLoginAllowService? _unsafeDirectLoginAllower; + + public WebFrontAuthHandler( IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + WebFrontAuthService authService, + IAuthenticationTypeSystem typeSystem, + IWebFrontAuthLoginService loginService, + IAuthenticationSchemeProvider schemeProvider, + IWebFrontAuthImpersonationService? impersonationService = null, + IWebFrontAuthUnsafeDirectLoginAllowService? unsafeDirectLoginAllower = null ) : base( options, logger, encoder, clock ) { - readonly static CSemVer.SVersion _version = CSemVer.InformationalVersion.ReadFromAssembly(Assembly.GetExecutingAssembly() ).Version ?? CSemVer.SVersion.ZeroVersion; - internal readonly static PathString _cSegmentPath = "/c"; - - readonly WebFrontAuthService _authService; - readonly IAuthenticationTypeSystem _typeSystem; - readonly IWebFrontAuthLoginService _loginService; - readonly IAuthenticationSchemeProvider _schemeProvider; - readonly IWebFrontAuthImpersonationService? _impersonationService; - readonly IWebFrontAuthUnsafeDirectLoginAllowService? _unsafeDirectLoginAllower; - - public WebFrontAuthHandler( IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock, - WebFrontAuthService authService, - IAuthenticationTypeSystem typeSystem, - IWebFrontAuthLoginService loginService, - IAuthenticationSchemeProvider schemeProvider, - IWebFrontAuthImpersonationService? impersonationService = null, - IWebFrontAuthUnsafeDirectLoginAllowService? unsafeDirectLoginAllower = null ) : base( options, logger, encoder, clock ) - { - _authService = authService; - _typeSystem = typeSystem; - _loginService = loginService; - _schemeProvider = schemeProvider; - _impersonationService = impersonationService; - _unsafeDirectLoginAllower = unsafeDirectLoginAllower; - } + _authService = authService; + _typeSystem = typeSystem; + _loginService = loginService; + _schemeProvider = schemeProvider; + _impersonationService = impersonationService; + _unsafeDirectLoginAllower = unsafeDirectLoginAllower; + } - static IActivityMonitor GetRequestMonitor( HttpContext c ) => c.RequestServices.GetRequiredService(); + static IActivityMonitor GetRequestMonitor( HttpContext c ) => c.RequestServices.GetRequiredService(); - public Task HandleRequestAsync() + public Task HandleRequestAsync() + { + if( Request.Path.StartsWithSegments( Options.EntryPath, out PathString remainder ) ) { - if( Request.Path.StartsWithSegments( Options.EntryPath, out PathString remainder ) ) + Response.SetNoCacheAndDefaultStatus( StatusCodes.Status404NotFound ); + if( remainder.StartsWithSegments( _cSegmentPath, StringComparison.Ordinal, out PathString cBased ) ) { - Response.SetNoCacheAndDefaultStatus( StatusCodes.Status404NotFound ); - if( remainder.StartsWithSegments( _cSegmentPath, StringComparison.Ordinal, out PathString cBased ) ) + if( cBased.Value == "/refresh" ) { - if( cBased.Value == "/refresh" ) - { - return HandleRefreshAsync(); - } - else if( cBased.Value == "/basicLogin" ) - { - if( _loginService.HasBasicLogin ) - { - if( HttpMethods.IsPost( Request.Method ) ) return DirectBasicLoginAsync( GetRequestMonitor( Context ) ); - Response.StatusCode = StatusCodes.Status405MethodNotAllowed; - } - } - else if( cBased.Value == "/startLogin" ) - { - return HandleStartLoginAsync( GetRequestMonitor( Context ) ); - } - else if( cBased.Value == "/unsafeDirectLogin" ) - { - if( HttpMethods.IsPost( Request.Method ) ) return HandleUnsafeDirectLoginAsync( GetRequestMonitor( Context ) ); - Response.StatusCode = StatusCodes.Status405MethodNotAllowed; - } - else if( cBased.Value == "/logout" ) - { - return HandleLogoutAsync(); - } - else if( cBased.Value == "/impersonate" ) + return HandleRefreshAsync(); + } + else if( cBased.Value == "/basicLogin" ) + { + if( _loginService.HasBasicLogin ) { - // When _impersonationService == null, we only support - // impersonation to the ActualUser itself: this clears any current impersonation. - if( HttpMethods.IsPost( Request.Method ) ) return HandleImpersonateAsync( GetRequestMonitor( Context ) ); + if( HttpMethods.IsPost( Request.Method ) ) return DirectBasicLoginAsync( GetRequestMonitor( Context ) ); Response.StatusCode = StatusCodes.Status405MethodNotAllowed; } } - else + else if( cBased.Value == "/startLogin" ) + { + return HandleStartLoginAsync( GetRequestMonitor( Context ) ); + } + else if( cBased.Value == "/unsafeDirectLogin" ) { - if( remainder.Value == "/token" ) return HandleTokenAsync(); + if( HttpMethods.IsPost( Request.Method ) ) return HandleUnsafeDirectLoginAsync( GetRequestMonitor( Context ) ); + Response.StatusCode = StatusCodes.Status405MethodNotAllowed; + } + else if( cBased.Value == "/logout" ) + { + return HandleLogoutAsync(); + } + else if( cBased.Value == "/impersonate" ) + { + // When _impersonationService == null, we only support + // impersonation to the ActualUser itself: this clears any current impersonation. + if( HttpMethods.IsPost( Request.Method ) ) return HandleImpersonateAsync( GetRequestMonitor( Context ) ); + Response.StatusCode = StatusCodes.Status405MethodNotAllowed; } - return Task.FromResult( true ); } - return Task.FromResult( false ); + else + { + if( remainder.Value == "/token" ) return HandleTokenAsync(); + } + return Task.FromResult( true ); } + return Task.FromResult( false ); + } - async Task HandleRefreshAsync() - { - var (fAuth, monitor) = await _authService.RefreshInfoAsync( Context, null, Request.Query.Keys.Contains( "callBackend" ) ); - JObject response = await GetRefreshResponseAndSetCookiesAsync( fAuth, Request.Query.Keys.Contains( "schemes" ), Request.Query.Keys.Contains( "version" ) ); - return await WriteResponseAsync( response ); - } + async Task HandleRefreshAsync() + { + var (fAuth, monitor) = await _authService.RefreshInfoAsync( Context, null, Request.Query.Keys.Contains( "callBackend" ) ); + JObject response = await GetRefreshResponseAndSetCookiesAsync( fAuth, Request.Query.Keys.Contains( "schemes" ), Request.Query.Keys.Contains( "version" ) ); + return await WriteResponseAsync( response ); + } - /// - /// Applies the (if not 0), handles the - /// and and sets the cookies. - /// - /// The authentication. - /// Whether authentications schemes must be returned. - /// Whether this assembly's version should be returned. - /// The JSON object. - async ValueTask GetRefreshResponseAndSetCookiesAsync( FrontAuthenticationInfo fAuth, bool addSchemes, bool addVersion ) + /// + /// Applies the (if not 0), handles the + /// and and sets the cookies. + /// + /// The authentication. + /// Whether authentications schemes must be returned. + /// Whether this assembly's version should be returned. + /// The JSON object. + async ValueTask GetRefreshResponseAndSetCookiesAsync( FrontAuthenticationInfo fAuth, bool addSchemes, bool addVersion ) + { + bool refreshable = _authService.ApplySlidingExpirationAndSetCookies( Context, ref fAuth ); + JObject response = _authService.CreateAuthResponse( Context, refreshable, fAuth ); + if( addSchemes ) { - bool refreshable = _authService.ApplySlidingExpirationAndSetCookies( Context, ref fAuth ); - JObject response = _authService.CreateAuthResponse( Context, refreshable, fAuth ); - if( addSchemes ) + IEnumerable? list = Options.AvailableSchemes; + if( list == null || !list.Any() ) { - IEnumerable? list = Options.AvailableSchemes; - if( list == null || !list.Any() ) + list = (await _schemeProvider.GetAllSchemesAsync().ConfigureAwait( false )) + .Select( s => s.Name ) + .Where( n => n != WebFrontAuthOptions.OnlyAuthenticationScheme ); + if( _loginService.HasBasicLogin ) { - list = (await _schemeProvider.GetAllSchemesAsync().ConfigureAwait( false )) - .Select( s => s.Name ) - .Where( n => n != WebFrontAuthOptions.OnlyAuthenticationScheme ); - if( _loginService.HasBasicLogin ) - { - list = list.Prepend( "Basic" ); - } + list = list.Prepend( "Basic" ); } - response.Add( "schemes", new JArray( list ) ); } - if( addVersion ) response.Add( "version", _version.ToString() ); - return response; + response.Add( "schemes", new JArray( list ) ); } + if( addVersion ) response.Add( "version", _version.ToString() ); + return response; + } + + async Task HandleLogoutAsync() + { + await _authService.LogoutCommandAsync( GetRequestMonitor( Context ), Context ); + await Context.Response.WriteAsync( null, StatusCodes.Status200OK ); + return true; + } - async Task HandleLogoutAsync() + async Task HandleStartLoginAsync( IActivityMonitor monitor ) + { + string scheme = Request.Query["scheme"]; + string? returnUrl = Request.Query["returnUrl"]; + string? callerOrigin = Request.Query["callerOrigin"]; + string? rememberMe = Request.Query["rememberMe"]; + string? sImpersonateActualUser = Request.Query["impersonateActualUser"]; + + IEnumerable> userData; + if( HttpMethods.IsPost( Request.Method ) ) { - await _authService.LogoutCommandAsync( GetRequestMonitor( Context ), Context ); - await Context.Response.WriteAsync( null, StatusCodes.Status200OK ); - return true; + callerOrigin ??= Request.Form["callerOrigin"]; + rememberMe ??= Request.Form["rememberMe"]; + sImpersonateActualUser ??= Request.Form["impersonateActualUser"]; + userData = Request.Form; } + else userData = Request.Query; - async Task HandleStartLoginAsync( IActivityMonitor monitor ) - { - string scheme = Request.Query["scheme"]; - string? returnUrl = Request.Query["returnUrl"]; - string? callerOrigin = Request.Query["callerOrigin"]; - string? rememberMe = Request.Query["rememberMe"]; - string? sImpersonateActualUser = Request.Query["impersonateActualUser"]; - - IEnumerable> userData; - if( HttpMethods.IsPost( Request.Method ) ) - { - callerOrigin ??= Request.Form["callerOrigin"]; - rememberMe ??= Request.Form["rememberMe"]; - sImpersonateActualUser ??= Request.Form["impersonateActualUser"]; - userData = Request.Form; - } - else userData = Request.Query; + userData = userData.Where( k => !string.Equals( k.Key, "scheme", StringComparison.OrdinalIgnoreCase ) + && !string.Equals( k.Key, "returnUrl", StringComparison.OrdinalIgnoreCase ) + && !string.Equals( k.Key, "callerOrigin", StringComparison.OrdinalIgnoreCase ) + && !string.Equals( k.Key, "rememberMe", StringComparison.OrdinalIgnoreCase ) + && !string.Equals( k.Key, "impersonateActualUser", StringComparison.OrdinalIgnoreCase ) ); - userData = userData.Where( k => !string.Equals( k.Key, "scheme", StringComparison.OrdinalIgnoreCase ) - && !string.Equals( k.Key, "returnUrl", StringComparison.OrdinalIgnoreCase ) - && !string.Equals( k.Key, "callerOrigin", StringComparison.OrdinalIgnoreCase ) - && !string.Equals( k.Key, "rememberMe", StringComparison.OrdinalIgnoreCase ) - && !string.Equals( k.Key, "impersonateActualUser", StringComparison.OrdinalIgnoreCase ) ); + var fAuthCurrent = _authService.EnsureAuthenticationInfo( Context, ref monitor ); - var fAuthCurrent = _authService.EnsureAuthenticationInfo( Context, ref monitor ); + // If "rememberMe" is not found, we keep the previous one (that is false if no current authentication exists). + // RememberMe defaults to false. + if( rememberMe != null ) + { + fAuthCurrent = fAuthCurrent.SetRememberMe( rememberMe == "1" || rememberMe.Equals( "true", StringComparison.OrdinalIgnoreCase ) ); + } - // If "rememberMe" is not found, we keep the previous one (that is false if no current authentication exists). - // RememberMe defaults to false. - if( rememberMe != null ) - { - fAuthCurrent = fAuthCurrent.SetRememberMe( rememberMe == "1" || rememberMe.Equals( "true", StringComparison.OrdinalIgnoreCase ) ); - } + bool impersonateActualUser = sImpersonateActualUser != null && (sImpersonateActualUser == "1" || sImpersonateActualUser.Equals( "true", StringComparison.OrdinalIgnoreCase )); - bool impersonateActualUser = sImpersonateActualUser != null && (sImpersonateActualUser == "1" || sImpersonateActualUser.Equals( "true", StringComparison.OrdinalIgnoreCase )); + var startContext = new WebFrontAuthStartLoginContext( Context, _authService, scheme, fAuthCurrent, impersonateActualUser, returnUrl, callerOrigin ); - var startContext = new WebFrontAuthStartLoginContext( Context, _authService, scheme, fAuthCurrent, impersonateActualUser, returnUrl, callerOrigin ); - - startContext.ValidateStartLoginRequest( monitor, userData ); - if( !startContext.HasError ) - { - await _authService.OnHandlerStartLoginAsync( monitor, startContext ); - } - if( startContext.HasError ) - { - Response.StatusCode = StatusCodes.Status400BadRequest; - await startContext.SendErrorAsync(); - } - else + startContext.ValidateStartLoginRequest( monitor, userData ); + if( !startContext.HasError ) + { + await _authService.OnHandlerStartLoginAsync( monitor, startContext ); + } + if( startContext.HasError ) + { + Response.StatusCode = StatusCodes.Status400BadRequest; + await startContext.SendErrorAsync(); + } + else + { + AuthenticationProperties p = new AuthenticationProperties(); + _authService.SetWFAData( p, fAuthCurrent, startContext.ImpersonateActualUser, startContext.Scheme, startContext.CallerOrigin, startContext.ReturnUrl, startContext.UserData ); + if( startContext.DynamicScopes != null ) { - AuthenticationProperties p = new AuthenticationProperties(); - _authService.SetWFAData( p, fAuthCurrent, startContext.ImpersonateActualUser, startContext.Scheme, startContext.CallerOrigin, startContext.ReturnUrl, startContext.UserData ); - if( startContext.DynamicScopes != null ) - { - // This is how wanted OAuth scope are transfered to the target. - p.Parameters.Add( "scope", startContext.DynamicScopes ); - } - await Context.ChallengeAsync( scheme, p ); + // This is how wanted OAuth scope are transfered to the target. + p.Parameters.Add( "scope", startContext.DynamicScopes ); } - return true; + await Context.ChallengeAsync( scheme, p ); } + return true; + } - #region Unsafe Direct Login - sealed class ProviderLoginRequest - { - public string Scheme { get; set; } - public object Payload { get; set; } - public bool RememberMe { get; set; } - public bool ImpersonateActualUser { get; set; } - public Dictionary UserData { get; } = new Dictionary(); + #region Unsafe Direct Login + sealed class ProviderLoginRequest + { + public string Scheme { get; set; } + public object Payload { get; set; } + public bool RememberMe { get; set; } + public bool ImpersonateActualUser { get; set; } + public Dictionary UserData { get; } = new Dictionary(); - public ProviderLoginRequest( string scheme, object? payload ) - { - Scheme = scheme; - Payload = payload ?? new object(); - } + public ProviderLoginRequest( string scheme, object? payload ) + { + Scheme = scheme; + Payload = payload ?? new object(); } + } - async Task HandleUnsafeDirectLoginAsync( IActivityMonitor monitor ) + async Task HandleUnsafeDirectLoginAsync( IActivityMonitor monitor ) + { + Response.StatusCode = StatusCodes.Status403Forbidden; + if( _unsafeDirectLoginAllower != null ) { - Response.StatusCode = StatusCodes.Status403Forbidden; - if( _unsafeDirectLoginAllower != null ) + string? body = await Request.TryReadSmallBodyAsStringAsync( 4096 ); + ProviderLoginRequest? req = body != null ? ReadDirectLoginRequest( monitor, body ) : null; + if( req != null + && await _unsafeDirectLoginAllower.AllowAsync( Context, monitor, req.Scheme, req.Payload ) ) { - string? body = await Request.TryReadSmallBodyAsStringAsync( 4096 ); - ProviderLoginRequest? req = body != null ? ReadDirectLoginRequest( monitor, body ) : null; - if( req != null - && await _unsafeDirectLoginAllower.AllowAsync( Context, monitor, req.Scheme, req.Payload ) ) - { - var wfaSC = new WebFrontAuthLoginContext( - Context, - _authService, - _typeSystem, - WebFrontAuthLoginMode.UnsafeDirectLogin, - callingScheme: req.Scheme, - req.Payload, - authProps: null, - req.Scheme, - _authService.EnsureAuthenticationInfo( Context, ref monitor ).SetRememberMe( req.RememberMe ), - req.ImpersonateActualUser, - returnUrl: null, - callerOrigin: null, - req.UserData - ); + var wfaSC = new WebFrontAuthLoginContext( + Context, + _authService, + _typeSystem, + WebFrontAuthLoginMode.UnsafeDirectLogin, + callingScheme: req.Scheme, + req.Payload, + authProps: null, + req.Scheme, + _authService.EnsureAuthenticationInfo( Context, ref monitor ).SetRememberMe( req.RememberMe ), + req.ImpersonateActualUser, + returnUrl: null, + callerOrigin: null, + req.UserData + ); - await _authService.UnifiedLoginAsync( monitor, wfaSC, actualLogin => - { - return _loginService.LoginAsync( Context, monitor, req.Scheme, req.Payload, actualLogin ); - } ); - } + await _authService.UnifiedLoginAsync( monitor, wfaSC, actualLogin => + { + return _loginService.LoginAsync( Context, monitor, req.Scheme, req.Payload, actualLogin ); + } ); } - return true; } + return true; + } - ProviderLoginRequest? ReadDirectLoginRequest( IActivityMonitor monitor, string body ) + ProviderLoginRequest? ReadDirectLoginRequest( IActivityMonitor monitor, string body ) + { + ProviderLoginRequest? req = null; + try { - ProviderLoginRequest? req = null; - try + //var root = JsonDocument.Parse( body ).RootElement; + //var provider = root.GetString( "provider" ); + //if( provider != null ) + //{ + // var payload = root.GetProperty( "payload" ); + // req = new ProviderLoginRequest( provider, payload ); + // req.RememberMe = root.GetProperty( "rememberMe" ).ValueKind == JsonValueKind.True; + // var userData = root.GetProperty( "userData" ); + // if( userData.ValueKind == JsonValueKind.Object ) + // { + // foreach( var prop in userData.EnumerateObject() ) + // { + // prop.Value. + // } + // } + + //} + // By using our poor StringMatcher here, we parse the JSON + // to basic List> because + // JObject are IEnumerable> and + // KeyValuePair is not covariant. Moreover JToken is not easily + // convertible (to basic types) without using the JToken type. + // A dependency on NewtonSoft.Json may not be suitable for some + // providers. + + + var m = new ROSpanCharMatcher( body ); + if( m.TryMatchAnyJSON( out object? val ) + && val is List<(string Key, object? Value)> o ) { - //var root = JsonDocument.Parse( body ).RootElement; - //var provider = root.GetString( "provider" ); - //if( provider != null ) - //{ - // var payload = root.GetProperty( "payload" ); - // req = new ProviderLoginRequest( provider, payload ); - // req.RememberMe = root.GetProperty( "rememberMe" ).ValueKind == JsonValueKind.True; - // var userData = root.GetProperty( "userData" ); - // if( userData.ValueKind == JsonValueKind.Object ) - // { - // foreach( var prop in userData.EnumerateObject() ) - // { - // prop.Value. - // } - // } - - //} - // By using our poor StringMatcher here, we parse the JSON - // to basic List> because - // JObject are IEnumerable> and - // KeyValuePair is not covariant. Moreover JToken is not easily - // convertible (to basic types) without using the JToken type. - // A dependency on NewtonSoft.Json may not be suitable for some - // providers. - - - var m = new ROSpanCharMatcher( body ); - if( m.TryMatchAnyJSON( out object? val ) - && val is List<(string Key, object? Value)> o ) + string? provider = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, "provider" ) ).Value as string; + if( !string.IsNullOrWhiteSpace( provider ) ) { - string? provider = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, "provider" ) ).Value as string; - if( !string.IsNullOrWhiteSpace( provider ) ) + req = new ProviderLoginRequest( provider, + o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.Payload ) ) ).Value ); + object? rem = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.RememberMe ) ) ).Value; + req.RememberMe = rem != null + && ( + ((rem is bool rb) && rb) + || + (rem is string s && (s == "1" || s.Equals( "true", StringComparison.OrdinalIgnoreCase ))) + ); + object? imp = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.ImpersonateActualUser ) ) ).Value; + req.ImpersonateActualUser = imp != null + && ( + ((imp is bool ri) && ri) + || + (imp is string sI && (sI == "1" || sI.Equals( "true", StringComparison.OrdinalIgnoreCase ))) + ); + var userData = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.UserData ) ) ).Value; + if( userData is List<(string Key, object Value)> data ) { - req = new ProviderLoginRequest( provider, - o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.Payload ) ) ).Value ); - object? rem = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.RememberMe ) ) ).Value; - req.RememberMe = rem != null - && ( - ((rem is bool rb) && rb) - || - (rem is string s && (s == "1" || s.Equals( "true", StringComparison.OrdinalIgnoreCase ))) - ); - object? imp = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.ImpersonateActualUser ) ) ).Value; - req.ImpersonateActualUser = imp != null - && ( - ((imp is bool ri) && ri) - || - (imp is string sI && (sI == "1" || sI.Equals( "true", StringComparison.OrdinalIgnoreCase ))) - ); - var userData = o.FirstOrDefault( kv => StringComparer.OrdinalIgnoreCase.Equals( kv.Key, nameof( ProviderLoginRequest.UserData ) ) ).Value; - if( userData is List<(string Key, object Value)> data ) + foreach( var (k, v) in data ) { - foreach( var (k, v) in data ) - { - req.UserData.Add( k, (string)v ); - } + req.UserData.Add( k, (string)v ); } } } } - catch( Exception ex ) - { - monitor.Error( WebFrontAuthService.WebFrontAuthMonitorTag, "Invalid payload.", ex ); - } - if( req == null ) Response.StatusCode = StatusCodes.Status400BadRequest; - return req; } + catch( Exception ex ) + { + monitor.Error( WebFrontAuthService.WebFrontAuthMonitorTag, "Invalid payload.", ex ); + } + if( req == null ) Response.StatusCode = StatusCodes.Status400BadRequest; + return req; + } - #endregion + #endregion - #region Basic Authentication support + #region Basic Authentication support - class BasicLoginRequest - { - public string? UserName { get; set; } - public string? Password { get; set; } - public bool RememberMe { get; set; } - public bool ImpersonateActualUser { get; set; } - public Dictionary UserData { get; } = new Dictionary(); - } + class BasicLoginRequest + { + public string? UserName { get; set; } + public string? Password { get; set; } + public bool RememberMe { get; set; } + public bool ImpersonateActualUser { get; set; } + public Dictionary UserData { get; } = new Dictionary(); + } - async Task DirectBasicLoginAsync( IActivityMonitor monitor ) + async Task DirectBasicLoginAsync( IActivityMonitor monitor ) + { + Debug.Assert( _loginService.HasBasicLogin ); + string? body = await Request.TryReadSmallBodyAsStringAsync( 4096 ); + BasicLoginRequest? req = body != null ? ReadBasicLoginRequest( monitor, body ) : null; + if( req != null ) { - Debug.Assert( _loginService.HasBasicLogin ); - string? body = await Request.TryReadSmallBodyAsStringAsync( 4096 ); - BasicLoginRequest? req = body != null ? ReadBasicLoginRequest( monitor, body ) : null; - if( req != null ) + Debug.Assert( req.UserName != null && req.Password != null ); + var wfaSC = new WebFrontAuthLoginContext( Context, + _authService, + _typeSystem, + WebFrontAuthLoginMode.BasicLogin, + "Basic", + Tuple.Create( req.UserName, req.Password ), + authProps: null, + initialScheme: "Basic", + _authService.EnsureAuthenticationInfo( Context, ref monitor ).SetRememberMe( req.RememberMe ), + req.ImpersonateActualUser, + returnUrl: null, + callerOrigin: null, + req.UserData ); + + await _authService.UnifiedLoginAsync( monitor, wfaSC, actualLogin => { - Debug.Assert( req.UserName != null && req.Password != null ); - var wfaSC = new WebFrontAuthLoginContext( Context, - _authService, - _typeSystem, - WebFrontAuthLoginMode.BasicLogin, - "Basic", - Tuple.Create( req.UserName, req.Password ), - authProps: null, - initialScheme: "Basic", - _authService.EnsureAuthenticationInfo( Context, ref monitor ).SetRememberMe( req.RememberMe ), - req.ImpersonateActualUser, - returnUrl: null, - callerOrigin: null, - req.UserData ); - - await _authService.UnifiedLoginAsync( monitor, wfaSC, actualLogin => - { - return _loginService.BasicLoginAsync( Context, monitor, req.UserName, req.Password, actualLogin ); - } ); - } - return true; + return _loginService.BasicLoginAsync( Context, monitor, req.UserName, req.Password, actualLogin ); + } ); } + return true; + } - BasicLoginRequest? ReadBasicLoginRequest( IActivityMonitor monitor, string body ) + BasicLoginRequest? ReadBasicLoginRequest( IActivityMonitor monitor, string body ) + { + BasicLoginRequest? req = null; + try { - BasicLoginRequest? req = null; - try + var r = JsonConvert.DeserializeObject( body ); + if( r != null + && !string.IsNullOrWhiteSpace( r.UserName ) + && !string.IsNullOrWhiteSpace( r.Password ) ) { - var r = JsonConvert.DeserializeObject( body ); - if( r != null - && !string.IsNullOrWhiteSpace( r.UserName ) - && !string.IsNullOrWhiteSpace( r.Password ) ) - { - req = r; - } + req = r; } - catch( Exception ex ) - { - monitor.Error( ex ); - } - if( req == null ) Response.StatusCode = StatusCodes.Status400BadRequest; - return req; } + catch( Exception ex ) + { + monitor.Error( ex ); + } + if( req == null ) Response.StatusCode = StatusCodes.Status400BadRequest; + return req; + } - #endregion + #endregion - #region Impersonation - async Task HandleImpersonateAsync( IActivityMonitor monitor ) + #region Impersonation + async Task HandleImpersonateAsync( IActivityMonitor monitor ) + { + Debug.Assert( HttpMethods.IsPost( Request.Method ) ); + Response.StatusCode = _impersonationService == null ? StatusCodes.Status404NotFound : StatusCodes.Status403Forbidden; + var fAuth = _authService.EnsureAuthenticationInfo( Context, ref monitor ); + if( fAuth.Info.ActualUser.UserId != 0 ) { - Debug.Assert( HttpMethods.IsPost( Request.Method ) ); - Response.StatusCode = _impersonationService == null ? StatusCodes.Status404NotFound : StatusCodes.Status403Forbidden; - var fAuth = _authService.EnsureAuthenticationInfo( Context, ref monitor ); - if( fAuth.Info.ActualUser.UserId != 0 ) + string? body = await Request.TryReadSmallBodyAsStringAsync( 1024 ); + int userId = -1; + string? userName = null; + if( body != null && TryReadUserKey( monitor, ref userId, ref userName, body ) ) { - string? body = await Request.TryReadSmallBodyAsStringAsync( 1024 ); - int userId = -1; - string? userName = null; - if( body != null && TryReadUserKey( monitor, ref userId, ref userName, body ) ) + if( userName == fAuth.Info.ActualUser.UserName || userId == fAuth.Info.ActualUser.UserId ) { - if( userName == fAuth.Info.ActualUser.UserName || userId == fAuth.Info.ActualUser.UserId ) - { - fAuth = fAuth.SetInfo( fAuth.Info.ClearImpersonation() ); - Response.StatusCode = StatusCodes.Status200OK; - } - else + fAuth = fAuth.SetInfo( fAuth.Info.ClearImpersonation() ); + Response.StatusCode = StatusCodes.Status200OK; + } + else + { + if( _impersonationService != null ) { - if( _impersonationService != null ) + IUserInfo? target = userName != null + ? await _impersonationService.ImpersonateAsync( Context, monitor, fAuth.Info, userName ) + : await _impersonationService.ImpersonateAsync( Context, monitor, fAuth.Info, userId ); + if( target != null ) { - IUserInfo? target = userName != null - ? await _impersonationService.ImpersonateAsync( Context, monitor, fAuth.Info, userName ) - : await _impersonationService.ImpersonateAsync( Context, monitor, fAuth.Info, userId ); - if( target != null ) - { - fAuth = fAuth.SetInfo( fAuth.Info.Impersonate( target ) ); - Response.StatusCode = StatusCodes.Status200OK; - } + fAuth = fAuth.SetInfo( fAuth.Info.Impersonate( target ) ); + Response.StatusCode = StatusCodes.Status200OK; } } - if( Response.StatusCode == StatusCodes.Status200OK ) - { - await Response.WriteAsync( await GetRefreshResponseAndSetCookiesAsync( fAuth, addSchemes: false, addVersion: false ) ); - } + } + if( Response.StatusCode == StatusCodes.Status200OK ) + { + await Response.WriteAsync( await GetRefreshResponseAndSetCookiesAsync( fAuth, addSchemes: false, addVersion: false ) ); } } - return true; } + return true; + } - bool TryReadUserKey( IActivityMonitor monitor, ref int userId, ref string? userName, string body ) + bool TryReadUserKey( IActivityMonitor monitor, ref int userId, ref string? userName, string body ) + { + var m = new ROSpanCharMatcher( body ); + List<(string Key, object? Value)>? param; + if( m.TryMatchAnyJSON( out object? val ) + && (param = val as List<(string, object?)>) != null + && param.Count == 1 ) { - var m = new ROSpanCharMatcher( body ); - List<(string Key, object? Value)>? param; - if( m.TryMatchAnyJSON( out object? val ) - && (param = val as List<(string, object?)>) != null - && param.Count == 1 ) + if( param[0].Key == "userName" ) { - if( param[0].Key == "userName" ) + if( param[0].Value is string n ) { - if( param[0].Value is string n ) - { - userName = n; - return true; - } + userName = n; + return true; } - if( param[0].Key == "userId" ) + } + if( param[0].Key == "userId" ) + { + if( param[0].Value is string n ) { - if( param[0].Value is string n ) + if( Int32.TryParse( n, NumberStyles.Integer, CultureInfo.InvariantCulture, out userId ) ) { - if( Int32.TryParse( n, NumberStyles.Integer, CultureInfo.InvariantCulture, out userId ) ) - { - return true; - } - } - else if( param[0].Value is double d ) - { - userId = (int)d; return true; } } + else if( param[0].Value is double d ) + { + userId = (int)d; + return true; + } } - Response.StatusCode = StatusCodes.Status400BadRequest; - return false; } + Response.StatusCode = StatusCodes.Status400BadRequest; + return false; + } - #endregion + #endregion - #region Authentication handling (handles standard Authenticate API). + #region Authentication handling (handles standard Authenticate API). - protected override Task HandleAuthenticateAsync() + protected override Task HandleAuthenticateAsync() + { + IActivityMonitor? monitor = null; + var fAuth = _authService.EnsureAuthenticationInfo( Context, ref monitor ); + if( fAuth.Info == null ) { - IActivityMonitor? monitor = null; - var fAuth = _authService.EnsureAuthenticationInfo( Context, ref monitor ); - if( fAuth.Info == null ) - { - return Task.FromResult( AuthenticateResult.Fail( "No current Authentication." ) ); - } - var principal = new ClaimsPrincipal(); - principal.AddIdentity( _typeSystem.AuthenticationInfo.ToClaimsIdentity( fAuth.Info, userInfoOnly: !Options.UseFullClaimsPrincipalOnAuthenticate ) ); - var ticket = new AuthenticationTicket( principal, new AuthenticationProperties(), Scheme.Name ); - return Task.FromResult( AuthenticateResult.Success( ticket ) ); + return Task.FromResult( AuthenticateResult.Fail( "No current Authentication." ) ); } + var principal = new ClaimsPrincipal(); + principal.AddIdentity( _typeSystem.AuthenticationInfo.ToClaimsIdentity( fAuth.Info, userInfoOnly: !Options.UseFullClaimsPrincipalOnAuthenticate ) ); + var ticket = new AuthenticationTicket( principal, new AuthenticationProperties(), Scheme.Name ); + return Task.FromResult( AuthenticateResult.Success( ticket ) ); + } - #endregion + #endregion - Task HandleTokenAsync() - { - IActivityMonitor? monitor = null; - var fAuth = _authService.EnsureAuthenticationInfo( Context, ref monitor ); - var o = new JObject( - new JProperty( "info", _typeSystem.AuthenticationInfo.ToJObject( fAuth.Info ) ), - new JProperty( "rememberMe", fAuth.RememberMe ) ); - return WriteResponseAsync( o ); - } + Task HandleTokenAsync() + { + IActivityMonitor? monitor = null; + var fAuth = _authService.EnsureAuthenticationInfo( Context, ref monitor ); + var o = new JObject( + new JProperty( "info", _typeSystem.AuthenticationInfo.ToJObject( fAuth.Info ) ), + new JProperty( "rememberMe", fAuth.RememberMe ) ); + return WriteResponseAsync( o ); + } - /// - /// Writes the JObject and always returns true. - /// - /// The object. - /// The http status. - /// Always true. - async Task WriteResponseAsync( JObject o, int code = StatusCodes.Status200OK ) - { - await Response.WriteAsync( o, code ); - return true; - } + /// + /// Writes the JObject and always returns true. + /// + /// The object. + /// The http status. + /// Always true. + async Task WriteResponseAsync( JObject o, int code = StatusCodes.Status200OK ) + { + await Response.WriteAsync( o, code ); + return true; } } diff --git a/CK.AspNet.Auth/WebFrontAuthLoginContext.cs b/CK.AspNet.Auth/WebFrontAuthLoginContext.cs index 8cd47f50..7368e29b 100644 --- a/CK.AspNet.Auth/WebFrontAuthLoginContext.cs +++ b/CK.AspNet.Auth/WebFrontAuthLoginContext.cs @@ -18,323 +18,321 @@ using System.Globalization; using CK.Core; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Encapsulates the sign in data issued by an external provider. +/// +internal class WebFrontAuthLoginContext : IWebFrontAuthValidateLoginContext, + IWebFrontAuthAutoCreateAccountContext, + IWebFrontAuthAutoBindingAccountContext, + IErrorContext { + readonly WebFrontAuthService _authenticationService; + UserLoginResult? _failedLogin; + internal UserLoginResult? _successfulLogin; + internal string? _errorId; + internal string? _errorText; + // This contains the initial authentication but with the + // requested "RememberMe" flag. + readonly FrontAuthenticationInfo _initialAuth; + // Used for Direct login (post return code). + int _httpErrorCode; + + internal WebFrontAuthLoginContext( HttpContext ctx, + WebFrontAuthService authService, + IAuthenticationTypeSystem typeSystem, + WebFrontAuthLoginMode loginMode, + string callingScheme, + object payload, + AuthenticationProperties? authProps, + string? initialScheme, + FrontAuthenticationInfo initialAuth, + bool impersonateActualUser, + string? returnUrl, + string? callerOrigin, + IDictionary userData ) + { + Debug.Assert( ctx != null && authService != null && typeSystem != null && !String.IsNullOrWhiteSpace( callingScheme ) && payload != null ); + HttpContext = ctx; + _authenticationService = authService; + AuthenticationTypeSystem = typeSystem; + LoginMode = loginMode; + CallingScheme = callingScheme; + Payload = payload; + + _initialAuth = initialAuth; + ImpersonateActualUser = impersonateActualUser; + + // CookieMode == None prevents any RememberMe. + // And note that when CurrentOptions.UseLongTermCookie is false, we nevertheless allow the "RememberMe" functionality: + // The cookie will be a non-session one (a regular cookie that will expire according to CurrentOptions.ExpireTimeSpan) + // and as such, provides a "short term resiliency", a "remember me for the next {ExpireTimeSpan} even if I close my browser" functionality. + RememberMe = initialAuth.RememberMe && authService.CookieMode != AuthenticationCookieMode.None; + + AuthenticationProperties = authProps; + InitialScheme = initialScheme; + ReturnUrl = returnUrl; + CallerOrigin = callerOrigin; + UserData = userData; + } + /// - /// Encapsulates the sign in data issued by an external provider. + /// Gets the authentication type system. /// - internal class WebFrontAuthLoginContext : IWebFrontAuthValidateLoginContext, - IWebFrontAuthAutoCreateAccountContext, - IWebFrontAuthAutoBindingAccountContext, - IErrorContext - { - readonly WebFrontAuthService _authenticationService; - UserLoginResult? _failedLogin; - internal UserLoginResult? _successfulLogin; - internal string? _errorId; - internal string? _errorText; - // This contains the initial authentication but with the - // requested "RememberMe" flag. - readonly FrontAuthenticationInfo _initialAuth; - // Used for Direct login (post return code). - int _httpErrorCode; - - internal WebFrontAuthLoginContext( HttpContext ctx, - WebFrontAuthService authService, - IAuthenticationTypeSystem typeSystem, - WebFrontAuthLoginMode loginMode, - string callingScheme, - object payload, - AuthenticationProperties? authProps, - string? initialScheme, - FrontAuthenticationInfo initialAuth, - bool impersonateActualUser, - string? returnUrl, - string? callerOrigin, - IDictionary userData ) - { - Debug.Assert( ctx != null && authService != null && typeSystem != null && !String.IsNullOrWhiteSpace( callingScheme ) && payload != null ); - HttpContext = ctx; - _authenticationService = authService; - AuthenticationTypeSystem = typeSystem; - LoginMode = loginMode; - CallingScheme = callingScheme; - Payload = payload; - - _initialAuth = initialAuth; - ImpersonateActualUser = impersonateActualUser; - - // CookieMode == None prevents any RememberMe. - // And note that when CurrentOptions.UseLongTermCookie is false, we nevertheless allow the "RememberMe" functionality: - // The cookie will be a non-session one (a regular cookie that will expire according to CurrentOptions.ExpireTimeSpan) - // and as such, provides a "short term resiliency", a "remember me for the next {ExpireTimeSpan} even if I close my browser" functionality. - RememberMe = initialAuth.RememberMe && authService.CookieMode != AuthenticationCookieMode.None; - - AuthenticationProperties = authProps; - InitialScheme = initialScheme; - ReturnUrl = returnUrl; - CallerOrigin = callerOrigin; - UserData = userData; - } + public IAuthenticationTypeSystem AuthenticationTypeSystem { get; } - /// - /// Gets the authentication type system. - /// - public IAuthenticationTypeSystem AuthenticationTypeSystem { get; } - - /// - /// Gets the current http context. - /// - public HttpContext HttpContext { get; } - - /// - /// Gets the endpoint that started the authentication. - /// - public WebFrontAuthLoginMode LoginMode { get; } - - /// - /// Gets the Authentication properties. - /// This is null when is - /// or . - /// - public AuthenticationProperties? AuthenticationProperties { get; } - - /// - /// Gets the return url if '/c/startLogin' has been called with a 'returnUrl' parameter (the "inline login"). - /// - public string? ReturnUrl { get; } - - /// - /// Gets the caller scheme and host. - /// Not null only if '/c/startLogin' has been called. - /// - public string? CallerOrigin { get; } - - /// - /// Gets whether the login wants to keep the previous logged in user as the - /// and becomes the . - /// - public bool ImpersonateActualUser { get; } - - /// - /// Gets the authentication provider on which .webfront/c/starLogin has been called. - /// This is "Basic" when is - /// and null when LoginMode is . - /// - public string? InitialScheme { get; } - - /// - /// Gets the calling authentication scheme. - /// This is usually the same as the . - /// - public string CallingScheme { get; } - - /// - /// Gets whether the authentication should be memorized (or be as transient as possible). - /// Note that this is always false when is used. - /// - public bool RememberMe { get; } - - /// - /// Gets the provider payload (type is provider dependent). - /// This is never null but may be an empty object when unsafe login is used with no payload. - /// - public object Payload { get; } - - /// - /// Gets the current authentication when .webfront/c/starLogin has been called - /// or the current authentication when is - /// or . - /// - public IAuthenticationInfo InitialAuthentication => _initialAuth.Info; - - /// - /// Gets the user data that follows the process. - /// - public IDictionary UserData { get; } - - /// - /// Gets whether SetError or SetSuccessfulLogin methods have been called. - /// - public bool IsHandled => _errorId != null || _successfulLogin != null; - - /// - /// Gets whether an error has already been set. - /// - public bool HasError => _errorId != null; - - /// - /// Sets an error message. - /// The returned error contains the and , - /// the , and . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// Error identifier (a dotted identifier string). Must not be null or empty. - /// The optional error message in clear text (typically in english). - public void SetError( string errorId, string? errorText = null ) - { - Throw.CheckNotNullOrWhiteSpaceArgument( errorId ); - _errorId = errorId; - _errorText = errorText; - _failedLogin = null; - } + /// + /// Gets the current http context. + /// + public HttpContext HttpContext { get; } - UserLoginResult? IWebFrontAuthAutoCreateAccountContext.SetError( string errorId, string? errorText ) - { - SetError( errorId, errorText ); - return null; - } + /// + /// Gets the endpoint that started the authentication. + /// + public WebFrontAuthLoginMode LoginMode { get; } - UserLoginResult? IWebFrontAuthAutoBindingAccountContext.SetError( string errorId, string? errorText ) - { - SetError( errorId, errorText ); - return null; - } + /// + /// Gets the Authentication properties. + /// This is null when is + /// or . + /// + public AuthenticationProperties? AuthenticationProperties { get; } - /// - /// Sets an error message. - /// The returned error has "errorId" set to the full name of the exception - /// and the "errorText" is the . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// The exception. - public void SetError( Exception ex ) - { - Throw.CheckNotNullArgument( ex ); - _errorId = ex.GetType().ToCSharpName(); - _errorText = ex.Message ?? "Exception has null message!"; - if( ex is ArgumentException ) _httpErrorCode = StatusCodes.Status400BadRequest; - else _httpErrorCode = 0; - _failedLogin = null; - } + /// + /// Gets the return url if '/c/startLogin' has been called with a 'returnUrl' parameter (the "inline login"). + /// + public string? ReturnUrl { get; } - UserLoginResult? IWebFrontAuthAutoCreateAccountContext.SetError( Exception ex ) - { - SetError( ex ); - return null; - } + /// + /// Gets the caller scheme and host. + /// Not null only if '/c/startLogin' has been called. + /// + public string? CallerOrigin { get; } - UserLoginResult? IWebFrontAuthAutoBindingAccountContext.SetError( Exception ex ) - { - SetError( ex ); - return null; - } + /// + /// Gets whether the login wants to keep the previous logged in user as the + /// and becomes the . + /// + public bool ImpersonateActualUser { get; } - /// - /// Sets a login failure. - /// The returned error contains the , , , - /// the "errorId" is "User.LoginFailure", the "errorMessage" is - /// and a specific "loginFailureCode" contains the . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// Must be not null and must be false. - public void SetError( UserLoginResult loginFailed ) - { - Throw.CheckArgument( loginFailed != null && !loginFailed.IsSuccess ); - _errorId = "User.LoginFailure"; - _errorText = loginFailed.LoginFailureReason; - _failedLogin = loginFailed; - Debug.Assert( _errorText != null ); - } + /// + /// Gets the authentication provider on which .webfront/c/starLogin has been called. + /// This is "Basic" when is + /// and null when LoginMode is . + /// + public string? InitialScheme { get; } - /// - /// Sets a successful login. - /// Must be called only if or - /// have not been called before. - /// - /// The result that must be successful. - public void SetSuccessfulLogin( UserLoginResult successResult ) - { - if( successResult == null || !successResult.IsSuccess ) throw new ArgumentException( "Must be a login success.", nameof(successResult) ); - if( _errorId != null ) throw new InvalidOperationException( $"An error ({_errorId}) has been already set." ); - _successfulLogin = successResult; - } + /// + /// Gets the calling authentication scheme. + /// This is usually the same as the . + /// + public string CallingScheme { get; } - internal async Task SendResponseAsync( IActivityMonitor monitor ) - { - if( !IsHandled ) throw new InvalidOperationException( "SetError or SetSuccessfulLogin must have been called." ); - if( _errorId != null ) - { - if( LoginMode == WebFrontAuthLoginMode.UnsafeDirectLogin - || LoginMode == WebFrontAuthLoginMode.BasicLogin ) - { - await SendDirectAuthenticationErrorAsync(); - return; - } - await SendRemoteAuthenticationErrorAsync(); - return; - } - Debug.Assert( _successfulLogin != null ); - WebFrontAuthService.LoginResult r = await _authenticationService.HandleLoginAsync( HttpContext, - monitor, - _successfulLogin, - CallingScheme, - InitialAuthentication, - RememberMe, - ImpersonateActualUser ); + /// + /// Gets whether the authentication should be memorized (or be as transient as possible). + /// Note that this is always false when is used. + /// + public bool RememberMe { get; } + + /// + /// Gets the provider payload (type is provider dependent). + /// This is never null but may be an empty object when unsafe login is used with no payload. + /// + public object Payload { get; } + + /// + /// Gets the current authentication when .webfront/c/starLogin has been called + /// or the current authentication when is + /// or . + /// + public IAuthenticationInfo InitialAuthentication => _initialAuth.Info; + + /// + /// Gets the user data that follows the process. + /// + public IDictionary UserData { get; } + + /// + /// Gets whether SetError or SetSuccessfulLogin methods have been called. + /// + public bool IsHandled => _errorId != null || _successfulLogin != null; + + /// + /// Gets whether an error has already been set. + /// + public bool HasError => _errorId != null; + + /// + /// Sets an error message. + /// The returned error contains the and , + /// the , and . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// Error identifier (a dotted identifier string). Must not be null or empty. + /// The optional error message in clear text (typically in english). + public void SetError( string errorId, string? errorText = null ) + { + Throw.CheckNotNullOrWhiteSpaceArgument( errorId ); + _errorId = errorId; + _errorText = errorText; + _failedLogin = null; + } + + UserLoginResult? IWebFrontAuthAutoCreateAccountContext.SetError( string errorId, string? errorText ) + { + SetError( errorId, errorText ); + return null; + } + + UserLoginResult? IWebFrontAuthAutoBindingAccountContext.SetError( string errorId, string? errorText ) + { + SetError( errorId, errorText ); + return null; + } + + /// + /// Sets an error message. + /// The returned error has "errorId" set to the full name of the exception + /// and the "errorText" is the . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// The exception. + public void SetError( Exception ex ) + { + Throw.CheckNotNullArgument( ex ); + _errorId = ex.GetType().ToCSharpName(); + _errorText = ex.Message ?? "Exception has null message!"; + if( ex is ArgumentException ) _httpErrorCode = StatusCodes.Status400BadRequest; + else _httpErrorCode = 0; + _failedLogin = null; + } + + UserLoginResult? IWebFrontAuthAutoCreateAccountContext.SetError( Exception ex ) + { + SetError( ex ); + return null; + } + + UserLoginResult? IWebFrontAuthAutoBindingAccountContext.SetError( Exception ex ) + { + SetError( ex ); + return null; + } + /// + /// Sets a login failure. + /// The returned error contains the , , , + /// the "errorId" is "User.LoginFailure", the "errorMessage" is + /// and a specific "loginFailureCode" contains the . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// Must be not null and must be false. + public void SetError( UserLoginResult loginFailed ) + { + Throw.CheckArgument( loginFailed != null && !loginFailed.IsSuccess ); + _errorId = "User.LoginFailure"; + _errorText = loginFailed.LoginFailureReason; + _failedLogin = loginFailed; + Debug.Assert( _errorText != null ); + } + + /// + /// Sets a successful login. + /// Must be called only if or + /// have not been called before. + /// + /// The result that must be successful. + public void SetSuccessfulLogin( UserLoginResult successResult ) + { + if( successResult == null || !successResult.IsSuccess ) throw new ArgumentException( "Must be a login success.", nameof( successResult ) ); + if( _errorId != null ) throw new InvalidOperationException( $"An error ({_errorId}) has been already set." ); + _successfulLogin = successResult; + } + + internal async Task SendResponseAsync( IActivityMonitor monitor ) + { + if( !IsHandled ) throw new InvalidOperationException( "SetError or SetSuccessfulLogin must have been called." ); + if( _errorId != null ) + { if( LoginMode == WebFrontAuthLoginMode.UnsafeDirectLogin || LoginMode == WebFrontAuthLoginMode.BasicLogin ) { - await SendDirectAuthenticationSuccessAsync( r ); + await SendDirectAuthenticationErrorAsync(); return; } - await SendRemoteAuthenticationSuccessAsync( r ); + await SendRemoteAuthenticationErrorAsync(); + return; } - - Task SendDirectAuthenticationSuccessAsync( WebFrontAuthService.LoginResult r ) + Debug.Assert( _successfulLogin != null ); + WebFrontAuthService.LoginResult r = await _authenticationService.HandleLoginAsync( HttpContext, + monitor, + _successfulLogin, + CallingScheme, + InitialAuthentication, + RememberMe, + ImpersonateActualUser ); + + if( LoginMode == WebFrontAuthLoginMode.UnsafeDirectLogin + || LoginMode == WebFrontAuthLoginMode.BasicLogin ) { - if( UserData != null ) r.Response.Add( UserData.ToJProperty() ); - return HttpContext.Response.WriteAsync( r.Response, StatusCodes.Status200OK ); + await SendDirectAuthenticationSuccessAsync( r ); + return; } + await SendRemoteAuthenticationSuccessAsync( r ); + } - Task SendDirectAuthenticationErrorAsync() - { - Debug.Assert( _errorId != null ); - int code = _httpErrorCode == 0 ? StatusCodes.Status401Unauthorized : _httpErrorCode; - var newAuth = ImpersonateActualUser - ? _initialAuth - : _initialAuth.SetUnsafeLevel(); - - JObject errObj = _authenticationService.CreateErrorAuthResponse( HttpContext, newAuth, _errorId, _errorText, InitialScheme, CallingScheme, UserData, _failedLogin ); - return HttpContext.Response.WriteAsync( errObj, code ); - } + Task SendDirectAuthenticationSuccessAsync( WebFrontAuthService.LoginResult r ) + { + if( UserData != null ) r.Response.Add( UserData.ToJProperty() ); + return HttpContext.Response.WriteAsync( r.Response, StatusCodes.Status200OK ); + } - Task SendRemoteAuthenticationSuccessAsync( WebFrontAuthService.LoginResult r ) - { - Debug.Assert( CallerOrigin != null, "/c/startLogin has been called." ); - if( ReturnUrl != null ) - { - // "inline" mode. - var caller = new Uri( CallerOrigin ); - var target = new Uri( caller, ReturnUrl ); - HttpContext.Response.Redirect( target.ToString() ); - return Task.CompletedTask; - } - // "popup" mode. - var data = new JObject( - new JProperty( "initialScheme", InitialScheme ), - new JProperty( "callingScheme", CallingScheme ) ); - data.Add( UserData.ToJProperty() ); - r.Response.Merge( data ); - return HttpContext.Response.WriteWindowPostMessageAsync( r.Response, CallerOrigin ); - } + Task SendDirectAuthenticationErrorAsync() + { + Debug.Assert( _errorId != null ); + int code = _httpErrorCode == 0 ? StatusCodes.Status401Unauthorized : _httpErrorCode; + var newAuth = ImpersonateActualUser + ? _initialAuth + : _initialAuth.SetUnsafeLevel(); + + JObject errObj = _authenticationService.CreateErrorAuthResponse( HttpContext, newAuth, _errorId, _errorText, InitialScheme, CallingScheme, UserData, _failedLogin ); + return HttpContext.Response.WriteAsync( errObj, code ); + } - Task SendRemoteAuthenticationErrorAsync() + Task SendRemoteAuthenticationSuccessAsync( WebFrontAuthService.LoginResult r ) + { + Debug.Assert( CallerOrigin != null, "/c/startLogin has been called." ); + if( ReturnUrl != null ) { - Debug.Assert( _errorId != null && _errorText != null ); - return _authenticationService.SendRemoteAuthenticationErrorAsync( - HttpContext, - ImpersonateActualUser ? _initialAuth : _initialAuth.SetUnsafeLevel(), - ReturnUrl, - CallerOrigin, - _errorId, - _errorText, - InitialScheme, - CallingScheme, - UserData, - _failedLogin ); + // "inline" mode. + var caller = new Uri( CallerOrigin ); + var target = new Uri( caller, ReturnUrl ); + HttpContext.Response.Redirect( target.ToString() ); + return Task.CompletedTask; } + // "popup" mode. + var data = new JObject( + new JProperty( "initialScheme", InitialScheme ), + new JProperty( "callingScheme", CallingScheme ) ); + data.Add( UserData.ToJProperty() ); + r.Response.Merge( data ); + return HttpContext.Response.WriteWindowPostMessageAsync( r.Response, CallerOrigin ); } + Task SendRemoteAuthenticationErrorAsync() + { + Debug.Assert( _errorId != null && _errorText != null ); + return _authenticationService.SendRemoteAuthenticationErrorAsync( + HttpContext, + ImpersonateActualUser ? _initialAuth : _initialAuth.SetUnsafeLevel(), + ReturnUrl, + CallerOrigin, + _errorId, + _errorText, + InitialScheme, + CallingScheme, + UserData, + _failedLogin ); + } } diff --git a/CK.AspNet.Auth/WebFrontAuthLoginMode.cs b/CK.AspNet.Auth/WebFrontAuthLoginMode.cs index c7e61b8d..db8f99a3 100644 --- a/CK.AspNet.Auth/WebFrontAuthLoginMode.cs +++ b/CK.AspNet.Auth/WebFrontAuthLoginMode.cs @@ -2,35 +2,34 @@ using System.Collections.Generic; using System.Text; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Defines the 3 different endpoints that can be used to start authentication. +/// +public enum WebFrontAuthLoginMode { /// - /// Defines the 3 different endpoints that can be used to start authentication. + /// No endpoint has been used. + /// This is the case when the authentication challenge has been called directly. /// - public enum WebFrontAuthLoginMode - { - /// - /// No endpoint has been used. - /// This is the case when the authentication challenge has been called directly. - /// - None, + None, - /// - /// Identifies the '.webfront/c/basicLogin' endpoint. - /// This is available only if the is true. - /// - BasicLogin, + /// + /// Identifies the '.webfront/c/basicLogin' endpoint. + /// This is available only if the is true. + /// + BasicLogin, - /// - /// Identifies the '.webfront/c/unsafeDirectLogin' endpoint. - /// This endpoint is disabled by default and can only be enabled thanks to the - /// optional service. - /// - UnsafeDirectLogin, + /// + /// Identifies the '.webfront/c/unsafeDirectLogin' endpoint. + /// This endpoint is disabled by default and can only be enabled thanks to the + /// optional service. + /// + UnsafeDirectLogin, - /// - /// Identifies the '.webfront/c/startLogin' endpoint used to challenge remote authentications. - /// - StartLogin - } + /// + /// Identifies the '.webfront/c/startLogin' endpoint used to challenge remote authentications. + /// + StartLogin } diff --git a/CK.AspNet.Auth/WebFrontAuthOptions.cs b/CK.AspNet.Auth/WebFrontAuthOptions.cs index e58144d5..3ff89431 100644 --- a/CK.AspNet.Auth/WebFrontAuthOptions.cs +++ b/CK.AspNet.Auth/WebFrontAuthOptions.cs @@ -4,186 +4,185 @@ using System; using System.Collections.Generic; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Options for . +/// Note that WebFrountAuth uses the Data protection API (https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction) +/// to manage secrets: an important part of the security configuration is delegated to this API. +/// +public class WebFrontAuthOptions : AuthenticationSchemeOptions { + static readonly PathString _entryPath = new PathString( "/.webfront" ); + + /// + /// The is not designed to be added multiple + /// times to an application, hence its name is unique. + /// + public const string OnlyAuthenticationScheme = "WebFrontAuth"; + + /// + /// Gets the entry point: "/.webfront". + /// + public PathString EntryPath => _entryPath; + + /// + /// Controls how much time the authentication will remain valid + /// from the point it is created. + /// Defaults to 20 minutes. + /// This time is extended if is set and + /// when "/c/refresh" is called. + /// This configuration can be changed dynamically: modifying the configuration will take the + /// new value into account. + /// + public TimeSpan ExpireTimeSpan { get; set; } = TimeSpan.FromMinutes( 20 ); + + /// + /// Controls how much time the long term, unsafe, authentication information + /// will remain valid from the point it is created. + /// Defaults to one year. + /// This configuration can be changed dynamically. + /// + public TimeSpan? UnsafeExpireTimeSpan { get; set; } = TimeSpan.FromDays( 366 ); + + /// + /// Gets whether is not null, greater than , + /// and is not . + /// + /// When true a long-lived cookie is used to store the unsafe, but long term, authentication information. + /// Its depends on . + /// Since the expiration is a dynamic configuration, this is also a dynamic configuration. + /// + /// + public bool UseLongTermCookie => UnsafeExpireTimeSpan.HasValue + && UnsafeExpireTimeSpan > ExpireTimeSpan + && CookieMode != AuthenticationCookieMode.None; + + /// + /// Gets or sets whether a complex claim must be set as the + /// when (and the + /// "WebFrontAuth" is the default scheme) or + /// with the "WebFrontAuth" scheme is called. + /// + /// Defaults to false: the ClaimsPrincipal contains only the safe user claims and ignores + /// any impersonation. + /// + /// + /// When true, the ClaimsPrincipal contains more complex claims: unsafe user claims, a claim for the + /// authentication level, the expirations if they exist and impersonation is handled thanks to the + /// . + /// + /// + /// This cannot be changed dynamically. + /// + /// + public bool UseFullClaimsPrincipalOnAuthenticate { get; set; } + + /// + /// Gets whether the authentication cookie (see ) requires or not https. + /// Note that the long term cookie uses sets to false since it + /// does not require any protection. + /// Defaults to . + /// This cannot be changed dynamically. + /// + public CookieSecurePolicy CookieSecurePolicy { get; set; } + + /// + /// Gets or sets if and how cookies are managed to store the authentication information. + /// + /// Defaults to . + /// + /// + /// Setting it to should NOT BE used in + /// most cases: this mode, that is the same as the standard Cookie ASP.Net authentication, + /// is for standard and classical Web application. + /// + /// + /// Setting it to disables all cookies: client apps + /// are no more "F5 resilient", this can be used for pure API implementations. + /// + /// + /// This cannot be changed dynamically. + /// + /// + public AuthenticationCookieMode CookieMode { get; set; } + + /// + /// Gets or sets a list of available schemes returned for information from '/c/refresh' endpoint + /// when 'schemes' appears in the query string. + /// + /// Defaults to null: schemes are the same as + /// when this is null or empty. + /// + /// + /// When not null (or empty), this list takes precedence over the login service's providers: all supported + /// schemes must be declared here (and unwanted ones must not appear). + /// + /// + /// This list does not forbid user login to non listed schemes, this is intended only for applications + /// to communicate with the user. + /// + /// + /// This configuration can be changed dynamically: modifying the configuration will take the + /// new schemes into account immediately. + /// + /// + public List? AvailableSchemes { get; set; } + + /// + /// Gets or sets the refresh validation time. + /// When set to other than the middleware will re-issue a new token + /// (and new authentication cookie if allows it) with a new expiration time any time it + /// processes a ".webfront/c/refresh" request. + /// + /// This applies to but not to . + /// This configuration can be changed dynamically: modifying the configuration will take the + /// new value into account. + /// + /// + public TimeSpan SlidingExpirationTime { get; set; } + + /// + /// Gets or sets the http header name. Defaults to "Authorization". + /// This cannot be changed dynamically. + /// + public string BearerHeaderName { get; set; } = "Authorization"; + + /// + /// Defines the initial critical time span when logged in through each schemes. + /// It is null by default: no schemes elevate a critical authentication level. + /// + public IDictionary? SchemesCriticalTimeSpan { get; set; } + + /// + /// Gets or sets the initial AuthCookieName. Defaults to ".webFront". + /// The long term cookie name equals to AuthCookieName suffixed by "LT". + /// This cannot be changed dynamically. + /// + public string AuthCookieName { get; set; } = ".webFront"; + + /// + /// Gets or sets whether + /// must be called each time "/.webfront/c/refresh" is called. + /// + /// Default to false: the login service method is called only if a callBackend parameter appear in the query string: "/.webfront/c/refresh?callBackend". + /// + /// + /// This configuration can be changed dynamically. + /// + /// + public bool AlwaysCallBackendOnRefresh { get; set; } + /// - /// Options for . - /// Note that WebFrountAuth uses the Data protection API (https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction) - /// to manage secrets: an important part of the security configuration is delegated to this API. + /// Gets a mutable list of accepted returnUrl prefixes. + /// + /// The returnUrl optional parameter submitted to the '/c/startLogin' end point (case of an "inline login" based + /// on page redirections rather that the recommended popup window) must exactly start with one of this + /// prefix ( is used) otherwise a 400 Bad Request error code is returned. + /// + /// + /// This cannot be changed dynamically. + /// /// - public class WebFrontAuthOptions : AuthenticationSchemeOptions - { - static readonly PathString _entryPath = new PathString( "/.webfront" ); - - /// - /// The is not designed to be added multiple - /// times to an application, hence its name is unique. - /// - public const string OnlyAuthenticationScheme = "WebFrontAuth"; - - /// - /// Gets the entry point: "/.webfront". - /// - public PathString EntryPath => _entryPath; - - /// - /// Controls how much time the authentication will remain valid - /// from the point it is created. - /// Defaults to 20 minutes. - /// This time is extended if is set and - /// when "/c/refresh" is called. - /// This configuration can be changed dynamically: modifying the configuration will take the - /// new value into account. - /// - public TimeSpan ExpireTimeSpan { get; set; } = TimeSpan.FromMinutes( 20 ); - - /// - /// Controls how much time the long term, unsafe, authentication information - /// will remain valid from the point it is created. - /// Defaults to one year. - /// This configuration can be changed dynamically. - /// - public TimeSpan? UnsafeExpireTimeSpan { get; set; } = TimeSpan.FromDays( 366 ); - - /// - /// Gets whether is not null, greater than , - /// and is not . - /// - /// When true a long-lived cookie is used to store the unsafe, but long term, authentication information. - /// Its depends on . - /// Since the expiration is a dynamic configuration, this is also a dynamic configuration. - /// - /// - public bool UseLongTermCookie => UnsafeExpireTimeSpan.HasValue - && UnsafeExpireTimeSpan > ExpireTimeSpan - && CookieMode != AuthenticationCookieMode.None; - - /// - /// Gets or sets whether a complex claim must be set as the - /// when (and the - /// "WebFrontAuth" is the default scheme) or - /// with the "WebFrontAuth" scheme is called. - /// - /// Defaults to false: the ClaimsPrincipal contains only the safe user claims and ignores - /// any impersonation. - /// - /// - /// When true, the ClaimsPrincipal contains more complex claims: unsafe user claims, a claim for the - /// authentication level, the expirations if they exist and impersonation is handled thanks to the - /// . - /// - /// - /// This cannot be changed dynamically. - /// - /// - public bool UseFullClaimsPrincipalOnAuthenticate { get; set; } - - /// - /// Gets whether the authentication cookie (see ) requires or not https. - /// Note that the long term cookie uses sets to false since it - /// does not require any protection. - /// Defaults to . - /// This cannot be changed dynamically. - /// - public CookieSecurePolicy CookieSecurePolicy { get; set; } - - /// - /// Gets or sets if and how cookies are managed to store the authentication information. - /// - /// Defaults to . - /// - /// - /// Setting it to should NOT BE used in - /// most cases: this mode, that is the same as the standard Cookie ASP.Net authentication, - /// is for standard and classical Web application. - /// - /// - /// Setting it to disables all cookies: client apps - /// are no more "F5 resilient", this can be used for pure API implementations. - /// - /// - /// This cannot be changed dynamically. - /// - /// - public AuthenticationCookieMode CookieMode { get; set; } - - /// - /// Gets or sets a list of available schemes returned for information from '/c/refresh' endpoint - /// when 'schemes' appears in the query string. - /// - /// Defaults to null: schemes are the same as - /// when this is null or empty. - /// - /// - /// When not null (or empty), this list takes precedence over the login service's providers: all supported - /// schemes must be declared here (and unwanted ones must not appear). - /// - /// - /// This list does not forbid user login to non listed schemes, this is intended only for applications - /// to communicate with the user. - /// - /// - /// This configuration can be changed dynamically: modifying the configuration will take the - /// new schemes into account immediately. - /// - /// - public List? AvailableSchemes { get; set; } - - /// - /// Gets or sets the refresh validation time. - /// When set to other than the middleware will re-issue a new token - /// (and new authentication cookie if allows it) with a new expiration time any time it - /// processes a ".webfront/c/refresh" request. - /// - /// This applies to but not to . - /// This configuration can be changed dynamically: modifying the configuration will take the - /// new value into account. - /// - /// - public TimeSpan SlidingExpirationTime { get; set; } - - /// - /// Gets or sets the http header name. Defaults to "Authorization". - /// This cannot be changed dynamically. - /// - public string BearerHeaderName { get; set; } = "Authorization"; - - /// - /// Defines the initial critical time span when logged in through each schemes. - /// It is null by default: no schemes elevate a critical authentication level. - /// - public IDictionary? SchemesCriticalTimeSpan { get; set; } - - /// - /// Gets or sets the initial AuthCookieName. Defaults to ".webFront". - /// The long term cookie name equals to AuthCookieName suffixed by "LT". - /// This cannot be changed dynamically. - /// - public string AuthCookieName { get; set; } = ".webFront"; - - /// - /// Gets or sets whether - /// must be called each time "/.webfront/c/refresh" is called. - /// - /// Default to false: the login service method is called only if a callBackend parameter appear in the query string: "/.webfront/c/refresh?callBackend". - /// - /// - /// This configuration can be changed dynamically. - /// - /// - public bool AlwaysCallBackendOnRefresh { get; set; } - - /// - /// Gets a mutable list of accepted returnUrl prefixes. - /// - /// The returnUrl optional parameter submitted to the '/c/startLogin' end point (case of an "inline login" based - /// on page redirections rather that the recommended popup window) must exactly start with one of this - /// prefix ( is used) otherwise a 400 Bad Request error code is returned. - /// - /// - /// This cannot be changed dynamically. - /// - /// - public List AllowedReturnUrls { get; } = new List(); - } + public List AllowedReturnUrls { get; } = new List(); } diff --git a/CK.AspNet.Auth/WebFrontAuthOptionsInstaller.cs b/CK.AspNet.Auth/WebFrontAuthOptionsInstaller.cs index 0ab16487..2ccaf4c6 100644 --- a/CK.AspNet.Auth/WebFrontAuthOptionsInstaller.cs +++ b/CK.AspNet.Auth/WebFrontAuthOptionsInstaller.cs @@ -8,20 +8,18 @@ using System.Linq; using System.Text; -namespace CK.DB.AspNet.Auth +namespace CK.DB.AspNet.Auth; + +/// +/// This automatically registers the configuration section named "CK-WebFrontAuth" +/// to be mapped to the . +/// +public class WebFrontAuthOptionsInstaller : IRealObject { - /// - /// This automatically registers the configuration section named "CK-WebFrontAuth" - /// to be mapped to the . - /// - public class WebFrontAuthOptionsInstaller : IRealObject + void ConfigureServices( StObjContextRoot.ServiceRegister reg ) { - void ConfigureServices( StObjContextRoot.ServiceRegister reg ) - { - reg.Services.AddOptions() - .Configure( ( opts, config ) => config.GetSection( "CK-WebFrontAuth" ).Bind( opts ) ); - reg.Services.AddSingleton, ConfigurationChangeTokenSource>(); - } + reg.Services.AddOptions() + .Configure( ( opts, config ) => config.GetSection( "CK-WebFrontAuth" ).Bind( opts ) ); + reg.Services.AddSingleton, ConfigurationChangeTokenSource>(); } - } diff --git a/CK.AspNet.Auth/WebFrontAuthService.cs b/CK.AspNet.Auth/WebFrontAuthService.cs index 05e15560..99bfb7ba 100644 --- a/CK.AspNet.Auth/WebFrontAuthService.cs +++ b/CK.AspNet.Auth/WebFrontAuthService.cs @@ -18,1183 +18,1182 @@ #nullable enable -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + +/// +/// Sealed implementation of the actual authentication service. +/// This implementation is registered as a singleton by , +/// it is not a and is a endpoint service (available only from the Global DI). +/// +[ContainerConfiguredSingletonService] +public sealed class WebFrontAuthService { /// - /// Sealed implementation of the actual authentication service. - /// This implementation is registered as a singleton by , - /// it is not a and is a endpoint service (available only from the Global DI). + /// The tag used for logs emitted related to Web Front Authentication or any + /// authentication related actions. + /// + public static readonly CKTrait WebFrontAuthMonitorTag = ActivityMonitor.Tags.Register( "WebFrontAuth" ); + + /// + /// Name of the authentication cookie. + /// + public string AuthCookieName { get; } + + /// + /// Name of the long term authentication cookie. /// - [ContainerConfiguredSingletonService] - public sealed class WebFrontAuthService + public string UnsafeCookieName => AuthCookieName + "LT"; + + internal readonly AuthenticationInfoTokenService _tokenService; + readonly IAuthenticationTypeSystem _typeSystem; + internal readonly IReadOnlyList _allowedReturnUrls; + readonly IWebFrontAuthLoginService _loginService; + + readonly FrontAuthenticationInfoSecureDataFormat _cookieFormat; + readonly ExtraDataSecureDataFormat _extraDataFormat; + readonly string _cookiePath; + readonly string _bearerHeaderName; + readonly CookieSecurePolicy _cookiePolicy; + /// + /// Don't use this directly! Use CurrentOptions property. + /// + readonly IOptionsMonitor _options; + readonly IWebFrontAuthValidateLoginService? _validateLoginService; + readonly IWebFrontAuthAutoCreateAccountService? _autoCreateAccountService; + readonly IWebFrontAuthAutoBindingAccountService? _autoBindingAccountService; + readonly IWebFrontAuthImpersonationService? _impersonationService; + readonly IWebFrontAuthDynamicScopeProvider? _dynamicScopeProvider; + + /// + /// Initializes a new . + /// + /// The token service (bound to the ). + /// Login service. + /// Monitored options. + /// Optional service that validates logins. + /// Optional service that enables account creation. + /// Optional service that enables account binding. + /// Optional service to support scope augmentation. + public WebFrontAuthService( AuthenticationInfoTokenService tokenService, + IAuthenticationTypeSystem typeSystem, + IWebFrontAuthLoginService loginService, + IOptionsMonitor options, + IWebFrontAuthValidateLoginService? validateLoginService = null, + IWebFrontAuthAutoCreateAccountService? autoCreateAccountService = null, + IWebFrontAuthAutoBindingAccountService? autoBindingAccountService = null, + IWebFrontAuthImpersonationService? impersonationService = null, + IWebFrontAuthDynamicScopeProvider? dynamicScopeProvider = null ) { - /// - /// The tag used for logs emitted related to Web Front Authentication or any - /// authentication related actions. - /// - public static readonly CKTrait WebFrontAuthMonitorTag = ActivityMonitor.Tags.Register( "WebFrontAuth" ); + _tokenService = tokenService; + _typeSystem = typeSystem; + _loginService = loginService; + _options = options; + _validateLoginService = validateLoginService; + _autoCreateAccountService = autoCreateAccountService; + _autoBindingAccountService = autoBindingAccountService; + _impersonationService = impersonationService; + _dynamicScopeProvider = dynamicScopeProvider; + WebFrontAuthOptions initialOptions = CurrentOptions; + var cookieFormat = new FrontAuthenticationInfoSecureDataFormat( tokenService.TypeSystem, tokenService.BaseDataProtector.CreateProtector( "Cookie", "v1" ) ); + var extraDataFormat = new ExtraDataSecureDataFormat( tokenService.BaseDataProtector.CreateProtector( "Extra", "v1" ) ); + _cookieFormat = cookieFormat; + _extraDataFormat = extraDataFormat; + Debug.Assert( WebFrontAuthHandler._cSegmentPath.ToString() == "/c" ); + _cookiePath = initialOptions.EntryPath + "/c/"; + _bearerHeaderName = initialOptions.BearerHeaderName; + CookieMode = initialOptions.CookieMode; + _cookiePolicy = initialOptions.CookieSecurePolicy; + AuthCookieName = initialOptions.AuthCookieName; + _allowedReturnUrls = initialOptions.AllowedReturnUrls.ToArray(); + } - /// - /// Name of the authentication cookie. - /// - public string AuthCookieName { get; } + /// + /// Gets the cookie mode. This is not a dynamic option: this is the value + /// captured when this service has been instantiated. + /// + public AuthenticationCookieMode CookieMode { get; } + + /// + /// Result for the . + /// + public sealed class BasicLoginCommandResult + { + internal BasicLoginCommandResult( IAuthenticationInfo? info, string? token, string? errorId, string? errorText ) + { + Throw.DebugAssert( (info == null) == (token == null) ); + Throw.DebugAssert( (info == null) != (errorId == null) ); + Throw.DebugAssert( "errorText => errorId", errorText == null || errorId != null ); + Info = info; + Token = token; + ErrorId = errorId; + if( errorId != null && errorText == null ) errorText = string.Empty; + ErrorText = errorText; + } /// - /// Name of the long term authentication cookie. + /// Gets whether the login succeeded. /// - public string UnsafeCookieName => AuthCookieName + "LT"; - - internal readonly AuthenticationInfoTokenService _tokenService; - readonly IAuthenticationTypeSystem _typeSystem; - internal readonly IReadOnlyList _allowedReturnUrls; - readonly IWebFrontAuthLoginService _loginService; - - readonly FrontAuthenticationInfoSecureDataFormat _cookieFormat; - readonly ExtraDataSecureDataFormat _extraDataFormat; - readonly string _cookiePath; - readonly string _bearerHeaderName; - readonly CookieSecurePolicy _cookiePolicy; + [MemberNotNullWhen( true, nameof( Info ), nameof( Token ) )] + [MemberNotNullWhen( false, nameof( ErrorId ), nameof( ErrorText ) )] + public bool Success => Info != null; + /// - /// Don't use this directly! Use CurrentOptions property. + /// Gets the non null authentication info on success. /// - readonly IOptionsMonitor _options; - readonly IWebFrontAuthValidateLoginService? _validateLoginService; - readonly IWebFrontAuthAutoCreateAccountService? _autoCreateAccountService; - readonly IWebFrontAuthAutoBindingAccountService? _autoBindingAccountService; - readonly IWebFrontAuthImpersonationService? _impersonationService; - readonly IWebFrontAuthDynamicScopeProvider? _dynamicScopeProvider; + public IAuthenticationInfo? Info { get; } /// - /// Initializes a new . + /// Gets the non null token on success. /// - /// The token service (bound to the ). - /// Login service. - /// Monitored options. - /// Optional service that validates logins. - /// Optional service that enables account creation. - /// Optional service that enables account binding. - /// Optional service to support scope augmentation. - public WebFrontAuthService( AuthenticationInfoTokenService tokenService, - IAuthenticationTypeSystem typeSystem, - IWebFrontAuthLoginService loginService, - IOptionsMonitor options, - IWebFrontAuthValidateLoginService? validateLoginService = null, - IWebFrontAuthAutoCreateAccountService? autoCreateAccountService = null, - IWebFrontAuthAutoBindingAccountService? autoBindingAccountService = null, - IWebFrontAuthImpersonationService? impersonationService = null, - IWebFrontAuthDynamicScopeProvider? dynamicScopeProvider = null ) - { - _tokenService = tokenService; - _typeSystem = typeSystem; - _loginService = loginService; - _options = options; - _validateLoginService = validateLoginService; - _autoCreateAccountService = autoCreateAccountService; - _autoBindingAccountService = autoBindingAccountService; - _impersonationService = impersonationService; - _dynamicScopeProvider = dynamicScopeProvider; - WebFrontAuthOptions initialOptions = CurrentOptions; - var cookieFormat = new FrontAuthenticationInfoSecureDataFormat( tokenService.TypeSystem, tokenService.BaseDataProtector.CreateProtector( "Cookie", "v1" ) ); - var extraDataFormat = new ExtraDataSecureDataFormat( tokenService.BaseDataProtector.CreateProtector( "Extra", "v1" ) ); - _cookieFormat = cookieFormat; - _extraDataFormat = extraDataFormat; - Debug.Assert( WebFrontAuthHandler._cSegmentPath.ToString() == "/c" ); - _cookiePath = initialOptions.EntryPath + "/c/"; - _bearerHeaderName = initialOptions.BearerHeaderName; - CookieMode = initialOptions.CookieMode; - _cookiePolicy = initialOptions.CookieSecurePolicy; - AuthCookieName = initialOptions.AuthCookieName; - _allowedReturnUrls = initialOptions.AllowedReturnUrls.ToArray(); - } + public string? Token { get; } /// - /// Gets the cookie mode. This is not a dynamic option: this is the value - /// captured when this service has been instantiated. + /// Gets a non null error identifier if is false. /// - public AuthenticationCookieMode CookieMode { get; } + public string? ErrorId { get; } /// - /// Result for the . + /// Gets a non null error text (possibly empty) if is false. /// - public sealed class BasicLoginCommandResult - { - internal BasicLoginCommandResult( IAuthenticationInfo? info, string? token, string? errorId, string? errorText ) - { - Throw.DebugAssert( (info == null) == (token == null) ); - Throw.DebugAssert( (info == null) != (errorId == null) ); - Throw.DebugAssert( "errorText => errorId", errorText == null || errorId != null ); - Info = info; - Token = token; - ErrorId = errorId; - if( errorId != null && errorText == null ) errorText = string.Empty; - ErrorText = errorText; - } + public string? ErrorText { get; } + } - /// - /// Gets whether the login succeeded. - /// - [MemberNotNullWhen( true, nameof( Info ), nameof( Token ) )] - [MemberNotNullWhen( false, nameof( ErrorId ), nameof( ErrorText ) )] - public bool Success => Info != null; - - /// - /// Gets the non null authentication info on success. - /// - public IAuthenticationInfo? Info { get; } - - /// - /// Gets the non null token on success. - /// - public string? Token { get; } - - /// - /// Gets a non null error identifier if is false. - /// - public string? ErrorId { get; } - - /// - /// Gets a non null error text (possibly empty) if is false. - /// - public string? ErrorText { get; } - } + /// + /// Attempts to login a user by its name/password. + /// must be true otherwise an is thrown. + /// + /// The monitor to use. + /// The current http context. + /// The user name. + /// The password. + /// Wether the + /// + /// + /// The login result. + public async Task BasicLoginCommandAsync( IActivityMonitor monitor, + HttpContext httpContext, + string userName, + string password, + TimeSpan? duration = null, + TimeSpan? criticalDuration = null, + bool impersonateActualUser = false ) + { + Throw.CheckState( _loginService.HasBasicLogin ); + Throw.CheckNotNullArgument( monitor ); + Throw.CheckNotNullArgument( httpContext ); + Throw.CheckNotNullArgument( userName ); + Throw.CheckNotNullArgument( password ); - /// - /// Attempts to login a user by its name/password. - /// must be true otherwise an is thrown. - /// - /// The monitor to use. - /// The current http context. - /// The user name. - /// The password. - /// Wether the - /// - /// - /// The login result. - public async Task BasicLoginCommandAsync( IActivityMonitor monitor, - HttpContext httpContext, - string userName, - string password, - TimeSpan? duration = null, - TimeSpan? criticalDuration = null, - bool impersonateActualUser = false ) + var current = EnsureAuthenticationInfo( httpContext, ref monitor ); + var ctx = new WebFrontAuthLoginContext( httpContext, + this, + _typeSystem, + WebFrontAuthLoginMode.BasicLogin, + "Basic", + Tuple.Create( userName, password ), + authProps: null, + initialScheme: "Basic", + current, + impersonateActualUser, + returnUrl: null, + callerOrigin: null, + new Dictionary() ); + + if( CheckLoginWhileImpersonation( monitor, current.Info, impersonateActualUser, ctx ) ) { - Throw.CheckState( _loginService.HasBasicLogin ); - Throw.CheckNotNullArgument( monitor ); - Throw.CheckNotNullArgument( httpContext ); - Throw.CheckNotNullArgument( userName ); - Throw.CheckNotNullArgument( password ); - - var current = EnsureAuthenticationInfo( httpContext, ref monitor ); - var ctx = new WebFrontAuthLoginContext( httpContext, - this, - _typeSystem, - WebFrontAuthLoginMode.BasicLogin, - "Basic", - Tuple.Create( userName, password ), - authProps: null, - initialScheme: "Basic", - current, - impersonateActualUser, - returnUrl: null, - callerOrigin: null, - new Dictionary() ); - - if( CheckLoginWhileImpersonation( monitor, current.Info, impersonateActualUser, ctx ) ) - { - await DoUnifiedLoginAsync( monitor, ctx, actualLogin => - { - return _loginService.BasicLoginAsync( httpContext, monitor, userName, password, actualLogin ); - } ); - } - if( ctx.HasError ) + await DoUnifiedLoginAsync( monitor, ctx, actualLogin => { - return new BasicLoginCommandResult( null, null, ctx._errorId, ctx._errorText ); - } - Throw.DebugAssert( ctx._successfulLogin != null ); - var fAuth = await HandleLoginCoreAsync( httpContext, - monitor, - ctx._successfulLogin, - "Basic", - initial: current.Info, - false, - impersonateActualUser, - duration, - criticalDuration ); - var token = _tokenService.UnsafeCreateAuthenticationToken( fAuth.Info ); - return new BasicLoginCommandResult( fAuth.Info, token, null, null ); + return _loginService.BasicLoginAsync( httpContext, monitor, userName, password, actualLogin ); + } ); } - - /// - /// Log out by clearing the cookies. - /// - /// The current Http context. - /// The awaitable. - public Task LogoutCommandAsync( IActivityMonitor monitor, HttpContext ctx ) + if( ctx.HasError ) { - FrontAuthenticationInfo fAuth = EnsureAuthenticationInfo( ctx, ref monitor ); - if( fAuth.Info.Level != AuthLevel.None ) - { - monitor.Info( WebFrontAuthMonitorTag, $"User '{fAuth.Info.ActualUser.UserName} ({fAuth.Info.ActualUser.UserId})' logged out." ); - } - ClearCookie( ctx, AuthCookieName ); - ClearCookie( ctx, UnsafeCookieName ); - return Task.CompletedTask; + return new BasicLoginCommandResult( null, null, ctx._errorId, ctx._errorText ); } + Throw.DebugAssert( ctx._successfulLogin != null ); + var fAuth = await HandleLoginCoreAsync( httpContext, + monitor, + ctx._successfulLogin, + "Basic", + initial: current.Info, + false, + impersonateActualUser, + duration, + criticalDuration ); + var token = _tokenService.UnsafeCreateAuthenticationToken( fAuth.Info ); + return new BasicLoginCommandResult( fAuth.Info, token, null, null ); + } - /// - /// Refreshes the current authentication - /// - /// The monitor to use. - /// The current http context. - /// Whether the must be called. - /// The authentication info and the token. - public async Task<(IAuthenticationInfo Info, string Token)> RefreshCommandAsync( IActivityMonitor monitor, - HttpContext httpContext, - bool callBackend = false ) + /// + /// Log out by clearing the cookies. + /// + /// The current Http context. + /// The awaitable. + public Task LogoutCommandAsync( IActivityMonitor monitor, HttpContext ctx ) + { + FrontAuthenticationInfo fAuth = EnsureAuthenticationInfo( ctx, ref monitor ); + if( fAuth.Info.Level != AuthLevel.None ) { - (var fAuth, _) = await RefreshInfoAsync( httpContext, monitor, callBackend ); - ApplySlidingExpirationAndSetCookies( httpContext, ref fAuth ); - return (fAuth.Info, _tokenService.UnsafeCreateAuthenticationToken( fAuth.Info )); + monitor.Info( WebFrontAuthMonitorTag, $"User '{fAuth.Info.ActualUser.UserName} ({fAuth.Info.ActualUser.UserId})' logged out." ); } + ClearCookie( ctx, AuthCookieName ); + ClearCookie( ctx, UnsafeCookieName ); + return Task.CompletedTask; + } - internal async Task<(FrontAuthenticationInfo,IActivityMonitor?)> RefreshInfoAsync( HttpContext ctx, IActivityMonitor? monitor, bool callBackend ) + /// + /// Refreshes the current authentication + /// + /// The monitor to use. + /// The current http context. + /// Whether the must be called. + /// The authentication info and the token. + public async Task<(IAuthenticationInfo Info, string Token)> RefreshCommandAsync( IActivityMonitor monitor, + HttpContext httpContext, + bool callBackend = false ) + { + (var fAuth, _) = await RefreshInfoAsync( httpContext, monitor, callBackend ); + ApplySlidingExpirationAndSetCookies( httpContext, ref fAuth ); + return (fAuth.Info, _tokenService.UnsafeCreateAuthenticationToken( fAuth.Info )); + } + + internal async Task<(FrontAuthenticationInfo, IActivityMonitor?)> RefreshInfoAsync( HttpContext ctx, IActivityMonitor? monitor, bool callBackend ) + { + FrontAuthenticationInfo fAuth = EnsureAuthenticationInfo( ctx, ref monitor ); + Debug.Assert( fAuth != null ); + if( CurrentOptions.AlwaysCallBackendOnRefresh || callBackend ) { - FrontAuthenticationInfo fAuth = EnsureAuthenticationInfo( ctx, ref monitor ); - Debug.Assert( fAuth != null ); - if( CurrentOptions.AlwaysCallBackendOnRefresh || callBackend ) - { - var newExpires = DateTime.UtcNow + CurrentOptions.ExpireTimeSpan; - monitor ??= GetRequestMonitor( ctx ); - fAuth = fAuth.SetInfo( await _loginService.RefreshAuthenticationInfoAsync( ctx, monitor, fAuth.Info, newExpires ) ); - } - return (fAuth,monitor); + var newExpires = DateTime.UtcNow + CurrentOptions.ExpireTimeSpan; + monitor ??= GetRequestMonitor( ctx ); + fAuth = fAuth.SetInfo( await _loginService.RefreshAuthenticationInfoAsync( ctx, monitor, fAuth.Info, newExpires ) ); } + return (fAuth, monitor); + } - internal bool ApplySlidingExpirationAndSetCookies( HttpContext httpContext, ref FrontAuthenticationInfo fAuth ) + internal bool ApplySlidingExpirationAndSetCookies( HttpContext httpContext, ref FrontAuthenticationInfo fAuth ) + { + var authInfo = fAuth.Info; + bool refreshable = false; + TimeSpan slidingExpirationTime = CurrentOptions.SlidingExpirationTime; + if( authInfo.Level >= AuthLevel.Normal && slidingExpirationTime > TimeSpan.Zero ) { - var authInfo = fAuth.Info; - bool refreshable = false; - TimeSpan slidingExpirationTime = CurrentOptions.SlidingExpirationTime; - if( authInfo.Level >= AuthLevel.Normal && slidingExpirationTime > TimeSpan.Zero ) + Debug.Assert( authInfo.Expires != null ); + refreshable = true; + DateTime newExp = DateTime.UtcNow + slidingExpirationTime; + if( newExp > authInfo.Expires.Value ) { - Debug.Assert( authInfo.Expires != null ); - refreshable = true; - DateTime newExp = DateTime.UtcNow + slidingExpirationTime; - if( newExp > authInfo.Expires.Value ) - { - fAuth = fAuth.SetInfo( authInfo.SetExpires( newExp ) ); - } + fAuth = fAuth.SetInfo( authInfo.SetExpires( newExp ) ); } - SetCookies( httpContext, fAuth ); - return refreshable; } + SetCookies( httpContext, fAuth ); + return refreshable; + } - /// - /// Direct generation of an authentication token from any . - /// is called with . - /// This is to be used with caution: the authentication token should never be sent to any client and should be - /// used only for secure server to server temporary authentication. - /// - /// The HttpContext. - /// The authentication info for which an authentication token must be obtained. - /// The url-safe secured authentication token string. - [Obsolete( "Please use the AuthenticationInfoTokenService.UnsafeCreateAuthenticationToken method instead.", true )] - public string UnsafeGetAuthenticationToken( HttpContext c, IAuthenticationInfo info ) + /// + /// Direct generation of an authentication token from any . + /// is called with . + /// This is to be used with caution: the authentication token should never be sent to any client and should be + /// used only for secure server to server temporary authentication. + /// + /// The HttpContext. + /// The authentication info for which an authentication token must be obtained. + /// The url-safe secured authentication token string. + [Obsolete( "Please use the AuthenticationInfoTokenService.UnsafeCreateAuthenticationToken method instead.", true )] + public string UnsafeGetAuthenticationToken( HttpContext c, IAuthenticationInfo info ) + { + if( c == null ) throw new ArgumentNullException( nameof( c ) ); + return _tokenService.UnsafeCreateAuthenticationToken( info ); + } + + /// + /// Simple helper that calls . + /// + /// The HttpContext. + /// The user identifier. + /// The user name. + /// The validity time span: the shorter the better. + /// The url-safe secured authentication token string. + [Obsolete( "Please use the AuthenticationInfoTokenService.UnsafeCreateAuthenticationToken method instead.", true )] + public string UnsafeGetAuthenticationToken( HttpContext c, int userId, string userName, TimeSpan validity ) + { + if( userName == null ) throw new ArgumentNullException( nameof( userName ) ); + var u = _tokenService.TypeSystem.UserInfo.Create( userId, userName ); + var info = _tokenService.TypeSystem.AuthenticationInfo.Create( u, DateTime.UtcNow.Add( validity ) ); + return UnsafeGetAuthenticationToken( c, info ); + } + + /// + /// Gets the current options. + /// This must be used for configurations that can be changed dynamically like + /// but not for non dynamic ones like . + /// + internal WebFrontAuthOptions CurrentOptions => _options.Get( WebFrontAuthOptions.OnlyAuthenticationScheme ); + + /// + /// Gets the monitor from the request service. + /// Must be called once and only once per request since a new ActivityMonitor is + /// created when hostBuilder.UseMonitoring() has not been used (the IActivityMonitor is not + /// available in the context). + /// + /// The http context. + /// An activity monitor. + IActivityMonitor GetRequestMonitor( HttpContext c ) + { + return c.RequestServices.GetService() ?? new ActivityMonitor( "WebFrontAuthService-Request" ); + } + + internal string ProtectExtraData( IDictionary info ) + { + Debug.Assert( info != null ); + return _extraDataFormat.Protect( info ); + } + + internal IDictionary UnprotectExtraData( string data ) + { + Debug.Assert( data != null ); + return _extraDataFormat.Unprotect( data )!; + } + + internal void SetWFAData( AuthenticationProperties p, + FrontAuthenticationInfo fAuth, + bool impersonateActualUser, + string? initialScheme, + string? callerOrigin, + string? returnUrl, + IDictionary userData ) + { + p.Items.Add( "WFA2C", _tokenService.ProtectFrontAuthenticationInfo( fAuth ) ); + if( !String.IsNullOrWhiteSpace( initialScheme ) ) { - if( c == null ) throw new ArgumentNullException( nameof( c ) ); - return _tokenService.UnsafeCreateAuthenticationToken( info ); + p.Items.Add( "WFA2S", initialScheme ); } - - /// - /// Simple helper that calls . - /// - /// The HttpContext. - /// The user identifier. - /// The user name. - /// The validity time span: the shorter the better. - /// The url-safe secured authentication token string. - [Obsolete( "Please use the AuthenticationInfoTokenService.UnsafeCreateAuthenticationToken method instead.", true )] - public string UnsafeGetAuthenticationToken( HttpContext c, int userId, string userName, TimeSpan validity ) + if( !String.IsNullOrWhiteSpace( callerOrigin ) ) { - if( userName == null ) throw new ArgumentNullException( nameof( userName ) ); - var u = _tokenService.TypeSystem.UserInfo.Create( userId, userName ); - var info = _tokenService.TypeSystem.AuthenticationInfo.Create( u, DateTime.UtcNow.Add( validity ) ); - return UnsafeGetAuthenticationToken( c, info ); + p.Items.Add( "WFA2O", callerOrigin ); } - - /// - /// Gets the current options. - /// This must be used for configurations that can be changed dynamically like - /// but not for non dynamic ones like . - /// - internal WebFrontAuthOptions CurrentOptions => _options.Get( WebFrontAuthOptions.OnlyAuthenticationScheme ); - - /// - /// Gets the monitor from the request service. - /// Must be called once and only once per request since a new ActivityMonitor is - /// created when hostBuilder.UseMonitoring() has not been used (the IActivityMonitor is not - /// available in the context). - /// - /// The http context. - /// An activity monitor. - IActivityMonitor GetRequestMonitor( HttpContext c ) + if( returnUrl != null ) { - return c.RequestServices.GetService() ?? new ActivityMonitor( "WebFrontAuthService-Request" ); + p.Items.Add( "WFA2R", returnUrl ); } - - internal string ProtectExtraData( IDictionary info ) + if( userData.Count != 0 ) { - Debug.Assert( info != null ); - return _extraDataFormat.Protect( info ); + p.Items.Add( "WFA2D", ProtectExtraData( userData ) ); } - - internal IDictionary UnprotectExtraData( string data ) + if( impersonateActualUser ) { - Debug.Assert( data != null ); - return _extraDataFormat.Unprotect( data )!; + p.Items.Add( "WFA2I", "" ); } + } - internal void SetWFAData( AuthenticationProperties p, - FrontAuthenticationInfo fAuth, - bool impersonateActualUser, - string? initialScheme, - string? callerOrigin, - string? returnUrl, - IDictionary userData ) + internal void GetWFAData( HttpContext h, + AuthenticationProperties? properties, + out FrontAuthenticationInfo fAuth, + out bool impersonateActualUser, + out string? initialScheme, + out string? callerOrigin, + out string? returnUrl, + out IDictionary userData ) + { + fAuth = GetFrontAuthenticationInfo( h, properties ); + userData = null!; + if( properties != null ) { - p.Items.Add( "WFA2C", _tokenService.ProtectFrontAuthenticationInfo( fAuth ) ); - if( !String.IsNullOrWhiteSpace( initialScheme ) ) - { - p.Items.Add( "WFA2S", initialScheme ); - } - if( !String.IsNullOrWhiteSpace( callerOrigin ) ) - { - p.Items.Add( "WFA2O", callerOrigin ); - } - if( returnUrl != null ) + properties.Items.TryGetValue( "WFA2S", out initialScheme ); + properties.Items.TryGetValue( "WFA2O", out callerOrigin ); + properties.Items.TryGetValue( "WFA2R", out returnUrl ); + if( properties.Items.TryGetValue( "WFA2D", out var sUData ) && sUData != null ) { - p.Items.Add( "WFA2R", returnUrl ); - } - if( userData.Count != 0 ) - { - p.Items.Add( "WFA2D", ProtectExtraData( userData ) ); - } - if( impersonateActualUser ) - { - p.Items.Add( "WFA2I", "" ); + userData = UnprotectExtraData( sUData ); } + impersonateActualUser = properties.Items.ContainsKey( "WFA2I" ); + } + else + { + initialScheme = callerOrigin = returnUrl = null; + impersonateActualUser = false; } + userData ??= new Dictionary(); + } - internal void GetWFAData( HttpContext h, - AuthenticationProperties? properties, - out FrontAuthenticationInfo fAuth, - out bool impersonateActualUser, - out string? initialScheme, - out string? callerOrigin, - out string? returnUrl, - out IDictionary userData ) + internal FrontAuthenticationInfo GetFrontAuthenticationInfo( HttpContext h, AuthenticationProperties? properties ) + { + if( !h.Items.TryGetValue( typeof( RemoteAuthenticationEventsContextExtensions ), out var fAuth ) ) { - fAuth = GetFrontAuthenticationInfo( h, properties ); - userData = null!; - if( properties != null ) + if( properties != null + && properties.Items.TryGetValue( "WFA2C", out var currentAuth ) + && currentAuth != null ) { - properties.Items.TryGetValue( "WFA2S", out initialScheme ); - properties.Items.TryGetValue( "WFA2O", out callerOrigin ); - properties.Items.TryGetValue( "WFA2R", out returnUrl ); - if( properties.Items.TryGetValue( "WFA2D", out var sUData ) && sUData != null ) - { - userData = UnprotectExtraData( sUData ); - } - impersonateActualUser = properties.Items.ContainsKey( "WFA2I" ); + fAuth = _tokenService.UnprotectFrontAuthenticationInfo( currentAuth ); } else { - initialScheme = callerOrigin = returnUrl = null; - impersonateActualUser = false; + // There should always be the WFA2C key in Authentication properties. + // However if it's not here, we return the AuthenticationType.None that has an empty DeviceId. + fAuth = _tokenService.TypeSystem.AuthenticationInfo.None; } - userData ??= new Dictionary(); + h.Items.Add( typeof( RemoteAuthenticationEventsContextExtensions ), fAuth ); } + Debug.Assert( fAuth != null ); + return (FrontAuthenticationInfo)fAuth; + } - internal FrontAuthenticationInfo GetFrontAuthenticationInfo( HttpContext h, AuthenticationProperties? properties ) + /// + /// Handles cached authentication header or calls ReadAndCacheAuthenticationHeader. + /// Never null, can be . + /// + /// The context. + /// The request monitor if it's available. Will be obtained if required. + /// + /// The cached or resolved authentication info. + /// + internal FrontAuthenticationInfo EnsureAuthenticationInfo( HttpContext c, [NotNullIfNotNull( "monitor" )] ref IActivityMonitor? monitor ) + { + FrontAuthenticationInfo? authInfo; + if( c.Items.TryGetValue( typeof( FrontAuthenticationInfo ), out object? o ) ) { - if( !h.Items.TryGetValue( typeof( RemoteAuthenticationEventsContextExtensions ), out var fAuth ) ) - { - if( properties != null - && properties.Items.TryGetValue( "WFA2C", out var currentAuth ) - && currentAuth != null ) - { - fAuth = _tokenService.UnprotectFrontAuthenticationInfo( currentAuth ); - } - else - { - // There should always be the WFA2C key in Authentication properties. - // However if it's not here, we return the AuthenticationType.None that has an empty DeviceId. - fAuth = _tokenService.TypeSystem.AuthenticationInfo.None; - } - h.Items.Add( typeof( RemoteAuthenticationEventsContextExtensions ), fAuth ); - } - Debug.Assert( fAuth != null ); - return (FrontAuthenticationInfo)fAuth; + authInfo = (FrontAuthenticationInfo)o!; } + else + { + authInfo = ReadAndCacheAuthenticationHeader( c, ref monitor ); + } + return authInfo; + } - /// - /// Handles cached authentication header or calls ReadAndCacheAuthenticationHeader. - /// Never null, can be . - /// - /// The context. - /// The request monitor if it's available. Will be obtained if required. - /// - /// The cached or resolved authentication info. - /// - internal FrontAuthenticationInfo EnsureAuthenticationInfo( HttpContext c, [NotNullIfNotNull("monitor")]ref IActivityMonitor? monitor ) + /// + /// Reads authentication header if possible or uses authentication Cookie (and ultimately falls back to + /// long term cookie) and caches authentication in request items. + /// + /// The context. + /// The request monitor if it's available. Will be obtained if required. + /// + /// The front authentication info. + /// + internal FrontAuthenticationInfo ReadAndCacheAuthenticationHeader( HttpContext c, ref IActivityMonitor? monitor ) + { + Debug.Assert( !c.Items.ContainsKey( typeof( FrontAuthenticationInfo ) ) ); + bool shouldSetCookies = false; + FrontAuthenticationInfo? fAuth = null; + // First try from the bearer: this is always the preferred way. + string authorization = c.Request.Headers[_bearerHeaderName]; + bool fromBearer = !string.IsNullOrEmpty( authorization ) + && authorization.StartsWith( "Bearer ", StringComparison.OrdinalIgnoreCase ); + if( fromBearer ) { - FrontAuthenticationInfo? authInfo; - if( c.Items.TryGetValue( typeof( FrontAuthenticationInfo ), out object? o ) ) + try { - authInfo = (FrontAuthenticationInfo)o!; + Debug.Assert( "Bearer ".Length == 7 ); + string token = authorization.Substring( 7 ).Trim(); + fAuth = _tokenService.UnprotectFrontAuthenticationInfo( token ); } - else + catch( Exception ex ) { - authInfo = ReadAndCacheAuthenticationHeader( c, ref monitor ); + monitor ??= GetRequestMonitor( c ); + monitor.Error( "While reading bearer.", ex ); } - return authInfo; } - - /// - /// Reads authentication header if possible or uses authentication Cookie (and ultimately falls back to - /// long term cookie) and caches authentication in request items. - /// - /// The context. - /// The request monitor if it's available. Will be obtained if required. - /// - /// The front authentication info. - /// - internal FrontAuthenticationInfo ReadAndCacheAuthenticationHeader( HttpContext c, ref IActivityMonitor? monitor ) + if( fAuth == null ) { - Debug.Assert( !c.Items.ContainsKey( typeof( FrontAuthenticationInfo ) ) ); - bool shouldSetCookies = false; - FrontAuthenticationInfo? fAuth = null; - // First try from the bearer: this is always the preferred way. - string authorization = c.Request.Headers[_bearerHeaderName]; - bool fromBearer = !string.IsNullOrEmpty( authorization ) - && authorization.StartsWith( "Bearer ", StringComparison.OrdinalIgnoreCase ); - if( fromBearer ) + // Best case is when we have the authentication cookie, otherwise use the long term cookie. + if( CookieMode != AuthenticationCookieMode.None && c.Request.Cookies.TryGetValue( AuthCookieName, out string? cookie ) ) { try { - Debug.Assert( "Bearer ".Length == 7 ); - string token = authorization.Substring( 7 ).Trim(); - fAuth = _tokenService.UnprotectFrontAuthenticationInfo( token ); + fAuth = _cookieFormat.Unprotect( cookie ); } catch( Exception ex ) { monitor ??= GetRequestMonitor( c ); - monitor.Error( "While reading bearer.", ex ); + monitor.Error( "While reading Cookie.", ex ); } } if( fAuth == null ) { - // Best case is when we have the authentication cookie, otherwise use the long term cookie. - if( CookieMode != AuthenticationCookieMode.None && c.Request.Cookies.TryGetValue( AuthCookieName, out string? cookie ) ) + if( CurrentOptions.UseLongTermCookie + && c.Request.Cookies.TryGetValue( UnsafeCookieName, out cookie ) + && cookie != null ) { try { - fAuth = _cookieFormat.Unprotect( cookie ); + var o = JObject.Parse( cookie ); + // The long term cookie contains a deviceId field. + string? deviceId = (string?)o[StdAuthenticationTypeSystem.DeviceIdKeyType]; + // We may have a "deviceId only" cookie. + IUserInfo? info = null; + if( o.ContainsKey( StdAuthenticationTypeSystem.UserIdKeyType ) ) + { + info = _tokenService.TypeSystem.UserInfo.FromJObject( o ); + } + var auth = _tokenService.TypeSystem.AuthenticationInfo.Create( info, deviceId: deviceId ); + Debug.Assert( auth.Level < AuthLevel.Normal, "No expiration is an Unsafe authentication." ); + // If there is a long term cookie with the user information, then we are "remembering"! + // (Checking UserId != 0 here is just to be safe since the anonymous must not "remember"). + fAuth = new FrontAuthenticationInfo( auth, rememberMe: info != null && info.UserId != 0 ); } catch( Exception ex ) { monitor ??= GetRequestMonitor( c ); - monitor.Error( "While reading Cookie.", ex ); + monitor.Error( "While reading Long Term Cookie.", ex ); } } if( fAuth == null ) { - if( CurrentOptions.UseLongTermCookie - && c.Request.Cookies.TryGetValue( UnsafeCookieName, out cookie ) - && cookie != null ) + // We have nothing or only errors: + // - If we could have something (either because CookieMode is AuthenticationCookieMode.RootPath or the request + // is inside the /.webfront/c), then we create a new unauthenticated info with a new device identifier. + // - If we are outside of the cookie context, we do nothing (otherwise we'll reset the current authentication). + if( CookieMode == AuthenticationCookieMode.RootPath + || (CookieMode == AuthenticationCookieMode.WebFrontPath + && (c.Request.Path.Value?.StartsWith( _cookiePath, StringComparison.OrdinalIgnoreCase ) ?? false)) ) { - try - { - var o = JObject.Parse( cookie ); - // The long term cookie contains a deviceId field. - string? deviceId = (string?)o[StdAuthenticationTypeSystem.DeviceIdKeyType]; - // We may have a "deviceId only" cookie. - IUserInfo? info = null; - if( o.ContainsKey( StdAuthenticationTypeSystem.UserIdKeyType ) ) - { - info = _tokenService.TypeSystem.UserInfo.FromJObject( o ); - } - var auth = _tokenService.TypeSystem.AuthenticationInfo.Create( info, deviceId: deviceId ); - Debug.Assert( auth.Level < AuthLevel.Normal, "No expiration is an Unsafe authentication." ); - // If there is a long term cookie with the user information, then we are "remembering"! - // (Checking UserId != 0 here is just to be safe since the anonymous must not "remember"). - fAuth = new FrontAuthenticationInfo( auth, rememberMe: info != null && info.UserId != 0 ); - } - catch( Exception ex ) - { - monitor ??= GetRequestMonitor( c ); - monitor.Error( "While reading Long Term Cookie.", ex ); - } + var deviceId = CreateNewDeviceId(); + var auth = _tokenService.TypeSystem.AuthenticationInfo.Create( null, deviceId: deviceId ); + fAuth = new FrontAuthenticationInfo( auth, rememberMe: false ); + Debug.Assert( auth.Level < AuthLevel.Normal, "No expiration is an Unsafe authentication." ); + // We set the long lived cookie if possible. The device identifier will be de facto persisted. + shouldSetCookies = true; } - if( fAuth == null ) + else { - // We have nothing or only errors: - // - If we could have something (either because CookieMode is AuthenticationCookieMode.RootPath or the request - // is inside the /.webfront/c), then we create a new unauthenticated info with a new device identifier. - // - If we are outside of the cookie context, we do nothing (otherwise we'll reset the current authentication). - if( CookieMode == AuthenticationCookieMode.RootPath - || (CookieMode == AuthenticationCookieMode.WebFrontPath - && (c.Request.Path.Value?.StartsWith( _cookiePath, StringComparison.OrdinalIgnoreCase ) ?? false)) ) - { - var deviceId = CreateNewDeviceId(); - var auth = _tokenService.TypeSystem.AuthenticationInfo.Create( null, deviceId: deviceId ); - fAuth = new FrontAuthenticationInfo( auth, rememberMe: false ); - Debug.Assert( auth.Level < AuthLevel.Normal, "No expiration is an Unsafe authentication." ); - // We set the long lived cookie if possible. The device identifier will be de facto persisted. - shouldSetCookies = true; - } - else - { - fAuth = new FrontAuthenticationInfo( _tokenService.TypeSystem.AuthenticationInfo.None, rememberMe: false ); - } + fAuth = new FrontAuthenticationInfo( _tokenService.TypeSystem.AuthenticationInfo.None, rememberMe: false ); } } } - // Upon each (non anonymous) authentication, when rooted Cookies are used and the SlidingExpiration is on, handles it. - if( fAuth.Info.Level >= AuthLevel.Normal && CookieMode == AuthenticationCookieMode.RootPath ) + } + // Upon each (non anonymous) authentication, when rooted Cookies are used and the SlidingExpiration is on, handles it. + if( fAuth.Info.Level >= AuthLevel.Normal && CookieMode == AuthenticationCookieMode.RootPath ) + { + var info = fAuth.Info; + TimeSpan slidingExpirationTime = CurrentOptions.SlidingExpirationTime; + TimeSpan halfSlidingExpirationTime = new TimeSpan( slidingExpirationTime.Ticks / 2 ); + if( info.Level >= AuthLevel.Normal + && CookieMode == AuthenticationCookieMode.RootPath + && halfSlidingExpirationTime > TimeSpan.Zero ) { - var info = fAuth.Info; - TimeSpan slidingExpirationTime = CurrentOptions.SlidingExpirationTime; - TimeSpan halfSlidingExpirationTime = new TimeSpan( slidingExpirationTime.Ticks / 2 ); - if( info.Level >= AuthLevel.Normal - && CookieMode == AuthenticationCookieMode.RootPath - && halfSlidingExpirationTime > TimeSpan.Zero ) + Debug.Assert( info.Expires.HasValue, "Since info.Level >= AuthLevel.Normal." ); + if( info.Expires.Value <= DateTime.UtcNow + halfSlidingExpirationTime ) { - Debug.Assert( info.Expires.HasValue, "Since info.Level >= AuthLevel.Normal." ); - if( info.Expires.Value <= DateTime.UtcNow + halfSlidingExpirationTime ) - { - fAuth = fAuth.SetInfo( info.SetExpires( DateTime.UtcNow + slidingExpirationTime ) ); - shouldSetCookies = true; - } + fAuth = fAuth.SetInfo( info.SetExpires( DateTime.UtcNow + slidingExpirationTime ) ); + shouldSetCookies = true; } } - if( shouldSetCookies ) SetCookies( c, fAuth ); - c.Items.Add( typeof( FrontAuthenticationInfo ), fAuth ); - return fAuth; } + if( shouldSetCookies ) SetCookies( c, fAuth ); + c.Items.Add( typeof( FrontAuthenticationInfo ), fAuth ); + return fAuth; + } - #region Cookie management + #region Cookie management - internal void SetCookies( HttpContext ctx, FrontAuthenticationInfo fAuth ) + internal void SetCookies( HttpContext ctx, FrontAuthenticationInfo fAuth ) + { + JObject? longTermCookie = CurrentOptions.UseLongTermCookie ? CreateLongTermCookiePayload( fAuth ) : null; + if( longTermCookie != null ) { - JObject? longTermCookie = CurrentOptions.UseLongTermCookie ? CreateLongTermCookiePayload( fAuth ) : null; - if( longTermCookie != null ) - { - string value = longTermCookie.ToString( Formatting.None ); - ctx.Response.Cookies.Append( UnsafeCookieName, value, CreateUnsafeCookieOptions( DateTime.UtcNow + CurrentOptions.UnsafeExpireTimeSpan ) ); - } - else ClearCookie( ctx, UnsafeCookieName ); - - if( CookieMode != AuthenticationCookieMode.None && fAuth.Info.Level >= AuthLevel.Normal ) - { - Debug.Assert( fAuth.Info.Expires.HasValue ); - string value = _cookieFormat.Protect( fAuth ); - // If we don't remember, we create a session cookie (no expiration). - ctx.Response.Cookies.Append( AuthCookieName, value, CreateAuthCookieOptions( ctx, fAuth.RememberMe ? fAuth.Info.Expires : null ) ); - } - else ClearCookie( ctx, AuthCookieName ); + string value = longTermCookie.ToString( Formatting.None ); + ctx.Response.Cookies.Append( UnsafeCookieName, value, CreateUnsafeCookieOptions( DateTime.UtcNow + CurrentOptions.UnsafeExpireTimeSpan ) ); } + else ClearCookie( ctx, UnsafeCookieName ); - JObject? CreateLongTermCookiePayload( FrontAuthenticationInfo fAuth ) + if( CookieMode != AuthenticationCookieMode.None && fAuth.Info.Level >= AuthLevel.Normal ) { - bool hasDeviceId = fAuth.Info.DeviceId.Length > 0; - JObject o; - if( fAuth.RememberMe && fAuth.Info.UnsafeActualUser.UserId != 0 ) - { - // The long term cookie stores the unsafe actual user: we are "remembering" so we don't need to store the RememberMe flag. - o = _tokenService.TypeSystem.UserInfo.ToJObject( fAuth.Info.UnsafeActualUser ); - } - else if( hasDeviceId ) - { - // We have no user identifier to remember or have no right to do so, but - // a device identifier exists: since we are allowed to UseLongTermCookie, then, use it! - o = new JObject(); - } - else - { - return null; - } - if( hasDeviceId ) - { - o.Add( StdAuthenticationTypeSystem.DeviceIdKeyType, fAuth.Info.DeviceId ); - } - return o; + Debug.Assert( fAuth.Info.Expires.HasValue ); + string value = _cookieFormat.Protect( fAuth ); + // If we don't remember, we create a session cookie (no expiration). + ctx.Response.Cookies.Append( AuthCookieName, value, CreateAuthCookieOptions( ctx, fAuth.RememberMe ? fAuth.Info.Expires : null ) ); } + else ClearCookie( ctx, AuthCookieName ); + } - CookieOptions CreateAuthCookieOptions( HttpContext ctx, DateTimeOffset? expires = null ) + JObject? CreateLongTermCookiePayload( FrontAuthenticationInfo fAuth ) + { + bool hasDeviceId = fAuth.Info.DeviceId.Length > 0; + JObject o; + if( fAuth.RememberMe && fAuth.Info.UnsafeActualUser.UserId != 0 ) { - return new CookieOptions() - { - Path = CookieMode == AuthenticationCookieMode.WebFrontPath - ? _cookiePath - : "/", - Expires = expires, - HttpOnly = true, - IsEssential = true, - Secure = _cookiePolicy == CookieSecurePolicy.SameAsRequest - ? ctx.Request.IsHttps - : _cookiePolicy == CookieSecurePolicy.Always - }; + // The long term cookie stores the unsafe actual user: we are "remembering" so we don't need to store the RememberMe flag. + o = _tokenService.TypeSystem.UserInfo.ToJObject( fAuth.Info.UnsafeActualUser ); } - - CookieOptions CreateUnsafeCookieOptions( DateTimeOffset? expires = null ) + else if( hasDeviceId ) { - return new CookieOptions() - { - Path = CookieMode == AuthenticationCookieMode.WebFrontPath - ? _cookiePath - : "/", - Secure = false, - Expires = expires, - HttpOnly = true - }; + // We have no user identifier to remember or have no right to do so, but + // a device identifier exists: since we are allowed to UseLongTermCookie, then, use it! + o = new JObject(); } - - void ClearCookie( HttpContext ctx, string cookieName ) + else { - ctx.Response.Cookies.Delete( cookieName, cookieName == AuthCookieName - ? CreateAuthCookieOptions( ctx ) - : CreateUnsafeCookieOptions() ); + return null; } + if( hasDeviceId ) + { + o.Add( StdAuthenticationTypeSystem.DeviceIdKeyType, fAuth.Info.DeviceId ); + } + return o; + } - #endregion + CookieOptions CreateAuthCookieOptions( HttpContext ctx, DateTimeOffset? expires = null ) + { + return new CookieOptions() + { + Path = CookieMode == AuthenticationCookieMode.WebFrontPath + ? _cookiePath + : "/", + Expires = expires, + HttpOnly = true, + IsEssential = true, + Secure = _cookiePolicy == CookieSecurePolicy.SameAsRequest + ? ctx.Request.IsHttps + : _cookiePolicy == CookieSecurePolicy.Always + }; + } - internal readonly struct LoginResult + CookieOptions CreateUnsafeCookieOptions( DateTimeOffset? expires = null ) + { + return new CookieOptions() { - /// - /// Standard JSON response. - /// It is mutable: properties can be appended. - /// - public readonly JObject Response; - - /// - /// Can be a None level. - /// - public readonly IAuthenticationInfo Info; - - public LoginResult( JObject r, IAuthenticationInfo a ) - { - Response = r; - Info = a; - } - } + Path = CookieMode == AuthenticationCookieMode.WebFrontPath + ? _cookiePath + : "/", + Secure = false, + Expires = expires, + HttpOnly = true + }; + } + + void ClearCookie( HttpContext ctx, string cookieName ) + { + ctx.Response.Cookies.Delete( cookieName, cookieName == AuthCookieName + ? CreateAuthCookieOptions( ctx ) + : CreateUnsafeCookieOptions() ); + } + + #endregion + + internal readonly struct LoginResult + { + /// + /// Standard JSON response. + /// It is mutable: properties can be appended. + /// + public readonly JObject Response; /// - /// Creates the authentication info, the standard JSON response and sets the cookies. - /// Note that the is updated in the . + /// Can be a None level. /// - /// The current Http context. - /// The user info to login. - /// - /// The calling scheme is used to set a critical expires depending on . - /// - /// The . - /// True to impersonate the current actor. - /// A login result with the JSON response and authentication info. - internal async ValueTask HandleLoginAsync( HttpContext c, - IActivityMonitor monitor, - UserLoginResult u, - string callingScheme, - IAuthenticationInfo initial, - bool rememberMe, - bool impersonateActualUser ) + public readonly IAuthenticationInfo Info; + + public LoginResult( JObject r, IAuthenticationInfo a ) { - FrontAuthenticationInfo fAuth = await HandleLoginCoreAsync( c, monitor, u, callingScheme, initial, rememberMe, impersonateActualUser ); - JObject response = CreateAuthResponse( c, refreshable: fAuth.Info.Level >= AuthLevel.Normal && CurrentOptions.SlidingExpirationTime > TimeSpan.Zero, - fAuth, - onLogin: u ); - return new LoginResult( response, fAuth.Info ); + Response = r; + Info = a; } + } + + /// + /// Creates the authentication info, the standard JSON response and sets the cookies. + /// Note that the is updated in the . + /// + /// The current Http context. + /// The user info to login. + /// + /// The calling scheme is used to set a critical expires depending on . + /// + /// The . + /// True to impersonate the current actor. + /// A login result with the JSON response and authentication info. + internal async ValueTask HandleLoginAsync( HttpContext c, + IActivityMonitor monitor, + UserLoginResult u, + string callingScheme, + IAuthenticationInfo initial, + bool rememberMe, + bool impersonateActualUser ) + { + FrontAuthenticationInfo fAuth = await HandleLoginCoreAsync( c, monitor, u, callingScheme, initial, rememberMe, impersonateActualUser ); + JObject response = CreateAuthResponse( c, refreshable: fAuth.Info.Level >= AuthLevel.Normal && CurrentOptions.SlidingExpirationTime > TimeSpan.Zero, + fAuth, + onLogin: u ); + return new LoginResult( response, fAuth.Info ); + } - async ValueTask HandleLoginCoreAsync( HttpContext c, - IActivityMonitor monitor, - UserLoginResult u, - string callingScheme, - IAuthenticationInfo initial, - bool rememberMe, - bool impersonateActualUser, - TimeSpan? expiresTimeSpan = null, - TimeSpan? criticalExpiresTimeSpan = null ) + async ValueTask HandleLoginCoreAsync( HttpContext c, + IActivityMonitor monitor, + UserLoginResult u, + string callingScheme, + IAuthenticationInfo initial, + bool rememberMe, + bool impersonateActualUser, + TimeSpan? expiresTimeSpan = null, + TimeSpan? criticalExpiresTimeSpan = null ) + { + string deviceId = initial.DeviceId; + if( deviceId.Length == 0 ) deviceId = CreateNewDeviceId(); + IAuthenticationInfo authInfo; + if( u.IsSuccess ) { - string deviceId = initial.DeviceId; - if( deviceId.Length == 0 ) deviceId = CreateNewDeviceId(); - IAuthenticationInfo authInfo; - if( u.IsSuccess ) + DateTime expires = DateTime.UtcNow + (expiresTimeSpan ?? CurrentOptions.ExpireTimeSpan); + DateTime? criticalExpires = null; + if( criticalExpiresTimeSpan.HasValue ) criticalExpires = DateTime.UtcNow + criticalExpiresTimeSpan.Value; + else { - DateTime expires = DateTime.UtcNow + (expiresTimeSpan ?? CurrentOptions.ExpireTimeSpan); - DateTime? criticalExpires = null; - if( criticalExpiresTimeSpan.HasValue ) criticalExpires = DateTime.UtcNow + criticalExpiresTimeSpan.Value; - else - { - // Handling Critical level configured for this scheme. - IDictionary? scts = CurrentOptions.SchemesCriticalTimeSpan; - if( scts != null - && scts.TryGetValue( callingScheme, out var criticalTimeSpan ) - && criticalTimeSpan > TimeSpan.Zero ) - { - criticalExpires = DateTime.UtcNow + criticalTimeSpan; - if( expires < criticalExpires ) expires = criticalExpires.Value; - } - } - authInfo = _tokenService.TypeSystem.AuthenticationInfo.Create( u.UserInfo, - expires, - criticalExpires, - deviceId ); - // If there is no _impersonationService or if the user impersonates hiself, we do nothing: - // this de facto resets any impersonation. - if( impersonateActualUser - && _impersonationService != null - && initial.ActualUser.UserId != 0 - && initial.ActualUser.UserId != u.UserInfo.UserId ) + // Handling Critical level configured for this scheme. + IDictionary? scts = CurrentOptions.SchemesCriticalTimeSpan; + if( scts != null + && scts.TryGetValue( callingScheme, out var criticalTimeSpan ) + && criticalTimeSpan > TimeSpan.Zero ) { - IUserInfo? target = null; - try - { - target = await _impersonationService.ImpersonateAsync( c, monitor, authInfo, initial.ActualUser.UserId ); - } - catch( Exception ex ) - { - monitor.Error( WebFrontAuthMonitorTag, $"Error while calling 'IWebFrontAuthImpersonationService.ImpersonateAsync()'.", ex ); - } - if( target != null ) - { - authInfo = authInfo.Impersonate( target ); - monitor.Info( WebFrontAuthMonitorTag, $"User '{authInfo.ActualUser.UserName} ({authInfo.ActualUser.UserId})' logged in and is impersonating '{target.UserName} ({target.UserId})'." ); - } - else - { - monitor.Warn( WebFrontAuthMonitorTag, $"Rejected impersonation for newly logged in '{authInfo.ActualUser.UserName} ({authInfo.ActualUser.UserId})' " + - $"into '{initial.ActualUser.UserName} ({initial.ActualUser.UserId})'." ); - } + criticalExpires = DateTime.UtcNow + criticalTimeSpan; + if( expires < criticalExpires ) expires = criticalExpires.Value; } } - else + authInfo = _tokenService.TypeSystem.AuthenticationInfo.Create( u.UserInfo, + expires, + criticalExpires, + deviceId ); + // If there is no _impersonationService or if the user impersonates hiself, we do nothing: + // this de facto resets any impersonation. + if( impersonateActualUser + && _impersonationService != null + && initial.ActualUser.UserId != 0 + && initial.ActualUser.UserId != u.UserInfo.UserId ) { - // With the introduction of the device identifier, authentication info should preserve its - // device identifier. - // On authentication failure, we could have kept the current authentication... But this could be misleading - // for clients: a failed login should fall back to the "anonymous". - // So we just create a new anonymous authentication (with the same deviceId). - authInfo = _tokenService.TypeSystem.AuthenticationInfo.Create( null, deviceId: deviceId ); + IUserInfo? target = null; + try + { + target = await _impersonationService.ImpersonateAsync( c, monitor, authInfo, initial.ActualUser.UserId ); + } + catch( Exception ex ) + { + monitor.Error( WebFrontAuthMonitorTag, $"Error while calling 'IWebFrontAuthImpersonationService.ImpersonateAsync()'.", ex ); + } + if( target != null ) + { + authInfo = authInfo.Impersonate( target ); + monitor.Info( WebFrontAuthMonitorTag, $"User '{authInfo.ActualUser.UserName} ({authInfo.ActualUser.UserId})' logged in and is impersonating '{target.UserName} ({target.UserId})'." ); + } + else + { + monitor.Warn( WebFrontAuthMonitorTag, $"Rejected impersonation for newly logged in '{authInfo.ActualUser.UserName} ({authInfo.ActualUser.UserId})' " + + $"into '{initial.ActualUser.UserName} ({initial.ActualUser.UserId})'." ); + } } - var fAuth = new FrontAuthenticationInfo( authInfo, rememberMe ); - c.Items[typeof( FrontAuthenticationInfo )] = fAuth; - SetCookies( c, fAuth ); - return fAuth; } - - /// - /// Creates a new device identifier. - /// If this must be changed, either the IWebFrontAuthLoginService or a new service or - /// may be the Options may do the job. - /// - /// The new device identifier. - static string CreateNewDeviceId() + else { - // Uses only url compliant characters and removes the = padding if it exists. - // Similar to base64url. See https://en.wikipedia.org/wiki/Base64 and https://tools.ietf.org/html/rfc4648. - return Convert.ToBase64String( Guid.NewGuid().ToByteArray() ).Replace( '+', '-' ).Replace( '/', '_' ).TrimEnd( '=' ); + // With the introduction of the device identifier, authentication info should preserve its + // device identifier. + // On authentication failure, we could have kept the current authentication... But this could be misleading + // for clients: a failed login should fall back to the "anonymous". + // So we just create a new anonymous authentication (with the same deviceId). + authInfo = _tokenService.TypeSystem.AuthenticationInfo.Create( null, deviceId: deviceId ); } + var fAuth = new FrontAuthenticationInfo( authInfo, rememberMe ); + c.Items[typeof( FrontAuthenticationInfo )] = fAuth; + SetCookies( c, fAuth ); + return fAuth; + } - /// - /// Centralized way to return an error: a redirect or a close of the window is emitted. - /// - internal Task SendRemoteAuthenticationErrorAsync( HttpContext c, - FrontAuthenticationInfo fAuth, - string? returnUrl, - string? callerOrigin, - string errorId, - string errorText, - string? initialScheme = null, - string? callingScheme = null, - IDictionary? userData = null, - UserLoginResult? failedLogin = null ) + /// + /// Creates a new device identifier. + /// If this must be changed, either the IWebFrontAuthLoginService or a new service or + /// may be the Options may do the job. + /// + /// The new device identifier. + static string CreateNewDeviceId() + { + // Uses only url compliant characters and removes the = padding if it exists. + // Similar to base64url. See https://en.wikipedia.org/wiki/Base64 and https://tools.ietf.org/html/rfc4648. + return Convert.ToBase64String( Guid.NewGuid().ToByteArray() ).Replace( '+', '-' ).Replace( '/', '_' ).TrimEnd( '=' ); + } + + /// + /// Centralized way to return an error: a redirect or a close of the window is emitted. + /// + internal Task SendRemoteAuthenticationErrorAsync( HttpContext c, + FrontAuthenticationInfo fAuth, + string? returnUrl, + string? callerOrigin, + string errorId, + string errorText, + string? initialScheme = null, + string? callingScheme = null, + IDictionary? userData = null, + UserLoginResult? failedLogin = null ) + { + if( returnUrl != null ) { - if( returnUrl != null ) + Debug.Assert( callerOrigin == null, "Since returnUrl is not null: /c/startLogin has been used without callerOrigin." ); + int idxQuery = returnUrl.IndexOf( '?' ); + var path = idxQuery > 0 + ? returnUrl.Substring( 0, idxQuery ) + : returnUrl; + var parameters = idxQuery > 0 + ? new QueryString( returnUrl.Substring( idxQuery ) ) + : new QueryString(); + parameters = parameters.Add( "errorId", errorId ); + if( !String.IsNullOrWhiteSpace( errorText ) && errorText != errorId ) { - Debug.Assert( callerOrigin == null, "Since returnUrl is not null: /c/startLogin has been used without callerOrigin." ); - int idxQuery = returnUrl.IndexOf( '?' ); - var path = idxQuery > 0 - ? returnUrl.Substring( 0, idxQuery ) - : returnUrl; - var parameters = idxQuery > 0 - ? new QueryString( returnUrl.Substring( idxQuery ) ) - : new QueryString(); - parameters = parameters.Add( "errorId", errorId ); - if( !String.IsNullOrWhiteSpace( errorText ) && errorText != errorId ) - { - parameters = parameters.Add( "errorText", errorText ); - } - // UnsafeActualUser must be removed. - // A ActualLevel must be added. - // => SetLevel(): if !impersonated => both changed. - // if impersonated => Level is set at most to the ActualLevel. - // => SetActualLevel(): if !impersonated => both changed. - // if impersonated => Sets the actual level. - // Level is clamped to be at most the ActualLevel. - // - // - // if( fAuth.Info.ActualLevel <= AuthLevel.Unsafe ) ?? - // { - // // Need a /c/downgradeLevel entry point ? (but we have it: Logout is downgrade to None + forget deviceId...) - // // "forget deviceId" on None should be in the WebFrontAuthOption... - // - // parameters = parameters.Add( "wfaAuthLevel", fAuth.Info.ActualLevel ); - // } - int loginFailureCode = failedLogin?.LoginFailureCode ?? 0; - if( loginFailureCode != 0 ) parameters = parameters.Add( "loginFailureCode", loginFailureCode.ToString( CultureInfo.InvariantCulture ) ); - if( initialScheme != null ) parameters = parameters.Add( "initialScheme", initialScheme ); - if( callingScheme != null ) parameters = parameters.Add( "callingScheme", callingScheme ); - - var target = new Uri( path + parameters.ToString() ); - c.Response.Redirect( target.ToString() ); - return Task.CompletedTask; + parameters = parameters.Add( "errorText", errorText ); } - Debug.Assert( callerOrigin != null, "Since returnUrl is null /c/startLogin has callerOrigin." ); - JObject errObj = CreateErrorAuthResponse( c, fAuth, errorId, errorText, initialScheme, callingScheme, userData, failedLogin ); - return c.Response.WriteWindowPostMessageAsync( errObj, callerOrigin ); + // UnsafeActualUser must be removed. + // A ActualLevel must be added. + // => SetLevel(): if !impersonated => both changed. + // if impersonated => Level is set at most to the ActualLevel. + // => SetActualLevel(): if !impersonated => both changed. + // if impersonated => Sets the actual level. + // Level is clamped to be at most the ActualLevel. + // + // + // if( fAuth.Info.ActualLevel <= AuthLevel.Unsafe ) ?? + // { + // // Need a /c/downgradeLevel entry point ? (but we have it: Logout is downgrade to None + forget deviceId...) + // // "forget deviceId" on None should be in the WebFrontAuthOption... + // + // parameters = parameters.Add( "wfaAuthLevel", fAuth.Info.ActualLevel ); + // } + int loginFailureCode = failedLogin?.LoginFailureCode ?? 0; + if( loginFailureCode != 0 ) parameters = parameters.Add( "loginFailureCode", loginFailureCode.ToString( CultureInfo.InvariantCulture ) ); + if( initialScheme != null ) parameters = parameters.Add( "initialScheme", initialScheme ); + if( callingScheme != null ) parameters = parameters.Add( "callingScheme", callingScheme ); + + var target = new Uri( path + parameters.ToString() ); + c.Response.Redirect( target.ToString() ); + return Task.CompletedTask; } + Debug.Assert( callerOrigin != null, "Since returnUrl is null /c/startLogin has callerOrigin." ); + JObject errObj = CreateErrorAuthResponse( c, fAuth, errorId, errorText, initialScheme, callingScheme, userData, failedLogin ); + return c.Response.WriteWindowPostMessageAsync( errObj, callerOrigin ); + } - /// - /// Creates a JSON response error object. - /// - /// The context. - /// - /// The current authentication info or a TypeSystem.AuthenticationInfo.None - /// info (with a device identifier if possible). - /// - /// The error identifier. - /// The error text. This can be null ( is the key). - /// The initial scheme. - /// The calling scheme. - /// Optional user data (can be null). - /// Optional failed login (can be null). - /// A {info,token,refreshable} object with error fields inside. - internal JObject CreateErrorAuthResponse( HttpContext c, - FrontAuthenticationInfo fAuth, - string errorId, - string? errorText, - string? initialScheme, - string? callingScheme, - IDictionary? userData, - UserLoginResult? failedLogin ) + /// + /// Creates a JSON response error object. + /// + /// The context. + /// + /// The current authentication info or a TypeSystem.AuthenticationInfo.None + /// info (with a device identifier if possible). + /// + /// The error identifier. + /// The error text. This can be null ( is the key). + /// The initial scheme. + /// The calling scheme. + /// Optional user data (can be null). + /// Optional failed login (can be null). + /// A {info,token,refreshable} object with error fields inside. + internal JObject CreateErrorAuthResponse( HttpContext c, + FrontAuthenticationInfo fAuth, + string errorId, + string? errorText, + string? initialScheme, + string? callingScheme, + IDictionary? userData, + UserLoginResult? failedLogin ) + { + var response = CreateAuthResponse( c, false, fAuth, failedLogin ); + response.Add( new JProperty( "errorId", errorId ) ); + if( !String.IsNullOrWhiteSpace( errorText ) && errorText != errorId ) { - var response = CreateAuthResponse( c, false, fAuth, failedLogin ); - response.Add( new JProperty( "errorId", errorId ) ); - if( !String.IsNullOrWhiteSpace( errorText ) && errorText != errorId ) - { - response.Add( new JProperty( "errorText", errorText ) ); - } - if( initialScheme != null ) response.Add( new JProperty( "initialScheme", initialScheme ) ); - if( callingScheme != null ) response.Add( new JProperty( "callingScheme", callingScheme ) ); - if( userData != null ) response.Add( userData.ToJProperty() ); - return response; + response.Add( new JProperty( "errorText", errorText ) ); } + if( initialScheme != null ) response.Add( new JProperty( "initialScheme", initialScheme ) ); + if( callingScheme != null ) response.Add( new JProperty( "callingScheme", callingScheme ) ); + if( userData != null ) response.Add( userData.ToJProperty() ); + return response; + } - /// - /// Creates a JSON response object. - /// - /// The context. - /// Whether the info is refreshable or not. - /// - /// The authentication info. - /// It is never null, even on error: it must be the current authentication info or a TypeSystem.AuthenticationInfo.None - /// info (with a device identifier if possible). - /// - /// Not null when this response is the result of an actual login (and not a refresh). - /// A {info,token,refreshable} object. - internal JObject CreateAuthResponse( HttpContext c, bool refreshable, FrontAuthenticationInfo fAuth, UserLoginResult? onLogin = null ) + /// + /// Creates a JSON response object. + /// + /// The context. + /// Whether the info is refreshable or not. + /// + /// The authentication info. + /// It is never null, even on error: it must be the current authentication info or a TypeSystem.AuthenticationInfo.None + /// info (with a device identifier if possible). + /// + /// Not null when this response is the result of an actual login (and not a refresh). + /// A {info,token,refreshable} object. + internal JObject CreateAuthResponse( HttpContext c, bool refreshable, FrontAuthenticationInfo fAuth, UserLoginResult? onLogin = null ) + { + var j = new JObject( + new JProperty( "info", _tokenService.TypeSystem.AuthenticationInfo.ToJObject( fAuth.Info ) ), + new JProperty( "token", _tokenService.ProtectFrontAuthenticationInfo( fAuth ) ), + new JProperty( "refreshable", refreshable ), + new JProperty( "rememberMe", fAuth.RememberMe ) ); + if( onLogin != null && !onLogin.IsSuccess ) { - var j = new JObject( - new JProperty( "info", _tokenService.TypeSystem.AuthenticationInfo.ToJObject( fAuth.Info ) ), - new JProperty( "token", _tokenService.ProtectFrontAuthenticationInfo( fAuth ) ), - new JProperty( "refreshable", refreshable ), - new JProperty( "rememberMe", fAuth.RememberMe ) ); - if( onLogin != null && !onLogin.IsSuccess ) - { - j.Add( new JProperty( "loginFailureCode", onLogin.LoginFailureCode ) ); - j.Add( new JProperty( "loginFailureReason", onLogin.LoginFailureReason ) ); - } - return j; + j.Add( new JProperty( "loginFailureCode", onLogin.LoginFailureCode ) ); + j.Add( new JProperty( "loginFailureReason", onLogin.LoginFailureReason ) ); } + return j; + } - internal async Task OnHandlerStartLoginAsync( IActivityMonitor m, WebFrontAuthStartLoginContext startContext ) + internal async Task OnHandlerStartLoginAsync( IActivityMonitor m, WebFrontAuthStartLoginContext startContext ) + { + try { - try + if( _dynamicScopeProvider != null ) { - if( _dynamicScopeProvider != null ) - { - startContext.DynamicScopes = await _dynamicScopeProvider.GetScopesAsync( m, startContext ); - } - } - catch( Exception ex ) - { - startContext.SetError( ex.GetType().FullName!, ex.Message ?? "Exception has null message!" ); + startContext.DynamicScopes = await _dynamicScopeProvider.GetScopesAsync( m, startContext ); } } + catch( Exception ex ) + { + startContext.SetError( ex.GetType().FullName!, ex.Message ?? "Exception has null message!" ); + } + } - /// - /// This method fully handles the request. - /// - /// Type of a payload object that is scheme dependent. - /// The remote authentication ticket. - /// - /// Configurator for the payload object: this action typically populates properties - /// from the principal claims. - /// - /// The awaitable. - public Task HandleRemoteAuthenticationAsync( TicketReceivedContext context, Action payloadConfigurator ) + /// + /// This method fully handles the request. + /// + /// Type of a payload object that is scheme dependent. + /// The remote authentication ticket. + /// + /// Configurator for the payload object: this action typically populates properties + /// from the principal claims. + /// + /// The awaitable. + public Task HandleRemoteAuthenticationAsync( TicketReceivedContext context, Action payloadConfigurator ) + { + Throw.CheckNotNullArgument( context ); + Throw.CheckNotNullArgument( payloadConfigurator ); + var monitor = GetRequestMonitor( context.HttpContext ); + + GetWFAData( context.HttpContext, + context.Properties, + out var fAuth, + out var impersonateActualUser, + out var initialScheme, + out var callerOrigin, + out var returnUrl, + out var userData ); + + string callingScheme = context.Scheme.Name; + object payload = _loginService.CreatePayload( context.HttpContext, monitor, callingScheme ); + payloadConfigurator( (T)payload ); + + // When Authentication Challenge has been called directly (LoginMode is WebFrontAuthLoginMode.None), we don't have + // any scheme: we steal the context.RedirectUri as being the final redirect url. + if( initialScheme == null ) { - Throw.CheckNotNullArgument( context ); - Throw.CheckNotNullArgument( payloadConfigurator ); - var monitor = GetRequestMonitor( context.HttpContext ); - - GetWFAData( context.HttpContext, - context.Properties, - out var fAuth, - out var impersonateActualUser, - out var initialScheme, - out var callerOrigin, - out var returnUrl, - out var userData ); - - string callingScheme = context.Scheme.Name; - object payload = _loginService.CreatePayload( context.HttpContext, monitor, callingScheme ); - payloadConfigurator( (T)payload ); - - // When Authentication Challenge has been called directly (LoginMode is WebFrontAuthLoginMode.None), we don't have - // any scheme: we steal the context.RedirectUri as being the final redirect url. - if( initialScheme == null ) - { - returnUrl = context.ReturnUri; - } - var wfaSC = new WebFrontAuthLoginContext( context.HttpContext, - this, - _tokenService.TypeSystem, - initialScheme != null - ? WebFrontAuthLoginMode.StartLogin - : WebFrontAuthLoginMode.None, - callingScheme, - payload, - context.Properties, - initialScheme, - fAuth, - impersonateActualUser, - returnUrl, - callerOrigin, - userData ); - // We always handle the response (we skip the final standard SignIn process). - context.HandleResponse(); - - return UnifiedLoginAsync( monitor, wfaSC, actualLogin => - { - return _loginService.LoginAsync( context.HttpContext, monitor, callingScheme, payload, actualLogin ); - } ); + returnUrl = context.ReturnUri; } + var wfaSC = new WebFrontAuthLoginContext( context.HttpContext, + this, + _tokenService.TypeSystem, + initialScheme != null + ? WebFrontAuthLoginMode.StartLogin + : WebFrontAuthLoginMode.None, + callingScheme, + payload, + context.Properties, + initialScheme, + fAuth, + impersonateActualUser, + returnUrl, + callerOrigin, + userData ); + // We always handle the response (we skip the final standard SignIn process). + context.HandleResponse(); - internal void ValidateCoreParameters( IActivityMonitor monitor, - WebFrontAuthLoginMode mode, - string? returnUrl, - string? callerOrigin, - IAuthenticationInfo current, - bool impersonateActualUser, - IErrorContext ctx ) + return UnifiedLoginAsync( monitor, wfaSC, actualLogin => { - if( mode == WebFrontAuthLoginMode.StartLogin ) - { - // ReturnUrl (inline) and CallerOrigin (popup) cannot be both null or both not null. - if( (returnUrl != null) == (callerOrigin != null) ) - { - ctx.SetError( "ReturnXOrCaller", "One and only one among returnUrl and callerOrigin must be specified." ); - monitor.Error( WebFrontAuthMonitorTag, "One and only one among returnUrl and callerOrigin must be specified." ); - return; - } - } - if( !CheckLoginWhileImpersonation( monitor, current, impersonateActualUser, ctx ) ) - { - return; - } - if( returnUrl != null - && !_allowedReturnUrls.Any( p => returnUrl.StartsWith( p, StringComparison.Ordinal ) ) ) + return _loginService.LoginAsync( context.HttpContext, monitor, callingScheme, payload, actualLogin ); + } ); + } + + internal void ValidateCoreParameters( IActivityMonitor monitor, + WebFrontAuthLoginMode mode, + string? returnUrl, + string? callerOrigin, + IAuthenticationInfo current, + bool impersonateActualUser, + IErrorContext ctx ) + { + if( mode == WebFrontAuthLoginMode.StartLogin ) + { + // ReturnUrl (inline) and CallerOrigin (popup) cannot be both null or both not null. + if( (returnUrl != null) == (callerOrigin != null) ) { - ctx.SetError( "DisallowedReturnUrl", $"The returnUrl='{returnUrl}' doesn't start with any of configured AllowedReturnUrls prefixes." ); - monitor.Error( WebFrontAuthMonitorTag, $"ReturnUrl '{returnUrl}' doesn't match: '{_allowedReturnUrls.Concatenate("', '")}'." ); + ctx.SetError( "ReturnXOrCaller", "One and only one among returnUrl and callerOrigin must be specified." ); + monitor.Error( WebFrontAuthMonitorTag, "One and only one among returnUrl and callerOrigin must be specified." ); return; } } - - /// - /// Login is always forbidden whenever the user is impersonated unless we must impersonate the actual user. - /// - static bool CheckLoginWhileImpersonation( IActivityMonitor monitor, IAuthenticationInfo current, bool impersonateActualUser, IErrorContext ctx ) + if( !CheckLoginWhileImpersonation( monitor, current, impersonateActualUser, ctx ) ) { - if( current.IsImpersonated && !impersonateActualUser ) - { - ctx.SetError( "LoginWhileImpersonation", "Login is not allowed while impersonation is active." ); - monitor.Error( WebFrontAuthMonitorTag, $"Login is not allowed while impersonation is active. UserId: {current.User.UserId}, ActualUserId: {current.ActualUser.UserId}." ); - return false; - } - return true; + return; + } + if( returnUrl != null + && !_allowedReturnUrls.Any( p => returnUrl.StartsWith( p, StringComparison.Ordinal ) ) ) + { + ctx.SetError( "DisallowedReturnUrl", $"The returnUrl='{returnUrl}' doesn't start with any of configured AllowedReturnUrls prefixes." ); + monitor.Error( WebFrontAuthMonitorTag, $"ReturnUrl '{returnUrl}' doesn't match: '{_allowedReturnUrls.Concatenate( "', '" )}'." ); + return; } + } - internal async Task UnifiedLoginAsync( IActivityMonitor monitor, WebFrontAuthLoginContext ctx, Func> logger ) + /// + /// Login is always forbidden whenever the user is impersonated unless we must impersonate the actual user. + /// + static bool CheckLoginWhileImpersonation( IActivityMonitor monitor, IAuthenticationInfo current, bool impersonateActualUser, IErrorContext ctx ) + { + if( current.IsImpersonated && !impersonateActualUser ) { - // Double check of the core parameters. - // If here the check fails, it means that the AuthenticationProperties have been tampered! - // This is highly unlikely. - ValidateCoreParameters( monitor, ctx.LoginMode, ctx.ReturnUrl, ctx.CallerOrigin, ctx.InitialAuthentication, ctx.ImpersonateActualUser, ctx ); - await DoUnifiedLoginAsync( monitor, ctx, logger ); - await ctx.SendResponseAsync( monitor ); + ctx.SetError( "LoginWhileImpersonation", "Login is not allowed while impersonation is active." ); + monitor.Error( WebFrontAuthMonitorTag, $"Login is not allowed while impersonation is active. UserId: {current.User.UserId}, ActualUserId: {current.ActualUser.UserId}." ); + return false; } + return true; + } + + internal async Task UnifiedLoginAsync( IActivityMonitor monitor, WebFrontAuthLoginContext ctx, Func> logger ) + { + // Double check of the core parameters. + // If here the check fails, it means that the AuthenticationProperties have been tampered! + // This is highly unlikely. + ValidateCoreParameters( monitor, ctx.LoginMode, ctx.ReturnUrl, ctx.CallerOrigin, ctx.InitialAuthentication, ctx.ImpersonateActualUser, ctx ); + await DoUnifiedLoginAsync( monitor, ctx, logger ); + await ctx.SendResponseAsync( monitor ); + } - async Task DoUnifiedLoginAsync( IActivityMonitor monitor, WebFrontAuthLoginContext ctx, Func> logger ) + async Task DoUnifiedLoginAsync( IActivityMonitor monitor, WebFrontAuthLoginContext ctx, Func> logger ) + { + UserLoginResult? u = null; + if( !ctx.HasError ) { - UserLoginResult? u = null; - if( !ctx.HasError ) - { - // The logger function must kindly return an unlogged UserLoginResult if it cannot log the user in. - u = await SafeCallLoginAsync( monitor, ctx, logger, actualLogin: _validateLoginService == null ); - } - if( !ctx.HasError ) + // The logger function must kindly return an unlogged UserLoginResult if it cannot log the user in. + u = await SafeCallLoginAsync( monitor, ctx, logger, actualLogin: _validateLoginService == null ); + } + if( !ctx.HasError ) + { + Debug.Assert( u != null ); + int currentlyLoggedIn = ctx.InitialAuthentication.User.UserId; + if( !u.IsSuccess ) { - Debug.Assert( u != null ); - int currentlyLoggedIn = ctx.InitialAuthentication.User.UserId; - if( !u.IsSuccess ) + // login failed. + // If the login failed because user is not registered: entering the account binding or auto registration features, + // but only if the login is not trying to impersonate the current actor. + if( ctx.ImpersonateActualUser ) { - // login failed. - // If the login failed because user is not registered: entering the account binding or auto registration features, - // but only if the login is not trying to impersonate the current actor. - if( ctx.ImpersonateActualUser ) - { - ctx.SetError( u ); - monitor.Error( WebFrontAuthMonitorTag, $"User.LoginError: Login with ImpersonateActualUser (current is {currentlyLoggedIn}) tried '{ctx.CallingScheme}' scheme and failed." ); - } - else + ctx.SetError( u ); + monitor.Error( WebFrontAuthMonitorTag, $"User.LoginError: Login with ImpersonateActualUser (current is {currentlyLoggedIn}) tried '{ctx.CallingScheme}' scheme and failed." ); + } + else + { + if( u.IsUnregisteredUser ) { - if( u.IsUnregisteredUser ) + if( currentlyLoggedIn != 0 ) { - if( currentlyLoggedIn != 0 ) + bool raiseError = true; + // A user is currently logged in. + if( _autoBindingAccountService != null ) { - bool raiseError = true; - // A user is currently logged in. - if( _autoBindingAccountService != null ) + UserLoginResult? uBound = await _autoBindingAccountService.BindAccountAsync( monitor, ctx ); + if( uBound != null ) { - UserLoginResult? uBound = await _autoBindingAccountService.BindAccountAsync( monitor, ctx ); - if( uBound != null ) + raiseError = false; + if( !uBound.IsSuccess ) ctx.SetError( uBound ); + else { - raiseError = false; - if( !uBound.IsSuccess ) ctx.SetError( uBound ); - else + if( u != uBound ) { - if( u != uBound ) - { - u = uBound; - monitor.Info( WebFrontAuthMonitorTag, $"Account.AutoBinding: {currentlyLoggedIn} now bound to '{ctx.CallingScheme}' scheme." ); - } + u = uBound; + monitor.Info( WebFrontAuthMonitorTag, $"Account.AutoBinding: {currentlyLoggedIn} now bound to '{ctx.CallingScheme}' scheme." ); } } } - if( raiseError ) - { - ctx.SetError( "Account.AutoBindingDisabled", "Automatic account binding is disabled." ); - monitor.Error( WebFrontAuthMonitorTag, $"Account.AutoBindingDisabled: {currentlyLoggedIn} tried '{ctx.CallingScheme}' scheme." ); - } } - else + if( raiseError ) { - bool raiseError = true; - if( _autoCreateAccountService != null ) - { - UserLoginResult? uAuto = await _autoCreateAccountService.CreateAccountAndLoginAsync( monitor, ctx ); - if( uAuto != null ) - { - raiseError = false; - if( !uAuto.IsSuccess ) ctx.SetError( uAuto ); - else u = uAuto; - } - } - if( raiseError ) - { - ctx.SetError( "User.AutoRegistrationDisabled", "Automatic user registration is disabled." ); - monitor.Error( WebFrontAuthMonitorTag, $"User.AutoRegistrationDisabled: Automatic user registration is disabled (scheme: {ctx.CallingScheme})." ); - } + ctx.SetError( "Account.AutoBindingDisabled", "Automatic account binding is disabled." ); + monitor.Error( WebFrontAuthMonitorTag, $"Account.AutoBindingDisabled: {currentlyLoggedIn} tried '{ctx.CallingScheme}' scheme." ); } } else { - ctx.SetError( u ); - monitor.Trace( WebFrontAuthMonitorTag, $"User.LoginError: ({u.LoginFailureCode}) {u.LoginFailureReason}" ); + bool raiseError = true; + if( _autoCreateAccountService != null ) + { + UserLoginResult? uAuto = await _autoCreateAccountService.CreateAccountAndLoginAsync( monitor, ctx ); + if( uAuto != null ) + { + raiseError = false; + if( !uAuto.IsSuccess ) ctx.SetError( uAuto ); + else u = uAuto; + } + } + if( raiseError ) + { + ctx.SetError( "User.AutoRegistrationDisabled", "Automatic user registration is disabled." ); + monitor.Error( WebFrontAuthMonitorTag, $"User.AutoRegistrationDisabled: Automatic user registration is disabled (scheme: {ctx.CallingScheme})." ); + } } } - } - else - { - // If a validation service is registered, the first call above - // did not actually logged the user in (actualLogin = false). - // We trigger the real login now if the validation service validates it. - if( _validateLoginService != null ) + else { - Debug.Assert( u.UserInfo != null ); - await _validateLoginService.ValidateLoginAsync( monitor, u.UserInfo, ctx ); - if( !ctx.HasError ) - { - u = await SafeCallLoginAsync( monitor, ctx, logger, actualLogin: true ); - } + ctx.SetError( u ); + monitor.Trace( WebFrontAuthMonitorTag, $"User.LoginError: ({u.LoginFailureCode}) {u.LoginFailureReason}" ); } } - // Eventually... - if( !ctx.HasError ) + } + else + { + // If a validation service is registered, the first call above + // did not actually logged the user in (actualLogin = false). + // We trigger the real login now if the validation service validates it. + if( _validateLoginService != null ) { - Debug.Assert( u != null && u.UserInfo != null, "Login succeeds." ); - if( currentlyLoggedIn != 0 && u.UserInfo.UserId != currentlyLoggedIn && !ctx.ImpersonateActualUser ) + Debug.Assert( u.UserInfo != null ); + await _validateLoginService.ValidateLoginAsync( monitor, u.UserInfo, ctx ); + if( !ctx.HasError ) { - monitor.Warn( WebFrontAuthMonitorTag, $"Account.Relogin: User {currentlyLoggedIn} logged again as {u.UserInfo.UserId} via '{ctx.CallingScheme}' scheme without logout." ); + u = await SafeCallLoginAsync( monitor, ctx, logger, actualLogin: true ); } - ctx.SetSuccessfulLogin( u ); - monitor.Info( WebFrontAuthMonitorTag, $"Logged in user {u.UserInfo.UserId} via '{ctx.CallingScheme}'." ); } } - } - - /// - /// Calls the actual logger function (that must kindly return an unlogged UserLoginResult if it cannot log the user in) - /// in a try/catch and sets an error on the context only if it throws. - /// - /// The monitor to use. - /// The login context. - /// The actual login function. - /// True for an actual login, false otherwise. - /// A login result (that may be unsuccessful). - static async Task SafeCallLoginAsync( IActivityMonitor monitor, WebFrontAuthLoginContext ctx, Func> logger, bool actualLogin ) - { - UserLoginResult? u = null; - try + // Eventually... + if( !ctx.HasError ) { - u = await logger( actualLogin ); - if( u == null ) + Debug.Assert( u != null && u.UserInfo != null, "Login succeeds." ); + if( currentlyLoggedIn != 0 && u.UserInfo.UserId != currentlyLoggedIn && !ctx.ImpersonateActualUser ) { - monitor.Fatal( WebFrontAuthMonitorTag, "Login service returned a null UserLoginResult." ); - ctx.SetError( "InternalError", "Login service returned a null UserLoginResult." ); + monitor.Warn( WebFrontAuthMonitorTag, $"Account.Relogin: User {currentlyLoggedIn} logged again as {u.UserInfo.UserId} via '{ctx.CallingScheme}' scheme without logout." ); } + ctx.SetSuccessfulLogin( u ); + monitor.Info( WebFrontAuthMonitorTag, $"Logged in user {u.UserInfo.UserId} via '{ctx.CallingScheme}'." ); } - catch( Exception ex ) + } + } + + /// + /// Calls the actual logger function (that must kindly return an unlogged UserLoginResult if it cannot log the user in) + /// in a try/catch and sets an error on the context only if it throws. + /// + /// The monitor to use. + /// The login context. + /// The actual login function. + /// True for an actual login, false otherwise. + /// A login result (that may be unsuccessful). + static async Task SafeCallLoginAsync( IActivityMonitor monitor, WebFrontAuthLoginContext ctx, Func> logger, bool actualLogin ) + { + UserLoginResult? u = null; + try + { + u = await logger( actualLogin ); + if( u == null ) { - monitor.Error( WebFrontAuthMonitorTag, "While calling login service.", ex ); - ctx.SetError( ex ); + monitor.Fatal( WebFrontAuthMonitorTag, "Login service returned a null UserLoginResult." ); + ctx.SetError( "InternalError", "Login service returned a null UserLoginResult." ); } - return u; } + catch( Exception ex ) + { + monitor.Error( WebFrontAuthMonitorTag, "While calling login service.", ex ); + ctx.SetError( ex ); + } + return u; } } diff --git a/CK.AspNet.Auth/WebFrontAuthStartLoginContext.cs b/CK.AspNet.Auth/WebFrontAuthStartLoginContext.cs index 57f812c0..47d6e8e6 100644 --- a/CK.AspNet.Auth/WebFrontAuthStartLoginContext.cs +++ b/CK.AspNet.Auth/WebFrontAuthStartLoginContext.cs @@ -10,173 +10,171 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth +namespace CK.AspNet.Auth; + + +/// +/// Captures initial login request information and provides a context to interact with the flow +/// before challenging the actual remote authentication. +/// +public sealed class WebFrontAuthStartLoginContext : IErrorContext { + readonly WebFrontAuthService _webFrontAuthService; + readonly FrontAuthenticationInfo _currentAuth; + string? _errorId; + string? _errorText; + + internal WebFrontAuthStartLoginContext( HttpContext ctx, + WebFrontAuthService authService, + string? scheme, + FrontAuthenticationInfo current, + bool impersonateActualUser, + string? returnUrl, + string? callerOrigin ) + { + Debug.Assert( ctx != null && authService != null ); + Debug.Assert( scheme != null ); + Debug.Assert( current != null ); + HttpContext = ctx; + _webFrontAuthService = authService; + _currentAuth = current; + // will be validated below. + Scheme = scheme ?? String.Empty; + // will be really set (or not on error) by Validate below. + UserData = null!; + ReturnUrl = returnUrl; + CallerOrigin = callerOrigin; + ImpersonateActualUser = impersonateActualUser; + } - /// - /// Captures initial login request information and provides a context to interact with the flow - /// before challenging the actual remote authentication. - /// - public sealed class WebFrontAuthStartLoginContext : IErrorContext + internal void ValidateStartLoginRequest( IActivityMonitor monitor, IEnumerable> userData ) { - readonly WebFrontAuthService _webFrontAuthService; - readonly FrontAuthenticationInfo _currentAuth; - string? _errorId; - string? _errorText; - - internal WebFrontAuthStartLoginContext( HttpContext ctx, - WebFrontAuthService authService, - string? scheme, - FrontAuthenticationInfo current, - bool impersonateActualUser, - string? returnUrl, - string? callerOrigin ) + if( string.IsNullOrWhiteSpace( Scheme ) ) { - Debug.Assert( ctx != null && authService != null ); - Debug.Assert( scheme != null ); - Debug.Assert( current != null ); - HttpContext = ctx; - _webFrontAuthService = authService; - _currentAuth = current; - // will be validated below. - Scheme = scheme ?? String.Empty; - // will be really set (or not on error) by Validate below. - UserData = null!; - ReturnUrl = returnUrl; - CallerOrigin = callerOrigin; - ImpersonateActualUser = impersonateActualUser; + SetError( "RequiredSchemeParameter", "A scheme parameter is required." ); + monitor.Error( WebFrontAuthService.WebFrontAuthMonitorTag, "Missing required scheme parameter." ); + return; } - - internal void ValidateStartLoginRequest( IActivityMonitor monitor, IEnumerable> userData ) + var ud = new Dictionary(); + foreach( var kv in userData ) { - if( string.IsNullOrWhiteSpace( Scheme ) ) + int c = kv.Value.Count; + if( c > 1 ) { - SetError( "RequiredSchemeParameter", "A scheme parameter is required." ); - monitor.Error( WebFrontAuthService.WebFrontAuthMonitorTag, "Missing required scheme parameter." ); + var msg = $"Form or Query data must not contain more than one string value per key: {kv.Key}: {kv.Value}."; + SetError( "MultipleUserDataValueNotSuported", msg ); + monitor.Error( WebFrontAuthService.WebFrontAuthMonitorTag, msg ); return; } - var ud = new Dictionary(); - foreach( var kv in userData ) - { - int c = kv.Value.Count; - if( c > 1 ) - { - var msg = $"Form or Query data must not contain more than one string value per key: {kv.Key}: {kv.Value}."; - SetError( "MultipleUserDataValueNotSuported", msg ); - monitor.Error( WebFrontAuthService.WebFrontAuthMonitorTag, msg ); - return; - } - ud.Add( kv.Key, c == 0 ? null : kv.Value[0] ); - } - UserData = ud; - _webFrontAuthService.ValidateCoreParameters( monitor, WebFrontAuthLoginMode.StartLogin, ReturnUrl, CallerOrigin, Current, ImpersonateActualUser, this ); + ud.Add( kv.Key, c == 0 ? null : kv.Value[0] ); } + UserData = ud; + _webFrontAuthService.ValidateCoreParameters( monitor, WebFrontAuthLoginMode.StartLogin, ReturnUrl, CallerOrigin, Current, ImpersonateActualUser, this ); + } - /// - /// Gets the http context. - /// - public HttpContext HttpContext { get; } - - /// - /// Gets the current authentication. - /// - public IAuthenticationInfo Current => _currentAuth.Info; - - /// - /// Gets whether the authentication should be memorized (or be as transient as possible). - /// Note that this is always false when is used. - /// - public bool RememberMe => _currentAuth.RememberMe; - - /// - /// Gets the scheme to challenge. - /// Never null or empty. - /// - public string Scheme { get; } - - /// - /// Gets the return url. Not null if and only if "inline login" is used. - /// This url is always checked against the set of allowed prefixes. - /// - public string? ReturnUrl { get; set; } - - /// - /// Gets the caller origin. Not null if and only if "popup login" is used. - /// - public string? CallerOrigin { get; } - - /// - /// Gets or sets whether the login wants to keep the previous logged in user as the - /// and becomes the . - /// - public bool ImpersonateActualUser { get; set; } - - /// - /// Gets the mutable user data. - /// - public IDictionary UserData { get; private set; } - - /// - /// Gets whether an error has been set. - /// - public bool HasError => _errorId != null; - - /// - /// Sets an error message. - /// The returned error contains the and , the - /// and . - /// Can be called multiple times: new error information replaces the previous one. - /// - /// Error identifier (a dotted identifier string). - /// The error message in clear text. - public void SetError( string errorId, string errorMessage ) - { - if( string.IsNullOrWhiteSpace( errorId ) ) throw new ArgumentNullException( nameof( errorId ) ); - if( string.IsNullOrWhiteSpace( errorMessage ) ) throw new ArgumentNullException( nameof( errorMessage ) ); - _errorId = errorId; - _errorText = errorMessage; - } + /// + /// Gets the http context. + /// + public HttpContext HttpContext { get; } - /// - /// Captures dynamic scopes from optional IWebFrontAuthDynamicScopeProvider.GetScopesAsync call. - /// This is internal since it is the optional that is used - /// to set it from . - /// - internal string[]? DynamicScopes; + /// + /// Gets the current authentication. + /// + public IAuthenticationInfo Current => _currentAuth.Info; - internal Task SendErrorAsync() - { - Debug.Assert( HasError ); - Debug.Assert( _errorId != null && _errorText != null ); + /// + /// Gets whether the authentication should be memorized (or be as transient as possible). + /// Note that this is always false when is used. + /// + public bool RememberMe => _currentAuth.RememberMe; + + /// + /// Gets the scheme to challenge. + /// Never null or empty. + /// + public string Scheme { get; } + + /// + /// Gets the return url. Not null if and only if "inline login" is used. + /// This url is always checked against the set of allowed prefixes. + /// + public string? ReturnUrl { get; set; } - // This is called on the initial request: if ReturnUrl is set (inline), we must not - // redirect the error there! - if( ReturnUrl != null ) + /// + /// Gets the caller origin. Not null if and only if "popup login" is used. + /// + public string? CallerOrigin { get; } + + /// + /// Gets or sets whether the login wants to keep the previous logged in user as the + /// and becomes the . + /// + public bool ImpersonateActualUser { get; set; } + + /// + /// Gets the mutable user data. + /// + public IDictionary UserData { get; private set; } + + /// + /// Gets whether an error has been set. + /// + public bool HasError => _errorId != null; + + /// + /// Sets an error message. + /// The returned error contains the and , the + /// and . + /// Can be called multiple times: new error information replaces the previous one. + /// + /// Error identifier (a dotted identifier string). + /// The error message in clear text. + public void SetError( string errorId, string errorMessage ) + { + if( string.IsNullOrWhiteSpace( errorId ) ) throw new ArgumentNullException( nameof( errorId ) ); + if( string.IsNullOrWhiteSpace( errorMessage ) ) throw new ArgumentNullException( nameof( errorMessage ) ); + _errorId = errorId; + _errorText = errorMessage; + } + + /// + /// Captures dynamic scopes from optional IWebFrontAuthDynamicScopeProvider.GetScopesAsync call. + /// This is internal since it is the optional that is used + /// to set it from . + /// + internal string[]? DynamicScopes; + + internal Task SendErrorAsync() + { + Debug.Assert( HasError ); + Debug.Assert( _errorId != null && _errorText != null ); + + // This is called on the initial request: if ReturnUrl is set (inline), we must not + // redirect the error there! + if( ReturnUrl != null ) + { + Debug.Assert( _errorId != null ); + JObject o = new JObject( new JProperty( "errorId", _errorId ) ); + if( !String.IsNullOrWhiteSpace( _errorText ) && _errorText != _errorId ) { - Debug.Assert( _errorId != null ); - JObject o = new JObject( new JProperty( "errorId", _errorId ) ); - if( !String.IsNullOrWhiteSpace( _errorText ) && _errorText != _errorId ) - { - o.Add( new JProperty( "errorText", _errorText ) ); - } - HttpContext.Response.ContentType = "application/json"; - return HttpContext.Response.WriteAsync( o.ToString( Newtonsoft.Json.Formatting.None ) ); + o.Add( new JProperty( "errorText", _errorText ) ); } - // We are in popup mode: use the SendRemoteAuthenticationError that generates a proper error message (including the - // downgraded authentication). - return _webFrontAuthService.SendRemoteAuthenticationErrorAsync( - HttpContext, - _currentAuth, - returnUrl: null, - CallerOrigin, - _errorId, - _errorText, - Scheme, - callingScheme: null, - UserData, - failedLogin: null ); + HttpContext.Response.ContentType = "application/json"; + return HttpContext.Response.WriteAsync( o.ToString( Newtonsoft.Json.Formatting.None ) ); } - + // We are in popup mode: use the SendRemoteAuthenticationError that generates a proper error message (including the + // downgraded authentication). + return _webFrontAuthService.SendRemoteAuthenticationErrorAsync( + HttpContext, + _currentAuth, + returnUrl: null, + CallerOrigin, + _errorId, + _errorText, + Scheme, + callingScheme: null, + UserData, + failedLogin: null ); } } diff --git a/CK.DB.AspNet.Auth/AuthenticationDatabaseServiceExtensions.cs b/CK.DB.AspNet.Auth/AuthenticationDatabaseServiceExtensions.cs index 0497f981..61f4606f 100644 --- a/CK.DB.AspNet.Auth/AuthenticationDatabaseServiceExtensions.cs +++ b/CK.DB.AspNet.Auth/AuthenticationDatabaseServiceExtensions.cs @@ -7,32 +7,31 @@ using System.Threading.Tasks; using CK.AspNet.Auth; -namespace CK.DB.Auth +namespace CK.DB.Auth; + +/// +/// Extends objects. +/// +public static class AuthenticationDatabaseServiceExtensions { /// - /// Extends objects. + /// Helper method that calls and + /// the helper method when + /// is successful or returns a failed based on dbResult error properties if it is on error. /// - public static class AuthenticationDatabaseServiceExtensions + /// This IAuthenticationDatabaseService. + /// The type system to use. + /// The call context to use. + /// The database result to transform. + /// The (never null) UserLoginResult. + public static async Task CreateUserLoginResultFromDatabaseAsync( this IAuthenticationDatabaseService @this, + SqlServer.ISqlCallContext ctx, + IAuthenticationTypeSystem typeSystem, + LoginResult dbResult ) { - /// - /// Helper method that calls and - /// the helper method when - /// is successful or returns a failed based on dbResult error properties if it is on error. - /// - /// This IAuthenticationDatabaseService. - /// The type system to use. - /// The call context to use. - /// The database result to transform. - /// The (never null) UserLoginResult. - public static async Task CreateUserLoginResultFromDatabaseAsync( this IAuthenticationDatabaseService @this, - SqlServer.ISqlCallContext ctx, - IAuthenticationTypeSystem typeSystem, - LoginResult dbResult ) - { - IUserInfo? info = dbResult.IsSuccess - ? typeSystem.UserInfo.FromUserAuthInfo( await @this.ReadUserAuthInfoAsync( ctx, 1, dbResult.UserId ) ) - : null; - return new UserLoginResult( info, dbResult.FailureCode, dbResult.FailureReason, dbResult.FailureCode == (int)KnownLoginFailureCode.UnregisteredUser ); - } + IUserInfo? info = dbResult.IsSuccess + ? typeSystem.UserInfo.FromUserAuthInfo( await @this.ReadUserAuthInfoAsync( ctx, 1, dbResult.UserId ) ) + : null; + return new UserLoginResult( info, dbResult.FailureCode, dbResult.FailureReason, dbResult.FailureCode == (int)KnownLoginFailureCode.UnregisteredUser ); } } diff --git a/CK.DB.AspNet.Auth/AuthenticationTypeSystemExtensions.cs b/CK.DB.AspNet.Auth/AuthenticationTypeSystemExtensions.cs index 9281da8e..f9c54ac0 100644 --- a/CK.DB.AspNet.Auth/AuthenticationTypeSystemExtensions.cs +++ b/CK.DB.AspNet.Auth/AuthenticationTypeSystemExtensions.cs @@ -6,30 +6,29 @@ using System.Linq; using System.Diagnostics.CodeAnalysis; -namespace CK.DB.Auth +namespace CK.DB.Auth; + +/// +/// Extends objects to handle database object model. +/// +public static class AuthenticationTypeSystemExtensions { /// - /// Extends objects to handle database object model. + /// Creates a from a database object. + /// Must return null if o is null. /// - public static class AuthenticationTypeSystemExtensions + /// This UserInfoType. + /// The user information handled by the database implementation. + /// The user info or null if o is null. + [return: NotNullIfNotNull( nameof( o ) )] + public static IUserInfo? FromUserAuthInfo( this IUserInfoType @this, IUserAuthInfo? o ) { - /// - /// Creates a from a database object. - /// Must return null if o is null. - /// - /// This UserInfoType. - /// The user information handled by the database implementation. - /// The user info or null if o is null. - [return: NotNullIfNotNull(nameof(o))] - public static IUserInfo? FromUserAuthInfo( this IUserInfoType @this, IUserAuthInfo? o ) - { - return o != null - ? @this.Create( - o.UserId, - o.UserName, - o.Schemes.Select( x => new StdUserSchemeInfo( x.Name, x.LastUsed ) ).ToArray() ) - : null; - } - + return o != null + ? @this.Create( + o.UserId, + o.UserName, + o.Schemes.Select( x => new StdUserSchemeInfo( x.Name, x.LastUsed ) ).ToArray() ) + : null; } + } diff --git a/CK.DB.AspNet.Auth/DefaultAutoBindingAccountService.cs b/CK.DB.AspNet.Auth/DefaultAutoBindingAccountService.cs index 39c95899..f6b1b422 100644 --- a/CK.DB.AspNet.Auth/DefaultAutoBindingAccountService.cs +++ b/CK.DB.AspNet.Auth/DefaultAutoBindingAccountService.cs @@ -10,68 +10,67 @@ using CK.SqlServer; using Microsoft.Extensions.DependencyInjection; -namespace CK.DB.AspNet.Auth +namespace CK.DB.AspNet.Auth; + +/// +/// Default implementation that will bind accounts as long as the currently logged +/// user is (but this can be changed). +/// +public class DefaultAutoBindingAccountService : IWebFrontAuthAutoBindingAccountService { + readonly IAuthenticationDatabaseService _authPackage; + /// - /// Default implementation that will bind accounts as long as the currently logged - /// user is (but this can be changed). + /// Initializes a new with sets + /// to true by default. /// - public class DefaultAutoBindingAccountService : IWebFrontAuthAutoBindingAccountService + /// The authentication database service. + public DefaultAutoBindingAccountService( IAuthenticationDatabaseService authPackage ) { - readonly IAuthenticationDatabaseService _authPackage; + _authPackage = authPackage; + RequiresCriticalLevel = true; + } - /// - /// Initializes a new with sets - /// to true by default. - /// - /// The authentication database service. - public DefaultAutoBindingAccountService( IAuthenticationDatabaseService authPackage ) - { - _authPackage = authPackage; - RequiresCriticalLevel = true; - } + /// + /// Gets or sets whether the account binding requires a current level. + /// Defaults to true. + /// This may be changed explictly at any time (but this is typically configured once at startup). + /// + public bool RequiresCriticalLevel { get; set; } - /// - /// Gets or sets whether the account binding requires a current level. - /// Defaults to true. - /// This may be changed explictly at any time (but this is typically configured once at startup). - /// - public bool RequiresCriticalLevel { get; set; } + /// + /// Called for each failed login when the user is currently logged in and + /// calls to bind + /// a new provider to the user. + /// + /// The monitor to use. + /// Account binding context. + /// + /// The login result where the contains the new scheme. + /// + public async Task BindAccountAsync( IActivityMonitor monitor, IWebFrontAuthAutoBindingAccountContext context ) + { + if( monitor == null ) throw new ArgumentNullException( nameof( monitor ) ); + if( context == null ) throw new ArgumentNullException( nameof( context ) ); + var auth = context.InitialAuthentication; + if( auth.IsImpersonated ) throw new ArgumentException( "Invalid impersonation.", nameof( context.InitialAuthentication ) ); - /// - /// Called for each failed login when the user is currently logged in and - /// calls to bind - /// a new provider to the user. - /// - /// The monitor to use. - /// Account binding context. - /// - /// The login result where the contains the new scheme. - /// - public async Task BindAccountAsync( IActivityMonitor monitor, IWebFrontAuthAutoBindingAccountContext context ) + if( RequiresCriticalLevel && auth.Level != AuthLevel.Critical ) { - if( monitor == null ) throw new ArgumentNullException( nameof( monitor ) ); - if( context == null ) throw new ArgumentNullException( nameof( context ) ); - var auth = context.InitialAuthentication; - if( auth.IsImpersonated ) throw new ArgumentException( "Invalid impersonation.", nameof( context.InitialAuthentication ) ); - - if( RequiresCriticalLevel && auth.Level != AuthLevel.Critical ) - { - return context.SetError( "User.AccountBinding.CriticalLevelRequired", "User must be logged in Critical level." ); - } - if( auth.Level < AuthLevel.Normal ) - { - return context.SetError( "User.AccountBinding.AtLeastNormalLevelRequired", "User must be logged at least in Normal level." ); - } - IGenericAuthenticationProvider p = _authPackage.FindRequiredProvider( context.CallingScheme ); - var ctx = context.HttpContext.RequestServices.GetRequiredService(); - // Here we trigger an actual login. - // If a bind-without-login is required once, we'll have to introduce an option or a parameter - // to specify it. - // In such case, CreateUserLoginResultFromDatabase must not be called but a UserLoginResult must be returned - // that is based on the current context.InitialAuthentication: in such case, the returned scemes is NOT modified. - UCLResult result = await p.CreateOrUpdateUserAsync( ctx, 1, auth.User.UserId, context.Payload, UCLMode.CreateOrUpdate|UCLMode.WithActualLogin ); - return await _authPackage.CreateUserLoginResultFromDatabaseAsync( ctx, context.AuthenticationTypeSystem, result.LoginResult ); + return context.SetError( "User.AccountBinding.CriticalLevelRequired", "User must be logged in Critical level." ); + } + if( auth.Level < AuthLevel.Normal ) + { + return context.SetError( "User.AccountBinding.AtLeastNormalLevelRequired", "User must be logged at least in Normal level." ); } + IGenericAuthenticationProvider p = _authPackage.FindRequiredProvider( context.CallingScheme ); + var ctx = context.HttpContext.RequestServices.GetRequiredService(); + // Here we trigger an actual login. + // If a bind-without-login is required once, we'll have to introduce an option or a parameter + // to specify it. + // In such case, CreateUserLoginResultFromDatabase must not be called but a UserLoginResult must be returned + // that is based on the current context.InitialAuthentication: in such case, the returned scemes is NOT modified. + UCLResult result = await p.CreateOrUpdateUserAsync( ctx, 1, auth.User.UserId, context.Payload, UCLMode.CreateOrUpdate | UCLMode.WithActualLogin ); + return await _authPackage.CreateUserLoginResultFromDatabaseAsync( ctx, context.AuthenticationTypeSystem, result.LoginResult ); } } diff --git a/CK.DB.AspNet.Auth/SqlWebFrontAuthLoginService.cs b/CK.DB.AspNet.Auth/SqlWebFrontAuthLoginService.cs index 91d3a9bc..4c510903 100644 --- a/CK.DB.AspNet.Auth/SqlWebFrontAuthLoginService.cs +++ b/CK.DB.AspNet.Auth/SqlWebFrontAuthLoginService.cs @@ -12,127 +12,126 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Authentication; -namespace CK.DB.AspNet.Auth +namespace CK.DB.AspNet.Auth; + + +/// +/// Implements bounds to a . +/// This class may be specialized (and since it is a ISingletonAmbientService, its specialization will +/// be automatically selected). +/// +public class SqlWebFrontAuthLoginService : IWebFrontAuthLoginService { + readonly IAuthenticationDatabaseService _authPackage; + readonly IAuthenticationTypeSystem _typeSystem; + readonly IReadOnlyList _providers; /// - /// Implements bounds to a . - /// This class may be specialized (and since it is a ISingletonAmbientService, its specialization will - /// be automatically selected). + /// Initializes a new . /// - public class SqlWebFrontAuthLoginService : IWebFrontAuthLoginService + /// The database service to use. + /// The authentication type system to use. + public SqlWebFrontAuthLoginService( IAuthenticationDatabaseService authPackage, IAuthenticationTypeSystem typeSystem ) { - readonly IAuthenticationDatabaseService _authPackage; - readonly IAuthenticationTypeSystem _typeSystem; - readonly IReadOnlyList _providers; - - /// - /// Initializes a new . - /// - /// The database service to use. - /// The authentication type system to use. - public SqlWebFrontAuthLoginService( IAuthenticationDatabaseService authPackage, IAuthenticationTypeSystem typeSystem ) - { - Throw.CheckNotNullArgument( authPackage ); - Throw.CheckNotNullArgument( typeSystem ); - _authPackage = authPackage; - _typeSystem = typeSystem; - _providers = _authPackage.AllProviders.Select( p => p.ProviderName ).ToArray(); - } + Throw.CheckNotNullArgument( authPackage ); + Throw.CheckNotNullArgument( typeSystem ); + _authPackage = authPackage; + _typeSystem = typeSystem; + _providers = _authPackage.AllProviders.Select( p => p.ProviderName ).ToArray(); + } - /// - /// Gets whether the basic authentication is available. - /// - public virtual bool HasBasicLogin => _authPackage.BasicProvider != null; + /// + /// Gets whether the basic authentication is available. + /// + public virtual bool HasBasicLogin => _authPackage.BasicProvider != null; - /// - /// Gets the existing providers' name that have been installed in the database. - /// - public virtual IReadOnlyList Providers => _providers; + /// + /// Gets the existing providers' name that have been installed in the database. + /// + public virtual IReadOnlyList Providers => _providers; - /// - /// Attempts to login. If it fails, null is returned. must be true for this - /// to be called. - /// - /// Current Http context. - /// The activity monitor to use. - /// The user name. - /// The password. - /// - /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: - /// only checks are done. - /// - /// The or null. - public virtual async Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin = true ) - { - Throw.CheckState( _authPackage.BasicProvider != null ); - var c = ctx.RequestServices.GetRequiredService(); - Debug.Assert( c.Monitor == monitor ); - LoginResult r = await _authPackage.BasicProvider.LoginUserAsync( c, userName, password, actualLogin ); - return await _authPackage.CreateUserLoginResultFromDatabaseAsync( c, _typeSystem, r ); - } + /// + /// Attempts to login. If it fails, null is returned. must be true for this + /// to be called. + /// + /// Current Http context. + /// The activity monitor to use. + /// The user name. + /// The password. + /// + /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: + /// only checks are done. + /// + /// The or null. + public virtual async Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin = true ) + { + Throw.CheckState( _authPackage.BasicProvider != null ); + var c = ctx.RequestServices.GetRequiredService(); + Debug.Assert( c.Monitor == monitor ); + LoginResult r = await _authPackage.BasicProvider.LoginUserAsync( c, userName, password, actualLogin ); + return await _authPackage.CreateUserLoginResultFromDatabaseAsync( c, _typeSystem, r ); + } - /// - /// Creates a payload object for a given scheme that can be used to - /// call . - /// - /// Current Http context. - /// The activity monitor to use. - /// The login scheme (either the provider name to use or starts with the provider name and a dot). - /// A new, empty, provider dependent login payload. - public virtual object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ) - { - return _authPackage.FindRequiredProvider( scheme, mustHavePayload: true ).CreatePayload(); - } + /// + /// Creates a payload object for a given scheme that can be used to + /// call . + /// + /// Current Http context. + /// The activity monitor to use. + /// The login scheme (either the provider name to use or starts with the provider name and a dot). + /// A new, empty, provider dependent login payload. + public virtual object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ) + { + return _authPackage.FindRequiredProvider( scheme, mustHavePayload: true ).CreatePayload(); + } - /// - /// Attempts to login a user using a scheme. - /// A provider for the scheme must exist and the payload must be compatible otherwise an - /// is thrown. - /// - /// Current Http context. - /// The activity monitor to use. - /// The scheme to use. - /// The provider dependent login payload. - /// - /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: - /// only checks are done. - /// - /// The login result. - public virtual async Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload, bool actualLogin = true ) - { - IGenericAuthenticationProvider p = _authPackage.FindRequiredProvider( scheme, false ); - var c = ctx.RequestServices.GetRequiredService(); - Debug.Assert( c.Monitor == monitor ); - LoginResult r = await p.LoginUserAsync( c, payload, actualLogin ); - return await _authPackage.CreateUserLoginResultFromDatabaseAsync( c, _typeSystem, r ); - } + /// + /// Attempts to login a user using a scheme. + /// A provider for the scheme must exist and the payload must be compatible otherwise an + /// is thrown. + /// + /// Current Http context. + /// The activity monitor to use. + /// The scheme to use. + /// The provider dependent login payload. + /// + /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success: + /// only checks are done. + /// + /// The login result. + public virtual async Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload, bool actualLogin = true ) + { + IGenericAuthenticationProvider p = _authPackage.FindRequiredProvider( scheme, false ); + var c = ctx.RequestServices.GetRequiredService(); + Debug.Assert( c.Monitor == monitor ); + LoginResult r = await p.LoginUserAsync( c, payload, actualLogin ); + return await _authPackage.CreateUserLoginResultFromDatabaseAsync( c, _typeSystem, r ); + } - /// - /// Refreshes a by reading the actual user and the impersonated user if any. - /// - /// The current http context. - /// The monitor to use. - /// The current authentication info that should be refreshed. Can be null (None authentication is returned). - /// New expiration date (can be the same as the current's one). - /// The refreshed information. Never null but may be the None authentication info. - public virtual async Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ) + /// + /// Refreshes a by reading the actual user and the impersonated user if any. + /// + /// The current http context. + /// The monitor to use. + /// The current authentication info that should be refreshed. Can be null (None authentication is returned). + /// New expiration date (can be the same as the current's one). + /// The refreshed information. Never null but may be the None authentication info. + public virtual async Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ) + { + if( current == null ) return _typeSystem.AuthenticationInfo.None; + var c = ctx.RequestServices.GetRequiredService(); + IUserAuthInfo? dbActual = await _authPackage.ReadUserAuthInfoAsync( c, current.UnsafeActualUser.UserId, current.UnsafeActualUser.UserId ); + Throw.DebugAssert( dbActual != null ); + IUserInfo? actual = _typeSystem.UserInfo.FromUserAuthInfo( dbActual ); + IAuthenticationInfo refreshed = _typeSystem.AuthenticationInfo.Create( actual, newExpires, current.CriticalExpires, current.DeviceId ); + if( refreshed.Level != AuthLevel.None && current.IsImpersonated ) { - if( current == null ) return _typeSystem.AuthenticationInfo.None; - var c = ctx.RequestServices.GetRequiredService(); - IUserAuthInfo? dbActual = await _authPackage.ReadUserAuthInfoAsync( c, current.UnsafeActualUser.UserId, current.UnsafeActualUser.UserId ); - Throw.DebugAssert( dbActual != null ); - IUserInfo? actual = _typeSystem.UserInfo.FromUserAuthInfo( dbActual ); - IAuthenticationInfo refreshed = _typeSystem.AuthenticationInfo.Create( actual, newExpires, current.CriticalExpires, current.DeviceId ); - if( refreshed.Level != AuthLevel.None && current.IsImpersonated ) - { - IUserAuthInfo? dbUser = await _authPackage.ReadUserAuthInfoAsync( c, current.UnsafeUser.UserId, current.UnsafeUser.UserId ); - Throw.DebugAssert( dbUser != null ); - IUserInfo user = _typeSystem.UserInfo.FromUserAuthInfo( dbUser ) ?? _typeSystem.UserInfo.Anonymous; - refreshed = refreshed.Impersonate( user ); - } - return refreshed; + IUserAuthInfo? dbUser = await _authPackage.ReadUserAuthInfoAsync( c, current.UnsafeUser.UserId, current.UnsafeUser.UserId ); + Throw.DebugAssert( dbUser != null ); + IUserInfo user = _typeSystem.UserInfo.FromUserAuthInfo( dbUser ) ?? _typeSystem.UserInfo.Anonymous; + refreshed = refreshed.Impersonate( user ); } - + return refreshed; } + } diff --git a/CK.Testing.AspNetServer.Auth/AspNetAuthServerTestHelperExtensions.cs b/CK.Testing.AspNetServer.Auth/AspNetAuthServerTestHelperExtensions.cs index 3cc05324..099cceb2 100644 --- a/CK.Testing.AspNetServer.Auth/AspNetAuthServerTestHelperExtensions.cs +++ b/CK.Testing.AspNetServer.Auth/AspNetAuthServerTestHelperExtensions.cs @@ -7,50 +7,49 @@ using System; using System.Threading.Tasks; -namespace CK.Testing +namespace CK.Testing; + +/// +/// Expose . +/// +public static class AspNetAuthServerTestHelperExtensions { /// - /// Expose . + /// Creates, configures and starts a that supports authentication with + /// (this is for tests only). + /// + /// If this doesn't have a implementation, + /// the is automatically registered and if no + /// exists, the is automatically registered. + /// /// - public static class AspNetAuthServerTestHelperExtensions + /// Note: IUserInfoProvider and IWebFrontAuthLoginService must obviously be coupled somehow. ISP principle + /// made us separate the 2 concerns but implementations should be coherent. This cannot be challenged + /// here (and in a way it shouldn't be). + /// + /// + /// This StObjMap. + /// Optional option configuration. + /// Optional application configurator. + /// A running Asp.NET server with authentication support. + public static Task CreateRunningAspNetAuthenticationServerAsync( this WebApplicationBuilder builder, + IStObjMap map, + Action? authOptions = null, + Action? configureApplication = null ) { - /// - /// Creates, configures and starts a that supports authentication with - /// (this is for tests only). - /// - /// If this doesn't have a implementation, - /// the is automatically registered and if no - /// exists, the is automatically registered. - /// - /// - /// Note: IUserInfoProvider and IWebFrontAuthLoginService must obviously be coupled somehow. ISP principle - /// made us separate the 2 concerns but implementations should be coherent. This cannot be challenged - /// here (and in a way it shouldn't be). - /// - /// - /// This StObjMap. - /// Optional option configuration. - /// Optional application configurator. - /// A running Asp.NET server with authentication support. - public static Task CreateRunningAspNetAuthenticationServerAsync( this WebApplicationBuilder builder, - IStObjMap map, - Action? authOptions = null, - Action? configureApplication = null ) + // Use TryAdd to allow manual services configuration if the CKomposable map is missing it. + if( !map.Services.Mappings.ContainsKey( typeof( IUserInfoProvider ) ) ) { - // Use TryAdd to allow manual services configuration if the CKomposable map is missing it. - if( !map.Services.Mappings.ContainsKey( typeof( IUserInfoProvider ) ) ) - { - builder.Services.TryAddSingleton(); - } - if( !map.Services.Mappings.ContainsKey( typeof( IWebFrontAuthLoginService ) ) ) - { - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - } - builder.AddUnsafeAllowAllCors(); - builder.AddWebFrontAuth( authOptions ); - return builder.CreateRunningAspNetServerAsync( map, configureApplication ); + builder.Services.TryAddSingleton(); } - + if( !map.Services.Mappings.ContainsKey( typeof( IWebFrontAuthLoginService ) ) ) + { + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + } + builder.AddUnsafeAllowAllCors(); + builder.AddWebFrontAuth( authOptions ); + return builder.CreateRunningAspNetServerAsync( map, configureApplication ); } + } diff --git a/CK.Testing.AspNetServer.Auth/AuthServerResponse.cs b/CK.Testing.AspNetServer.Auth/AuthServerResponse.cs index a8f5bc87..b1aee90a 100644 --- a/CK.Testing.AspNetServer.Auth/AuthServerResponse.cs +++ b/CK.Testing.AspNetServer.Auth/AuthServerResponse.cs @@ -6,67 +6,66 @@ using System.Text; using System.Threading.Tasks; -namespace CK.Testing +namespace CK.Testing; + +/// +/// Models the response of the authentication server. +/// +public sealed class AuthServerResponse { /// - /// Models the response of the authentication server. + /// Gets the authentication info. /// - public sealed class AuthServerResponse - { - /// - /// Gets the authentication info. - /// - public IAuthenticationInfo? Info { get; set; } + public IAuthenticationInfo? Info { get; set; } - /// - /// Gets the token. - /// - public string? Token { get; set; } + /// + /// Gets the token. + /// + public string? Token { get; set; } - public bool RememberMe { get; set; } + public bool RememberMe { get; set; } - public bool Refreshable { get; set; } + public bool Refreshable { get; set; } - public string? ErrorId { get; set; } + public string? ErrorId { get; set; } - public string? ErrorText { get; set; } + public string? ErrorText { get; set; } - public string?[]? Schemes { get; set; } + public string?[]? Schemes { get; set; } - public string? Version { get; set; } + public string? Version { get; set; } - public IList<(string, string?)> UserData { get; } = new List<(string, string?)>(); + public IList<(string, string?)> UserData { get; } = new List<(string, string?)>(); - /// - /// Parse a server response. - /// - /// The type system. - /// The json string. - /// The server response. - public static AuthServerResponse Parse( IAuthenticationTypeSystem t, string json ) + /// + /// Parse a server response. + /// + /// The type system. + /// The json string. + /// The server response. + public static AuthServerResponse Parse( IAuthenticationTypeSystem t, string json ) + { + JObject o = JObject.Parse( json ); + var r = new AuthServerResponse(); + if( o["info"]?.Type == JTokenType.Object ) { - JObject o = JObject.Parse( json ); - var r = new AuthServerResponse(); - if( o["info"]?.Type == JTokenType.Object ) - { - r.Info = t.AuthenticationInfo.FromJObject( (JObject)o["info"]! ); - } - r.Token = (string?)o["token"]; - r.Refreshable = (bool?)o["refreshable"] ?? false; - r.RememberMe = (bool?)o["rememberMe"] ?? false; - r.Schemes = o["schemes"]?.Values().ToArray(); - r.Version = (string?)o["version"]; - var userData = (JObject?)o["userData"]; - if( userData != null ) + r.Info = t.AuthenticationInfo.FromJObject( (JObject)o["info"]! ); + } + r.Token = (string?)o["token"]; + r.Refreshable = (bool?)o["refreshable"] ?? false; + r.RememberMe = (bool?)o["rememberMe"] ?? false; + r.Schemes = o["schemes"]?.Values().ToArray(); + r.Version = (string?)o["version"]; + var userData = (JObject?)o["userData"]; + if( userData != null ) + { + foreach( var kv in userData ) { - foreach( var kv in userData ) - { - r.UserData.Add( (kv.Key, (string?)kv.Value) ); - } + r.UserData.Add( (kv.Key, (string?)kv.Value) ); } - r.ErrorId = (string?)o["errorId"]; - r.ErrorText = (string?)o["errorText"]; - return r; } + r.ErrorId = (string?)o["errorId"]; + r.ErrorText = (string?)o["errorText"]; + return r; } } diff --git a/CK.Testing.AspNetServer.Auth/AuthenticationCookieValues.cs b/CK.Testing.AspNetServer.Auth/AuthenticationCookieValues.cs index c9712688..b11d552b 100644 --- a/CK.Testing.AspNetServer.Auth/AuthenticationCookieValues.cs +++ b/CK.Testing.AspNetServer.Auth/AuthenticationCookieValues.cs @@ -1,25 +1,24 @@ using Newtonsoft.Json.Linq; -namespace CK.Testing +namespace CK.Testing; + +/// +/// Captures the authentication cookie values. +/// +/// +/// +/// +/// +/// +public record struct AuthenticationCookieValues( string? AuthCookie, JObject? LTCookie, string? LTDeviceId, string? LTUserId, string? LTUserName ) { - /// - /// Captures the authentication cookie values. - /// - /// - /// - /// - /// - /// - public record struct AuthenticationCookieValues( string? AuthCookie, JObject? LTCookie, string? LTDeviceId, string? LTUserId, string? LTUserName ) + public static implicit operator (string? AuthCookie, JObject? LTCookie, string? LTDeviceId, string? LTUserId, string? LTUserName)( AuthenticationCookieValues value ) { - public static implicit operator (string? AuthCookie, JObject? LTCookie, string? LTDeviceId, string? LTUserId, string? LTUserName)( AuthenticationCookieValues value ) - { - return (value.AuthCookie, value.LTCookie, value.LTDeviceId, value.LTUserId, value.LTUserName); - } + return (value.AuthCookie, value.LTCookie, value.LTDeviceId, value.LTUserId, value.LTUserName); + } - public static implicit operator AuthenticationCookieValues( (string? AuthCookie, JObject? LTCookie, string? LTDeviceId, string? LTUserId, string? LTUserName) value ) - { - return new AuthenticationCookieValues( value.AuthCookie, value.LTCookie, value.LTDeviceId, value.LTUserId, value.LTUserName ); - } + public static implicit operator AuthenticationCookieValues( (string? AuthCookie, JObject? LTCookie, string? LTDeviceId, string? LTUserId, string? LTUserName) value ) + { + return new AuthenticationCookieValues( value.AuthCookie, value.LTCookie, value.LTDeviceId, value.LTUserId, value.LTUserName ); } } diff --git a/CK.Testing.AspNetServer.Auth/FakeUserDatabase.cs b/CK.Testing.AspNetServer.Auth/FakeUserDatabase.cs index 38416ff7..ab3a09c2 100644 --- a/CK.Testing.AspNetServer.Auth/FakeUserDatabase.cs +++ b/CK.Testing.AspNetServer.Auth/FakeUserDatabase.cs @@ -5,42 +5,40 @@ using System.Linq; using System; -namespace CK.Testing +namespace CK.Testing; + +/// +/// Fake user database that contains "System" (1), "Alice" (3711), "Albert" (3712), "Robert" (3713) and "Hubert" (3714). +/// "Alice", "Albert" and "Hubert" have the "Basic" provider. +/// "Hubert" has the "Google" provider. +/// +/// is totally mutable and everything is virtual. +/// +/// +[ExcludeCKType] +public class FakeUserDatabase : IUserInfoProvider { - /// - /// Fake user database that contains "System" (1), "Alice" (3711), "Albert" (3712), "Robert" (3713) and "Hubert" (3714). - /// "Alice", "Albert" and "Hubert" have the "Basic" provider. - /// "Hubert" has the "Google" provider. - /// - /// is totally mutable and everything is virtual. - /// - /// - [ExcludeCKType] - public class FakeUserDatabase : IUserInfoProvider - { - readonly List _users; - readonly IAuthenticationTypeSystem _typeSystem; + readonly List _users; + readonly IAuthenticationTypeSystem _typeSystem; - public FakeUserDatabase( IAuthenticationTypeSystem typeSystem ) - { - _users = - [ - typeSystem.UserInfo.Create( 1, "System" ), - typeSystem.UserInfo.Create( 3711, "Alice", [new StdUserSchemeInfo( "Basic", DateTime.MinValue )] ), - typeSystem.UserInfo.Create( 3712, "Albert", [new StdUserSchemeInfo( "Basic", DateTime.MinValue )] ), - typeSystem.UserInfo.Create( 3713, "Robert" ), - typeSystem.UserInfo.Create( 3714, "Hubert", [new StdUserSchemeInfo( "Basic", DateTime.MinValue ), new StdUserSchemeInfo( "Google", DateTime.MinValue )] ) - ]; - _typeSystem = typeSystem; - } + public FakeUserDatabase( IAuthenticationTypeSystem typeSystem ) + { + _users = + [ + typeSystem.UserInfo.Create( 1, "System" ), + typeSystem.UserInfo.Create( 3711, "Alice", [new StdUserSchemeInfo( "Basic", DateTime.MinValue )] ), + typeSystem.UserInfo.Create( 3712, "Albert", [new StdUserSchemeInfo( "Basic", DateTime.MinValue )] ), + typeSystem.UserInfo.Create( 3713, "Robert" ), + typeSystem.UserInfo.Create( 3714, "Hubert", [new StdUserSchemeInfo( "Basic", DateTime.MinValue ), new StdUserSchemeInfo( "Google", DateTime.MinValue )] ) + ]; + _typeSystem = typeSystem; + } - public virtual IList AllUsers => _users; + public virtual IList AllUsers => _users; - public virtual ValueTask GetUserInfoAsync( IActivityMonitor monitor, int userId ) - { - var u = _users.FirstOrDefault( u => u.UserId == userId ) ?? _typeSystem.UserInfo.Anonymous; - return ValueTask.FromResult( u ); - } + public virtual ValueTask GetUserInfoAsync( IActivityMonitor monitor, int userId ) + { + var u = _users.FirstOrDefault( u => u.UserId == userId ) ?? _typeSystem.UserInfo.Anonymous; + return ValueTask.FromResult( u ); } - } diff --git a/CK.Testing.AspNetServer.Auth/FakeWebFrontAuthLoginService.cs b/CK.Testing.AspNetServer.Auth/FakeWebFrontAuthLoginService.cs index 1dd2327e..dee53eea 100644 --- a/CK.Testing.AspNetServer.Auth/FakeWebFrontAuthLoginService.cs +++ b/CK.Testing.AspNetServer.Auth/FakeWebFrontAuthLoginService.cs @@ -7,127 +7,125 @@ using System.Linq; using System; -namespace CK.Testing +namespace CK.Testing; + +/// +/// Fake login service bound to the that +/// implements only "Basic" login based on the availability +/// and password "success" for every existing users. +/// +/// This class can be totally specialized. +/// +/// +[ExcludeCKType] +public class FakeWebFrontAuthLoginService : IWebFrontAuthLoginService { + readonly IAuthenticationTypeSystem _typeSystem; + readonly FakeUserDatabase _userDB; + /// - /// Fake login service bound to the that - /// implements only "Basic" login based on the availability - /// and password "success" for every existing users. - /// - /// This class can be totally specialized. - /// + /// Initializes a new . /// - [ExcludeCKType] - public class FakeWebFrontAuthLoginService : IWebFrontAuthLoginService + /// The authentication type system. + /// The fake user database. + public FakeWebFrontAuthLoginService( IAuthenticationTypeSystem typeSystem, FakeUserDatabase userDB ) { - readonly IAuthenticationTypeSystem _typeSystem; - readonly FakeUserDatabase _userDB; - - /// - /// Initializes a new . - /// - /// The authentication type system. - /// The fake user database. - public FakeWebFrontAuthLoginService( IAuthenticationTypeSystem typeSystem, FakeUserDatabase userDB ) - { - _typeSystem = typeSystem; - _userDB = userDB; - } + _typeSystem = typeSystem; + _userDB = userDB; + } - /// - /// Gets the fake user database. - /// - public FakeUserDatabase UserDatabase => _userDB; + /// + /// Gets the fake user database. + /// + public FakeUserDatabase UserDatabase => _userDB; - /// - /// - /// This default implementation always returns true. - /// - public virtual bool HasBasicLogin => true; + /// + /// + /// This default implementation always returns true. + /// + public virtual bool HasBasicLogin => true; - /// - /// - /// This default implementation returns "Basic". - /// - public virtual IReadOnlyList Providers => ["Basic"]; + /// + /// + /// This default implementation returns "Basic". + /// + public virtual IReadOnlyList Providers => ["Basic"]; - /// - /// - /// This default implementation expect only the "Basic" and returns a List<(string, object?)> - /// that should be filled with a "userName" and "password" key-value pairs before calling . - /// - /// Any other scheme throws a . - /// - /// - public virtual object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ) + /// + /// + /// This default implementation expect only the "Basic" and returns a List<(string, object?)> + /// that should be filled with a "userName" and "password" key-value pairs before calling . + /// + /// Any other scheme throws a . + /// + /// + public virtual object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ) + { + if( scheme == "Basic" ) { - if( scheme == "Basic" ) - { - return new List<(string, object?)>(); - } - throw new ArgumentException( $"Unknown scheme '{scheme}'." ); + return new List<(string, object?)>(); } + throw new ArgumentException( $"Unknown scheme '{scheme}'." ); + } - /// - /// - /// This default implementation succeeds for every user of the registered in "Basic" provider - /// when the password is "success". - /// - public virtual Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin ) + /// + /// + /// This default implementation succeeds for every user of the registered in "Basic" provider + /// when the password is "success". + /// + public virtual Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin ) + { + IUserInfo? u = null; + if( password == "success" ) { - IUserInfo? u = null; - if( password == "success" ) + u = _userDB.AllUsers.FirstOrDefault( i => i.UserName == userName ); + if( u != null && u.Schemes.Any( p => p.Name == "Basic" ) ) { - u = _userDB.AllUsers.FirstOrDefault( i => i.UserName == userName ); - if( u != null && u.Schemes.Any( p => p.Name == "Basic" ) ) - { - _userDB.AllUsers.Remove( u ); - u = _typeSystem.UserInfo.Create( u.UserId, u.UserName, new[] { new StdUserSchemeInfo( "Basic", DateTime.UtcNow ) } ); - _userDB.AllUsers.Add( u ); - } - return Task.FromResult( new UserLoginResult( u, 0, null, false ) ); + _userDB.AllUsers.Remove( u ); + u = _typeSystem.UserInfo.Create( u.UserId, u.UserName, new[] { new StdUserSchemeInfo( "Basic", DateTime.UtcNow ) } ); + _userDB.AllUsers.Add( u ); } - return Task.FromResult( new UserLoginResult( null, 1, "Login failed!", false ) ); + return Task.FromResult( new UserLoginResult( u, 0, null, false ) ); } + return Task.FromResult( new UserLoginResult( null, 1, "Login failed!", false ) ); + } - /// - /// - /// This default implementation only handles the "Basic" List<KeyValuePair<string, object>> payload - /// and calls . - /// - public virtual Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string providerName, object payload, bool actualLogin ) - { - Throw.CheckArgument( "Only Basic is supported.", providerName == "Basic" ); - if( payload is not List<(string Key, object Value)> o ) throw new ArgumentException( "Invalid payload (expected list of (string,object)).", nameof( payload ) ); - return BasicLoginAsync( ctx, - monitor, - (string)o.FirstOrDefault( kv => kv.Key == "userName" ).Value, - (string)o.FirstOrDefault( kv => kv.Key == "password" ).Value, - actualLogin ); - } + /// + /// + /// This default implementation only handles the "Basic" List<KeyValuePair<string, object>> payload + /// and calls . + /// + public virtual Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string providerName, object payload, bool actualLogin ) + { + Throw.CheckArgument( "Only Basic is supported.", providerName == "Basic" ); + if( payload is not List<(string Key, object Value)> o ) throw new ArgumentException( "Invalid payload (expected list of (string,object)).", nameof( payload ) ); + return BasicLoginAsync( ctx, + monitor, + (string)o.FirstOrDefault( kv => kv.Key == "userName" ).Value, + (string)o.FirstOrDefault( kv => kv.Key == "password" ).Value, + actualLogin ); + } - /// - /// - /// This default implementation is a standard implementation of a refresh based on the existence of the - /// in the database. - /// - public virtual Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ) + /// + /// + /// This default implementation is a standard implementation of a refresh based on the existence of the + /// in the database. + /// + public virtual Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ) + { + current = current.CheckExpiration(); + if( current.Level < AuthLevel.Normal ) { - current = current.CheckExpiration(); - if( current.Level < AuthLevel.Normal ) - { - return Task.FromResult( current ); - } - var stillHere = _userDB.AllUsers.FirstOrDefault( i => i.UserName == current.ActualUser.UserName ); - if( stillHere != null ) - { - monitor.Info( $"Refreshed authentication for '{current.ActualUser.UserName}'." ); - return Task.FromResult( current.SetExpires( newExpires ) ); - } - monitor.Info( $"Failed to refresh authentication for '{current.ActualUser.UserName}'." ); - current = _typeSystem.AuthenticationInfo.Create( current.ActualUser, deviceId: current.DeviceId ); return Task.FromResult( current ); } + var stillHere = _userDB.AllUsers.FirstOrDefault( i => i.UserName == current.ActualUser.UserName ); + if( stillHere != null ) + { + monitor.Info( $"Refreshed authentication for '{current.ActualUser.UserName}'." ); + return Task.FromResult( current.SetExpires( newExpires ) ); + } + monitor.Info( $"Failed to refresh authentication for '{current.ActualUser.UserName}'." ); + current = _typeSystem.AuthenticationInfo.Create( current.ActualUser, deviceId: current.DeviceId ); + return Task.FromResult( current ); } - } diff --git a/CK.Testing.AspNetServer.Auth/RunningAspNetAuthServerExtensions.cs b/CK.Testing.AspNetServer.Auth/RunningAspNetAuthServerExtensions.cs index 4ddab382..8ecda7e0 100644 --- a/CK.Testing.AspNetServer.Auth/RunningAspNetAuthServerExtensions.cs +++ b/CK.Testing.AspNetServer.Auth/RunningAspNetAuthServerExtensions.cs @@ -14,342 +14,341 @@ using FluentAssertions; using Microsoft.AspNetCore.Http; -namespace CK.Testing +namespace CK.Testing; + +public static class RunningAspNetAuthServerExtensions { - public static class RunningAspNetAuthServerExtensions - { - /// - /// The refresh authentication uri. - /// - public const string RefreshUri = "/.webfront/c/refresh"; + /// + /// The refresh authentication uri. + /// + public const string RefreshUri = "/.webfront/c/refresh"; - /// - /// The unsafe direct login uri. - /// - public const string UnsafeDirectLoginUri = "/.webfront/c/unsafeDirectLogin"; + /// + /// The unsafe direct login uri. + /// + public const string UnsafeDirectLoginUri = "/.webfront/c/unsafeDirectLogin"; - /// - /// The basic login uri. - /// - public const string BasicLoginUri = "/.webfront/c/basicLogin"; + /// + /// The basic login uri. + /// + public const string BasicLoginUri = "/.webfront/c/basicLogin"; - /// - /// The impersonate uri. - /// - public const string ImpersonateUri = "/.webfront/c/impersonate"; + /// + /// The impersonate uri. + /// + public const string ImpersonateUri = "/.webfront/c/impersonate"; - /// - /// The logout uri. - /// - public const string LogoutUri = "/.webfront/c/logout"; + /// + /// The logout uri. + /// + public const string LogoutUri = "/.webfront/c/logout"; - /// - /// The clear token uri. - /// - public const string TokenExplainUri = "/.webfront/token"; + /// + /// The clear token uri. + /// + public const string TokenExplainUri = "/.webfront/token"; - /// - /// The start login uri. - /// - public const string StartLoginUri = "/.webfront/c/startLogin"; + /// + /// The start login uri. + /// + public const string StartLoginUri = "/.webfront/c/startLogin"; - /// - /// Gets the . - /// - /// This server. - /// The . - public static IAuthenticationTypeSystem GetAuthenticationTypeSystem( this RunningAspNetServer server ) => server.Services.GetRequiredService(); + /// + /// Gets the . + /// + /// This server. + /// The . + public static IAuthenticationTypeSystem GetAuthenticationTypeSystem( this RunningAspNetServer server ) => server.Services.GetRequiredService(); - /// - /// Gets the . - /// - /// This server. - /// The authentication options. - public static WebFrontAuthOptions GetAuthenticationOptions( this RunningAspNetServer server ) - { - var o = server.Services.GetRequiredService>(); - return o.Get( WebFrontAuthOptions.OnlyAuthenticationScheme ); - } + /// + /// Gets the . + /// + /// This server. + /// The authentication options. + public static WebFrontAuthOptions GetAuthenticationOptions( this RunningAspNetServer server ) + { + var o = server.Services.GetRequiredService>(); + return o.Get( WebFrontAuthOptions.OnlyAuthenticationScheme ); + } - /// - /// Calls and returns the server response. - /// - /// This client. - /// - /// True to trigger a call to . - /// By default, when is false, the - /// is forwarded. - /// - /// True to return the version. - /// True to return the available authentication schemes. - /// The server response. - public static async Task AuthenticationRefreshAsync( this RunningAspNetServer.RunningClient client, - bool callBackend = false, - bool version = false, - bool schemes = false ) + /// + /// Calls and returns the server response. + /// + /// This client. + /// + /// True to trigger a call to . + /// By default, when is false, the + /// is forwarded. + /// + /// True to return the version. + /// True to return the available authentication schemes. + /// The server response. + public static async Task AuthenticationRefreshAsync( this RunningAspNetServer.RunningClient client, + bool callBackend = false, + bool version = false, + bool schemes = false ) + { + var url = RefreshUri; + if( callBackend || version || schemes ) { - var url = RefreshUri; - if( callBackend || version || schemes ) + url += '?'; + if( callBackend ) url += "callBackend"; + if( version ) { - url += '?'; - if( callBackend ) url += "callBackend"; - if( version ) - { - if( callBackend ) url += '&'; - url += "version"; - } - if( schemes ) - { - if( callBackend || version ) url += '&'; - url += "schemes"; - } + if( callBackend ) url += '&'; + url += "version"; + } + if( schemes ) + { + if( callBackend || version ) url += '&'; + url += "schemes"; } - using HttpResponseMessage tokenRefresh = await client.GetAsync( url ); - tokenRefresh.EnsureSuccessStatusCode(); - return await HandleResponseAsync( client, tokenRefresh ); } + using HttpResponseMessage tokenRefresh = await client.GetAsync( url ); + tokenRefresh.EnsureSuccessStatusCode(); + return await HandleResponseAsync( client, tokenRefresh ); + } - static async Task HandleResponseAsync( RunningAspNetServer.RunningClient client, HttpResponseMessage m ) - { - var r = AuthServerResponse.Parse( client.Server.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); - client.Token = r.Token; - return r; - } + static async Task HandleResponseAsync( RunningAspNetServer.RunningClient client, HttpResponseMessage m ) + { + var r = AuthServerResponse.Parse( client.Server.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); + client.Token = r.Token; + return r; + } - /// - /// Gets . This clears the . - /// - /// This client. - /// The awaitable. - public static async Task AuthenticationLogoutAsync( this RunningAspNetServer.RunningClient client ) - { - using HttpResponseMessage nop = await client.GetAsync( LogoutUri ); - client.Token = null; - } + /// + /// Gets . This clears the . + /// + /// This client. + /// The awaitable. + public static async Task AuthenticationLogoutAsync( this RunningAspNetServer.RunningClient client ) + { + using HttpResponseMessage nop = await client.GetAsync( LogoutUri ); + client.Token = null; + } - /// - /// Calls and returns the server response if it is successful. - /// - /// This client. - /// The user name to impersonate. - /// The server response or null if impersonation failed. - public static async Task AuthenticationImpersonateAsync( this RunningAspNetServer.RunningClient client, string userName ) - { - using HttpResponseMessage tokenRefresh = await client.PostJsonAsync( ImpersonateUri, $$"""{"userName":"{{userName}}"}""" ); - return tokenRefresh.StatusCode == System.Net.HttpStatusCode.OK - ? await HandleResponseAsync( client, tokenRefresh ) - : null; - } + /// + /// Calls and returns the server response if it is successful. + /// + /// This client. + /// The user name to impersonate. + /// The server response or null if impersonation failed. + public static async Task AuthenticationImpersonateAsync( this RunningAspNetServer.RunningClient client, string userName ) + { + using HttpResponseMessage tokenRefresh = await client.PostJsonAsync( ImpersonateUri, $$"""{"userName":"{{userName}}"}""" ); + return tokenRefresh.StatusCode == System.Net.HttpStatusCode.OK + ? await HandleResponseAsync( client, tokenRefresh ) + : null; + } - /// - /// Calls and returns the server response if it is successful. - /// - /// This client. - /// The user identifier to impersonate. - /// The server response or null if impersonation failed. - public static async Task AuthenticationImpersonateAsync( this RunningAspNetServer.RunningClient client, int userId ) - { - using HttpResponseMessage tokenRefresh = await client.PostJsonAsync( ImpersonateUri, $$"""{"userId":{{userId}}}""" ); - return tokenRefresh.StatusCode == System.Net.HttpStatusCode.OK - ? await HandleResponseAsync( client, tokenRefresh ) - : null; - } + /// + /// Calls and returns the server response if it is successful. + /// + /// This client. + /// The user identifier to impersonate. + /// The server response or null if impersonation failed. + public static async Task AuthenticationImpersonateAsync( this RunningAspNetServer.RunningClient client, int userId ) + { + using HttpResponseMessage tokenRefresh = await client.PostJsonAsync( ImpersonateUri, $$"""{"userId":{{userId}}}""" ); + return tokenRefresh.StatusCode == System.Net.HttpStatusCode.OK + ? await HandleResponseAsync( client, tokenRefresh ) + : null; + } - /// - /// Calls (or if is true) - /// and check the and cookies set by the server. - /// - /// This client. - /// The user name. - /// True if the login must succeed, false otherwise. - /// True to use the on Basic scheme. - /// False to not remember the authentication. - /// True to impersonate the current user. - /// Optional user data (will be in . - /// Password to use. - /// The server response. - public static async Task AuthenticationBasicLoginAsync( this RunningAspNetServer.RunningClient client, - string userName, - bool expectSuccess, - bool useGenericWrapper = false, - bool rememberMe = true, - bool impersonateActualUser = false, - string? jsonUserData = null, - string password = "success" ) + /// + /// Calls (or if is true) + /// and check the and cookies set by the server. + /// + /// This client. + /// The user name. + /// True if the login must succeed, false otherwise. + /// True to use the on Basic scheme. + /// False to not remember the authentication. + /// True to impersonate the current user. + /// Optional user data (will be in . + /// Password to use. + /// The server response. + public static async Task AuthenticationBasicLoginAsync( this RunningAspNetServer.RunningClient client, + string userName, + bool expectSuccess, + bool useGenericWrapper = false, + bool rememberMe = true, + bool impersonateActualUser = false, + string? jsonUserData = null, + string password = "success" ) + { + string uri; + string body; + if( useGenericWrapper ) { - string uri; - string body; - if( useGenericWrapper ) + uri = UnsafeDirectLoginUri; + if( rememberMe ) { - uri = UnsafeDirectLoginUri; - if( rememberMe ) + if( impersonateActualUser ) { - if( impersonateActualUser ) - { - body = $$"""{"Provider":"Basic", "RememberMe":true, "ImpersonateActualUser":true, "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; - } - else - { - body = $$"""{"Provider":"Basic", "RememberMe":true, "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; - } + body = $$"""{"Provider":"Basic", "RememberMe":true, "ImpersonateActualUser":true, "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; } else { - if( impersonateActualUser ) - { - body = $$"""{"Provider":"Basic", "ImpersonateActualUser":true, "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; - } - else - { - body = $$"""{"Provider":"Basic", "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; - } + body = $$"""{"Provider":"Basic", "RememberMe":true, "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; } } else { - uri = BasicLoginUri; - if( rememberMe ) + if( impersonateActualUser ) { - if( impersonateActualUser ) - { - body = $$"""{"userName":"{{userName}}", "password":"{{password}}", "rememberMe":true, "impersonateActualUser":true"""; - } - else - { - body = $$"""{"userName":"{{userName}}", "password":"{{password}}", "rememberMe":true"""; - } + body = $$"""{"Provider":"Basic", "ImpersonateActualUser":true, "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; } else { - if( impersonateActualUser ) - { - body = $$"""{"userName":"{{userName}}", "password":"{{password}}","ImpersonateActualUser":true"""; - } - else - { - body = $$"""{"userName":"{{userName}}", "password":"{{password}}" """; - } + body = $$"""{"Provider":"Basic", "Payload": {"userName":"{{userName}}","password":"{{password}}"}"""; } } - if( jsonUserData != null ) - { - body += $$""", "userData": {{jsonUserData}}"""; - } - body += "}"; - using HttpResponseMessage responseMessage = await client.PostJsonAsync( uri, body ); - var response = await HandleResponseAsync( client, responseMessage ); - Throw.DebugAssert( response.Info != null ); - if( expectSuccess ) + } + else + { + uri = BasicLoginUri; + if( rememberMe ) { - responseMessage.EnsureSuccessStatusCode(); - response.Info.Level.Should().BeOneOf( AuthLevel.Normal, AuthLevel.Critical ); - response.Info.ActualUser.UserName.Should().Be( userName ); - CheckClientCookies( client, response, rememberMe ); + if( impersonateActualUser ) + { + body = $$"""{"userName":"{{userName}}", "password":"{{password}}", "rememberMe":true, "impersonateActualUser":true"""; + } + else + { + body = $$"""{"userName":"{{userName}}", "password":"{{password}}", "rememberMe":true"""; + } } else { - // TODO: Precise the login failure behavior (make it configurable?). - CheckClientCookies( client, response, rememberMe ); - } - return response; - - static void CheckClientCookies( RunningAspNetServer.RunningClient client, AuthServerResponse response, bool expectedRememberMe ) - { - var options = client.Server.GetAuthenticationOptions(); - var cookieName = options.AuthCookieName; - var ltCookieName = cookieName + "LT"; - switch( options.CookieMode ) - { - case AuthenticationCookieMode.WebFrontPath: - { - client.CookieContainer.GetCookies( client.BaseAddress ).Should().BeEmpty(); - var allCookies = client.CookieContainer.GetCookies( new Uri( client.BaseAddress, "/.webfront/c/" ) ); - allCookies.Should().HaveCount( 2 ); - CookieIsNotTheSameAsToken( allCookies, cookieName, response ); - CheckLongTermCookie( expectedRememberMe, allCookies, ltCookieName ); - break; - } - case AuthenticationCookieMode.RootPath: - { - var allCookies = client.CookieContainer.GetCookies( client.BaseAddress ); - allCookies.Should().HaveCount( 2 ); - CookieIsNotTheSameAsToken( allCookies, cookieName, response ); - CheckLongTermCookie( expectedRememberMe, allCookies, ltCookieName ); - break; - } - case AuthenticationCookieMode.None: - { - // RemmeberMe returned by the server is always false when CookieMode is None. - expectedRememberMe = false; - client.CookieContainer.GetCookies( client.BaseAddress ).Should().BeEmpty(); - client.CookieContainer.GetCookies( new Uri( client.BaseAddress, "/.webfront/c/" ) ).Should().BeEmpty(); - break; - } - } - response.RememberMe.Should().Be( expectedRememberMe ); - - static void CheckLongTermCookie( bool rememberMe, System.Net.CookieCollection all, string cookieName ) + if( impersonateActualUser ) { - var cookie = all.Single( c => c.Name == cookieName ).Value; - cookie = HttpUtility.UrlDecode( cookie ); - var longTerm = JObject.Parse( cookie ); - ((string?)longTerm[StdAuthenticationTypeSystem.DeviceIdKeyType]).Should().NotBeNullOrEmpty( "There is always a non empty 'device' member." ); - longTerm.ContainsKey( StdAuthenticationTypeSystem.UserIdKeyType ).Should().Be( rememberMe, "The user is here only when remember is true." ); + body = $$"""{"userName":"{{userName}}", "password":"{{password}}","ImpersonateActualUser":true"""; } - - static void CookieIsNotTheSameAsToken( System.Net.CookieCollection all, string cookieName, AuthServerResponse r ) + else { - var cookie = all.Single( c => c.Name == cookieName ).Value; - cookie = HttpUtility.UrlDecode( cookie ); - cookie.Should().NotBe( r.Token ); + body = $$"""{"userName":"{{userName}}", "password":"{{password}}" """; } } } + if( jsonUserData != null ) + { + body += $$""", "userData": {{jsonUserData}}"""; + } + body += "}"; + using HttpResponseMessage responseMessage = await client.PostJsonAsync( uri, body ); + var response = await HandleResponseAsync( client, responseMessage ); + Throw.DebugAssert( response.Info != null ); + if( expectSuccess ) + { + responseMessage.EnsureSuccessStatusCode(); + response.Info.Level.Should().BeOneOf( AuthLevel.Normal, AuthLevel.Critical ); + response.Info.ActualUser.UserName.Should().Be( userName ); + CheckClientCookies( client, response, rememberMe ); + } + else + { + // TODO: Precise the login failure behavior (make it configurable?). + CheckClientCookies( client, response, rememberMe ); + } + return response; - /// - /// Reads the authentication cookie and long term cookie. - /// - /// This client. - /// The cookie information. - public static AuthenticationCookieValues AuthenticationReadCookies( this RunningAspNetServer.RunningClient client ) + static void CheckClientCookies( RunningAspNetServer.RunningClient client, AuthServerResponse response, bool expectedRememberMe ) { - var options = GetAuthenticationOptions( client.Server ); - System.Net.CookieCollection? all = null; + var options = client.Server.GetAuthenticationOptions(); + var cookieName = options.AuthCookieName; + var ltCookieName = cookieName + "LT"; switch( options.CookieMode ) { case AuthenticationCookieMode.WebFrontPath: - { - all = client.CookieContainer.GetCookies( new Uri( client.BaseAddress, "/.webfront/c/" ) ); - break; - } + { + client.CookieContainer.GetCookies( client.BaseAddress ).Should().BeEmpty(); + var allCookies = client.CookieContainer.GetCookies( new Uri( client.BaseAddress, "/.webfront/c/" ) ); + allCookies.Should().HaveCount( 2 ); + CookieIsNotTheSameAsToken( allCookies, cookieName, response ); + CheckLongTermCookie( expectedRememberMe, allCookies, ltCookieName ); + break; + } case AuthenticationCookieMode.RootPath: - { - all = client.CookieContainer.GetCookies( client.BaseAddress ); - break; - } - default: - Throw.DebugAssert( options.CookieMode == AuthenticationCookieMode.None ); + { + var allCookies = client.CookieContainer.GetCookies( client.BaseAddress ); + allCookies.Should().HaveCount( 2 ); + CookieIsNotTheSameAsToken( allCookies, cookieName, response ); + CheckLongTermCookie( expectedRememberMe, allCookies, ltCookieName ); break; + } + case AuthenticationCookieMode.None: + { + // RemmeberMe returned by the server is always false when CookieMode is None. + expectedRememberMe = false; + client.CookieContainer.GetCookies( client.BaseAddress ).Should().BeEmpty(); + client.CookieContainer.GetCookies( new Uri( client.BaseAddress, "/.webfront/c/" ) ).Should().BeEmpty(); + break; + } } + response.RememberMe.Should().Be( expectedRememberMe ); - string? authCookie = all?.SingleOrDefault( c => c.Name == options.AuthCookieName )?.Value; - JObject? ltCookie = null; - string? ltDeviceId = null; - string? ltUserId = null; - string? ltUserName = null; + static void CheckLongTermCookie( bool rememberMe, System.Net.CookieCollection all, string cookieName ) + { + var cookie = all.Single( c => c.Name == cookieName ).Value; + cookie = HttpUtility.UrlDecode( cookie ); + var longTerm = JObject.Parse( cookie ); + ((string?)longTerm[StdAuthenticationTypeSystem.DeviceIdKeyType]).Should().NotBeNullOrEmpty( "There is always a non empty 'device' member." ); + longTerm.ContainsKey( StdAuthenticationTypeSystem.UserIdKeyType ).Should().Be( rememberMe, "The user is here only when remember is true." ); + } - var ltCookieStr = all?.SingleOrDefault( c => c.Name == options.AuthCookieName + "LT" )?.Value; - if( ltCookieStr != null ) + static void CookieIsNotTheSameAsToken( System.Net.CookieCollection all, string cookieName, AuthServerResponse r ) { - ltCookieStr = HttpUtility.UrlDecode( ltCookieStr ); - ltCookie = JObject.Parse( ltCookieStr ); - ltDeviceId = (string?)ltCookie[StdAuthenticationTypeSystem.DeviceIdKeyType]; - ltUserId = (string?)ltCookie[StdAuthenticationTypeSystem.UserIdKeyType]; - ltUserName = (string?)ltCookie[StdAuthenticationTypeSystem.UserNameKeyType]; + var cookie = all.Single( c => c.Name == cookieName ).Value; + cookie = HttpUtility.UrlDecode( cookie ); + cookie.Should().NotBe( r.Token ); } - return (authCookie, ltCookie, ltDeviceId, ltUserId, ltUserName); } + } + /// + /// Reads the authentication cookie and long term cookie. + /// + /// This client. + /// The cookie information. + public static AuthenticationCookieValues AuthenticationReadCookies( this RunningAspNetServer.RunningClient client ) + { + var options = GetAuthenticationOptions( client.Server ); + System.Net.CookieCollection? all = null; + switch( options.CookieMode ) + { + case AuthenticationCookieMode.WebFrontPath: + { + all = client.CookieContainer.GetCookies( new Uri( client.BaseAddress, "/.webfront/c/" ) ); + break; + } + case AuthenticationCookieMode.RootPath: + { + all = client.CookieContainer.GetCookies( client.BaseAddress ); + break; + } + default: + Throw.DebugAssert( options.CookieMode == AuthenticationCookieMode.None ); + break; + } + + string? authCookie = all?.SingleOrDefault( c => c.Name == options.AuthCookieName )?.Value; + JObject? ltCookie = null; + string? ltDeviceId = null; + string? ltUserId = null; + string? ltUserName = null; + + var ltCookieStr = all?.SingleOrDefault( c => c.Name == options.AuthCookieName + "LT" )?.Value; + if( ltCookieStr != null ) + { + ltCookieStr = HttpUtility.UrlDecode( ltCookieStr ); + ltCookie = JObject.Parse( ltCookieStr ); + ltDeviceId = (string?)ltCookie[StdAuthenticationTypeSystem.DeviceIdKeyType]; + ltUserId = (string?)ltCookie[StdAuthenticationTypeSystem.UserIdKeyType]; + ltUserName = (string?)ltCookie[StdAuthenticationTypeSystem.UserNameKeyType]; + } + return (authCookie, ltCookie, ltDeviceId, ltUserId, ltUserName); } + } diff --git a/CodeCakeBuilder/Build.cs b/CodeCakeBuilder/Build.cs index 9f351b48..4d200f70 100644 --- a/CodeCakeBuilder/Build.cs +++ b/CodeCakeBuilder/Build.cs @@ -2,77 +2,76 @@ using Cake.Core; using Cake.Core.Diagnostics; -namespace CodeCake -{ - /// - /// Standard build "script". - /// +namespace CodeCake; + +/// +/// Standard build "script". +/// - public partial class Build : CodeCakeHost +public partial class Build : CodeCakeHost +{ + public Build() { - public Build() - { - Cake.Log.Verbosity = Verbosity.Diagnostic; - - StandardGlobalInfo globalInfo = CreateStandardGlobalInfo() - .AddDotnet() - .SetCIBuildTag(); - - Task( "Check-Repository" ) - .Does( () => - { - globalInfo.TerminateIfShouldStop(); - } ); - - Task( "Clean" ) - .IsDependentOn( "Check-Repository" ) - .Does( () => - { - globalInfo.GetDotnetSolution().Clean(); - Cake.CleanDirectories( globalInfo.ReleasesFolder.ToString() ); - } ); - - - Task( "Build" ) - .IsDependentOn( "Check-Repository" ) - .IsDependentOn( "Clean" ) - .Does( () => - { - globalInfo.GetDotnetSolution().Build(); - } ); - - Task( "Unit-Testing" ) - .IsDependentOn( "Build" ) - .WithCriteria( () => Cake.InteractiveMode() == InteractiveMode.NoInteraction - || Cake.ReadInteractiveOption( "RunUnitTests", "Run Unit Tests?", 'Y', 'N' ) == 'Y' ) - .Does( () => - { - globalInfo.GetDotnetSolution().SolutionTest(); - } ); - - Task( "Create-Packages" ) - .WithCriteria( () => globalInfo.IsValid ) - .IsDependentOn( "Unit-Testing" ) - .Does( () => - { - globalInfo.GetDotnetSolution().Pack(); - } ); - - - Task( "Push-Packages" ) - .WithCriteria( () => globalInfo.IsValid ) - .IsDependentOn( "Create-Packages" ) - .Does( async () => - { - await globalInfo.PushArtifactsAsync(); - } ); - - // The Default task for this script can be set here. - Task( "Default" ) - .IsDependentOn( "Push-Packages" ); - - } + Cake.Log.Verbosity = Verbosity.Diagnostic; + + StandardGlobalInfo globalInfo = CreateStandardGlobalInfo() + .AddDotnet() + .SetCIBuildTag(); + + Task( "Check-Repository" ) + .Does( () => + { + globalInfo.TerminateIfShouldStop(); + } ); + + Task( "Clean" ) + .IsDependentOn( "Check-Repository" ) + .Does( () => + { + globalInfo.GetDotnetSolution().Clean(); + Cake.CleanDirectories( globalInfo.ReleasesFolder.ToString() ); + } ); + + Task( "Build" ) + .IsDependentOn( "Check-Repository" ) + .IsDependentOn( "Clean" ) + .Does( () => + { + globalInfo.GetDotnetSolution().Build(); + } ); + + Task( "Unit-Testing" ) + .IsDependentOn( "Build" ) + .WithCriteria( () => Cake.InteractiveMode() == InteractiveMode.NoInteraction + || Cake.ReadInteractiveOption( "RunUnitTests", "Run Unit Tests?", 'Y', 'N' ) == 'Y' ) + .Does( () => + { + globalInfo.GetDotnetSolution().SolutionTest(); + } ); + + Task( "Create-Packages" ) + .WithCriteria( () => globalInfo.IsValid ) + .IsDependentOn( "Unit-Testing" ) + .Does( () => + { + globalInfo.GetDotnetSolution().Pack(); + } ); + + + Task( "Push-Packages" ) + .WithCriteria( () => globalInfo.IsValid ) + .IsDependentOn( "Create-Packages" ) + .Does( async () => + { + await globalInfo.PushArtifactsAsync(); + } ); + + // The Default task for this script can be set here. + Task( "Default" ) + .IsDependentOn( "Push-Packages" ); } + + } diff --git a/CodeCakeBuilder/StandardGlobalInfo.cs b/CodeCakeBuilder/StandardGlobalInfo.cs index 929bc6de..a6be3893 100644 --- a/CodeCakeBuilder/StandardGlobalInfo.cs +++ b/CodeCakeBuilder/StandardGlobalInfo.cs @@ -222,7 +222,7 @@ void AzurePipelineUpdateBuildVersion( string buildInstruction ) IAzurePipelinesProvider azure = Cake.AzurePipelines(); try { - if( appVeyor.IsRunningOnAppVeyor ) + if( appVeyor.IsRunningOnAppVeyor ) { appVeyor.UpdateBuildVersion( AddSkipped( BuildInfo.Version.ToString() ) ); } diff --git a/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs b/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs index c177a4af..69b04617 100644 --- a/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs +++ b/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs @@ -87,9 +87,10 @@ public NuGetArtifactType( StandardGlobalInfo globalInfo, DotnetSolution solution /// /// The set of remote NuGet feeds (in practice at most one). protected override IEnumerable GetRemoteFeeds() - {if( GlobalInfo.BuildInfo.Version.PackageQuality >= CSemVer.PackageQuality.ReleaseCandidate ) yield return new RemoteFeed( this, "nuget.org", "https://api.nuget.org/v3/index.json", "NUGET_ORG_PUSH_API_KEY" ); -if( GlobalInfo.BuildInfo.Version.PackageQuality <= CSemVer.PackageQuality.CI ) yield return new SignatureVSTSFeed( this, "Signature-OpenSource","NetCore3", "Feeds"); -} + { + if( GlobalInfo.BuildInfo.Version.PackageQuality >= CSemVer.PackageQuality.ReleaseCandidate ) yield return new RemoteFeed( this, "nuget.org", "https://api.nuget.org/v3/index.json", "NUGET_ORG_PUSH_API_KEY" ); + if( GlobalInfo.BuildInfo.Version.PackageQuality <= CSemVer.PackageQuality.CI ) yield return new SignatureVSTSFeed( this, "Signature-OpenSource", "NetCore3", "Feeds" ); + } /// /// Gets the local target feeds. diff --git a/Tests/CK.AspNet.Auth.Tests/AuthenticationInfoInjectionTests.cs b/Tests/CK.AspNet.Auth.Tests/AuthenticationInfoInjectionTests.cs index c860261d..cfcb515f 100644 --- a/Tests/CK.AspNet.Auth.Tests/AuthenticationInfoInjectionTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/AuthenticationInfoInjectionTests.cs @@ -11,49 +11,47 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class AuthenticationInfoInjectionTests { - [TestFixture] - public class AuthenticationInfoInjectionTests + + class AuthenticationInfoDependent { + public readonly IAuthenticationInfo AuthInfo; - class AuthenticationInfoDependent + public AuthenticationInfoDependent( IAuthenticationInfo auth ) { - public readonly IAuthenticationInfo AuthInfo; - - public AuthenticationInfoDependent( IAuthenticationInfo auth ) - { - AuthInfo = auth; - } + AuthInfo = auth; } + } - [Test] - public async Task IAuthenticationInfo_is_injected_by_AddWebFrontAuth_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - configureServices: services => services.AddScoped(), - configureApplication: app => + [Test] + public async Task IAuthenticationInfo_is_injected_by_AddWebFrontAuth_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + configureServices: services => services.AddScoped(), + configureApplication: app => + { + app.Use( next => { - app.Use( next => + return async c => { - return async c => + await next( c ); + if( c.Request.Path.StartsWithSegments( "/TestAuth" ) ) { - await next( c ); - if( c.Request.Path.StartsWithSegments( "/TestAuth" ) ) - { - var a = c.RequestServices.GetRequiredService(); - a.AuthInfo.User.UserName.Should().Be( "Albert" ); - c.Response.StatusCode = (int)HttpStatusCode.PaymentRequired; - } - }; - } ); + var a = c.RequestServices.GetRequiredService(); + a.AuthInfo.User.UserName.Should().Be( "Albert" ); + c.Response.StatusCode = (int)HttpStatusCode.PaymentRequired; + } + }; } ); - - AuthServerResponse r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - runningServer.Client.Token = r.Token; - var m = await runningServer.Client.GetAsync( "/TestAuth" ); - m.StatusCode.Should().Be( HttpStatusCode.PaymentRequired ); - } - } + } ); + AuthServerResponse r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + runningServer.Client.Token = r.Token; + var m = await runningServer.Client.GetAsync( "/TestAuth" ); + m.StatusCode.Should().Be( HttpStatusCode.PaymentRequired ); + } } diff --git a/Tests/CK.AspNet.Auth.Tests/CriticalLevelTests.cs b/Tests/CK.AspNet.Auth.Tests/CriticalLevelTests.cs index 3897a636..ed68d55b 100644 --- a/Tests/CK.AspNet.Auth.Tests/CriticalLevelTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/CriticalLevelTests.cs @@ -7,104 +7,103 @@ using FluentAssertions; using NUnit.Framework; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class CriticalLevelTests { - [TestFixture] - public class CriticalLevelTests + [Test] + public async Task when_no_dictionary_is_set_returns_normal_Async() { - [Test] - public async Task when_no_dictionary_is_set_returns_normal_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: options => options.ExpireTimeSpan = TimeSpan.FromHours( 1 ) ); + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: options => options.ExpireTimeSpan = TimeSpan.FromHours( 1 ) ); - var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( response.Info != null ); - response.Info.Level.Should().Be( AuthLevel.Normal ); - response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); - response.Info.CriticalExpires.HasValue.Should().BeFalse(); - } + var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( response.Info != null ); + response.Info.Level.Should().Be( AuthLevel.Normal ); + response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); + response.Info.CriticalExpires.HasValue.Should().BeFalse(); + } + + [Test] + public async Task when_dictionary_has_no_matching_key_returns_normal_Async() + { + // Ignored (hopefully). + var scts = new Dictionary { { "SomeScheme", TimeSpan.FromHours( 1 ) } }; - [Test] - public async Task when_dictionary_has_no_matching_key_returns_normal_Async() + void SetOptions( WebFrontAuthOptions options ) { - // Ignored (hopefully). - var scts = new Dictionary { { "SomeScheme", TimeSpan.FromHours( 1 ) } }; + options.ExpireTimeSpan = TimeSpan.FromHours( 1 ); + options.SchemesCriticalTimeSpan = scts; + } - void SetOptions( WebFrontAuthOptions options ) - { - options.ExpireTimeSpan = TimeSpan.FromHours( 1 ); - options.SchemesCriticalTimeSpan = scts; - } + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); + var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( response.Info != null ); + response.Info.Level.Should().Be( AuthLevel.Normal ); + response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); + response.Info.CriticalExpires.HasValue.Should().BeFalse(); - var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( response.Info != null ); - response.Info.Level.Should().Be( AuthLevel.Normal ); - response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); - response.Info.CriticalExpires.HasValue.Should().BeFalse(); + } - } + [Test] + public async Task when_dictionary_has_matching_key_with_valid_value_returns_critical_Async() + { + var scts = new Dictionary { { "Basic", TimeSpan.FromHours( 1 ) } }; - [Test] - public async Task when_dictionary_has_matching_key_with_valid_value_returns_critical_Async() + void SetOptions( WebFrontAuthOptions options ) { - var scts = new Dictionary { { "Basic", TimeSpan.FromHours( 1 ) } }; + options.ExpireTimeSpan = TimeSpan.FromHours( 2 ); + options.SchemesCriticalTimeSpan = scts; + } - void SetOptions( WebFrontAuthOptions options ) - { - options.ExpireTimeSpan = TimeSpan.FromHours( 2 ); - options.SchemesCriticalTimeSpan = scts; - } + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); + var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( response.Info != null ); + response.Info.Level.Should().Be( AuthLevel.Critical ); + response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 2 ), TimeSpan.FromSeconds( 60 ) ); + response.Info.CriticalExpires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); - var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( response.Info != null ); - response.Info.Level.Should().Be( AuthLevel.Critical ); - response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 2 ), TimeSpan.FromSeconds( 60 ) ); - response.Info.CriticalExpires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); + } - } + [Test] + public async Task when_dictionary_has_matching_key_with_invalid_value_returns_normal_Async() + { + var scts = new Dictionary { { "Basic", TimeSpan.FromHours( -1 ) } }; - [Test] - public async Task when_dictionary_has_matching_key_with_invalid_value_returns_normal_Async() + void SetOptions( WebFrontAuthOptions options ) { - var scts = new Dictionary { { "Basic", TimeSpan.FromHours( -1 ) } }; + options.ExpireTimeSpan = TimeSpan.FromHours( 1 ); + options.SchemesCriticalTimeSpan = scts; + } - void SetOptions( WebFrontAuthOptions options ) - { - options.ExpireTimeSpan = TimeSpan.FromHours( 1 ); - options.SchemesCriticalTimeSpan = scts; - } + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); + var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( response.Info != null ); + response.Info.Level.Should().Be( AuthLevel.Normal ); + response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); + response.Info.CriticalExpires.HasValue.Should().BeFalse(); + } - var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( response.Info != null ); - response.Info.Level.Should().Be( AuthLevel.Normal ); - response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 1 ), TimeSpan.FromSeconds( 60 ) ); - response.Info.CriticalExpires.HasValue.Should().BeFalse(); - } + [Test] + public async Task when_expires_is_shorter_than_critical_expires_then_expires_is_extended_Async() + { + var scts = new Dictionary { { "Basic", TimeSpan.FromHours( 2 ) } }; - [Test] - public async Task when_expires_is_shorter_than_critical_expires_then_expires_is_extended_Async() + void SetOptions( WebFrontAuthOptions options ) { - var scts = new Dictionary { { "Basic", TimeSpan.FromHours( 2 ) } }; - - void SetOptions( WebFrontAuthOptions options ) - { - options.ExpireTimeSpan = TimeSpan.FromHours( 1 ); - options.SchemesCriticalTimeSpan = scts; - } + options.ExpireTimeSpan = TimeSpan.FromHours( 1 ); + options.SchemesCriticalTimeSpan = scts; + } - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: SetOptions ); - var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( response.Info != null ); - response.Info.Level.Should().Be( AuthLevel.Critical ); - response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 2 ), TimeSpan.FromSeconds( 60 ) ); - response.Info.CriticalExpires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 2 ), TimeSpan.FromSeconds( 60 ) ); - } + var response = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( response.Info != null ); + response.Info.Level.Should().Be( AuthLevel.Critical ); + response.Info.Expires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 2 ), TimeSpan.FromSeconds( 60 ) ); + response.Info.CriticalExpires.Should().BeCloseTo( DateTime.UtcNow + TimeSpan.FromHours( 2 ), TimeSpan.FromSeconds( 60 ) ); } } diff --git a/Tests/CK.AspNet.Auth.Tests/DeviceIdTests.cs b/Tests/CK.AspNet.Auth.Tests/DeviceIdTests.cs index 690c63ee..6832ebaf 100644 --- a/Tests/CK.AspNet.Auth.Tests/DeviceIdTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/DeviceIdTests.cs @@ -10,145 +10,144 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class DeviceIdTests { - [TestFixture] - public class DeviceIdTests + [Test] + public async Task DeviceId_is_not_set_until_wefront_call_and_is_not_changed_Async() { - [Test] - public async Task DeviceId_is_not_set_until_wefront_call_and_is_not_changed_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); - string? deviceId = null; - { - using var message = await runningServer.Client.GetAsync( "echo/outside" ); - var textMessage = await message.Content.ReadAsStringAsync(); - textMessage.Should().Be( "/outside" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().BeNull(); - cookies.LTDeviceId.Should().BeNull(); - cookies.LTUserId.Should().BeNull(); - } - { - await runningServer.Client.AuthenticationRefreshAsync(); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().BeNull(); - cookies.LTDeviceId.Should().NotBeNullOrEmpty(); - deviceId = cookies.LTDeviceId; - } - { - using var message = await runningServer.Client.GetAsync( "echo/hop" ); - var textMessage = await message.Content.ReadAsStringAsync(); - textMessage.Should().Be( "/hop" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().BeNull(); - cookies.LTDeviceId.Should().Be( deviceId ); - } - { - await runningServer.Client.AuthenticationRefreshAsync(); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().BeNull(); - cookies.LTDeviceId.Should().NotBeNullOrEmpty(); - cookies.LTDeviceId.Should().Be( deviceId ); - } + string? deviceId = null; + { + using var message = await runningServer.Client.GetAsync( "echo/outside" ); + var textMessage = await message.Content.ReadAsStringAsync(); + textMessage.Should().Be( "/outside" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().BeNull(); + cookies.LTDeviceId.Should().BeNull(); + cookies.LTUserId.Should().BeNull(); + } + { + await runningServer.Client.AuthenticationRefreshAsync(); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().BeNull(); + cookies.LTDeviceId.Should().NotBeNullOrEmpty(); + deviceId = cookies.LTDeviceId; } - - [TestCase( true )] - [TestCase( false )] - public async Task DeviceId_is_independent_of_the_authentication_Async( bool callRefreshFirst ) { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + using var message = await runningServer.Client.GetAsync( "echo/hop" ); + var textMessage = await message.Content.ReadAsStringAsync(); + textMessage.Should().Be( "/hop" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().BeNull(); + cookies.LTDeviceId.Should().Be( deviceId ); + } + { + await runningServer.Client.AuthenticationRefreshAsync(); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().BeNull(); + cookies.LTDeviceId.Should().NotBeNullOrEmpty(); + cookies.LTDeviceId.Should().Be( deviceId ); + } + } - string? deviceId = null; - { - using var message = await runningServer.Client.GetAsync( "echo/none-yet" ); - var textMessage = await message.Content.ReadAsStringAsync(); - textMessage.Should().Be( "/none-yet" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().BeNull(); - cookies.LTDeviceId.Should().BeNull(); - cookies.LTUserId.Should().BeNull(); - } + [TestCase( true )] + [TestCase( false )] + public async Task DeviceId_is_independent_of_the_authentication_Async( bool callRefreshFirst ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + + string? deviceId = null; + { + using var message = await runningServer.Client.GetAsync( "echo/none-yet" ); + var textMessage = await message.Content.ReadAsStringAsync(); + textMessage.Should().Be( "/none-yet" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().BeNull(); + cookies.LTDeviceId.Should().BeNull(); + cookies.LTUserId.Should().BeNull(); + } + if( callRefreshFirst ) + { + var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); + Throw.DebugAssert( refreshResponse.Info != null ); + refreshResponse.Info.Level.Should().Be( CK.Auth.AuthLevel.None ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().BeNull(); + cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); + deviceId = cookies.LTDeviceId; + } + { + await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", expectSuccess: true, rememberMe: false ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); if( callRefreshFirst ) { - var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); - Throw.DebugAssert( refreshResponse.Info != null ); - refreshResponse.Info.Level.Should().Be( CK.Auth.AuthLevel.None ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().BeNull(); - cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); - deviceId = cookies.LTDeviceId; - } - { - await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", expectSuccess: true, rememberMe: false ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); - if( callRefreshFirst ) - { - cookies.LTDeviceId.Should().Be( deviceId ); - } - else deviceId = cookies.LTDeviceId; - } - { - using var message = await runningServer.Client.GetAsync( "echo/hop" ); - var textMessage = await message.Content.ReadAsStringAsync(); - textMessage.Should().Be( "/hop" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().Be( deviceId ); - } - { - var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); - Throw.DebugAssert( refreshResponse.Info != null ); - refreshResponse.Info.User.UserName.Should().Be( "Albert" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().Be( deviceId ); - } - { - // Calling without Token: the call is "Anonymous" but nothing must have changed. - runningServer.Client.Token = null; - using var message = await runningServer.Client.GetAsync( "echo/plop?userName" ); - var textMessage = await message.Content.ReadAsStringAsync(); - textMessage.Should().Be( "/plop => ?userName (UserName: '')" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().Be( deviceId ); - } - string? token = null; - { - var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); - Throw.DebugAssert( refreshResponse.Info != null ); - refreshResponse.Info.User.UserName.Should().Be( "Albert" ); - token = refreshResponse.Token; - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().Be( deviceId ); - } - { - // Calling with a Token. - runningServer.Client.Token = token; - using var message = await runningServer.Client.GetAsync( "echo/plop?userName" ); - var textMessage = await message.Content.ReadAsStringAsync(); - textMessage.Should().Be( "/plop => ?userName (UserName: 'Albert')" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().Be( deviceId ); - } - { - var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); - Throw.DebugAssert( refreshResponse.Info != null ); - refreshResponse.Info.User.UserName.Should().Be( "Albert" ); - var cookies = runningServer.Client.AuthenticationReadCookies(); - cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); - cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); cookies.LTDeviceId.Should().Be( deviceId ); } + else deviceId = cookies.LTDeviceId; + } + { + using var message = await runningServer.Client.GetAsync( "echo/hop" ); + var textMessage = await message.Content.ReadAsStringAsync(); + textMessage.Should().Be( "/hop" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().Be( deviceId ); + } + { + var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); + Throw.DebugAssert( refreshResponse.Info != null ); + refreshResponse.Info.User.UserName.Should().Be( "Albert" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().Be( deviceId ); + } + { + // Calling without Token: the call is "Anonymous" but nothing must have changed. + runningServer.Client.Token = null; + using var message = await runningServer.Client.GetAsync( "echo/plop?userName" ); + var textMessage = await message.Content.ReadAsStringAsync(); + textMessage.Should().Be( "/plop => ?userName (UserName: '')" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().Be( deviceId ); + } + string? token = null; + { + var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); + Throw.DebugAssert( refreshResponse.Info != null ); + refreshResponse.Info.User.UserName.Should().Be( "Albert" ); + token = refreshResponse.Token; + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().Be( deviceId ); + } + { + // Calling with a Token. + runningServer.Client.Token = token; + using var message = await runningServer.Client.GetAsync( "echo/plop?userName" ); + var textMessage = await message.Content.ReadAsStringAsync(); + textMessage.Should().Be( "/plop => ?userName (UserName: 'Albert')" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().Be( deviceId ); + } + { + var refreshResponse = await runningServer.Client.AuthenticationRefreshAsync(); + Throw.DebugAssert( refreshResponse.Info != null ); + refreshResponse.Info.User.UserName.Should().Be( "Albert" ); + var cookies = runningServer.Client.AuthenticationReadCookies(); + cookies.AuthCookie.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().NotBeNullOrWhiteSpace(); + cookies.LTDeviceId.Should().Be( deviceId ); } - } + } diff --git a/Tests/CK.AspNet.Auth.Tests/ImpersonateCurrentActorTests.cs b/Tests/CK.AspNet.Auth.Tests/ImpersonateCurrentActorTests.cs index 66888e7d..5ca1c092 100644 --- a/Tests/CK.AspNet.Auth.Tests/ImpersonateCurrentActorTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/ImpersonateCurrentActorTests.cs @@ -11,176 +11,175 @@ using System.Threading.Tasks; using static CK.Testing.MonitorTestHelper; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class ImpersonateActualUserTests { - [TestFixture] - public class ImpersonateActualUserTests + class BasicDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService { - class BasicDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService + public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) { - public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) - { - return Task.FromResult( scheme == "Basic" ); - } + return Task.FromResult( scheme == "Basic" ); } + } - class ImpersonationAllowAliceToAlbert : IWebFrontAuthImpersonationService - { - readonly FakeWebFrontAuthLoginService _loginService; + class ImpersonationAllowAliceToAlbert : IWebFrontAuthImpersonationService + { + readonly FakeWebFrontAuthLoginService _loginService; - public ImpersonationAllowAliceToAlbert( FakeWebFrontAuthLoginService loginService ) - { - _loginService = loginService; - } + public ImpersonationAllowAliceToAlbert( FakeWebFrontAuthLoginService loginService ) + { + _loginService = loginService; + } - public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, int userId ) - { - return Task.FromResult( DoAllow( info, userId ) ); - } + public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, int userId ) + { + return Task.FromResult( DoAllow( info, userId ) ); + } - public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, string userName ) - { - return Task.FromResult( DoAllow( info, userName ) ); - } + public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, string userName ) + { + return Task.FromResult( DoAllow( info, userName ) ); + } - IUserInfo? DoAllow( IAuthenticationInfo info, object nameOrId ) + IUserInfo? DoAllow( IAuthenticationInfo info, object nameOrId ) + { + if( info.ActualUser.UserName == "Alice" ) { - if( info.ActualUser.UserName == "Alice" ) + var target = nameOrId is int id + ? _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserId == id ) + : _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserName == (string)nameOrId ); + // Alice is allowed to impersonate Albert. + if( target != null && target.UserName == "Albert" ) { - var target = nameOrId is int id - ? _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserId == id ) - : _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserName == (string)nameOrId ); - // Alice is allowed to impersonate Albert. - if( target != null && target.UserName == "Albert" ) - { - return target; - } + return target; } - return null; } - + return null; } - [TestCase( true )] - [TestCase( false )] - public async Task impersonateActualUser_parameter_can_login_and_impersonate_the_already_logged_user_Async( bool useGenericWrapper ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - configureServices: services => + } + + [TestCase( true )] + [TestCase( false )] + public async Task impersonateActualUser_parameter_can_login_and_impersonate_the_already_logged_user_Async( bool useGenericWrapper ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + configureServices: services => + { + if( useGenericWrapper ) { - if( useGenericWrapper ) - { - services.AddSingleton(); - } - services.AddSingleton(); - } ); - - var r1 = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper ); - Throw.DebugAssert( r1.Info != null ); - r1.Info.ActualUser.UserName.Should().Be( "Albert" ); - r1.Info.User.UserName.Should().Be( "Albert" ); - r1.Info.IsImpersonated.Should().BeFalse(); - - var r2 = await runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true, useGenericWrapper: useGenericWrapper, impersonateActualUser: true ); - Throw.DebugAssert( r2.Info != null ); - r2.Info.ActualUser.UserName.Should().Be( "Alice", "Alice is now the actual user." ); - r2.Info.User.UserName.Should().Be( "Albert", "Alice is impersonating Albert." ); - r2.Info.IsImpersonated.Should().BeTrue(); - - // Impersonate to Alice: this clears the impersonation. - AuthServerResponse? r = await runningServer.Client.AuthenticationImpersonateAsync( "Alice" ); - Throw.DebugAssert( r?.Info != null ); - r.Info.User.UserName.Should().Be( "Alice" ); - r.Info.ActualUser.UserName.Should().Be( "Alice" ); - r.Info.IsImpersonated.Should().BeFalse(); - } + services.AddSingleton(); + } + services.AddSingleton(); + } ); + + var r1 = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper ); + Throw.DebugAssert( r1.Info != null ); + r1.Info.ActualUser.UserName.Should().Be( "Albert" ); + r1.Info.User.UserName.Should().Be( "Albert" ); + r1.Info.IsImpersonated.Should().BeFalse(); + + var r2 = await runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true, useGenericWrapper: useGenericWrapper, impersonateActualUser: true ); + Throw.DebugAssert( r2.Info != null ); + r2.Info.ActualUser.UserName.Should().Be( "Alice", "Alice is now the actual user." ); + r2.Info.User.UserName.Should().Be( "Albert", "Alice is impersonating Albert." ); + r2.Info.IsImpersonated.Should().BeTrue(); + + // Impersonate to Alice: this clears the impersonation. + AuthServerResponse? r = await runningServer.Client.AuthenticationImpersonateAsync( "Alice" ); + Throw.DebugAssert( r?.Info != null ); + r.Info.User.UserName.Should().Be( "Alice" ); + r.Info.ActualUser.UserName.Should().Be( "Alice" ); + r.Info.IsImpersonated.Should().BeFalse(); + } - [TestCase(true)] - [TestCase(false)] - public async Task impersonateActualUser_parameter_is_harmless_when_the_user_is_already_logged_or_no_user_is_already_logged_Async( bool useGenericWrapper ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => + [TestCase( true )] + [TestCase( false )] + public async Task impersonateActualUser_parameter_is_harmless_when_the_user_is_already_logged_or_no_user_is_already_logged_Async( bool useGenericWrapper ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + if( useGenericWrapper ) { - if( useGenericWrapper ) - { - services.AddSingleton(); - } - } ); - - var r1 = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, impersonateActualUser: true ); - Throw.DebugAssert( r1.Info != null ); - r1.Info.Level.Should().Be( CK.Auth.AuthLevel.Normal ); - r1.Info.ActualUser.UserName.Should().Be( "Albert" ); - r1.Info.User.UserName.Should().Be( "Albert" ); - r1.Info.IsImpersonated.Should().BeFalse(); - - var r2 = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, impersonateActualUser: true ); - Throw.DebugAssert( r2.Info != null ); - r2.Info.Level.Should().Be( CK.Auth.AuthLevel.Normal ); - r2.Info.ActualUser.UserName.Should().Be( "Albert" ); - r2.Info.User.UserName.Should().Be( "Albert" ); - r2.Info.IsImpersonated.Should().BeFalse(); - } + services.AddSingleton(); + } + } ); + + var r1 = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, impersonateActualUser: true ); + Throw.DebugAssert( r1.Info != null ); + r1.Info.Level.Should().Be( CK.Auth.AuthLevel.Normal ); + r1.Info.ActualUser.UserName.Should().Be( "Albert" ); + r1.Info.User.UserName.Should().Be( "Albert" ); + r1.Info.IsImpersonated.Should().BeFalse(); + + var r2 = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, impersonateActualUser: true ); + Throw.DebugAssert( r2.Info != null ); + r2.Info.Level.Should().Be( CK.Auth.AuthLevel.Normal ); + r2.Info.ActualUser.UserName.Should().Be( "Albert" ); + r2.Info.User.UserName.Should().Be( "Albert" ); + r2.Info.IsImpersonated.Should().BeFalse(); + } - [TestCase( true, true, false )] - [TestCase( false, true, false )] - [TestCase( true, false, false )] - [TestCase( false, false, false )] - [TestCase( true, true, true )] - [TestCase( false, true, true )] - [TestCase( true, false, true )] - [TestCase( false, false, true )] - public async Task user_can_clear_its_own_impersonation_by_impersonating_to_itself_Async( bool byUserId, - bool useLoginToLeaveImpersonation, - bool useLoginCommand ) + [TestCase( true, true, false )] + [TestCase( false, true, false )] + [TestCase( true, false, false )] + [TestCase( false, false, false )] + [TestCase( true, true, true )] + [TestCase( false, true, true )] + [TestCase( true, false, true )] + [TestCase( false, false, true )] + public async Task user_can_clear_its_own_impersonation_by_impersonating_to_itself_Async( bool byUserId, + bool useLoginToLeaveImpersonation, + bool useLoginCommand ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + services.AddSingleton(); + } ); + + // Login Albert. + var initial = await (useLoginCommand + ? runningServer.Client.LoginViaLocalCommandAsync( "Albert" ) + : runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true )); + Throw.DebugAssert( initial.Info != null ); + initial.Info.IsImpersonated.Should().BeFalse(); + initial.Info.User.UserName.Should().Be( "Albert" ); + + // Alice impersonates Albert. + var imp = await (useLoginCommand + ? runningServer.Client.LoginViaLocalCommandAsync( "Alice", impersonateActualUser: true ) + : runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true, impersonateActualUser: true )); + Throw.DebugAssert( imp.Info != null ); + imp.Info.IsImpersonated.Should().BeTrue(); + imp.Info.ActualUser.UserName.Should().Be( "Alice" ); + imp.Info.User.UserName.Should().Be( "Albert" ); + + if( useLoginToLeaveImpersonation ) { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - services.AddSingleton(); - } ); - - // Login Albert. - var initial = await (useLoginCommand - ? runningServer.Client.LoginViaLocalCommandAsync( "Albert" ) - : runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true )); - Throw.DebugAssert( initial.Info != null ); - initial.Info.IsImpersonated.Should().BeFalse(); - initial.Info.User.UserName.Should().Be( "Albert" ); - - // Alice impersonates Albert. - var imp = await (useLoginCommand - ? runningServer.Client.LoginViaLocalCommandAsync( "Alice", impersonateActualUser: true ) - : runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true, impersonateActualUser: true )); + // When Alice re-logs herself, the impersonation is cleared. + // impersonateActualUser doesn't matter. + bool impersonateActualUser = Environment.TickCount % 2 == 0; + imp = await (useLoginCommand + ? runningServer.Client.LoginViaLocalCommandAsync( "Alice", impersonateActualUser: true ) + : runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true, impersonateActualUser: true )); Throw.DebugAssert( imp.Info != null ); - imp.Info.IsImpersonated.Should().BeTrue(); + imp.Info.IsImpersonated.Should().BeFalse(); imp.Info.ActualUser.UserName.Should().Be( "Alice" ); - imp.Info.User.UserName.Should().Be( "Albert" ); - - if( useLoginToLeaveImpersonation ) - { - // When Alice re-logs herself, the impersonation is cleared. - // impersonateActualUser doesn't matter. - bool impersonateActualUser = Environment.TickCount % 2 == 0; - imp = await (useLoginCommand - ? runningServer.Client.LoginViaLocalCommandAsync( "Alice", impersonateActualUser: true ) - : runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true, impersonateActualUser: true )); - Throw.DebugAssert( imp.Info != null ); - imp.Info.IsImpersonated.Should().BeFalse(); - imp.Info.ActualUser.UserName.Should().Be( "Alice" ); - } - else - { - // When Alice impersonates to Alice, the impersonation is cleared. - var r = byUserId - ? await runningServer.Client.AuthenticationImpersonateAsync( imp.Info.ActualUser.UserId ) - : await runningServer.Client.AuthenticationImpersonateAsync( "Alice" ); - Throw.DebugAssert( r?.Info != null ); - r.Info.IsImpersonated.Should().BeFalse(); - r.Info.User.UserName.Should().Be( "Alice" ); - } } - + else + { + // When Alice impersonates to Alice, the impersonation is cleared. + var r = byUserId + ? await runningServer.Client.AuthenticationImpersonateAsync( imp.Info.ActualUser.UserId ) + : await runningServer.Client.AuthenticationImpersonateAsync( "Alice" ); + Throw.DebugAssert( r?.Info != null ); + r.Info.IsImpersonated.Should().BeFalse(); + r.Info.User.UserName.Should().Be( "Alice" ); + } } + } diff --git a/Tests/CK.AspNet.Auth.Tests/ImpersonationTests.cs b/Tests/CK.AspNet.Auth.Tests/ImpersonationTests.cs index 34053422..e76fb83d 100644 --- a/Tests/CK.AspNet.Auth.Tests/ImpersonationTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/ImpersonationTests.cs @@ -11,153 +11,152 @@ using System.Text; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class ImpersonationTests { - [TestFixture] - public class ImpersonationTests + [Test] + public async Task when_no_impersonation_service_is_registered_404_NotFound_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + + using HttpResponseMessage m1 = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, """{ "userName": "Robert" }""" ); + m1.StatusCode.Should().Be( HttpStatusCode.NotFound ); + + await runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true ); + using HttpResponseMessage m2 = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, """{ "userName": "Robert" }""" ); + m2.StatusCode.Should().Be( HttpStatusCode.NotFound ); + } + + [Test] + public async Task user_can_always_clear_its_own_impersonation_even_if_no_impersonation_service_exists_Async() { - [Test] - public async Task when_no_impersonation_service_is_registered_404_NotFound_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); - - using HttpResponseMessage m1 = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, """{ "userName": "Robert" }""" ); - m1.StatusCode.Should().Be( HttpStatusCode.NotFound ); - - await runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true ); - using HttpResponseMessage m2 = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, """{ "userName": "Robert" }""" ); - m2.StatusCode.Should().Be( HttpStatusCode.NotFound ); - } - - [Test] - public async Task user_can_always_clear_its_own_impersonation_even_if_no_impersonation_service_exists_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); - - await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - - var r = await runningServer.Client.AuthenticationImpersonateAsync( "Albert" ); - Throw.DebugAssert( r?.Info != null ); - r.Info.IsImpersonated.Should().BeFalse(); - r.Info.User.UserName.Should().Be( "Albert" ); - r.Info.ActualUser.UserName.Should().Be( "Albert" ); - } - - [TestCase( true )] - [TestCase( false )] - public async Task user_can_clear_its_own_impersonation_by_impersonating_to_itself_Async( bool byUserId ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - services.AddSingleton(); - } ); - - // Login Albert. - await runningServer.Client.AuthenticationBasicLoginAsync("Albert", true ); - // ...and impersonate Robert. - var r = await runningServer.Client.AuthenticationImpersonateAsync( "Robert" ); - Throw.DebugAssert( r?.Info != null ); - r.Info.IsImpersonated.Should().BeTrue(); - r.Info.User.UserName.Should().Be( "Robert" ); - r.Info.ActualUser.UserName.Should().Be( "Albert" ); - - // Impersonating again in Robert: nothing changes. - r = byUserId - ? await runningServer.Client.AuthenticationImpersonateAsync( r.Info.User.UserId ) - : await runningServer.Client.AuthenticationImpersonateAsync( "Robert" ); - Throw.DebugAssert( r?.Info != null ); - r.Info.IsImpersonated.Should().BeTrue(); - r.Info.User.UserName.Should().Be( "Robert" ); - r.Info.ActualUser.UserName.Should().Be( "Albert" ); - - // When Albert impersonates to Albert, the impersonation is cleared. - r = byUserId - ? await runningServer.Client.AuthenticationImpersonateAsync( r.Info.ActualUser.UserId ) - : await runningServer.Client.AuthenticationImpersonateAsync( "Albert" ); - Throw.DebugAssert( r?.Info != null ); - r.Info.IsImpersonated.Should().BeFalse(); - r.Info.User.UserName.Should().Be( "Albert" ); - r.Info.ActualUser.UserName.Should().Be( "Albert" ); - } - - [Test] - public async Task anonymous_can_not_impersonate_with_403_Forbidden_but_allowed_user_can_with_200_OK_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - services.AddSingleton(); - } ); - - using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, @"{ ""userName"": ""Robert"" }" ); - m.StatusCode.Should().Be( HttpStatusCode.Forbidden ); - - await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - var r = await runningServer.Client.AuthenticationImpersonateAsync( "Robert" ); - Throw.DebugAssert( r?.Info != null ); - r.Info.IsImpersonated.Should().BeTrue(); - r.Info.User.UserName.Should().Be( "Robert" ); - r.Info.ActualUser.UserName.Should().Be( "Albert" ); - } - - [Test] - public async Task impersonate_can_be_called_with_userId_instead_of_userName_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - services.AddSingleton(); - } ); - - await runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true ); - var r = await runningServer.Client.AuthenticationImpersonateAsync( 3712 ); - Throw.DebugAssert( r?.Info != null ); - r.Info.IsImpersonated.Should().BeTrue(); - - r.Info.User.UserId.Should().Be( 3712 ); - r.Info.User.UserName.Should().Be( "Albert" ); - - r.Info.ActualUser.UserId.Should().Be( 3711 ); - r.Info.ActualUser.UserName.Should().Be( "Alice" ); - } - - [Test] - public async Task impersonate_to_an_unknown_userName_or_userId_fails_with_403_Forbidden_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - services.AddSingleton(); - } ); - - await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - using HttpResponseMessage m1 = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, @"{ ""userId"": 1e34 }" ); - m1.StatusCode.Should().Be( HttpStatusCode.Forbidden ); - - using HttpResponseMessage m2 = await runningServer.Client.PostJsonAsync(RunningAspNetAuthServerExtensions.ImpersonateUri, @"{ ""userName"": ""kexistepas"" }"); - m2.StatusCode.Should().Be( HttpStatusCode.Forbidden ); - } - - [TestCase( "" )] - [TestCase( "{" )] - [TestCase( @"""not a json object""" )] - [TestCase( @"{""name"":""n""}" )] - [TestCase( @"{""id"":3}" )] - [TestCase( @"{""userName"":3}" )] - [TestCase( @"{""userId"": ""36bis""}" )] - [TestCase( @"{""userName"":""Robert"",""userId"":3}" )] - public async Task impersonate_with_invalid_body_fails_with_400_BadRequest_Async( string body ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - services.AddSingleton(); - } ); - await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, body ); - m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); - } + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + + var r = await runningServer.Client.AuthenticationImpersonateAsync( "Albert" ); + Throw.DebugAssert( r?.Info != null ); + r.Info.IsImpersonated.Should().BeFalse(); + r.Info.User.UserName.Should().Be( "Albert" ); + r.Info.ActualUser.UserName.Should().Be( "Albert" ); + } + + [TestCase( true )] + [TestCase( false )] + public async Task user_can_clear_its_own_impersonation_by_impersonating_to_itself_Async( bool byUserId ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + services.AddSingleton(); + } ); + + // Login Albert. + await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + // ...and impersonate Robert. + var r = await runningServer.Client.AuthenticationImpersonateAsync( "Robert" ); + Throw.DebugAssert( r?.Info != null ); + r.Info.IsImpersonated.Should().BeTrue(); + r.Info.User.UserName.Should().Be( "Robert" ); + r.Info.ActualUser.UserName.Should().Be( "Albert" ); + + // Impersonating again in Robert: nothing changes. + r = byUserId + ? await runningServer.Client.AuthenticationImpersonateAsync( r.Info.User.UserId ) + : await runningServer.Client.AuthenticationImpersonateAsync( "Robert" ); + Throw.DebugAssert( r?.Info != null ); + r.Info.IsImpersonated.Should().BeTrue(); + r.Info.User.UserName.Should().Be( "Robert" ); + r.Info.ActualUser.UserName.Should().Be( "Albert" ); + + // When Albert impersonates to Albert, the impersonation is cleared. + r = byUserId + ? await runningServer.Client.AuthenticationImpersonateAsync( r.Info.ActualUser.UserId ) + : await runningServer.Client.AuthenticationImpersonateAsync( "Albert" ); + Throw.DebugAssert( r?.Info != null ); + r.Info.IsImpersonated.Should().BeFalse(); + r.Info.User.UserName.Should().Be( "Albert" ); + r.Info.ActualUser.UserName.Should().Be( "Albert" ); } + + [Test] + public async Task anonymous_can_not_impersonate_with_403_Forbidden_but_allowed_user_can_with_200_OK_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + services.AddSingleton(); + } ); + + using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, @"{ ""userName"": ""Robert"" }" ); + m.StatusCode.Should().Be( HttpStatusCode.Forbidden ); + + await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + var r = await runningServer.Client.AuthenticationImpersonateAsync( "Robert" ); + Throw.DebugAssert( r?.Info != null ); + r.Info.IsImpersonated.Should().BeTrue(); + r.Info.User.UserName.Should().Be( "Robert" ); + r.Info.ActualUser.UserName.Should().Be( "Albert" ); + } + + [Test] + public async Task impersonate_can_be_called_with_userId_instead_of_userName_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + services.AddSingleton(); + } ); + + await runningServer.Client.AuthenticationBasicLoginAsync( "Alice", true ); + var r = await runningServer.Client.AuthenticationImpersonateAsync( 3712 ); + Throw.DebugAssert( r?.Info != null ); + r.Info.IsImpersonated.Should().BeTrue(); + + r.Info.User.UserId.Should().Be( 3712 ); + r.Info.User.UserName.Should().Be( "Albert" ); + + r.Info.ActualUser.UserId.Should().Be( 3711 ); + r.Info.ActualUser.UserName.Should().Be( "Alice" ); + } + + [Test] + public async Task impersonate_to_an_unknown_userName_or_userId_fails_with_403_Forbidden_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + services.AddSingleton(); + } ); + + await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + using HttpResponseMessage m1 = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, @"{ ""userId"": 1e34 }" ); + m1.StatusCode.Should().Be( HttpStatusCode.Forbidden ); + + using HttpResponseMessage m2 = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, @"{ ""userName"": ""kexistepas"" }" ); + m2.StatusCode.Should().Be( HttpStatusCode.Forbidden ); + } + + [TestCase( "" )] + [TestCase( "{" )] + [TestCase( @"""not a json object""" )] + [TestCase( @"{""name"":""n""}" )] + [TestCase( @"{""id"":3}" )] + [TestCase( @"{""userName"":3}" )] + [TestCase( @"{""userId"": ""36bis""}" )] + [TestCase( @"{""userName"":""Robert"",""userId"":3}" )] + public async Task impersonate_with_invalid_body_fails_with_400_BadRequest_Async( string body ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + services.AddSingleton(); + } ); + await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.ImpersonateUri, body ); + m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + } + } diff --git a/Tests/CK.AspNet.Auth.Tests/LocalHelper.cs b/Tests/CK.AspNet.Auth.Tests/LocalHelper.cs index 0b46bd34..ecf6496e 100644 --- a/Tests/CK.AspNet.Auth.Tests/LocalHelper.cs +++ b/Tests/CK.AspNet.Auth.Tests/LocalHelper.cs @@ -11,93 +11,91 @@ using System.Threading.Tasks; using static CK.Testing.MonitorTestHelper; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +static class LocalHelper { - static class LocalHelper + public static Task CreateLocalAuthServerAsync( Action? configureServices = null, + Action? configureApplication = null, + Action? webFrontAuthOptions = null ) { - public static Task CreateLocalAuthServerAsync( Action? configureServices = null, - Action? configureApplication = null, - Action? webFrontAuthOptions = null ) - { - var builder = WebApplication.CreateSlimBuilder(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton( sp => sp.GetRequiredService() ); - builder.Services.AddSingleton(); - builder.Services.AddSingleton( sp => sp.GetRequiredService() ); - builder.Services.AddScoped(); - builder.Services.AddScoped( sp => sp.GetRequiredService() ); - builder.AddWebFrontAuth( webFrontAuthOptions ); - configureServices?.Invoke( builder.Services ); + var builder = WebApplication.CreateSlimBuilder(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton( sp => sp.GetRequiredService() ); + builder.Services.AddSingleton(); + builder.Services.AddSingleton( sp => sp.GetRequiredService() ); + builder.Services.AddScoped(); + builder.Services.AddScoped( sp => sp.GetRequiredService() ); + builder.AddWebFrontAuth( webFrontAuthOptions ); + configureServices?.Invoke( builder.Services ); - return builder.CreateRunningAspNetServerAsync( configureApplication: app => + return builder.CreateRunningAspNetServerAsync( configureApplication: app => + { + app.Use( next => { - app.Use( next => + return async ctx => { - return async ctx => + if( ctx.Request.Path.StartsWithSegments( "/echo", out var remaining ) ) { - if( ctx.Request.Path.StartsWithSegments( "/echo", out var remaining ) ) - { - var echo = remaining.ToString(); - if( ctx.Request.QueryString.HasValue ) echo += " => " + ctx.Request.QueryString; + var echo = remaining.ToString(); + if( ctx.Request.QueryString.HasValue ) echo += " => " + ctx.Request.QueryString; - if( remaining.StartsWithSegments( "/error", out var errorCode ) && Int32.TryParse( errorCode, out var error ) ) - { - ctx.Response.StatusCode = error; - echo += $" (StatusCode set to '{error}')"; - } - if( ctx.Request.Query.ContainsKey( "userName" ) ) - { - var authInfo = CKAspNetAuthHttpContextExtensions.GetAuthenticationInfo( ctx ); - echo += $" (UserName: '{authInfo.User.UserName}')"; - } - await ctx.Response.Body.WriteAsync( System.Text.Encoding.UTF8.GetBytes( echo ) ); - } - else if( ctx.Request.Path.StartsWithSegments( "/CallChallengeAsync", out _ ) ) - { - await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.ChallengeAsync( ctx ); - ctx.User.Identity!.Name.Should().Be( "Albert" ); - } - else if( ctx.Request.Path.StartsWithSegments( "/ComingFromCris/LogoutCommand", out _ ) ) - { - var s = app.ApplicationServices.GetRequiredService(); - await s.LogoutCommandAsync( new ActivityMonitor(), ctx ); - ctx.Response.StatusCode = 200; - } - else if( ctx.Request.Path.StartsWithSegments( "/ComingFromCris/LoginCommand", out _ ) ) + if( remaining.StartsWithSegments( "/error", out var errorCode ) && Int32.TryParse( errorCode, out var error ) ) { - var s = app.ApplicationServices.GetRequiredService(); - var r = await s.BasicLoginCommandAsync( new ActivityMonitor(), - ctx, - ctx.Request.Query["userName"], - "success", - impersonateActualUser: ctx.Request.Query["impersonateActualUser"] == "True" ); - ctx.Response.StatusCode = 200; - Throw.DebugAssert( r.Token != null ); - await ctx.Response.WriteAsync( r.Token ); + ctx.Response.StatusCode = error; + echo += $" (StatusCode set to '{error}')"; } - else + if( ctx.Request.Query.ContainsKey( "userName" ) ) { - await next( ctx ); + var authInfo = CKAspNetAuthHttpContextExtensions.GetAuthenticationInfo( ctx ); + echo += $" (UserName: '{authInfo.User.UserName}')"; } - }; - } ); - configureApplication?.Invoke( app ); + await ctx.Response.Body.WriteAsync( System.Text.Encoding.UTF8.GetBytes( echo ) ); + } + else if( ctx.Request.Path.StartsWithSegments( "/CallChallengeAsync", out _ ) ) + { + await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.ChallengeAsync( ctx ); + ctx.User.Identity!.Name.Should().Be( "Albert" ); + } + else if( ctx.Request.Path.StartsWithSegments( "/ComingFromCris/LogoutCommand", out _ ) ) + { + var s = app.ApplicationServices.GetRequiredService(); + await s.LogoutCommandAsync( new ActivityMonitor(), ctx ); + ctx.Response.StatusCode = 200; + } + else if( ctx.Request.Path.StartsWithSegments( "/ComingFromCris/LoginCommand", out _ ) ) + { + var s = app.ApplicationServices.GetRequiredService(); + var r = await s.BasicLoginCommandAsync( new ActivityMonitor(), + ctx, + ctx.Request.Query["userName"], + "success", + impersonateActualUser: ctx.Request.Query["impersonateActualUser"] == "True" ); + ctx.Response.StatusCode = 200; + Throw.DebugAssert( r.Token != null ); + await ctx.Response.WriteAsync( r.Token ); + } + else + { + await next( ctx ); + } + }; } ); - } - - public static async Task LoginViaLocalCommandAsync( this RunningAspNetServer.RunningClient client, - string userName, - bool impersonateActualUser = false ) - { - using HttpResponseMessage getResponse = await client.GetAsync( $"/ComingFromCris/LoginCommand?userName={userName}&impersonateActualUser={impersonateActualUser}" ); - var token = await getResponse.Content.ReadAsStringAsync(); - client.Token = token; - var r = await client.AuthenticationRefreshAsync(); - return r; - } + configureApplication?.Invoke( app ); + } ); + } + public static async Task LoginViaLocalCommandAsync( this RunningAspNetServer.RunningClient client, + string userName, + bool impersonateActualUser = false ) + { + using HttpResponseMessage getResponse = await client.GetAsync( $"/ComingFromCris/LoginCommand?userName={userName}&impersonateActualUser={impersonateActualUser}" ); + var token = await getResponse.Content.ReadAsStringAsync(); + client.Token = token; + var r = await client.AuthenticationRefreshAsync(); + return r; } } diff --git a/Tests/CK.AspNet.Auth.Tests/RememberMeTests.cs b/Tests/CK.AspNet.Auth.Tests/RememberMeTests.cs index 18ca0ada..bc4a05ca 100644 --- a/Tests/CK.AspNet.Auth.Tests/RememberMeTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/RememberMeTests.cs @@ -7,47 +7,46 @@ using System.Linq; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class RememberMeTests { - [TestFixture] - public class RememberMeTests + [TestCase( true, false )] + [TestCase( true, true )] + [TestCase( false, true )] + [TestCase( false, false )] + public async Task remember_me_sets_appropriate_cookies_Async( bool useGenericWrapper, bool rememberMe ) { - [TestCase( true, false )] - [TestCase( true, true )] - [TestCase( false, true )] - [TestCase( false, false )] - public async Task remember_me_sets_appropriate_cookies_Async( bool useGenericWrapper, bool rememberMe ) + // + // Note: SlidingExpiration_works_as_expected_in_bearer_only_mode_by_calling_refresh_endpoint + // test challenges the None cookie mode and the rememberMe option. + // + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( services => { - // - // Note: SlidingExpiration_works_as_expected_in_bearer_only_mode_by_calling_refresh_endpoint - // test challenges the None cookie mode and the rememberMe option. - // - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( services => - { - // To support useGenericWrapper = true, we need to allow UnsafeDirectLogin. - if( useGenericWrapper ) - { - services.AddSingleton(); - } - } ); - - var options = new WebFrontAuthOptions(); - AuthServerResponse r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, rememberMe: rememberMe ); - Throw.DebugAssert( r.Info != null ); - r.Info.User.UserName.Should().Be( "Albert" ); - r.RememberMe.Should().Be( rememberMe ); - var cookies = runningServer.Client.CookieContainer.GetCookies( new Uri( $"{runningServer.ServerAddress}/.webfront/c/" ) ); - cookies.Should().HaveCount( 2 ); - if( rememberMe ) + // To support useGenericWrapper = true, we need to allow UnsafeDirectLogin. + if( useGenericWrapper ) { - cookies.Should().Match( all => all.All( c => c.Expires > DateTime.UtcNow ) ); + services.AddSingleton(); } - else - { - var authCookie = cookies.Single( c => c.Name == options.AuthCookieName ); - authCookie.Expires.Should().Be( DateTime.MinValue, "RememberMe is false: the authentication cookie uses a session lifetime." ); - } - } + } ); + var options = new WebFrontAuthOptions(); + AuthServerResponse r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, rememberMe: rememberMe ); + Throw.DebugAssert( r.Info != null ); + r.Info.User.UserName.Should().Be( "Albert" ); + r.RememberMe.Should().Be( rememberMe ); + var cookies = runningServer.Client.CookieContainer.GetCookies( new Uri( $"{runningServer.ServerAddress}/.webfront/c/" ) ); + cookies.Should().HaveCount( 2 ); + if( rememberMe ) + { + cookies.Should().Match( all => all.All( c => c.Expires > DateTime.UtcNow ) ); + } + else + { + var authCookie = cookies.Single( c => c.Name == options.AuthCookieName ); + authCookie.Expires.Should().Be( DateTime.MinValue, "RememberMe is false: the authentication cookie uses a session lifetime." ); + } } + } diff --git a/Tests/CK.AspNet.Auth.Tests/Services/AllDirectLoginAllower.cs b/Tests/CK.AspNet.Auth.Tests/Services/AllDirectLoginAllower.cs index fd89fd62..e92b4407 100644 --- a/Tests/CK.AspNet.Auth.Tests/Services/AllDirectLoginAllower.cs +++ b/Tests/CK.AspNet.Auth.Tests/Services/AllDirectLoginAllower.cs @@ -2,11 +2,9 @@ using Microsoft.AspNetCore.Http; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests -{ - class AllDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService - { - public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) => Task.FromResult( true ); - } +namespace CK.AspNet.Auth.Tests; +class AllDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService +{ + public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) => Task.FromResult( true ); } diff --git a/Tests/CK.AspNet.Auth.Tests/Services/ImpersonationForEverybodyService.cs b/Tests/CK.AspNet.Auth.Tests/Services/ImpersonationForEverybodyService.cs index dee6f7dc..d163e1ca 100644 --- a/Tests/CK.AspNet.Auth.Tests/Services/ImpersonationForEverybodyService.cs +++ b/Tests/CK.AspNet.Auth.Tests/Services/ImpersonationForEverybodyService.cs @@ -8,28 +8,27 @@ using CK.Core; using Microsoft.AspNetCore.Http; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + + +class ImpersonationForEverybodyService : IWebFrontAuthImpersonationService { + // We cannot use the IUserInfoProvider here since it only handles user identifier and not user name. + readonly FakeWebFrontAuthLoginService _loginService; - class ImpersonationForEverybodyService : IWebFrontAuthImpersonationService + public ImpersonationForEverybodyService( FakeWebFrontAuthLoginService loginService ) { - // We cannot use the IUserInfoProvider here since it only handles user identifier and not user name. - readonly FakeWebFrontAuthLoginService _loginService; - - public ImpersonationForEverybodyService( FakeWebFrontAuthLoginService loginService ) - { - _loginService = loginService; - } + _loginService = loginService; + } - public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, int userId ) - { - return Task.FromResult( _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserId == userId ) ); - } + public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, int userId ) + { + return Task.FromResult( _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserId == userId ) ); + } - public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, string userName ) - { - Throw.CheckNotNullArgument( userName ); - return Task.FromResult( _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserName == userName ) ); - } + public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, string userName ) + { + Throw.CheckNotNullArgument( userName ); + return Task.FromResult( _loginService.UserDatabase.AllUsers.FirstOrDefault( u => u.UserName == userName ) ); } } diff --git a/Tests/CK.AspNet.Auth.Tests/Services/NoSchemeLoginService.cs b/Tests/CK.AspNet.Auth.Tests/Services/NoSchemeLoginService.cs index 2e363152..6f566147 100644 --- a/Tests/CK.AspNet.Auth.Tests/Services/NoSchemeLoginService.cs +++ b/Tests/CK.AspNet.Auth.Tests/Services/NoSchemeLoginService.cs @@ -7,36 +7,35 @@ using Microsoft.AspNetCore.Http; using CK.Core; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +class NoSchemeLoginService : IWebFrontAuthLoginService { - class NoSchemeLoginService : IWebFrontAuthLoginService + public NoSchemeLoginService( IAuthenticationTypeSystem typeSystem ) { - public NoSchemeLoginService( IAuthenticationTypeSystem typeSystem ) - { - } + } - public bool HasBasicLogin => false; + public bool HasBasicLogin => false; - public IReadOnlyList Providers => new string[0]; + public IReadOnlyList Providers => new string[0]; - public Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin ) - { - throw new NotSupportedException(); - } + public Task BasicLoginAsync( HttpContext ctx, IActivityMonitor monitor, string userName, string password, bool actualLogin ) + { + throw new NotSupportedException(); + } - public object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ) - { - throw new NotSupportedException(); - } + public object CreatePayload( HttpContext ctx, IActivityMonitor monitor, string scheme ) + { + throw new NotSupportedException(); + } - public Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string providerName, object payload, bool actualLogin ) - { - throw new NotSupportedException(); - } + public Task LoginAsync( HttpContext ctx, IActivityMonitor monitor, string providerName, object payload, bool actualLogin ) + { + throw new NotSupportedException(); + } - public Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ) - { - throw new NotSupportedException(); - } + public Task RefreshAuthenticationInfoAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo current, DateTime newExpires ) + { + throw new NotSupportedException(); } } diff --git a/Tests/CK.AspNet.Auth.Tests/UserDataTests.cs b/Tests/CK.AspNet.Auth.Tests/UserDataTests.cs index e9b58160..19fde245 100644 --- a/Tests/CK.AspNet.Auth.Tests/UserDataTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/UserDataTests.cs @@ -8,50 +8,49 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class UserDataTests { - [TestFixture] - public class UserDataTests + class BasicDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService { - class BasicDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService + public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) { - public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) - { - return Task.FromResult( scheme == "Basic" ); - } + return Task.FromResult( scheme == "Basic" ); } + } - [TestCase( true )] - [TestCase( false )] - public async Task basic_login_userData_Async( bool useGenericWrapper ) + [TestCase( true )] + [TestCase( false )] + public async Task basic_login_userData_Async( bool useGenericWrapper ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( services => { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( services => + if( useGenericWrapper ) { - if( useGenericWrapper ) - { - services.AddSingleton(); - } - } ); - - var expectation = new List<(string, string?)>(); - { - expectation.Add( ("d", "a") ); - var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, jsonUserData: @"{""d"":""a""}" ); - r.UserData.Should().BeEquivalentTo( expectation ); + services.AddSingleton(); } - { - expectation.Add( ("e", "b") ); - var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, jsonUserData: @"{""d"":""a"",""e"":""b""}" ); - r.UserData.Should().BeEquivalentTo( expectation ); - } - { - expectation.Add( ("f", null) ); - expectation.Add( ("g", String.Empty) ); - var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, jsonUserData: @"{""d"":""a"",""e"":""b"",""f"":null,""g"":""""}" ); - r.UserData.Should().BeEquivalentTo( expectation ); - } - } + } ); + var expectation = new List<(string, string?)>(); + { + expectation.Add( ("d", "a") ); + var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, jsonUserData: @"{""d"":""a""}" ); + r.UserData.Should().BeEquivalentTo( expectation ); + } + { + expectation.Add( ("e", "b") ); + var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, jsonUserData: @"{""d"":""a"",""e"":""b""}" ); + r.UserData.Should().BeEquivalentTo( expectation ); + } + { + expectation.Add( ("f", null) ); + expectation.Add( ("g", String.Empty) ); + var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper, jsonUserData: @"{""d"":""a"",""e"":""b"",""f"":null,""g"":""""}" ); + r.UserData.Should().BeEquivalentTo( expectation ); + } } + } diff --git a/Tests/CK.AspNet.Auth.Tests/WebFrontAuthServiceTests.cs b/Tests/CK.AspNet.Auth.Tests/WebFrontAuthServiceTests.cs index 36cbcfae..b0d949f3 100644 --- a/Tests/CK.AspNet.Auth.Tests/WebFrontAuthServiceTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/WebFrontAuthServiceTests.cs @@ -3,24 +3,23 @@ using System.Diagnostics; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class WebFrontAuthServiceTests { - [TestFixture] - public class WebFrontAuthServiceTests + /// + /// Calling ChallengeAsync leads to WebFrontAuthService.HandleAuthenticate that sets HttpContext.User principal + /// from the current IAuthenticationInfo: the actual test is in LocalTestHelper inline middleware. + /// + [Test] + public async Task calling_challenge_Async() { - /// - /// Calling ChallengeAsync leads to WebFrontAuthService.HandleAuthenticate that sets HttpContext.User principal - /// from the current IAuthenticationInfo: the actual test is in LocalTestHelper inline middleware. - /// - [Test] - public async Task calling_challenge_Async() - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); - // Login: the 2 cookies are set on .webFront/c/ path. - var login = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Debug.Assert( login.Info != null ); - await runningServer.Client.GetAsync( "/CallChallengeAsync" ); - } + // Login: the 2 cookies are set on .webFront/c/ path. + var login = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Debug.Assert( login.Info != null ); + await runningServer.Client.GetAsync( "/CallChallengeAsync" ); } } diff --git a/Tests/CK.AspNet.Auth.Tests/WebFrontHandlerTests.cs b/Tests/CK.AspNet.Auth.Tests/WebFrontHandlerTests.cs index a6e69817..042fb3cc 100644 --- a/Tests/CK.AspNet.Auth.Tests/WebFrontHandlerTests.cs +++ b/Tests/CK.AspNet.Auth.Tests/WebFrontHandlerTests.cs @@ -13,332 +13,331 @@ using System.Net.Http; using System.Threading.Tasks; -namespace CK.AspNet.Auth.Tests +namespace CK.AspNet.Auth.Tests; + +[TestFixture] +public class WebFrontHandlerTests { - [TestFixture] - public class WebFrontHandlerTests + const string basicLoginUri = "/.webfront/c/basicLogin"; + const string unsafeDirectLoginUri = "/.webfront/c/unsafeDirectLogin"; + const string refreshUri = "/.webfront/c/refresh"; + const string logoutUri = "/.webfront/c/logout"; + const string tokenExplainUri = "/.webfront/token"; + + [Test] + public async Task a_successful_basic_login_returns_valid_info_and_token_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + + HttpResponseMessage response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, """{"userName":"Albert","password":"success"}""" ); + response.EnsureSuccessStatusCode(); + var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await response.Content.ReadAsStringAsync() ); + Debug.Assert( r.Info != null ); + r.Info.User.UserId.Should().Be( 3712 ); + r.Info.User.UserName.Should().Be( "Albert" ); + r.Info.User.Schemes.Should().HaveCount( 1 ); + r.Info.User.Schemes[0].Name.Should().Be( "Basic" ); + r.Info.User.Schemes[0].LastUsed.Should().BeCloseTo( DateTime.UtcNow, TimeSpan.FromMilliseconds( 1500 ) ); + r.Info.ActualUser.Should().BeSameAs( r.Info.User ); + r.Info.Level.Should().Be( AuthLevel.Normal ); + r.Info.IsImpersonated.Should().BeFalse(); + r.Token.Should().NotBeNullOrWhiteSpace(); + r.Refreshable.Should().BeFalse( "Since by default Options.SlidingExpirationTime is 0." ); + } + + [Test] + public async Task basic_login_is_404NotFound_when_no_BasicAuthenticationProvider_exists_Async() { - const string basicLoginUri = "/.webfront/c/basicLogin"; - const string unsafeDirectLoginUri = "/.webfront/c/unsafeDirectLogin"; - const string refreshUri = "/.webfront/c/refresh"; - const string logoutUri = "/.webfront/c/logout"; - const string tokenExplainUri = "/.webfront/token"; - - [Test] - public async Task a_successful_basic_login_returns_valid_info_and_token_Async() + // This replaces the IWebFrontAuthLoginService (the last added one wins). + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( services => services.AddSingleton() ); + + HttpResponseMessage response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, """{"userName":"Albert","password":"success"}""" ); + response.StatusCode.Should().Be( HttpStatusCode.NotFound ); + } + + class BasicDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService + { + public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); - - HttpResponseMessage response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, """{"userName":"Albert","password":"success"}""" ); - response.EnsureSuccessStatusCode(); - var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await response.Content.ReadAsStringAsync() ); - Debug.Assert( r.Info != null ); - r.Info.User.UserId.Should().Be( 3712 ); - r.Info.User.UserName.Should().Be( "Albert" ); - r.Info.User.Schemes.Should().HaveCount( 1 ); - r.Info.User.Schemes[0].Name.Should().Be( "Basic" ); - r.Info.User.Schemes[0].LastUsed.Should().BeCloseTo( DateTime.UtcNow, TimeSpan.FromMilliseconds( 1500 ) ); - r.Info.ActualUser.Should().BeSameAs( r.Info.User ); - r.Info.Level.Should().Be( AuthLevel.Normal ); - r.Info.IsImpersonated.Should().BeFalse(); - r.Token.Should().NotBeNullOrWhiteSpace(); - r.Refreshable.Should().BeFalse( "Since by default Options.SlidingExpirationTime is 0." ); + return Task.FromResult( scheme == "Basic" ); } + } - [Test] - public async Task basic_login_is_404NotFound_when_no_BasicAuthenticationProvider_exists_Async() + [TestCase( AuthenticationCookieMode.WebFrontPath, false )] + [TestCase( AuthenticationCookieMode.RootPath, false )] + [TestCase( AuthenticationCookieMode.WebFrontPath, true )] + [TestCase( AuthenticationCookieMode.RootPath, true )] + public async Task successful_login_set_the_cookies_on_the_webfront_c_path_and_these_cookies_can_be_used_to_restore_the_authentication_Async( AuthenticationCookieMode mode, + bool useGenericWrapper ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + if( useGenericWrapper ) + { + services.AddSingleton(); + } + }, + webFrontAuthOptions: opt => opt.CookieMode = mode ); + + // Login: the 2 cookies are set on .webFront/c/ path. + var login = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper ); + Debug.Assert( login.Info != null ); + DateTime basicLoginTime = login.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed; + string? originalToken = login.Token; + // Request with token: the authentication is based on the token. { - // This replaces the IWebFrontAuthLoginService (the last added one wins). - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( services => services.AddSingleton() ); - - HttpResponseMessage response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, """{"userName":"Albert","password":"success"}""" ); - response.StatusCode.Should().Be( HttpStatusCode.NotFound ); + runningServer.Client.Token = originalToken; + using HttpResponseMessage tokenRefresh = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.RefreshUri ); + tokenRefresh.EnsureSuccessStatusCode(); + var c = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await tokenRefresh.Content.ReadAsStringAsync() ); + Debug.Assert( c.Info != null ); + c.Info.Level.Should().Be( AuthLevel.Normal ); + c.Info.User.UserName.Should().Be( "Albert" ); + c.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed.Should().Be( basicLoginTime ); } - - class BasicDirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService + // Token less request: the authentication is restored from the cookie. { - public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) - { - return Task.FromResult( scheme == "Basic" ); - } + runningServer.Client.Token = null; + var tokenLessResponse = await runningServer.Client.AuthenticationRefreshAsync(); + Debug.Assert( tokenLessResponse.Info != null ); + tokenLessResponse.Info.Level.Should().Be( AuthLevel.Normal ); + tokenLessResponse.Info.User.UserName.Should().Be( "Albert" ); + tokenLessResponse.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed.Should().Be( basicLoginTime ); } - - [TestCase( AuthenticationCookieMode.WebFrontPath, false )] - [TestCase( AuthenticationCookieMode.RootPath, false )] - [TestCase( AuthenticationCookieMode.WebFrontPath, true )] - [TestCase( AuthenticationCookieMode.RootPath, true )] - public async Task successful_login_set_the_cookies_on_the_webfront_c_path_and_these_cookies_can_be_used_to_restore_the_authentication_Async( AuthenticationCookieMode mode, - bool useGenericWrapper ) + // Request with token and ?schemes query parametrers: we receive the providers. { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - if( useGenericWrapper ) - { - services.AddSingleton(); - } - }, - webFrontAuthOptions: opt => opt.CookieMode = mode ); - - // Login: the 2 cookies are set on .webFront/c/ path. - var login = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper: useGenericWrapper ); - Debug.Assert( login.Info != null ); - DateTime basicLoginTime = login.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed; - string? originalToken = login.Token; - // Request with token: the authentication is based on the token. - { - runningServer.Client.Token = originalToken; - using HttpResponseMessage tokenRefresh = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.RefreshUri ); - tokenRefresh.EnsureSuccessStatusCode(); - var c = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await tokenRefresh.Content.ReadAsStringAsync() ); - Debug.Assert( c.Info != null ); - c.Info.Level.Should().Be( AuthLevel.Normal ); - c.Info.User.UserName.Should().Be( "Albert" ); - c.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed.Should().Be( basicLoginTime ); - } - // Token less request: the authentication is restored from the cookie. - { - runningServer.Client.Token = null; - var tokenLessResponse = await runningServer.Client.AuthenticationRefreshAsync(); - Debug.Assert( tokenLessResponse.Info != null ); - tokenLessResponse.Info.Level.Should().Be( AuthLevel.Normal ); - tokenLessResponse.Info.User.UserName.Should().Be( "Albert" ); - tokenLessResponse.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed.Should().Be( basicLoginTime ); - } - // Request with token and ?schemes query parametrers: we receive the providers. - { - runningServer.Client.Token = originalToken; - var tokenRefresh = await runningServer.Client.AuthenticationRefreshAsync( schemes: true ); - Debug.Assert( tokenRefresh.Info != null ); - tokenRefresh.Info.Level.Should().Be( AuthLevel.Normal ); - tokenRefresh.Info.User.UserName.Should().Be( "Albert" ); - tokenRefresh.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed.Should().Be( basicLoginTime ); - tokenRefresh.Schemes.Should().ContainSingle( "Basic" ); - } + runningServer.Client.Token = originalToken; + var tokenRefresh = await runningServer.Client.AuthenticationRefreshAsync( schemes: true ); + Debug.Assert( tokenRefresh.Info != null ); + tokenRefresh.Info.Level.Should().Be( AuthLevel.Normal ); + tokenRefresh.Info.User.UserName.Should().Be( "Albert" ); + tokenRefresh.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed.Should().Be( basicLoginTime ); + tokenRefresh.Schemes.Should().ContainSingle( "Basic" ); } + } - [TestCase( AuthenticationCookieMode.WebFrontPath )] - [TestCase( AuthenticationCookieMode.RootPath )] - public async Task bad_tokens_are_ignored_as_long_as_cookies_can_be_used_Async( AuthenticationCookieMode mode ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = mode ); + [TestCase( AuthenticationCookieMode.WebFrontPath )] + [TestCase( AuthenticationCookieMode.RootPath )] + public async Task bad_tokens_are_ignored_as_long_as_cookies_can_be_used_Async( AuthenticationCookieMode mode ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = mode ); - var firstLogin = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + var firstLogin = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - string badToken = firstLogin.Token + 'B'; - runningServer.Client.Token = badToken; - AuthServerResponse c = await runningServer.Client.AuthenticationRefreshAsync(); - c.Info.Should().BeEquivalentTo( firstLogin.Info, "Authentication has been restored from cookies." ); - c.Token.Should().NotBe( badToken ); - } + string badToken = firstLogin.Token + 'B'; + runningServer.Client.Token = badToken; + AuthServerResponse c = await runningServer.Client.AuthenticationRefreshAsync(); + c.Info.Should().BeEquivalentTo( firstLogin.Info, "Authentication has been restored from cookies." ); + c.Token.Should().NotBe( badToken ); + } - [TestCase( AuthenticationCookieMode.WebFrontPath, true )] - [TestCase( AuthenticationCookieMode.RootPath, true )] - [TestCase( AuthenticationCookieMode.WebFrontPath, false )] - [TestCase( AuthenticationCookieMode.RootPath, false )] - public async Task logout_removes_both_cookies_Async( AuthenticationCookieMode mode, bool logoutWithToken ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = mode ); + [TestCase( AuthenticationCookieMode.WebFrontPath, true )] + [TestCase( AuthenticationCookieMode.RootPath, true )] + [TestCase( AuthenticationCookieMode.WebFrontPath, false )] + [TestCase( AuthenticationCookieMode.RootPath, false )] + public async Task logout_removes_both_cookies_Async( AuthenticationCookieMode mode, bool logoutWithToken ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = mode ); - // Login: the 2 cookies are set. - var firstLogin = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( firstLogin.Info != null ); - DateTime basicLoginTime = firstLogin.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed; - runningServer.Client.Token.Should().Be( firstLogin.Token, "The LoginViaBasicProviderAsync updates the client.Token." ); + // Login: the 2 cookies are set. + var firstLogin = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( firstLogin.Info != null ); + DateTime basicLoginTime = firstLogin.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed; + runningServer.Client.Token.Should().Be( firstLogin.Token, "The LoginViaBasicProviderAsync updates the client.Token." ); - // Logout - if( !logoutWithToken ) runningServer.Client.Token = null; + // Logout + if( !logoutWithToken ) runningServer.Client.Token = null; - await runningServer.Client.AuthenticationLogoutAsync(); - runningServer.Client.Token.Should().BeNull( "The AuthenticationLogout() clears the client token." ); + await runningServer.Client.AuthenticationLogoutAsync(); + runningServer.Client.Token.Should().BeNull( "The AuthenticationLogout() clears the client token." ); - // Refresh: no authentication. - var r = await runningServer.Client.AuthenticationRefreshAsync(); - Throw.DebugAssert( r.Info != null ); - r.Info.Level.Should().Be( AuthLevel.None ); - } + // Refresh: no authentication. + var r = await runningServer.Client.AuthenticationRefreshAsync(); + Throw.DebugAssert( r.Info != null ); + r.Info.Level.Should().Be( AuthLevel.None ); + } - [TestCase( AuthenticationCookieMode.WebFrontPath, true )] - [TestCase( AuthenticationCookieMode.RootPath, true )] - [TestCase( AuthenticationCookieMode.WebFrontPath, false )] - [TestCase( AuthenticationCookieMode.RootPath, false )] - public async Task LogoutCommand_removes_both_cookies_Async( AuthenticationCookieMode mode, bool logoutWithToken ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = mode ); + [TestCase( AuthenticationCookieMode.WebFrontPath, true )] + [TestCase( AuthenticationCookieMode.RootPath, true )] + [TestCase( AuthenticationCookieMode.WebFrontPath, false )] + [TestCase( AuthenticationCookieMode.RootPath, false )] + public async Task LogoutCommand_removes_both_cookies_Async( AuthenticationCookieMode mode, bool logoutWithToken ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = mode ); - // Login: the 2 cookies are set. - var firstLogin = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( firstLogin.Info != null ); - DateTime basicLoginTime = firstLogin.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed; - runningServer.Client.Token.Should().Be( firstLogin.Token, "The LoginViaBasicProviderAsync updates the client.Token." ); + // Login: the 2 cookies are set. + var firstLogin = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( firstLogin.Info != null ); + DateTime basicLoginTime = firstLogin.Info.User.Schemes.Single( p => p.Name == "Basic" ).LastUsed; + runningServer.Client.Token.Should().Be( firstLogin.Token, "The LoginViaBasicProviderAsync updates the client.Token." ); - // Logout - if( !logoutWithToken ) runningServer.Client.Token = null; + // Logout + if( !logoutWithToken ) runningServer.Client.Token = null; - using HttpResponseMessage logout = await runningServer.Client.GetAsync( "ComingFromCris/LogoutCommand" ); - logout.EnsureSuccessStatusCode(); + using HttpResponseMessage logout = await runningServer.Client.GetAsync( "ComingFromCris/LogoutCommand" ); + logout.EnsureSuccessStatusCode(); - // Refresh: no authentication. - runningServer.Client.Token = null; - var r = await runningServer.Client.AuthenticationRefreshAsync(); - Throw.DebugAssert( r.Info != null ); - r.Info.Level.Should().Be( AuthLevel.None ); - } + // Refresh: no authentication. + runningServer.Client.Token = null; + var r = await runningServer.Client.AuthenticationRefreshAsync(); + Throw.DebugAssert( r.Info != null ); + r.Info.Level.Should().Be( AuthLevel.None ); + } + + [Test] + public async Task invalid_payload_to_basic_login_returns_a_400_bad_request_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + + HttpResponseMessage response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, "{\"userName\":\"\",\"password\":\"success\"}" ); + response.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + runningServer.Client.CookieContainer.GetCookies( new Uri( $"{runningServer.ServerAddress}/.webfront/c/" ) ).Should().HaveCount( 0 ); + response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, "{\"userName\":\"toto\",\"password\":\"\"}" ); + response.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, "not a json" ); + response.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + } - [Test] - public async Task invalid_payload_to_basic_login_returns_a_400_bad_request_Async() + [TestCase( false, Description = "With cookies on the .webfront path." )] + [TestCase( true, Description = "With cookies on the root path." )] + public async Task webfront_token_endpoint_returns_the_current_authentication_indented_JSON_and_enables_to_test_actual_authentication_Async( bool rootCookiePath ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = rootCookiePath + ? AuthenticationCookieMode.RootPath + : AuthenticationCookieMode.WebFrontPath ); + var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); - - HttpResponseMessage response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, "{\"userName\":\"\",\"password\":\"success\"}" ); - response.StatusCode.Should().Be( HttpStatusCode.BadRequest ); - runningServer.Client.CookieContainer.GetCookies( new Uri( $"{runningServer.ServerAddress}/.webfront/c/" ) ).Should().HaveCount( 0 ); - response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, "{\"userName\":\"toto\",\"password\":\"\"}" ); - response.StatusCode.Should().Be( HttpStatusCode.BadRequest ); - response = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.BasicLoginUri, "not a json" ); - response.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + // With token: it always works. + runningServer.Client.Token.Should().Be( r.Token ); + HttpResponseMessage req = await runningServer.Client.GetAsync( tokenExplainUri ); + var tokenClear = await req.Content.ReadAsStringAsync(); + tokenClear.Should().Contain( "Albert" ); } - - [TestCase( false, Description = "With cookies on the .webfront path." )] - [TestCase( true, Description = "With cookies on the root path." )] - public async Task webfront_token_endpoint_returns_the_current_authentication_indented_JSON_and_enables_to_test_actual_authentication_Async( bool rootCookiePath ) { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => opt.CookieMode = rootCookiePath - ? AuthenticationCookieMode.RootPath - : AuthenticationCookieMode.WebFrontPath ); - var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + // Without token: it works only when CookieMode is AuthenticationCookieMode.RootPath. + runningServer.Client.Token = null; + HttpResponseMessage req = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.TokenExplainUri ); + var tokenClear = await req.Content.ReadAsStringAsync(); + if( rootCookiePath ) { - // With token: it always works. - runningServer.Client.Token.Should().Be( r.Token ); - HttpResponseMessage req = await runningServer.Client.GetAsync( tokenExplainUri ); - var tokenClear = await req.Content.ReadAsStringAsync(); + // Authentication Cookie has been used. tokenClear.Should().Contain( "Albert" ); } + else { - // Without token: it works only when CookieMode is AuthenticationCookieMode.RootPath. - runningServer.Client.Token = null; - HttpResponseMessage req = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.TokenExplainUri ); - var tokenClear = await req.Content.ReadAsStringAsync(); - if( rootCookiePath ) - { - // Authentication Cookie has been used. - tokenClear.Should().Contain( "Albert" ); - } - else - { - tokenClear.Should().NotContain( "Albert" ); - } + tokenClear.Should().NotContain( "Albert" ); } } + } - [TestCase( true, false )] - [TestCase( true, true )] - [TestCase( false, true )] - [TestCase( false, false )] - public async Task SlidingExpiration_works_as_expected_in_bearer_only_mode_by_calling_refresh_endpoint_Async( bool useGenericWrapper, bool rememberMe ) - { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( - services => - { - if( useGenericWrapper ) - { - services.AddSingleton(); - } - }, - webFrontAuthOptions: opt => + [TestCase( true, false )] + [TestCase( true, true )] + [TestCase( false, true )] + [TestCase( false, false )] + public async Task SlidingExpiration_works_as_expected_in_bearer_only_mode_by_calling_refresh_endpoint_Async( bool useGenericWrapper, bool rememberMe ) + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( + services => + { + if( useGenericWrapper ) { - opt.ExpireTimeSpan = TimeSpan.FromSeconds( 2.0 ); - opt.SlidingExpirationTime = TimeSpan.FromSeconds( 10 ); - opt.CookieMode = AuthenticationCookieMode.None; - } ); + services.AddSingleton(); + } + }, + webFrontAuthOptions: opt => + { + opt.ExpireTimeSpan = TimeSpan.FromSeconds( 2.0 ); + opt.SlidingExpirationTime = TimeSpan.FromSeconds( 10 ); + opt.CookieMode = AuthenticationCookieMode.None; + } ); - // This test is far from perfect but does the job without clock injection. - AuthServerResponse auth = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper, rememberMe ); - Throw.DebugAssert( auth.Info!.Expires != null ); + // This test is far from perfect but does the job without clock injection. + AuthServerResponse auth = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, useGenericWrapper, rememberMe ); + Throw.DebugAssert( auth.Info!.Expires != null ); - DateTime next = auth.Info.Expires.Value - TimeSpan.FromSeconds( 1.7 ); - while( next > DateTime.UtcNow ) ; + DateTime next = auth.Info.Expires.Value - TimeSpan.FromSeconds( 1.7 ); + while( next > DateTime.UtcNow ) ; - runningServer.Client.Token = auth.Token; - AuthServerResponse refresh = await runningServer.Client.AuthenticationRefreshAsync(); - Throw.DebugAssert( refresh.Info!.Expires != null ); - refresh.Info.Expires.Value.Should().BeAfter( auth.Info.Expires.Value.AddSeconds( 1 ), "Refresh increased the expiration time." ); + runningServer.Client.Token = auth.Token; + AuthServerResponse refresh = await runningServer.Client.AuthenticationRefreshAsync(); + Throw.DebugAssert( refresh.Info!.Expires != null ); + refresh.Info.Expires.Value.Should().BeAfter( auth.Info.Expires.Value.AddSeconds( 1 ), "Refresh increased the expiration time." ); - refresh.RememberMe.Should().BeFalse( "In CookieMode None, RememberMe is always false, no matter what." ); - } + refresh.RememberMe.Should().BeFalse( "In CookieMode None, RememberMe is always false, no matter what." ); + } - [Test] - public async Task SlidingExpiration_works_as_expected_in_rooted_Cookie_mode_where_any_request_can_do_the_job_Async() + [Test] + public async Task SlidingExpiration_works_as_expected_in_rooted_Cookie_mode_where_any_request_can_do_the_job_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => - { - opt.CookieMode = AuthenticationCookieMode.RootPath; - opt.ExpireTimeSpan = TimeSpan.FromSeconds( 2.0 ); - opt.SlidingExpirationTime = TimeSpan.FromSeconds( 10 ); - } ); + opt.CookieMode = AuthenticationCookieMode.RootPath; + opt.ExpireTimeSpan = TimeSpan.FromSeconds( 2.0 ); + opt.SlidingExpirationTime = TimeSpan.FromSeconds( 10 ); + } ); - // This test is far from perfect but does the job without clock injection. - AuthServerResponse auth = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); - Throw.DebugAssert( auth.Info!.Expires != null ); + // This test is far from perfect but does the job without clock injection. + AuthServerResponse auth = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true ); + Throw.DebugAssert( auth.Info!.Expires != null ); - DateTime expCookie1 = runningServer.Client.CookieContainer.GetCookies( runningServer.Client.BaseAddress )[".webFront"]!.Expires.ToUniversalTime(); - expCookie1.Should().BeCloseTo( auth.Info.Expires.Value, precision: TimeSpan.FromSeconds( 1 ) ); + DateTime expCookie1 = runningServer.Client.CookieContainer.GetCookies( runningServer.Client.BaseAddress )[".webFront"]!.Expires.ToUniversalTime(); + expCookie1.Should().BeCloseTo( auth.Info.Expires.Value, precision: TimeSpan.FromSeconds( 1 ) ); - DateTime next = auth.Info.Expires.Value - TimeSpan.FromSeconds( 1.7 ); - while( next > DateTime.UtcNow ) ; + DateTime next = auth.Info.Expires.Value - TimeSpan.FromSeconds( 1.7 ); + while( next > DateTime.UtcNow ) ; - // Calling token endpoint (like any other endpoint that sollicitates authentication) is enough. - using HttpResponseMessage req = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.TokenExplainUri ); - var response = JObject.Parse( await req.Content.ReadAsStringAsync() ); + // Calling token endpoint (like any other endpoint that sollicitates authentication) is enough. + using HttpResponseMessage req = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.TokenExplainUri ); + var response = JObject.Parse( await req.Content.ReadAsStringAsync() ); - ((bool?)response["rememberMe"]).Should().BeTrue(); - IAuthenticationInfo? refresh = runningServer.GetAuthenticationTypeSystem().AuthenticationInfo.FromJObject( (JObject?)response["info"] ); - Throw.DebugAssert( refresh!.Expires != null ); + ((bool?)response["rememberMe"]).Should().BeTrue(); + IAuthenticationInfo? refresh = runningServer.GetAuthenticationTypeSystem().AuthenticationInfo.FromJObject( (JObject?)response["info"] ); + Throw.DebugAssert( refresh!.Expires != null ); - refresh.Expires.Value.Should().BeAfter( auth.Info.Expires.Value, "Token life time has been increased." ); - Throw.DebugAssert( refresh.Expires != null ); + refresh.Expires.Value.Should().BeAfter( auth.Info.Expires.Value, "Token life time has been increased." ); + Throw.DebugAssert( refresh.Expires != null ); - DateTime expCookie2 = runningServer.Client.CookieContainer.GetCookies( runningServer.Client.BaseAddress )[".webFront"]!.Expires.ToUniversalTime(); - expCookie2.Should().BeCloseTo( refresh.Expires.Value, precision: TimeSpan.FromSeconds( 1 ) ); - } + DateTime expCookie2 = runningServer.Client.CookieContainer.GetCookies( runningServer.Client.BaseAddress )[".webFront"]!.Expires.ToUniversalTime(); + expCookie2.Should().BeCloseTo( refresh.Expires.Value, precision: TimeSpan.FromSeconds( 1 ) ); + } - [Test] - public async Task AllowedReturnUrls_quick_test_Async() + [Test] + public async Task AllowedReturnUrls_quick_test_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync( webFrontAuthOptions: opt => - { - opt.AllowedReturnUrls.Add( "https://yes.yes" ); - } ); - - { - // This scheme is not known but the test of the return url is done before. - using var m = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode( "https://no.no" ) ); - m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); - (await m.Content.ReadAsStringAsync()).Should() - .Be( """{"errorId":"DisallowedReturnUrl","errorText":"The returnUrl='https://no.no' doesn't start with any of configured AllowedReturnUrls prefixes."}""" ); - - // Invalid schemes triggers an error 500 in AspNet ChallengeAsync. - // The exception is "No authentication handler is registered for the scheme 'NONE'. The registered schemes are: WebFrontAuth. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("NONE",...)?" - // TODO: since our scheme is provided by the front, we SHOULD test the available schemes and return a 400 instead of a 500. - using var m2 = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode( "https://yes.yes" ) ); - m2.StatusCode.Should().Be( HttpStatusCode.InternalServerError ); - - using var m3 = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode( "https://yes.yes/hello" ) ); - m3.StatusCode.Should().Be( HttpStatusCode.InternalServerError ); - } - } + opt.AllowedReturnUrls.Add( "https://yes.yes" ); + } ); - [Test] - public async Task empty_AllowedReturnUrls_forbids_any_inline_login_Async() { - await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); - // This scheme is not known but the test of the return url is done before. - using var m = await runningServer.Client.GetAsync(RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode("https://un.reg.ister.ed")); + using var m = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode( "https://no.no" ) ); m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); (await m.Content.ReadAsStringAsync()).Should() - .Be( """{"errorId":"DisallowedReturnUrl","errorText":"The returnUrl='https://un.reg.ister.ed' doesn't start with any of configured AllowedReturnUrls prefixes."}""" ); + .Be( """{"errorId":"DisallowedReturnUrl","errorText":"The returnUrl='https://no.no' doesn't start with any of configured AllowedReturnUrls prefixes."}""" ); + + // Invalid schemes triggers an error 500 in AspNet ChallengeAsync. + // The exception is "No authentication handler is registered for the scheme 'NONE'. The registered schemes are: WebFrontAuth. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("NONE",...)?" + // TODO: since our scheme is provided by the front, we SHOULD test the available schemes and return a 400 instead of a 500. + using var m2 = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode( "https://yes.yes" ) ); + m2.StatusCode.Should().Be( HttpStatusCode.InternalServerError ); + + using var m3 = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode( "https://yes.yes/hello" ) ); + m3.StatusCode.Should().Be( HttpStatusCode.InternalServerError ); } + } + [Test] + public async Task empty_AllowedReturnUrls_forbids_any_inline_login_Async() + { + await using var runningServer = await LocalHelper.CreateLocalAuthServerAsync(); + + // This scheme is not known but the test of the return url is done before. + using var m = await runningServer.Client.GetAsync( RunningAspNetAuthServerExtensions.StartLoginUri + "?scheme=NONE&returnUrl=" + WebUtility.UrlEncode( "https://un.reg.ister.ed" ) ); + m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + (await m.Content.ReadAsStringAsync()).Should() + .Be( """{"errorId":"DisallowedReturnUrl","errorText":"The returnUrl='https://un.reg.ister.ed' doesn't start with any of configured AllowedReturnUrls prefixes."}""" ); } + } diff --git a/Tests/CK.DB.AspNet.Auth.This.Tests/BasicAuthenticationTests.cs b/Tests/CK.DB.AspNet.Auth.This.Tests/BasicAuthenticationTests.cs index 74e75187..9f3fe3a9 100644 --- a/Tests/CK.DB.AspNet.Auth.This.Tests/BasicAuthenticationTests.cs +++ b/Tests/CK.DB.AspNet.Auth.This.Tests/BasicAuthenticationTests.cs @@ -19,260 +19,258 @@ using System.Threading.Tasks; using static CK.Testing.MonitorTestHelper; -namespace CK.DB.AspNet.Auth.Tests +namespace CK.DB.AspNet.Auth.Tests; + +[TestFixture] +public partial class BasicAuthenticationTests { - [TestFixture] - public partial class BasicAuthenticationTests + [TestCase( true )] + [TestCase( false )] + public async Task basic_authentication_via_generic_wrapper_on_a_created_user_Async( bool allowed ) { - [TestCase( true )] - [TestCase( false )] - public async Task basic_authentication_via_generic_wrapper_on_a_created_user_Async( bool allowed ) - { - using var allowConfigure = DirectLoginAllower.SetAllow( allowed ? DirectLoginAllower.What.BasicOnly : DirectLoginAllower.What.None ); + using var allowConfigure = DirectLoginAllower.SetAllow( allowed ? DirectLoginAllower.What.BasicOnly : DirectLoginAllower.What.None ); - var builder = WebApplication.CreateSlimBuilder(); - builder.AddApplicationIdentityServiceConfiguration(); - builder.Services.AddSingleton(); - await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); + var builder = WebApplication.CreateSlimBuilder(); + builder.AddApplicationIdentityServiceConfiguration(); + builder.Services.AddSingleton(); + await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); - var user = runningServer.Services.GetRequiredService(); - var auth = runningServer.Services.GetRequiredService(); - var basic = auth.FindRequiredProvider( "Basic", mustHavePayload: false ); + var user = runningServer.Services.GetRequiredService(); + var auth = runningServer.Services.GetRequiredService(); + var basic = auth.FindRequiredProvider( "Basic", mustHavePayload: false ); - using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); + using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); - string userName = Guid.NewGuid().ToString(); - int idUser = await user.CreateUserAsync( ctx, 1, userName ); - basic.CreateOrUpdateUser( ctx, 1, idUser, "pass" ); + string userName = Guid.NewGuid().ToString(); + int idUser = await user.CreateUserAsync( ctx, 1, userName ); + basic.CreateOrUpdateUser( ctx, 1, idUser, "pass" ); - string? deviceId = null; + string? deviceId = null; + { + var param = new JObject( new JProperty( "provider", "Basic" ), + new JProperty( "payload", new JObject( + new JProperty( "userName", userName ), + new JProperty( "password", "pass" ) ) ) ); + using HttpResponseMessage authBasic = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); + if( allowed ) { - var param = new JObject( new JProperty( "provider", "Basic" ), - new JProperty( "payload", new JObject( - new JProperty( "userName", userName ), - new JProperty( "password", "pass" ) ) ) ); - using HttpResponseMessage authBasic = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); - if( allowed ) - { - authBasic.EnsureSuccessStatusCode(); - var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await authBasic.Content.ReadAsStringAsync() ); - Throw.DebugAssert( r.Info != null ); - r.Info.Level.Should().Be( AuthLevel.Normal ); - r.Info.User.UserId.Should().Be( idUser ); - r.Info.User.Schemes.Select( p => p.Name ).Should().BeEquivalentTo( new[] { "Basic" } ); - r.Token.Should().NotBeNullOrWhiteSpace(); - deviceId = r.Info.DeviceId; - Throw.DebugAssert( deviceId != null ); - } - else - { - authBasic.StatusCode.Should().Be( HttpStatusCode.Forbidden ); - } + authBasic.EnsureSuccessStatusCode(); + var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await authBasic.Content.ReadAsStringAsync() ); + Throw.DebugAssert( r.Info != null ); + r.Info.Level.Should().Be( AuthLevel.Normal ); + r.Info.User.UserId.Should().Be( idUser ); + r.Info.User.Schemes.Select( p => p.Name ).Should().BeEquivalentTo( new[] { "Basic" } ); + r.Token.Should().NotBeNullOrWhiteSpace(); + deviceId = r.Info.DeviceId; + Throw.DebugAssert( deviceId != null ); } - if( allowed ) + else { - var payload = new JObject( new JProperty( "userName", userName ), new JProperty( "password", "failed" ) ); - var param = new JObject( new JProperty( "provider", "Basic" ), new JProperty( "payload", payload ) ); - using HttpResponseMessage authFailed = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); - authFailed.StatusCode.Should().Be( HttpStatusCode.Unauthorized ); - var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await authFailed.Content.ReadAsStringAsync() ); - ShouldBeUnsafeUser( r, idUser, deviceId! ); + authBasic.StatusCode.Should().Be( HttpStatusCode.Forbidden ); } } - - [TestCase( "Albert", "pass" )] - [TestCase( "Paula", "pass" )] - public async Task basic_authentication_on_user_Async( string userName, string password ) + if( allowed ) { - var builder = WebApplication.CreateSlimBuilder(); - builder.AddApplicationIdentityServiceConfiguration(); - await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); + var payload = new JObject( new JProperty( "userName", userName ), new JProperty( "password", "failed" ) ); + var param = new JObject( new JProperty( "provider", "Basic" ), new JProperty( "payload", payload ) ); + using HttpResponseMessage authFailed = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); + authFailed.StatusCode.Should().Be( HttpStatusCode.Unauthorized ); + var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await authFailed.Content.ReadAsStringAsync() ); + ShouldBeUnsafeUser( r, idUser, deviceId! ); + } + } - var user = runningServer.Services.GetRequiredService(); - var basic = runningServer.Services.GetRequiredService(); + [TestCase( "Albert", "pass" )] + [TestCase( "Paula", "pass" )] + public async Task basic_authentication_on_user_Async( string userName, string password ) + { + var builder = WebApplication.CreateSlimBuilder(); + builder.AddApplicationIdentityServiceConfiguration(); + await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); - using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); + var user = runningServer.Services.GetRequiredService(); + var basic = runningServer.Services.GetRequiredService(); - int idUser = await user.CreateUserAsync( ctx, 1, userName ); - if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); - await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); + using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); - string deviceId; - { - var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, true, password: password ); - Throw.DebugAssert( r.Info != null ); - deviceId = r.Info.DeviceId; - deviceId.Should().NotBeNullOrWhiteSpace(); - } - { - var rFailed = await runningServer.Client.AuthenticationBasicLoginAsync( userName, false, password: "failed" + password ); - ShouldBeUnsafeUser( rFailed, idUser, deviceId ); - } - } + int idUser = await user.CreateUserAsync( ctx, 1, userName ); + if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); + await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); - static void ShouldBeUnsafeUser( AuthServerResponse r, int idUser, string deviceId ) + string deviceId; { + var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, true, password: password ); Throw.DebugAssert( r.Info != null ); - r.Info.Level.Should().Be( AuthLevel.Unsafe ); - r.Info.User.UserId.Should().Be( 0 ); - r.Info.ActualUser.UserId.Should().Be( 0 ); - r.Info.UnsafeUser.UserId.Should().Be( idUser ); - r.Token.Should().NotBeNullOrWhiteSpace(); - r.Info.DeviceId.Should().Be( deviceId ); + deviceId = r.Info.DeviceId; + deviceId.Should().NotBeNullOrWhiteSpace(); } - - [Test] - public async Task unsafe_direct_login_returns_BadRequest_and_JSON_ArgumentException_when_payload_is_not_in_the_expected_format_Async() { - using var allowConfigure = DirectLoginAllower.SetAllow( DirectLoginAllower.What.All ); + var rFailed = await runningServer.Client.AuthenticationBasicLoginAsync( userName, false, password: "failed" + password ); + ShouldBeUnsafeUser( rFailed, idUser, deviceId ); + } + } - var builder = WebApplication.CreateSlimBuilder(); - builder.AddApplicationIdentityServiceConfiguration(); - builder.Services.AddSingleton(); - await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); + static void ShouldBeUnsafeUser( AuthServerResponse r, int idUser, string deviceId ) + { + Throw.DebugAssert( r.Info != null ); + r.Info.Level.Should().Be( AuthLevel.Unsafe ); + r.Info.User.UserId.Should().Be( 0 ); + r.Info.ActualUser.UserId.Should().Be( 0 ); + r.Info.UnsafeUser.UserId.Should().Be( idUser ); + r.Token.Should().NotBeNullOrWhiteSpace(); + r.Info.DeviceId.Should().Be( deviceId ); + } - // Missing userName or userId. - { - var param = new JObject( new JProperty( "provider", "Basic" ), - new JProperty( "payload", - new JObject( new JProperty( "password", "pass" ) ) ) ); - using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); - m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); - var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); - r.ErrorId.Should().Be( "System.ArgumentException" ); - r.ErrorText.Should().Be( "Invalid payload. Missing 'UserId' -> int or 'UserName' -> string entry. (Parameter 'payload')" ); - } - // Missing password. - { - var param = new JObject( new JProperty( "provider", "Basic" ), - new JProperty( "payload", - new JObject( new JProperty( "userId", "3712" ) ) ) ); - using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); - m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); - var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); - r.ErrorId.Should().Be( "System.ArgumentException" ); - r.ErrorText.Should().Be( "Invalid payload. Missing 'Password' -> string entry. (Parameter 'payload')" ); - } - // Totally invalid payload. - { - var param = new JObject( new JProperty( "provider", "Basic" ), - new JProperty( "payload", "Nimp" ) ); - using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); - m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); - var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); - r.ErrorId.Should().Be( "System.ArgumentException" ); - r.ErrorText.Should().Be( "Invalid payload. It must be either a Tuple or ValueTuple (int,string) or (string,string) or a IDictionary or IEnumerable> or IEnumerable<(string,object?)> with 'Password' -> string and 'UserId' -> int or 'UserName' -> string entries. (Parameter 'payload')" ); - } - } + [Test] + public async Task unsafe_direct_login_returns_BadRequest_and_JSON_ArgumentException_when_payload_is_not_in_the_expected_format_Async() + { + using var allowConfigure = DirectLoginAllower.SetAllow( DirectLoginAllower.What.All ); + + var builder = WebApplication.CreateSlimBuilder(); + builder.AddApplicationIdentityServiceConfiguration(); + builder.Services.AddSingleton(); + await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); - [TestCase( "Albert", "pass", true )] - [TestCase( "Paula", "pass", false )] - public async Task IWebFrontAuthValidateLoginService_can_prevent_unsafe_direct_login_Async( string userName, string password, bool okInEvil ) + // Missing userName or userId. { - using var allowConfigure = DirectLoginAllower.SetAllow( DirectLoginAllower.What.All ); + var param = new JObject( new JProperty( "provider", "Basic" ), + new JProperty( "payload", + new JObject( new JProperty( "password", "pass" ) ) ) ); + using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); + m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); + r.ErrorId.Should().Be( "System.ArgumentException" ); + r.ErrorText.Should().Be( "Invalid payload. Missing 'UserId' -> int or 'UserName' -> string entry. (Parameter 'payload')" ); + } + // Missing password. + { + var param = new JObject( new JProperty( "provider", "Basic" ), + new JProperty( "payload", + new JObject( new JProperty( "userId", "3712" ) ) ) ); + using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); + m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); + r.ErrorId.Should().Be( "System.ArgumentException" ); + r.ErrorText.Should().Be( "Invalid payload. Missing 'Password' -> string entry. (Parameter 'payload')" ); + } + // Totally invalid payload. + { + var param = new JObject( new JProperty( "provider", "Basic" ), + new JProperty( "payload", "Nimp" ) ); + using HttpResponseMessage m = await runningServer.Client.PostJsonAsync( RunningAspNetAuthServerExtensions.UnsafeDirectLoginUri, param.ToString() ); + m.StatusCode.Should().Be( HttpStatusCode.BadRequest ); + var r = AuthServerResponse.Parse( runningServer.GetAuthenticationTypeSystem(), await m.Content.ReadAsStringAsync() ); + r.ErrorId.Should().Be( "System.ArgumentException" ); + r.ErrorText.Should().Be( "Invalid payload. It must be either a Tuple or ValueTuple (int,string) or (string,string) or a IDictionary or IEnumerable> or IEnumerable<(string,object?)> with 'Password' -> string and 'UserId' -> int or 'UserName' -> string entries. (Parameter 'payload')" ); + } + } - var builder = WebApplication.CreateSlimBuilder(); - builder.AddApplicationIdentityServiceConfiguration(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton (); - await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); + [TestCase( "Albert", "pass", true )] + [TestCase( "Paula", "pass", false )] + public async Task IWebFrontAuthValidateLoginService_can_prevent_unsafe_direct_login_Async( string userName, string password, bool okInEvil ) + { + using var allowConfigure = DirectLoginAllower.SetAllow( DirectLoginAllower.What.All ); - var user = runningServer.Services.GetRequiredService(); - var basic = runningServer.Services.GetRequiredService(); + var builder = WebApplication.CreateSlimBuilder(); + builder.AddApplicationIdentityServiceConfiguration(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); - using var ctx = new SqlStandardCallContext(); + var user = runningServer.Services.GetRequiredService(); + var basic = runningServer.Services.GetRequiredService(); - await ctx[user].Connection.EnsureOpenAsync(); - int idUser = await user.CreateUserAsync( ctx, 1, userName ); - if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); - await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); + using var ctx = new SqlStandardCallContext(); - string deviceId; + await ctx[user].Connection.EnsureOpenAsync(); + int idUser = await user.CreateUserAsync( ctx, 1, userName ); + if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); + await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); + + string deviceId; + { + var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, true, useGenericWrapper: true, password: password, jsonUserData: """{"zone":"good"}""" ); + Throw.DebugAssert( r.Info != null ); + deviceId = r.Info.DeviceId; + deviceId.Should().NotBeNullOrWhiteSpace(); + r.UserData.Should().Contain( [("zone", "good")] ); + } + + { + var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, okInEvil, useGenericWrapper: true, password: password, jsonUserData: """{"zone":"<&>vil"}""" ); + Throw.DebugAssert( r.Info != null ); + if( okInEvil ) { - var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, true, useGenericWrapper: true, password: password, jsonUserData: """{"zone":"good"}""" ); Throw.DebugAssert( r.Info != null ); - deviceId = r.Info.DeviceId; - deviceId.Should().NotBeNullOrWhiteSpace(); - r.UserData.Should().Contain( [("zone", "good")] ); + r.Info.Level.Should().Be( AuthLevel.Normal ); + r.Info.User.UserId.Should().Be( idUser ); + r.Info.User.Schemes.Select( p => p.Name ).Should().BeEquivalentTo( ["Basic"] ); + r.Token.Should().NotBeNullOrWhiteSpace(); + r.UserData.Should().Contain( [("zone", "<&>vil")] ); } - + else { - var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, okInEvil, useGenericWrapper: true, password: password, jsonUserData: """{"zone":"<&>vil"}""" ); - Throw.DebugAssert( r.Info != null ); - if( okInEvil ) - { - Throw.DebugAssert( r.Info != null ); - r.Info.Level.Should().Be( AuthLevel.Normal ); - r.Info.User.UserId.Should().Be( idUser ); - r.Info.User.Schemes.Select( p => p.Name ).Should().BeEquivalentTo( ["Basic"] ); - r.Token.Should().NotBeNullOrWhiteSpace(); - r.UserData.Should().Contain( [( "zone", "<&>vil" )] ); - } - else - { - ShouldBeUnsafeUser( r, idUser, deviceId ); - r.ErrorId.Should().Be( "Validation" ); - r.ErrorText.Should().Be( "Paula must not go in the <&>vil Zone!" ); - r.UserData.Should().Contain( [("zone", "<&>vil")] ); - } + ShouldBeUnsafeUser( r, idUser, deviceId ); + r.ErrorId.Should().Be( "Validation" ); + r.ErrorText.Should().Be( "Paula must not go in the <&>vil Zone!" ); + r.UserData.Should().Contain( [("zone", "<&>vil")] ); } } + } - [TestCase( "Albert", "pass", true )] - [TestCase( "Paula", "pass", false )] - public async Task IWebFrontAuthValidateLoginService_can_prevent_basic_login_Async( string userName, string password, bool okInEvil ) - { - var builder = WebApplication.CreateSlimBuilder(); - builder.AddApplicationIdentityServiceConfiguration(); - builder.Services.AddSingleton(); - await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); + [TestCase( "Albert", "pass", true )] + [TestCase( "Paula", "pass", false )] + public async Task IWebFrontAuthValidateLoginService_can_prevent_basic_login_Async( string userName, string password, bool okInEvil ) + { + var builder = WebApplication.CreateSlimBuilder(); + builder.AddApplicationIdentityServiceConfiguration(); + builder.Services.AddSingleton(); + await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); - var user = runningServer.Services.GetRequiredService(); - var basic = runningServer.Services.GetRequiredService(); + var user = runningServer.Services.GetRequiredService(); + var basic = runningServer.Services.GetRequiredService(); - using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); + using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); - await ctx[user].Connection.EnsureOpenAsync(); - int idUser = await user.CreateUserAsync( ctx, 1, userName ); - if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); - await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); + await ctx[user].Connection.EnsureOpenAsync(); + int idUser = await user.CreateUserAsync( ctx, 1, userName ); + if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); + await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); - string deviceId; + string deviceId; + { + // Zone is "good". + var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, true, password: password, jsonUserData: """{"zone":"good"}""" ); + Throw.DebugAssert( r.Info != null ); + deviceId = r.Info.DeviceId; + r.Info.Level.Should().Be( AuthLevel.Normal ); + r.Info.User.UserId.Should().Be( idUser ); + r.Info.User.Schemes.Select( p => p.Name ).Should().BeEquivalentTo( ["Basic"] ); + r.Token.Should().NotBeNullOrWhiteSpace(); + r.UserData.Should().Contain( [("zone", "good")] ); + } + { + // Zone is "<&>vil". + var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, okInEvil, password: password, jsonUserData: """{"zone":"<&>vil"}""" ); + if( okInEvil ) // When userName is "Albert". { - // Zone is "good". - var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, true, password: password, jsonUserData: """{"zone":"good"}""" ); Throw.DebugAssert( r.Info != null ); - deviceId = r.Info.DeviceId; r.Info.Level.Should().Be( AuthLevel.Normal ); r.Info.User.UserId.Should().Be( idUser ); r.Info.User.Schemes.Select( p => p.Name ).Should().BeEquivalentTo( ["Basic"] ); r.Token.Should().NotBeNullOrWhiteSpace(); - r.UserData.Should().Contain( [("zone", "good")] ); + r.ErrorId.Should().BeNull(); + r.ErrorText.Should().BeNull(); + r.UserData.Should().Contain( [("zone", "<&>vil")] ); } + else // When userName is "Paula". { - // Zone is "<&>vil". - var r = await runningServer.Client.AuthenticationBasicLoginAsync( userName, okInEvil, password: password, jsonUserData: """{"zone":"<&>vil"}""" ); - if( okInEvil ) // When userName is "Albert". - { - Throw.DebugAssert( r.Info != null ); - r.Info.Level.Should().Be( AuthLevel.Normal ); - r.Info.User.UserId.Should().Be( idUser ); - r.Info.User.Schemes.Select( p => p.Name ).Should().BeEquivalentTo( ["Basic"] ); - r.Token.Should().NotBeNullOrWhiteSpace(); - r.ErrorId.Should().BeNull(); - r.ErrorText.Should().BeNull(); - r.UserData.Should().Contain( [("zone", "<&>vil")] ); - } - else // When userName is "Paula". - { - ShouldBeUnsafeUser( r, idUser, deviceId ); - r.ErrorId.Should().Be( "Validation" ); - r.ErrorText.Should().Be( "Paula must not go in the <&>vil Zone!" ); - r.UserData.Should().Contain( [("zone", "<&>vil")] ); - } + ShouldBeUnsafeUser( r, idUser, deviceId ); + r.ErrorId.Should().Be( "Validation" ); + r.ErrorText.Should().Be( "Paula must not go in the <&>vil Zone!" ); + r.UserData.Should().Contain( [("zone", "<&>vil")] ); } } - } } diff --git a/Tests/CK.DB.AspNet.Auth.This.Tests/DBSetup.cs b/Tests/CK.DB.AspNet.Auth.This.Tests/DBSetup.cs index c911f41a..d9dc72cf 100644 --- a/Tests/CK.DB.AspNet.Auth.This.Tests/DBSetup.cs +++ b/Tests/CK.DB.AspNet.Auth.This.Tests/DBSetup.cs @@ -1,9 +1,8 @@ using NUnit.Framework; -namespace DBSetup +namespace DBSetup; + +[TestFixture] +public class DBSetup : CK.DB.Tests.DBSetup { - [TestFixture] - public class DBSetup : CK.DB.Tests.DBSetup - { - } } diff --git a/Tests/CK.DB.AspNet.Auth.This.Tests/RefreshTests.cs b/Tests/CK.DB.AspNet.Auth.This.Tests/RefreshTests.cs index 6049b592..975fa8f0 100644 --- a/Tests/CK.DB.AspNet.Auth.This.Tests/RefreshTests.cs +++ b/Tests/CK.DB.AspNet.Auth.This.Tests/RefreshTests.cs @@ -13,79 +13,78 @@ using System.Threading.Tasks; using static CK.Testing.MonitorTestHelper; -namespace CK.DB.AspNet.Auth.Tests +namespace CK.DB.AspNet.Auth.Tests; + +[TestFixture] +public class RefreshTests { - [TestFixture] - public class RefreshTests + [Test] + public async Task refreshing_with_callBackend_correctly_handles_impersonation_changes_Async() + { + var builder = WebApplication.CreateSlimBuilder(); + builder.Services.AddSingleton(); + builder.AddApplicationIdentityServiceConfiguration(); + await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); + + var user = runningServer.Services.GetRequiredService(); + var basic = runningServer.Services.GetRequiredService(); + + using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); + + int idAlbert = await SetupUserAsync( ctx, "Albert", "pass", user, basic ); + int idPaula = await SetupUserAsync( ctx, "Paula", "pass", user, basic ); + + var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, password: "pass" ); + + var newAlbertName = Guid.NewGuid().ToString(); + await user.UserNameSetAsync( ctx, 1, idAlbert, newAlbertName ); + + r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); + Throw.DebugAssert( r.Info != null ); + r.Info.User.UserId.Should().Be( idAlbert ); + r.Info.User.UserName.Should().Be( newAlbertName ); + + r = await runningServer.Client.AuthenticationImpersonateAsync( idPaula ); + Throw.DebugAssert( r.Info != null ); + r.Info.User.UserName.Should().Be( "Paula" ); + r.Info.ActualUser.UserName.Should().Be( newAlbertName ); + + var rRefresh = await runningServer.Client.AuthenticationRefreshAsync(); + rRefresh.Info.Should().BeEquivalentTo( r.Info, o => o.Excluding( info => info.Expires ) ); + + newAlbertName = Guid.NewGuid().ToString(); + var newPaulaName = Guid.NewGuid().ToString(); + await user.UserNameSetAsync( ctx, 1, idAlbert, newAlbertName ); + await user.UserNameSetAsync( ctx, 1, idPaula, newPaulaName ); + + r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); + Throw.DebugAssert( r.Info != null ); + r.Info.User.UserName.Should().Be( newPaulaName ); + r.Info.ActualUser.UserName.Should().Be( newAlbertName ); + + await user.UserNameSetAsync( ctx, 1, idPaula, "Paula" ); + r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); + Throw.DebugAssert( r.Info != null ); + r.Info.User.UserName.Should().Be( "Paula" ); + r.Info.ActualUser.UserName.Should().Be( newAlbertName ); + + await user.UserNameSetAsync( ctx, 1, idAlbert, "Albert" ); + r = await runningServer.Client.AuthenticationImpersonateAsync( idAlbert ); + Throw.DebugAssert( r.Info != null ); + r.Info.User.UserId.Should().Be( idAlbert ); + r.Info.User.UserName.Should().Be( newAlbertName, + "Impersonation in the ImpersonationForEverybodyService does not refresh the actual user." ); + + r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); + Throw.DebugAssert( r.Info != null ); + r.Info.ActualUser.UserName.Should().Be( "Albert" ); + } + + static async Task SetupUserAsync( SqlStandardCallContext ctx, string userName, string password, UserTable user, IBasicAuthenticationProvider basic ) { - [Test] - public async Task refreshing_with_callBackend_correctly_handles_impersonation_changes_Async() - { - var builder = WebApplication.CreateSlimBuilder(); - builder.Services.AddSingleton(); - builder.AddApplicationIdentityServiceConfiguration(); - await using var runningServer = await builder.CreateRunningAspNetAuthenticationServerAsync( SharedEngine.Map ); - - var user = runningServer.Services.GetRequiredService(); - var basic = runningServer.Services.GetRequiredService(); - - using var ctx = new SqlStandardCallContext( TestHelper.Monitor ); - - int idAlbert = await SetupUserAsync( ctx, "Albert", "pass", user, basic ); - int idPaula = await SetupUserAsync( ctx, "Paula", "pass", user, basic ); - - var r = await runningServer.Client.AuthenticationBasicLoginAsync( "Albert", true, password: "pass" ); - - var newAlbertName = Guid.NewGuid().ToString(); - await user.UserNameSetAsync( ctx, 1, idAlbert, newAlbertName ); - - r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); - Throw.DebugAssert( r.Info != null ); - r.Info.User.UserId.Should().Be( idAlbert ); - r.Info.User.UserName.Should().Be( newAlbertName ); - - r = await runningServer.Client.AuthenticationImpersonateAsync( idPaula ); - Throw.DebugAssert( r.Info != null ); - r.Info.User.UserName.Should().Be( "Paula" ); - r.Info.ActualUser.UserName.Should().Be( newAlbertName ); - - var rRefresh = await runningServer.Client.AuthenticationRefreshAsync(); - rRefresh.Info.Should().BeEquivalentTo( r.Info, o => o.Excluding( info => info.Expires ) ); - - newAlbertName = Guid.NewGuid().ToString(); - var newPaulaName = Guid.NewGuid().ToString(); - await user.UserNameSetAsync( ctx, 1, idAlbert, newAlbertName ); - await user.UserNameSetAsync( ctx, 1, idPaula, newPaulaName ); - - r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); - Throw.DebugAssert( r.Info != null ); - r.Info.User.UserName.Should().Be( newPaulaName ); - r.Info.ActualUser.UserName.Should().Be( newAlbertName ); - - await user.UserNameSetAsync( ctx, 1, idPaula, "Paula" ); - r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); - Throw.DebugAssert( r.Info != null ); - r.Info.User.UserName.Should().Be( "Paula" ); - r.Info.ActualUser.UserName.Should().Be( newAlbertName ); - - await user.UserNameSetAsync( ctx, 1, idAlbert, "Albert" ); - r = await runningServer.Client.AuthenticationImpersonateAsync( idAlbert ); - Throw.DebugAssert( r.Info != null ); - r.Info.User.UserId.Should().Be( idAlbert ); - r.Info.User.UserName.Should().Be( newAlbertName, - "Impersonation in the ImpersonationForEverybodyService does not refresh the actual user." ); - - r = await runningServer.Client.AuthenticationRefreshAsync( callBackend: true ); - Throw.DebugAssert( r.Info != null ); - r.Info.ActualUser.UserName.Should().Be( "Albert" ); - } - - static async Task SetupUserAsync( SqlStandardCallContext ctx, string userName, string password, UserTable user, IBasicAuthenticationProvider basic ) - { - int idUser = await user.CreateUserAsync( ctx, 1, userName ); - if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); - await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); - return idUser; - } + int idUser = await user.CreateUserAsync( ctx, 1, userName ); + if( idUser == -1 ) idUser = await user.FindByNameAsync( ctx, userName ); + await basic.CreateOrUpdatePasswordUserAsync( ctx, 1, idUser, password ); + return idUser; } } diff --git a/Tests/CK.DB.AspNet.Auth.This.Tests/Services/DirectLoginAllower.cs b/Tests/CK.DB.AspNet.Auth.This.Tests/Services/DirectLoginAllower.cs index 53dad471..bcf80757 100644 --- a/Tests/CK.DB.AspNet.Auth.This.Tests/Services/DirectLoginAllower.cs +++ b/Tests/CK.DB.AspNet.Auth.This.Tests/Services/DirectLoginAllower.cs @@ -4,30 +4,28 @@ using System; using System.Threading.Tasks; -namespace CK.DB.AspNet.Auth.Tests +namespace CK.DB.AspNet.Auth.Tests; + +[ExcludeCKType] +public class DirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService { - [ExcludeCKType] - public class DirectLoginAllower : IWebFrontAuthUnsafeDirectLoginAllowService + public enum What { - public enum What - { - None, - BasicOnly, - All - } - - public static What Allowed { get; private set; } + None, + BasicOnly, + All + } - public static IDisposable SetAllow( What a ) - { - Allowed = a; - return Util.CreateDisposableAction( () => Allowed = What.None ); - } + public static What Allowed { get; private set; } - public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) - { - return Allowed switch { What.BasicOnly => Task.FromResult( scheme == "Basic" ), What.All => Task.FromResult( true ), _ => Task.FromResult( false ) }; - } + public static IDisposable SetAllow( What a ) + { + Allowed = a; + return Util.CreateDisposableAction( () => Allowed = What.None ); } + public Task AllowAsync( HttpContext ctx, IActivityMonitor monitor, string scheme, object payload ) + { + return Allowed switch { What.BasicOnly => Task.FromResult( scheme == "Basic" ), What.All => Task.FromResult( true ), _ => Task.FromResult( false ) }; + } } diff --git a/Tests/CK.DB.AspNet.Auth.This.Tests/Services/ImpersonationForEveryBodyService.cs b/Tests/CK.DB.AspNet.Auth.This.Tests/Services/ImpersonationForEveryBodyService.cs index 07a1f47d..68355b5f 100644 --- a/Tests/CK.DB.AspNet.Auth.This.Tests/Services/ImpersonationForEveryBodyService.cs +++ b/Tests/CK.DB.AspNet.Auth.This.Tests/Services/ImpersonationForEveryBodyService.cs @@ -9,30 +9,28 @@ using System; using System.Threading.Tasks; -namespace CK.DB.AspNet.Auth.Tests -{ - [ExcludeCKType] - public class ImpersonationForEverybodyService : IWebFrontAuthImpersonationService - { - readonly IAuthenticationTypeSystem _typeSystem; - readonly IAuthenticationDatabaseService _db; +namespace CK.DB.AspNet.Auth.Tests; - public ImpersonationForEverybodyService( IAuthenticationTypeSystem typeSystem, IAuthenticationDatabaseService db ) - { - _typeSystem = typeSystem; - _db = db; - } +[ExcludeCKType] +public class ImpersonationForEverybodyService : IWebFrontAuthImpersonationService +{ + readonly IAuthenticationTypeSystem _typeSystem; + readonly IAuthenticationDatabaseService _db; - public async Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, int userId ) - { - IUserAuthInfo? dbUser = await _db.ReadUserAuthInfoAsync( ctx.RequestServices.GetRequiredService(), 1, userId ); - return _typeSystem.UserInfo.FromUserAuthInfo( dbUser ); - } + public ImpersonationForEverybodyService( IAuthenticationTypeSystem typeSystem, IAuthenticationDatabaseService db ) + { + _typeSystem = typeSystem; + _db = db; + } - public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, string userName ) - { - throw new NotImplementedException( "Not tested." ); - } + public async Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, int userId ) + { + IUserAuthInfo? dbUser = await _db.ReadUserAuthInfoAsync( ctx.RequestServices.GetRequiredService(), 1, userId ); + return _typeSystem.UserInfo.FromUserAuthInfo( dbUser ); } + public Task ImpersonateAsync( HttpContext ctx, IActivityMonitor monitor, IAuthenticationInfo info, string userName ) + { + throw new NotImplementedException( "Not tested." ); + } } diff --git a/Tests/CK.DB.AspNet.Auth.This.Tests/Services/NoEvilZoneForPaula.cs b/Tests/CK.DB.AspNet.Auth.This.Tests/Services/NoEvilZoneForPaula.cs index 40672617..8e04b486 100644 --- a/Tests/CK.DB.AspNet.Auth.This.Tests/Services/NoEvilZoneForPaula.cs +++ b/Tests/CK.DB.AspNet.Auth.This.Tests/Services/NoEvilZoneForPaula.cs @@ -4,23 +4,21 @@ using System.Linq; using System.Threading.Tasks; -namespace CK.DB.AspNet.Auth.Tests +namespace CK.DB.AspNet.Auth.Tests; + +/// +/// Client calls login with userData that contains a Zone. +/// +[ExcludeCKType] +public class NoEvilZoneForPaula : IWebFrontAuthValidateLoginService { - /// - /// Client calls login with userData that contains a Zone. - /// - [ExcludeCKType] - public class NoEvilZoneForPaula : IWebFrontAuthValidateLoginService + public Task ValidateLoginAsync( IActivityMonitor monitor, IUserInfo loggedInUser, IWebFrontAuthValidateLoginContext context ) { - public Task ValidateLoginAsync( IActivityMonitor monitor, IUserInfo loggedInUser, IWebFrontAuthValidateLoginContext context ) + if( loggedInUser.UserName == "Paula" + && context.UserData.Any( kv => kv.Key == "zone" && kv.Value == "<&>vil" ) ) { - if( loggedInUser.UserName == "Paula" - && context.UserData.Any( kv => kv.Key == "zone" && kv.Value == "<&>vil" ) ) - { - context.SetError( "Validation", "Paula must not go in the <&>vil Zone!" ); - } - return Task.CompletedTask; + context.SetError( "Validation", "Paula must not go in the <&>vil Zone!" ); } + return Task.CompletedTask; } - }